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

222 lines
5.9 KiB
TypeScript
Raw Normal View History

2025-06-06 03:42:04 +00:00
import { Readable, Writable } from "stream"
2025-05-31 18:41:00 +00:00
import path from "path"
2025-05-21 02:00:00 +00:00
import {
createMessageConnection,
StreamMessageReader,
StreamMessageWriter,
2025-05-31 18:41:00 +00: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-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-05-31 18:41:00 +00:00
export type Info = 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
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-06-06 03:42:04 +00:00
export async function create(input: {
cmd: string[]
serverID: string
initialization?: any
}) {
const app = App.info()
2025-06-06 03:42:04 +00:00
log.info("starting client", {
...input,
cwd: app.path.cwd,
})
const server = Bun.spawn({
cmd: input.cmd,
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
cwd: app.path.cwd,
2025-05-31 18:41:00 +00:00
})
2025-05-21 02:00:00 +00:00
2025-06-06 03:42:04 +00:00
const stdout = new Readable({
read() {},
construct(callback) {
const reader = server.stdout.getReader()
const pump = async () => {
try {
while (true) {
const { done, value } = await reader.read()
if (done) {
this.push(null)
break
}
this.push(Buffer.from(value))
}
} catch (error) {
this.destroy(
error instanceof Error ? error : new Error(String(error)),
)
}
}
pump()
callback()
},
})
const stdin = new Writable({
write(chunk, _encoding, callback) {
server.stdin.write(chunk)
callback()
},
})
2025-05-21 02:00:00 +00:00
const connection = createMessageConnection(
2025-06-06 03:42:04 +00:00
new StreamMessageReader(stdout),
new StreamMessageWriter(stdin),
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-05-21 02:00:00 +00:00
log.info("textDocument/publishDiagnostics", {
2025-05-21 02:27:30 +00:00
path,
2025-05-31 18:41:00 +00:00
})
diagnostics.set(path, params.diagnostics)
Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
})
2025-06-06 03:42:04 +00:00
connection.onRequest("workspace/configuration", async () => {
return [{}]
})
2025-05-31 18:41:00 +00:00
connection.listen()
2025-05-21 02:00:00 +00:00
2025-06-06 03:42:04 +00:00
const response = await connection.sendRequest("initialize", {
2025-05-21 02:00:00 +00:00
processId: server.pid,
2025-06-06 03:42:04 +00:00
workspaceFolders: [
{
name: "workspace",
uri: "file://" + app.path.cwd,
2025-05-21 02:00:00 +00:00
},
2025-06-06 03:42:04 +00:00
],
initializationOptions: {
...input.initialization,
2025-05-21 02:00:00 +00:00
},
capabilities: {
workspace: {
configuration: true,
},
textDocument: {
synchronization: {
2025-06-06 03:42:04 +00:00
didOpen: true,
2025-05-21 02:00:00 +00:00
},
publishDiagnostics: {
versionSupport: true,
},
},
},
2025-05-31 18:41:00 +00:00
})
await connection.sendNotification("initialized", {})
log.info("initialized")
2025-05-21 02:00:00 +00:00
2025-05-31 18:41:00 +00:00
const files = new Set<string>()
2025-05-26 21:15:41 +00:00
2025-05-21 02:00:00 +00:00
const result = {
2025-05-26 21:15:41 +00:00
get clientID() {
2025-05-31 18:41:00 +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-06-06 03:42:04 +00:00
input.path = path.isAbsolute(input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
2025-05-31 18:41:00 +00:00
const file = Bun.file(input.path)
const text = await file.text()
const opened = files.has(input.path)
2025-05-26 21:15:41 +00:00
if (!opened) {
2025-05-31 18:41:00 +00:00
log.info("textDocument/didOpen", input)
diagnostics.delete(input.path)
const extension = path.extname(input.path)
const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
2025-05-26 21:15:41 +00:00
await connection.sendNotification("textDocument/didOpen", {
textDocument: {
uri: `file://` + input.path,
languageId,
version: Date.now(),
text,
2025-05-26 21:15:41 +00:00
},
2025-05-31 18:41:00 +00:00
})
files.add(input.path)
return
2025-05-26 21:15:41 +00:00
}
2025-05-31 18:41:00 +00:00
log.info("textDocument/didChange", input)
diagnostics.delete(input.path)
2025-05-21 02:00:00 +00:00
await connection.sendNotification("textDocument/didChange", {
textDocument: {
uri: `file://` + input.path,
version: Date.now(),
2025-05-21 02:00:00 +00:00
},
contentChanges: [
{
text,
},
],
2025-05-31 18:41:00 +00:00
})
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-06-06 03:42:04 +00:00
input.path = path.isAbsolute(input.path)
? input.path
: path.resolve(app.path.cwd, input.path)
2025-05-31 18:41:00 +00:00
log.info("waiting for diagnostics", input)
let unsub: () => void
let timeout: NodeJS.Timeout
2025-05-21 02:00:00 +00:00
return await Promise.race([
new Promise<void>(async (resolve) => {
2025-05-21 02:27:30 +00:00
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
2025-05-26 21:15:41 +00:00
if (
event.properties.path === input.path &&
event.properties.serverID === result.clientID
) {
2025-05-31 18:41:00 +00:00
log.info("got diagnostics", input)
clearTimeout(timeout)
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-05-21 02:27:30 +00:00
new Promise<void>((resolve) => {
timeout = setTimeout(() => {
2025-05-31 18:41:00 +00:00
log.info("timed out refreshing diagnostics", input)
unsub?.()
resolve()
}, 5000)
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-05-26 18:52:38 +00:00
async shutdown() {
2025-05-31 18:41:00 +00:00
log.info("shutting down")
connection.end()
connection.dispose()
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-05-31 18:41:00 +00:00
return result
2025-05-21 02:00:00 +00:00
}
}