Early codeaction support
This commit is contained in:
parent
c2ebaa17b8
commit
a884736063
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "miller",
|
||||
"productName": "miller",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"description": "Column-based code editor",
|
||||
"main": ".vite/build/main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
// Minimal LSP integration helper for the editor.
|
||||
// Keeps all LSP-specific logic in one place so it's easy to review.
|
||||
|
||||
import type * as lsp from "vscode-languageserver-protocol";
|
||||
import { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
|
|
@ -10,6 +8,7 @@ import {
|
|||
Workspace,
|
||||
hoverTooltips,
|
||||
signatureHelp,
|
||||
WorkspaceMapping
|
||||
} from "@codemirror/lsp-client";
|
||||
|
||||
import { serverCompletion } from "./lsp/completion";
|
||||
|
|
@ -79,6 +78,23 @@ export function inferLanguageFromPath(
|
|||
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx")
|
||||
return "typescript";
|
||||
if (ext === "py") return "python";
|
||||
if (ext === "c" || ext === "h" || ext === "cpp" || ext === "hpp")
|
||||
return "cpp";
|
||||
if (ext === "java") return "java";
|
||||
if (ext === "go") return "go";
|
||||
if (ext === "rs") return "rust";
|
||||
if (ext === "php") return "php";
|
||||
if (ext === "rb") return "ruby";
|
||||
if (ext === "cs") return "csharp";
|
||||
if (ext === "html" || ext === "htm") return "html";
|
||||
if (ext === "css" || ext === "scss" || ext === "less") return "css";
|
||||
if (ext === "json") return "json";
|
||||
if (ext === "xml") return "xml";
|
||||
if (ext === "yaml" || ext === "yml") return "yaml";
|
||||
if (ext === "md") return "markdown";
|
||||
if (ext === "lua") return "lua";
|
||||
if (ext === "sh" || ext === "bash") return "shellscript";
|
||||
if (ext === "hs") return "haskell";
|
||||
// add more mappings as needed
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -158,6 +174,22 @@ class OpenFileWorkspace extends Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
export function applyWorkspaceEdit(mapping: WorkspaceMapping, workspace: Workspace, edit: lsp.WorkspaceEdit, userEvent: string) {
|
||||
for (const uri in edit.changes) {
|
||||
const lspChanges = edit.changes[uri];
|
||||
const file = workspace.getFile(uri);
|
||||
if (!lspChanges.length || !file) continue;
|
||||
workspace.updateFile(uri, {
|
||||
changes: lspChanges.map(change => ({
|
||||
from: mapping.mapPosition(uri, change.range.start),
|
||||
to: mapping.mapPosition(uri, change.range.end),
|
||||
insert: change.newText,
|
||||
})),
|
||||
userEvent,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Public helper: attempt to create an LSP extension for `filePath`.
|
||||
// Returns an empty array (no-op extension) on failure so callers can safely
|
||||
// reconfigure their compartments with the returned value.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import * as lsp from "vscode-languageserver-protocol";
|
||||
import { StateEffect, StateField } from "@codemirror/state";
|
||||
import { GutterMarker, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
||||
|
||||
type GutterCodeAction = {
|
||||
|
||||
}
|
||||
|
||||
const codeActionGutterMarker = new class extends GutterMarker {
|
||||
toDOM() { return document.createTextNode("💡"); }
|
||||
};
|
||||
|
||||
const codeActionGutterEffect = StateEffect.define<lsp.CodeAction[]>();
|
||||
|
||||
const codeActionGutterState = StateField.define<lsp.CodeAction[]>({
|
||||
create() { return []; },
|
||||
update(updateCodeActions, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(codeActionGutterEffect)) {
|
||||
return e.value;
|
||||
}
|
||||
}
|
||||
return updateCodeActions;
|
||||
}
|
||||
});
|
||||
|
||||
const syncCodeAction = ViewPlugin.fromClass(
|
||||
class {
|
||||
pending: any | null = null;
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
if (this.pending != null) clearTimeout(this.pending);
|
||||
this.pending = setTimeout(() => {
|
||||
this.pending = null;
|
||||
const plugin = LSPPlugin.get(update.view);
|
||||
if (plugin) {
|
||||
plugin.client.sync();
|
||||
updateCodeActions(plugin);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
destroy() {
|
||||
if (this.pending != null) clearTimeout(this.pending);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function updateCodeActions(plugin: LSPPlugin) {
|
||||
const hasCap = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["codeActionProvider"] : null;
|
||||
if (hasCap === false) return Promise.resolve(null);
|
||||
const params: lsp.CodeActionParams = {
|
||||
textDocument: { uri: plugin.uri },
|
||||
range: {
|
||||
start: plugin.toPosition(0),
|
||||
end: plugin.toPosition(plugin.syncedDoc.length),
|
||||
},
|
||||
context: {
|
||||
diagnostics: [], // TODO fix
|
||||
triggerKind: lsp.CodeActionTriggerKind.Automatic,
|
||||
}
|
||||
}
|
||||
plugin.client.request<
|
||||
lsp.CodeActionParams,
|
||||
(lsp.Command | lsp.CodeAction)[]
|
||||
>(
|
||||
"textDocument/codeAction",
|
||||
params
|
||||
).then((actions) => {
|
||||
const file = plugin.client.workspace.getFile(plugin.uri) as OpenFile;
|
||||
if (!file) return false;
|
||||
if ()
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export function codeAction(config: {}): LSPClientExtension {
|
||||
return {
|
||||
clientCapabilities: {
|
||||
codeAction: {
|
||||
dataSupport: true,
|
||||
resolveSupport: ["edit"],
|
||||
}
|
||||
},
|
||||
editorExtension: syncCodeAction,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,55 @@
|
|||
import type * as lsp from "vscode-languageserver-protocol";
|
||||
import { ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import * as lsp from "vscode-languageserver-protocol";
|
||||
import { StateField, StateEffect } from "@codemirror/state";
|
||||
import { showPanel, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
||||
import { OpenFile } from "../filestate";
|
||||
import { Text } from "@codemirror/state";
|
||||
import { applyWorkspaceEdit } from "../lsp";
|
||||
|
||||
import van from "vanjs-core";
|
||||
const v = van.tags;
|
||||
|
||||
type CodeAction = (lsp.CodeAction | lsp.Command);
|
||||
|
||||
const setCodeActions = StateEffect.define<CodeAction[]>();
|
||||
|
||||
const codeActionState = StateField.define<CodeAction[]>({
|
||||
create: () => null,
|
||||
update(value, tr) {
|
||||
for (let e of tr.effects) {
|
||||
if (e.is(setCodeActions)) value = e.value;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
provide: f => showPanel.from(f, ca => ca ? createCodeActionPanel : null),
|
||||
});
|
||||
|
||||
function createCodeActionPanel(view: EditorView) {
|
||||
const plugin = LSPPlugin.get(view);
|
||||
const actions = view.state.field(codeActionState);
|
||||
const mapping = plugin.client.workspaceMapping();
|
||||
// TODO cleanup mapping when done
|
||||
// TODO use codemirror/view.showDialog instead
|
||||
const list = v.ul(actions.map(a => {
|
||||
if (!Object.hasOwn(a, "edit")) return null;
|
||||
const action = a as lsp.CodeAction;
|
||||
// TODO action resolving
|
||||
const onclick = () => {
|
||||
applyWorkspaceEdit(mapping, plugin.client.workspace, action.edit, "codeaction");
|
||||
view.dispatch({effects: setCodeActions.of(null)});
|
||||
}
|
||||
return v.li(v.a({onclick}, action.title));
|
||||
}));
|
||||
const closeBtn = v.button({
|
||||
class: "absolute top-0 right-5",
|
||||
type: "button",
|
||||
name: "close",
|
||||
"aria-label": view.state.phrase("close"),
|
||||
onclick: () => view.dispatch({effects: setCodeActions.of(null)}),
|
||||
}, "×");
|
||||
const dom = v.div(list, closeBtn);
|
||||
return { top: false, dom };
|
||||
}
|
||||
|
||||
function toSeverity(sev: lsp.DiagnosticSeverity) {
|
||||
return sev == 1
|
||||
|
|
@ -38,6 +85,26 @@ function fromPosition(doc: Text, pos: lsp.Position): number {
|
|||
return line.from + pos.character;
|
||||
}
|
||||
|
||||
function fetchCodeActionsForDiagnostic(plugin: LSPPlugin, diag: lsp.Diagnostic): Promise<CodeAction[]> {
|
||||
const hasCap = plugin.client.serverCapabilities ? !!plugin.client.serverCapabilities["codeActionProvider"] : null;
|
||||
if (hasCap === false) return Promise.resolve(null);
|
||||
const params: lsp.CodeActionParams = {
|
||||
textDocument: { uri: plugin.uri },
|
||||
range: diag.range,
|
||||
context: {
|
||||
diagnostics: [diag], // TODO multiple?
|
||||
triggerKind: lsp.CodeActionTriggerKind.Invoked,
|
||||
}
|
||||
}
|
||||
return plugin.client.request<
|
||||
lsp.CodeActionParams,
|
||||
(lsp.Command | lsp.CodeAction)[]
|
||||
>(
|
||||
"textDocument/codeAction",
|
||||
params
|
||||
);
|
||||
}
|
||||
|
||||
export function serverDiagnostics(): LSPClientExtension {
|
||||
return {
|
||||
clientCapabilities: {
|
||||
|
|
@ -55,6 +122,7 @@ export function serverDiagnostics(): LSPClientExtension {
|
|||
if (params.version != null && params.version != file.version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file.setDiagnostics(
|
||||
params.diagnostics.map((item) => ({
|
||||
from: file.changes.mapPos(
|
||||
|
|
@ -65,11 +133,20 @@ export function serverDiagnostics(): LSPClientExtension {
|
|||
),
|
||||
severity: toSeverity(item.severity ?? 1),
|
||||
message: item.message,
|
||||
actions: [{
|
||||
name: "Solve",
|
||||
apply: async (view) => {
|
||||
const plugin = LSPPlugin.get(view);
|
||||
const a = await fetchCodeActionsForDiagnostic(plugin, item);
|
||||
const effects = setCodeActions.of(a);
|
||||
view.dispatch({ effects });
|
||||
},
|
||||
}]
|
||||
})),
|
||||
);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
editorExtension: autoSync,
|
||||
editorExtension: [autoSync, codeActionState],
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ const lspServers = new Map<string, LspEntry>();
|
|||
const fallbackServerForLanguage: Record<string, string | undefined> = {
|
||||
typescript: "npx typescript-language-server --log-level 4 --stdio",
|
||||
python: "pylsp --check-parent-process",
|
||||
haskell: "haskell-language-server-wrapper --lsp",
|
||||
rust: "rust-analyzer",
|
||||
cpp: "clangd",
|
||||
};
|
||||
|
||||
function ensureLspForKey(
|
||||
|
|
|
|||
Loading…
Reference in New Issue