diff options
Diffstat (limited to 'js/src/lsp')
| -rw-r--r-- | js/src/lsp/client.ts | 58 | ||||
| -rw-r--r-- | js/src/lsp/index.ts | 86 | ||||
| -rw-r--r-- | js/src/lsp/language.ts | 6 |
3 files changed, 108 insertions, 42 deletions
diff --git a/js/src/lsp/client.ts b/js/src/lsp/client.ts index 0c9f9d06c..ce2adebda 100644 --- a/js/src/lsp/client.ts +++ b/js/src/lsp/client.ts @@ -5,6 +5,7 @@ import { StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node"; +import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"; import { App } from "../app"; import { Log } from "../util/log"; import { LANGUAGE_EXTENSIONS } from "./language"; @@ -16,16 +17,19 @@ export namespace LSPClient { export type Info = Awaited<ReturnType<typeof create>>; + export type Diagnostic = VSCodeDiagnostic; + export const Event = { Diagnostics: Bus.event( "lsp.client.diagnostics", z.object({ + serverID: z.string(), path: z.string(), }), ), }; - export async function create(input: { cmd: string[] }) { + export async function create(input: { cmd: string[]; serverID: string }) { log.info("starting client", input); let version = 0; @@ -41,14 +45,14 @@ export namespace LSPClient { new StreamMessageWriter(server.stdin), ); - const diagnostics = new Map<string, any>(); + const diagnostics = new Map<string, Diagnostic[]>(); connection.onNotification("textDocument/publishDiagnostics", (params) => { const path = new URL(params.uri).pathname; log.info("textDocument/publishDiagnostics", { path, }); diagnostics.set(path, params.diagnostics); - Bus.publish(Event.Diagnostics, { path }); + Bus.publish(Event.Diagnostics, { path, serverID: input.serverID }); }); connection.listen(); @@ -114,34 +118,43 @@ export namespace LSPClient { await connection.sendNotification("initialized", {}); log.info("initialized"); + const files = new Set<string>(); + const result = { + get clientID() { + return input.serverID; + }, get connection() { return connection; }, notify: { async open(input: { path: string }) { - log.info("textDocument/didOpen", input); - diagnostics.delete(input.path); const text = await Bun.file(input.path).text(); - const languageId = LANGUAGE_EXTENSIONS[path.extname(input.path)]; - await connection.sendNotification("textDocument/didOpen", { - textDocument: { - uri: `file://` + input.path, - languageId, - version: 1, - text: text, - }, - }); - }, - async change(input: { path: string }) { + const opened = files.has(input.path); + if (!opened) { + log.info("textDocument/didOpen", input); + diagnostics.delete(input.path); + const extension = path.extname(input.path); + const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"; + await connection.sendNotification("textDocument/didOpen", { + textDocument: { + uri: `file://` + input.path, + languageId, + version: 1, + text: text, + }, + }); + files.add(input.path); + return; + } + log.info("textDocument/didChange", input); diagnostics.delete(input.path); - const text = await Bun.file(input.path).text(); version++; await connection.sendNotification("textDocument/didChange", { textDocument: { uri: `file://` + input.path, - version: Date.now(), + version, }, contentChanges: [ { @@ -154,21 +167,24 @@ export namespace LSPClient { get diagnostics() { return diagnostics; }, - async refreshDiagnostics(input: { path: string }) { + async waitForDiagnostics(input: { path: string }) { log.info("refreshing diagnostics", input); let unsub: () => void; let timeout: NodeJS.Timeout; return await Promise.race([ new Promise<void>(async (resolve) => { unsub = Bus.subscribe(Event.Diagnostics, (event) => { - if (event.properties.path === input.path) { + if ( + event.properties.path === input.path && + event.properties.serverID === result.clientID + ) { log.info("refreshed diagnostics", input); clearTimeout(timeout); unsub?.(); resolve(); } }); - await result.notify.change(input); + await result.notify.open(input); }), new Promise<void>((resolve) => { timeout = setTimeout(() => { diff --git a/js/src/lsp/index.ts b/js/src/lsp/index.ts index 80503fb1f..358c6a549 100644 --- a/js/src/lsp/index.ts +++ b/js/src/lsp/index.ts @@ -1,6 +1,7 @@ import { App } from "../app"; import { Log } from "../util/log"; import { LSPClient } from "./client"; +import path from "path"; export namespace LSP { const log = Log.create({ service: "lsp" }); @@ -10,17 +11,8 @@ export namespace LSP { async () => { const clients = new Map<string, LSPClient.Info>(); - // QUESTION: how lazy should lsp auto discovery be? should it not initialize until a file is opened? - clients.set( - "typescript", - await LSPClient.create({ - cmd: ["bun", "x", "typescript-language-server", "--stdio"], - }), - ); - return { clients, - diagnostics: new Map<string, any>(), }; }, async (state) => { @@ -30,7 +22,39 @@ export namespace LSP { }, ); - export async function run<T>( + export async function file(input: string) { + const extension = path.parse(input).ext; + const s = await state(); + const matches = AUTO.filter((x) => x.extensions.includes(extension)); + for (const match of matches) { + const existing = s.clients.get(match.id); + if (existing) continue; + const client = await LSPClient.create({ + cmd: match.command, + serverID: match.id, + }); + s.clients.set(match.id, client); + } + await run(async (client) => { + const wait = client.waitForDiagnostics({ path: input }); + await client.notify.open({ path: input }); + return wait; + }); + } + + export async function diagnostics() { + const results: Record<string, LSPClient.Diagnostic[]> = {}; + for (const result of await run(async (client) => client.diagnostics)) { + for (const [path, diagnostics] of result.entries()) { + const arr = results[path] || []; + arr.push(...diagnostics); + results[path] = arr; + } + } + return results; + } + + async function run<T>( input: (client: LSPClient.Info) => Promise<T>, ): Promise<T[]> { const clients = await state().then((x) => [...x.clients.values()]); @@ -39,28 +63,48 @@ export namespace LSP { } const AUTO: { + id: string; command: string[]; extensions: string[]; install?: () => Promise<void>; }[] = [ { + id: "typescript", command: ["bun", "x", "typescript-language-server", "--stdio"], extensions: [ - "ts", - "tsx", - "js", - "jsx", - "mjs", - "cjs", - "mts", - "cts", - "mtsx", - "ctsx", + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".mts", + ".cts", + ".mtsx", + ".ctsx", ], }, { + id: "golang", command: ["gopls"], - extensions: ["go"], + extensions: [".go"], }, ]; + + export namespace Diagnostic { + export function pretty(diagnostic: LSPClient.Diagnostic) { + const severityMap = { + 1: "ERROR", + 2: "WARN", + 3: "INFO", + 4: "HINT", + }; + + const severity = severityMap[diagnostic.severity || 1]; + const line = diagnostic.range.start.line + 1; + const col = diagnostic.range.start.character + 1; + + return `${severity} [${line}:${col}] ${diagnostic.message}`; + } + } } diff --git a/js/src/lsp/language.ts b/js/src/lsp/language.ts index 14b32edfa..e28d7a79c 100644 --- a/js/src/lsp/language.ts +++ b/js/src/lsp/language.ts @@ -76,8 +76,14 @@ export const LANGUAGE_EXTENSIONS: Record<string, string> = { ".swift": "swift", ".ts": "typescript", ".tsx": "typescriptreact", + ".mts": "typescript", + ".cts": "typescript", + ".mtsx": "typescriptreact", + ".ctsx": "typescriptreact", ".xml": "xml", ".xsl": "xsl", ".yaml": "yaml", ".yml": "yaml", + ".mjs": "javascript", + ".cjs": "javascript", } as const; |
