opencode_offline/packages/opencode/src/lsp/client.ts

199 lines
5.7 KiB
TypeScript
Raw Normal View History

2025-06-01 02:41:00 +08:00
import path from "path"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
2025-06-01 02:41:00 +08:00
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { App } from "../app/app"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Bus } from "../bus"
import z from "zod"
2025-06-07 11:21:57 +08:00
import type { LSPServer } from "./server"
import { NamedError } from "../util/error"
2025-07-01 10:46:42 +08:00
import { withTimeout } from "../util/timeout"
2025-05-21 10:00:00 +08:00
export namespace LSPClient {
2025-06-01 02:41:00 +08:00
const log = Log.create({ service: "lsp.client" })
2025-05-21 10:00:00 +08:00
2025-06-07 11:21:57 +08:00
export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
2025-05-21 10:00:00 +08:00
2025-06-01 02:41:00 +08:00
export type Diagnostic = VSCodeDiagnostic
2025-05-27 05:15:41 +08:00
export const InitializeError = NamedError.create(
"LSPInitializeError",
z.object({
serverID: z.string(),
}),
)
2025-05-21 10:27:30 +08:00
export const Event = {
Diagnostics: Bus.event(
"lsp.client.diagnostics",
z.object({
2025-05-27 05:15:41 +08:00
serverID: z.string(),
2025-05-21 10:27:30 +08:00
path: z.string(),
}),
),
2025-06-01 02:41:00 +08:00
}
2025-05-21 10:27:30 +08:00
2025-07-09 06:14:24 +08:00
export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
const app = App.info()
2025-07-09 06:14:24 +08:00
const l = log.clone().tag("serverID", input.serverID)
l.info("starting client")
2025-06-06 11:42:04 +08:00
2025-05-21 10:00:00 +08:00
const connection = createMessageConnection(
2025-07-09 06:14:24 +08:00
new StreamMessageReader(input.server.process.stdout),
new StreamMessageWriter(input.server.process.stdin),
2025-06-01 02:41:00 +08:00
)
2025-05-21 10:00:00 +08:00
2025-06-01 02:41:00 +08:00
const diagnostics = new Map<string, Diagnostic[]>()
2025-05-21 10:00:00 +08:00
connection.onNotification("textDocument/publishDiagnostics", (params) => {
2025-06-01 02:41:00 +08:00
const path = new URL(params.uri).pathname
2025-07-09 06:14:24 +08:00
l.info("textDocument/publishDiagnostics", {
2025-05-21 10:27:30 +08:00
path,
2025-06-01 02:41:00 +08:00
})
2025-07-01 10:46:42 +08:00
const exists = diagnostics.has(path)
2025-06-01 02:41:00 +08:00
diagnostics.set(path, params.diagnostics)
2025-07-09 06:14:24 +08:00
if (!exists && input.serverID === "typescript") return
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
})
connection.onRequest("window/workDoneProgress/create", (params) => {
l.info("window/workDoneProgress/create", params)
return null
2025-06-01 02:41:00 +08:00
})
2025-06-06 11:42:04 +08:00
connection.onRequest("workspace/configuration", async () => {
return [{}]
})
2025-06-01 02:41:00 +08:00
connection.listen()
2025-05-21 10:00:00 +08:00
2025-07-09 06:14:24 +08:00
l.info("sending initialize")
2025-07-01 10:46:42 +08:00
await withTimeout(
connection.sendRequest("initialize", {
2025-07-09 06:14:24 +08:00
rootUri: "file://" + input.root,
processId: input.server.process.pid,
workspaceFolders: [
{
name: "workspace",
2025-07-09 06:14:24 +08:00
uri: "file://" + input.root,
},
],
initializationOptions: {
2025-07-09 06:14:24 +08:00
...input.server.initialization,
2025-05-21 10:00:00 +08:00
},
capabilities: {
2025-07-09 06:14:24 +08:00
window: {
workDoneProgress: true,
},
workspace: {
configuration: true,
2025-05-21 10:00:00 +08:00
},
textDocument: {
synchronization: {
didOpen: true,
didChange: true,
},
publishDiagnostics: {
versionSupport: true,
},
2025-05-21 10:00:00 +08:00
},
},
}),
2025-07-01 10:46:42 +08:00
5_000,
2025-07-04 08:16:16 +08:00
).catch((err) => {
2025-07-09 06:14:24 +08:00
l.error("initialize error", { error: err })
2025-07-04 08:16:16 +08:00
throw new InitializeError(
2025-07-09 06:14:24 +08:00
{ serverID: input.serverID },
2025-07-04 08:16:16 +08:00
{
cause: err,
},
)
2025-07-01 10:46:42 +08:00
})
2025-07-04 08:16:16 +08:00
2025-06-01 02:41:00 +08:00
await connection.sendNotification("initialized", {})
2025-05-21 10:00:00 +08:00
2025-06-07 05:20:01 +08:00
const files: {
[path: string]: number
} = {}
2025-05-27 05:15:41 +08:00
2025-05-21 10:00:00 +08:00
const result = {
2025-07-09 06:14:24 +08:00
root: input.root,
2025-06-11 04:29:35 +08:00
get serverID() {
2025-07-09 06:14:24 +08:00
return input.serverID
2025-05-27 05:15:41 +08:00
},
2025-05-21 10:00:00 +08:00
get connection() {
2025-06-01 02:41:00 +08:00
return connection
2025-05-21 10:00:00 +08:00
},
notify: {
async open(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
2025-06-01 02:41:00 +08:00
const file = Bun.file(input.path)
const text = await file.text()
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
2025-06-07 05:20:01 +08:00
const version = files[input.path]
2025-07-01 10:46:42 +08:00
if (version !== undefined) {
const next = version + 1
files[input.path] = next
log.info("textDocument/didChange", { path: input.path, version: next })
await connection.sendNotification("textDocument/didChange", {
2025-05-27 05:15:41 +08:00
textDocument: {
uri: `file://` + input.path,
version: next,
2025-05-27 05:15:41 +08:00
},
contentChanges: [{ text }],
2025-06-01 02:41:00 +08:00
})
return
2025-05-27 05:15:41 +08:00
}
2025-07-01 10:46:42 +08:00
log.info("textDocument/didOpen", input)
2025-06-01 02:41:00 +08:00
diagnostics.delete(input.path)
2025-07-01 10:46:42 +08:00
await connection.sendNotification("textDocument/didOpen", {
2025-05-21 10:00:00 +08:00
textDocument: {
uri: `file://` + input.path,
2025-07-01 10:46:42 +08:00
languageId,
version: 0,
text,
2025-05-21 10:00:00 +08:00
},
2025-06-01 02:41:00 +08:00
})
2025-07-01 10:46:42 +08:00
files[input.path] = 0
return
2025-05-21 10:00:00 +08:00
},
},
get diagnostics() {
2025-06-01 02:41:00 +08:00
return diagnostics
2025-05-21 10:00:00 +08:00
},
2025-05-27 05:15:41 +08:00
async waitForDiagnostics(input: { path: string }) {
input.path = path.isAbsolute(input.path) ? input.path : path.resolve(app.path.cwd, input.path)
2025-06-01 02:41:00 +08:00
log.info("waiting for diagnostics", input)
let unsub: () => void
2025-07-01 10:46:42 +08:00
return await withTimeout(
new Promise<void>((resolve) => {
2025-05-21 10:27:30 +08:00
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
2025-06-01 02:41:00 +08:00
log.info("got diagnostics", input)
unsub?.()
resolve()
2025-05-21 10:27:30 +08:00
}
2025-06-01 02:41:00 +08:00
})
2025-05-21 10:00:00 +08:00
}),
2025-07-02 01:50:57 +08:00
3000,
)
.catch(() => {})
.finally(() => {
unsub?.()
})
2025-05-21 10:00:00 +08:00
},
2025-05-27 02:52:38 +08:00
async shutdown() {
2025-07-09 06:14:24 +08:00
l.info("shutting down")
2025-06-01 02:41:00 +08:00
connection.end()
connection.dispose()
input.server.process.kill()
2025-07-09 06:14:24 +08:00
l.info("shutdown")
2025-05-27 02:52:38 +08:00
},
2025-06-01 02:41:00 +08:00
}
2025-05-21 10:00:00 +08:00
2025-07-09 06:14:24 +08:00
l.info("initialized")
2025-06-01 02:41:00 +08:00
return result
2025-05-21 10:00:00 +08:00
}
}