This commit is contained in:
Dax Raad
2025-05-26 17:15:41 -04:00
parent 0ad8738933
commit de9f144858
9 changed files with 195 additions and 78 deletions

View File

@@ -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(() => {

View File

@@ -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}`;
}
}
}

View File

@@ -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;