From db5f33e961ed78eaa43ea75dbb8bfefc16ebff48 Mon Sep 17 00:00:00 2001 From: Quinten Kock Date: Tue, 2 Dec 2025 23:22:39 +0100 Subject: [PATCH] Fix diagnostic propagation --- src/app/filestate.ts | 23 +++++++++++----- src/app/lsp.ts | 16 ++++++++--- src/app/lsp/diagnostics.ts | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 src/app/lsp/diagnostics.ts diff --git a/src/app/filestate.ts b/src/app/filestate.ts index 343b17e..8c2f512 100644 --- a/src/app/filestate.ts +++ b/src/app/filestate.ts @@ -8,6 +8,7 @@ import { ChangeSet, } from "@codemirror/state"; import { history } from "@codemirror/commands"; +import { Diagnostic, setDiagnostics } from "@codemirror/lint"; import { Editor } from "./editor"; import van, { State } from "vanjs-core"; import { WorkspaceFile } from "@codemirror/lsp-client"; @@ -104,6 +105,11 @@ export class OpenFile implements WorkspaceFile { createEditor(): Editor { const editor = new Editor(this); this.editors.push(editor); + editor.dispatch( + editor.view.state.update( + setDiagnostics(editor.view.state, this.diagnostics || []), + ), + ); return editor; } @@ -172,12 +178,7 @@ export class OpenFile implements WorkspaceFile { es.forEach((e) => e.dispatch(e.view.state.update(trs), true)); } else { this.editors.forEach((e) => { - const changes = transaction.changes; - const userEvent = transaction.annotation(Transaction.userEvent); - const annotations = userEvent - ? [Transaction.userEvent.of(userEvent)] - : []; - e.dispatch(e.view.state.update({ changes, annotations }), true); + e.dispatch(e.view.state.update(trs), true); }); } } @@ -215,4 +216,14 @@ export class OpenFile implements WorkspaceFile { if (this.editors.length > 0) return this.editors[0].view; return null; } + + private diagnostics: Diagnostic[]; + setDiagnostics(diagnostics: Diagnostic[]) { + this.diagnostics = diagnostics; + for (const editor of this.editors) { + editor.view.dispatch( + setDiagnostics(editor.view.state, diagnostics), + ); + } + } } diff --git a/src/app/lsp.ts b/src/app/lsp.ts index a80a978..143a539 100644 --- a/src/app/lsp.ts +++ b/src/app/lsp.ts @@ -7,11 +7,14 @@ import { EditorView } from "@codemirror/view"; import { LSPClient, LSPPlugin, - languageServerExtensions, Workspace, + serverCompletion, + hoverTooltips, + signatureHelp, } from "@codemirror/lsp-client"; import { OpenFile } from "./filestate"; +import { serverDiagnostics } from "./lsp/diagnostics"; // Create a very small MessagePort-based transport implementation // compatible with @codemirror/lsp-client's expected Transport interface. @@ -108,9 +111,9 @@ class OpenFileWorkspace extends Workspace { file.version = this.nextFileVersion(file.uri); file.changes = ChangeSet.empty(file.doc.length); } - for(const e of file.editors) { + for (const e of file.editors) { const plugin = LSPPlugin.get(e.view); - if(plugin) { + if (plugin) { plugin.clear(); } } @@ -257,7 +260,12 @@ export async function createLspExtension( try { const client = new LSPClient({ - extensions: languageServerExtensions(), + extensions: [ + serverDiagnostics(), + serverCompletion(), + hoverTooltips(), + signatureHelp(), + ], rootUri: rootUri, workspace: (c) => new OpenFileWorkspace(c), }); diff --git a/src/app/lsp/diagnostics.ts b/src/app/lsp/diagnostics.ts new file mode 100644 index 0000000..c52f4fa --- /dev/null +++ b/src/app/lsp/diagnostics.ts @@ -0,0 +1,55 @@ +import type * as lsp from "vscode-languageserver-protocol" +import {setDiagnostics} from "@codemirror/lint" +import {ViewPlugin, ViewUpdate} from "@codemirror/view" +import {LSPPlugin, LSPClientExtension} from "@codemirror/lsp-client" +import {OpenFile} from "../filestate" +import { Text } from "@codemirror/state" + +function toSeverity(sev: lsp.DiagnosticSeverity) { + return sev == 1 ? "error" : sev == 2 ? "warning" : sev == 3 ? "info" : "hint" +} + +const autoSync = 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 + let plugin = LSPPlugin.get(update.view) + if (plugin) plugin.client.sync() + }, 500) + } + } + destroy() { + if (this.pending != null) clearTimeout(this.pending) + } +}) + +function fromPosition(doc: Text, pos: lsp.Position): number { + let line = doc.line(pos.line + 1) + return line.from + pos.character +} + +export function serverDiagnostics(): LSPClientExtension { + return { + clientCapabilities: {textDocument: {publishDiagnostics: {versionSupport: true}}}, + notificationHandlers: { + "textDocument/publishDiagnostics": (client, params: lsp.PublishDiagnosticsParams) => { + let file = client.workspace.getFile(params.uri) as OpenFile; + if (!file || params.version != null && params.version != file.version) return false; + for(const view of file.editors.map(e => e.view)) { + const mapPos = (p: number) => file.changes ? file.changes.mapPos(p) : p; + file.setDiagnostics(params.diagnostics.map(item => ({ + from: mapPos(fromPosition(file.doc, item.range.start)), + to: mapPos(fromPosition(file.doc, item.range.end)), + severity: toSeverity(item.severity ?? 1), + message: item.message, + }))); + } + return true + } + }, + editorExtension: autoSync + } +}