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

185 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-06-01 02:41:00 +08:00
import path from "path"
2025-05-21 10:00:00 +08:00
import {
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
2025-06-01 02:41:00 +08:00
} from "vscode-jsonrpc/node"
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-06-11 04:29:35 +08:00
export async function create(serverID: string, server: LSPServer.Handle) {
const app = App.info()
2025-06-11 04:29:35 +08:00
log.info("starting client", { id: serverID })
2025-06-06 11:42:04 +08:00
2025-05-21 10:00:00 +08:00
const connection = createMessageConnection(
2025-06-11 04:29:35 +08:00
new StreamMessageReader(server.process.stdout),
new StreamMessageWriter(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-05-21 10:00:00 +08:00
log.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-01 10:46:42 +08:00
if (!exists && serverID === "typescript") return
2025-06-11 04:29:35 +08:00
Bus.publish(Event.Diagnostics, { path, serverID })
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
log.info("sending initialize", { id: serverID })
2025-07-01 10:46:42 +08:00
await withTimeout(
connection.sendRequest("initialize", {
processId: server.process.pid,
workspaceFolders: [
{
name: "workspace",
uri: "file://" + app.path.cwd,
},
],
initializationOptions: {
...server.initialization,
2025-05-21 10:00:00 +08:00
},
capabilities: {
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,
).catch(() => {
throw new InitializeError({ serverID })
})
2025-06-01 02:41:00 +08:00
await connection.sendNotification("initialized", {})
log.info("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-06-11 04:29:35 +08:00
get serverID() {
return 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 }) {
2025-06-06 11:42:04 +08:00
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()
2025-06-07 05:20:01 +08:00
const version = files[input.path]
2025-07-01 10:46:42 +08:00
if (version !== undefined) {
2025-06-01 02:41:00 +08:00
diagnostics.delete(input.path)
2025-07-01 10:46:42 +08:00
await connection.sendNotification("textDocument/didClose", {
2025-05-27 05:15:41 +08:00
textDocument: {
uri: `file://` + input.path,
},
2025-06-01 02:41:00 +08:00
})
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
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
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 }) {
2025-06-06 11:42:04 +08:00
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) => {
2025-05-27 05:15:41 +08:00
if (
event.properties.path === input.path &&
2025-06-11 04:29:35 +08:00
event.properties.serverID === result.serverID
2025-05-27 05:15:41 +08:00
) {
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-01 10:46:42 +08:00
log.info("shutting down", { serverID })
2025-06-01 02:41:00 +08:00
connection.end()
connection.dispose()
2025-07-01 04:45:13 +08:00
server.process.kill("SIGTERM")
2025-07-01 10:46:42 +08:00
log.info("shutdown", { serverID })
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-06-01 02:41:00 +08:00
return result
2025-05-21 10:00:00 +08:00
}
}