summaryrefslogtreecommitdiffhomepage
path: root/js/src/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/lsp')
-rw-r--r--js/src/lsp/client.ts58
-rw-r--r--js/src/lsp/index.ts86
-rw-r--r--js/src/lsp/language.ts6
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;