diff options
| author | Dax Raad <[email protected]> | 2025-05-20 22:00:00 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-26 12:40:17 -0400 |
| commit | 2860a2bb1a1f227c26b02f1325454ab79d6f6451 (patch) | |
| tree | 2a52a5c159025c6ff6854af2995c24c4d7d46b77 /js/src/lsp/client.ts | |
| parent | 9b564f0b73d099d40c79517213211ba81b3312c6 (diff) | |
| download | opencode-2860a2bb1a1f227c26b02f1325454ab79d6f6451.tar.gz opencode-2860a2bb1a1f227c26b02f1325454ab79d6f6451.zip | |
sync
Diffstat (limited to 'js/src/lsp/client.ts')
| -rw-r--r-- | js/src/lsp/client.ts | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/js/src/lsp/client.ts b/js/src/lsp/client.ts new file mode 100644 index 000000000..e6cfdb2eb --- /dev/null +++ b/js/src/lsp/client.ts @@ -0,0 +1,178 @@ +import { spawn } from "child_process"; +import path from "path"; +import { + createMessageConnection, + Disposable, + StreamMessageReader, + StreamMessageWriter, +} from "vscode-jsonrpc/node"; +import { App } from "../app"; +import { Log } from "../util/log"; +import { LANGUAGE_EXTENSIONS } from "./language"; + +export namespace LSPClient { + const log = Log.create({ service: "lsp.client" }); + + export type Info = Awaited<ReturnType<typeof create>>; + + export async function create(input: { cmd: string[] }) { + log.info("starting client", input); + let version = 0; + + const app = await App.use(); + const [command, ...args] = input.cmd; + const server = spawn(command, args, { + stdio: ["pipe", "pipe", "pipe"], + cwd: app.root, + }); + + const connection = createMessageConnection( + new StreamMessageReader(server.stdout), + new StreamMessageWriter(server.stdin), + ); + + const diagnostics = new Map<string, any>(); + connection.onNotification("textDocument/publishDiagnostics", (params) => { + log.info("textDocument/publishDiagnostics", { + path: new URL(params.uri).pathname, + }); + diagnostics.set(new URL(params.uri).pathname, params.diagnostics); + }); + connection.listen(); + + await connection.sendRequest("initialize", { + processId: server.pid, + initializationOptions: { + workspaceFolders: [ + { + name: "workspace", + uri: "file://" + app.root, + }, + ], + tsserver: { + path: require.resolve("typescript/lib/tsserver.js"), + }, + }, + capabilities: { + workspace: { + configuration: true, + didChangeConfiguration: { + dynamicRegistration: true, + }, + didChangeWatchedFiles: { + dynamicRegistration: true, + relativePatternSupport: true, + }, + }, + textDocument: { + synchronization: { + dynamicRegistration: true, + didSave: true, + }, + completion: { + completionItem: {}, + }, + codeLens: { + dynamicRegistration: true, + }, + documentSymbol: {}, + codeAction: { + codeActionLiteralSupport: { + codeActionKind: { + valueSet: [], + }, + }, + }, + publishDiagnostics: { + versionSupport: true, + }, + semanticTokens: { + requests: { + range: {}, + full: {}, + }, + tokenTypes: [], + tokenModifiers: [], + formats: [], + }, + }, + window: {}, + }, + }); + await connection.sendNotification("initialized", {}); + log.info("initialized"); + + const result = { + 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 }) { + 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(), + }, + contentChanges: [ + { + text, + }, + ], + }); + }, + }, + get diagnostics() { + return diagnostics; + }, + async refreshDiagnostics(input: { path: string }) { + log.info("refreshing diagnostics", input); + let notif: Disposable | undefined; + return await Promise.race([ + new Promise<void>(async (resolve) => { + notif = connection.onNotification( + "textDocument/publishDiagnostics", + (params) => { + log.info("refreshed diagnostics", input); + if (new URL(params.uri).pathname === input.path) { + diagnostics.set( + new URL(params.uri).pathname, + params.diagnostics, + ); + resolve(); + notif?.dispose(); + } + }, + ); + await result.notify.change(input); + }), + new Promise<void>((resolve) => + setTimeout(() => { + notif?.dispose(); + resolve(); + }, 5000), + ), + ]); + }, + }; + + return result; + } +} |
