Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/opencode/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ export namespace LSPClient {
uri: pathToFileURL(input.root).href,
},
])
let alive = true
connection.onClose(() => {
l.info("connection closed")
alive = false
})
connection.onError((err) => {
l.error("connection error", { error: err })
})

connection.listen()

l.info("sending initialize")
Expand Down Expand Up @@ -141,6 +150,9 @@ export namespace LSPClient {
get serverID() {
return input.serverID
},
get alive() {
return alive
},
get connection() {
return connection
},
Expand Down
36 changes: 30 additions & 6 deletions packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,18 +275,42 @@ export namespace LSP {
return false
}

async function removeClient(s: Awaited<ReturnType<typeof state>>, client: LSPClient.Info) {
const idx = s.clients.indexOf(client)
if (idx !== -1) s.clients.splice(idx, 1)
s.broken.add(client.root + client.serverID)
client.shutdown().catch(() => {})
log.info("removed dead LSP client", {
serverID: client.serverID,
root: client.root,
})
}

export async function touchFile(input: string, waitForDiagnostics?: boolean) {
log.info("touching file", { file: input })
const s = await state()
const clients = await getClients(input)
await Promise.all(
clients.map(async (client) => {
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
if (!client.alive) {
await removeClient(s, client)
return
}
try {
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
} catch (err: any) {
log.error("failed to touch file", { err, file: input })
if (
err?.message?.includes("Connection is closed") ||
err?.message?.includes("connection is disposed")
) {
await removeClient(s, client)
}
}
}),
).catch((err) => {
log.error("failed to touch file", { err, file: input })
})
)
}

export async function diagnostics() {
Expand Down
26 changes: 26 additions & 0 deletions packages/opencode/test/lsp/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,30 @@ describe("LSPClient interop", () => {

await client.shutdown()
})

test("alive becomes false when server process is killed", async () => {
const handle = spawnFakeServer() as any

const client = await Instance.provide({
directory: process.cwd(),
fn: () =>
LSPClient.create({
serverID: "fake",
server: handle as unknown as LSPServer.Handle,
root: process.cwd(),
}),
})

expect(client.alive).toBe(true)

// Kill the server process (simulates unexpected LSP death)
handle.process.kill()

// Wait for onClose to propagate
await new Promise((r) => setTimeout(r, 200))
expect(client.alive).toBe(false)

// Cleanup
client.shutdown().catch(() => {})
})
})
Loading