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 | |
| parent | 9b564f0b73d099d40c79517213211ba81b3312c6 (diff) | |
| download | opencode-2860a2bb1a1f227c26b02f1325454ab79d6f6451.tar.gz opencode-2860a2bb1a1f227c26b02f1325454ab79d6f6451.zip | |
sync
Diffstat (limited to 'js/src/lsp')
| -rw-r--r-- | js/src/lsp/client.ts | 178 | ||||
| -rw-r--r-- | js/src/lsp/index.ts | 31 | ||||
| -rw-r--r-- | js/src/lsp/language.ts | 83 |
3 files changed, 292 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; + } +} diff --git a/js/src/lsp/index.ts b/js/src/lsp/index.ts new file mode 100644 index 000000000..ac200fc51 --- /dev/null +++ b/js/src/lsp/index.ts @@ -0,0 +1,31 @@ +import { App } from "../app"; +import { Log } from "../util/log"; +import { LSPClient } from "./client"; + +export namespace LSP { + const log = Log.create({ service: "lsp" }); + + const state = App.state("lsp", async () => { + const clients = new Map<string, LSPClient.Info>(); + + clients.set( + "typescript", + await LSPClient.create({ + cmd: ["bun", "x", "typescript-language-server", "--stdio"], + }), + ); + + return { + clients, + diagnostics: new Map<string, any>(), + }; + }); + + export async function run<T>( + input: (client: LSPClient.Info) => Promise<T>, + ): Promise<T[]> { + const clients = await state().then((x) => [...x.clients.values()]); + const tasks = clients.map((x) => input(x)); + return Promise.all(tasks); + } +} diff --git a/js/src/lsp/language.ts b/js/src/lsp/language.ts new file mode 100644 index 000000000..0a9bc0f7f --- /dev/null +++ b/js/src/lsp/language.ts @@ -0,0 +1,83 @@ +export const LANGUAGE_EXTENSIONS: Record<string, string> = { + ".abap": "abap", + ".bat": "bat", + ".bib": "bibtex", + ".bibtex": "bibtex", + ".clj": "clojure", + ".coffee": "coffeescript", + ".c": "c", + ".cpp": "cpp", + ".cxx": "cpp", + ".cc": "cpp", + ".c++": "cpp", + ".cs": "csharp", + ".css": "css", + ".d": "d", + ".pas": "pascal", + ".pascal": "pascal", + ".diff": "diff", + ".patch": "diff", + ".dart": "dart", + ".dockerfile": "dockerfile", + ".ex": "elixir", + ".exs": "elixir", + ".erl": "erlang", + ".hrl": "erlang", + ".fs": "fsharp", + ".fsi": "fsharp", + ".fsx": "fsharp", + ".fsscript": "fsharp", + ".gitcommit": "git-commit", + ".gitrebase": "git-rebase", + ".go": "go", + ".groovy": "groovy", + ".hbs": "handlebars", + ".handlebars": "handlebars", + ".hs": "haskell", + ".html": "html", + ".htm": "html", + ".ini": "ini", + ".java": "java", + ".js": "javascript", + ".jsx": "javascriptreact", + ".json": "json", + ".tex": "latex", + ".latex": "latex", + ".less": "less", + ".lua": "lua", + ".makefile": "makefile", + makefile: "makefile", + ".md": "markdown", + ".markdown": "markdown", + ".m": "objective-c", + ".mm": "objective-cpp", + ".pl": "perl", + ".pm": "perl6", + ".php": "php", + ".ps1": "powershell", + ".psm1": "powershell", + ".pug": "jade", + ".jade": "jade", + ".py": "python", + ".r": "r", + ".cshtml": "razor", + ".razor": "razor", + ".rb": "ruby", + ".rs": "rust", + ".scss": "scss", + ".sass": "sass", + ".scala": "scala", + ".shader": "shaderlab", + ".sh": "shellscript", + ".bash": "shellscript", + ".zsh": "shellscript", + ".ksh": "shellscript", + ".sql": "sql", + ".swift": "swift", + ".ts": "typescript", + ".tsx": "typescriptreact", + ".xml": "xml", + ".xsl": "xsl", + ".yaml": "yaml", + ".yml": "yaml", +}; |
