Early codeaction support
This commit is contained in:
parent
c2ebaa17b8
commit
a884736063
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "miller",
|
"name": "miller",
|
||||||
"productName": "miller",
|
"productName": "miller",
|
||||||
"version": "0.2.3",
|
"version": "0.2.4",
|
||||||
"description": "Column-based code editor",
|
"description": "Column-based code editor",
|
||||||
"main": ".vite/build/main.js",
|
"main": ".vite/build/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// Minimal LSP integration helper for the editor.
|
import type * as lsp from "vscode-languageserver-protocol";
|
||||||
// Keeps all LSP-specific logic in one place so it's easy to review.
|
|
||||||
|
|
||||||
import { Extension, TransactionSpec } from "@codemirror/state";
|
import { Extension, TransactionSpec } from "@codemirror/state";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
|
|
||||||
|
|
@ -10,6 +8,7 @@ import {
|
||||||
Workspace,
|
Workspace,
|
||||||
hoverTooltips,
|
hoverTooltips,
|
||||||
signatureHelp,
|
signatureHelp,
|
||||||
|
WorkspaceMapping
|
||||||
} from "@codemirror/lsp-client";
|
} from "@codemirror/lsp-client";
|
||||||
|
|
||||||
import { serverCompletion } from "./lsp/completion";
|
import { serverCompletion } from "./lsp/completion";
|
||||||
|
|
@ -79,6 +78,23 @@ export function inferLanguageFromPath(
|
||||||
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx")
|
if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx")
|
||||||
return "typescript";
|
return "typescript";
|
||||||
if (ext === "py") return "python";
|
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
|
// add more mappings as needed
|
||||||
return undefined;
|
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`.
|
// Public helper: attempt to create an LSP extension for `filePath`.
|
||||||
// Returns an empty array (no-op extension) on failure so callers can safely
|
// Returns an empty array (no-op extension) on failure so callers can safely
|
||||||
// reconfigure their compartments with the returned value.
|
// 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,17 +1,64 @@
|
||||||
import type * as lsp from "vscode-languageserver-protocol";
|
import * as lsp from "vscode-languageserver-protocol";
|
||||||
import { ViewPlugin, ViewUpdate } from "@codemirror/view";
|
import { StateField, StateEffect } from "@codemirror/state";
|
||||||
|
import { showPanel, EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||||
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
import { LSPPlugin, LSPClientExtension } from "@codemirror/lsp-client";
|
||||||
import { OpenFile } from "../filestate";
|
import { OpenFile } from "../filestate";
|
||||||
import { Text } from "@codemirror/state";
|
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) {
|
function toSeverity(sev: lsp.DiagnosticSeverity) {
|
||||||
return sev == 1
|
return sev == 1
|
||||||
? "error"
|
? "error"
|
||||||
: sev == 2
|
: sev == 2
|
||||||
? "warning"
|
? "warning"
|
||||||
: sev == 3
|
: sev == 3
|
||||||
? "info"
|
? "info"
|
||||||
: "hint";
|
: "hint";
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoSync = ViewPlugin.fromClass(
|
const autoSync = ViewPlugin.fromClass(
|
||||||
|
|
@ -38,6 +85,26 @@ function fromPosition(doc: Text, pos: lsp.Position): number {
|
||||||
return line.from + pos.character;
|
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 {
|
export function serverDiagnostics(): LSPClientExtension {
|
||||||
return {
|
return {
|
||||||
clientCapabilities: {
|
clientCapabilities: {
|
||||||
|
|
@ -55,6 +122,7 @@ export function serverDiagnostics(): LSPClientExtension {
|
||||||
if (params.version != null && params.version != file.version) {
|
if (params.version != null && params.version != file.version) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.setDiagnostics(
|
file.setDiagnostics(
|
||||||
params.diagnostics.map((item) => ({
|
params.diagnostics.map((item) => ({
|
||||||
from: file.changes.mapPos(
|
from: file.changes.mapPos(
|
||||||
|
|
@ -65,11 +133,20 @@ export function serverDiagnostics(): LSPClientExtension {
|
||||||
),
|
),
|
||||||
severity: toSeverity(item.severity ?? 1),
|
severity: toSeverity(item.severity ?? 1),
|
||||||
message: item.message,
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editorExtension: autoSync,
|
editorExtension: [autoSync, codeActionState],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ const lspServers = new Map<string, LspEntry>();
|
||||||
const fallbackServerForLanguage: Record<string, string | undefined> = {
|
const fallbackServerForLanguage: Record<string, string | undefined> = {
|
||||||
typescript: "npx typescript-language-server --log-level 4 --stdio",
|
typescript: "npx typescript-language-server --log-level 4 --stdio",
|
||||||
python: "pylsp --check-parent-process",
|
python: "pylsp --check-parent-process",
|
||||||
|
haskell: "haskell-language-server-wrapper --lsp",
|
||||||
|
rust: "rust-analyzer",
|
||||||
|
cpp: "clangd",
|
||||||
};
|
};
|
||||||
|
|
||||||
function ensureLspForKey(
|
function ensureLspForKey(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue