cloudaxe-opencode/packages/opencode/src/lsp/client.ts

223 lines
6.1 KiB
TypeScript
Raw Normal View History

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