summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorFilip <[email protected]>2025-11-12 07:21:55 +0100
committerGitHub <[email protected]>2025-11-12 00:21:55 -0600
commitaa2e2c76c0f44c3676d1b3d3ce03716e82408391 (patch)
tree3d8a3303c60e5586b7299b4e1b1d76b732d9efb4
parent7c2d4ee79aed9d368f9684e4e9e88ffaf8de2d58 (diff)
downloadopencode-aa2e2c76c0f44c3676d1b3d3ce03716e82408391.tar.gz
opencode-aa2e2c76c0f44c3676d1b3d3ce03716e82408391.zip
fix: clangd hanging fixed (#3611)
Co-authored-by: Aiden Cline <[email protected]> Co-authored-by: GitHub Action <[email protected]>
-rw-r--r--packages/opencode/src/lsp/index.ts81
-rw-r--r--packages/opencode/src/lsp/server.ts162
-rw-r--r--packages/plugin/package.json2
-rw-r--r--packages/sdk/js/package.json2
4 files changed, 173 insertions, 74 deletions
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index 44cf263f0..6b5379742 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -103,6 +103,7 @@ export namespace LSP {
broken: new Set<string>(),
servers,
clients,
+ spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
}
},
async (state) => {
@@ -145,31 +146,21 @@ export namespace LSP {
const s = await state()
const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = []
- for (const server of Object.values(s.servers)) {
- if (server.extensions.length && !server.extensions.includes(extension)) continue
- const root = await server.root(file)
- if (!root) continue
- if (s.broken.has(root + server.id)) continue
- const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
- if (match) {
- result.push(match)
- continue
- }
+ async function schedule(server: LSPServer.Info, root: string, key: string) {
const handle = await server
.spawn(root)
- .then((h) => {
- if (h === undefined) {
- s.broken.add(root + server.id)
- }
- return h
+ .then((value) => {
+ if (!value) s.broken.add(key)
+ return value
})
.catch((err) => {
- s.broken.add(root + server.id)
+ s.broken.add(key)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
- if (!handle) continue
+
+ if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
@@ -177,18 +168,63 @@ export namespace LSP {
server: handle,
root,
}).catch((err) => {
- s.broken.add(root + server.id)
+ s.broken.add(key)
handle.process.kill()
- log.error(`Failed to initialize LSP client ${server.id}`, {
- error: err,
- })
+ log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})
- if (!client) continue
+
+ if (!client) {
+ handle.process.kill()
+ return undefined
+ }
+
+ const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
+ if (existing) {
+ handle.process.kill()
+ return existing
+ }
+
s.clients.push(client)
+ return client
+ }
+
+ for (const server of Object.values(s.servers)) {
+ if (server.extensions.length && !server.extensions.includes(extension)) continue
+ const root = await server.root(file)
+ if (!root) continue
+ if (s.broken.has(root + server.id)) continue
+
+ const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
+ if (match) {
+ result.push(match)
+ continue
+ }
+
+ const inflight = s.spawning.get(root + server.id)
+ if (inflight) {
+ const client = await inflight
+ if (!client) continue
+ result.push(client)
+ continue
+ }
+
+ const task = schedule(server, root, root + server.id)
+ s.spawning.set(root + server.id, task)
+
+ task.finally(() => {
+ if (s.spawning.get(root + server.id) === task) {
+ s.spawning.delete(root + server.id)
+ }
+ })
+
+ const client = await task
+ if (!client) continue
+
result.push(client)
Bus.publish(Event.Updated, {})
}
+
return result
}
@@ -199,6 +235,7 @@ export namespace LSP {
if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
+
return wait
}).catch((err) => {
log.error("failed to touch file", { err, file: input })
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index d9fff5b11..281d1a0ed 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -632,73 +632,135 @@ export namespace LSPServer {
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) {
- let bin = Bun.which("clangd", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
- })
- if (!bin) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading clangd from GitHub releases")
+ const args = ["--background-index", "--clang-tidy"]
+ const fromPath = Bun.which("clangd")
+ if (fromPath) {
+ return {
+ process: spawn(fromPath, args, {
+ cwd: root,
+ }),
+ }
+ }
- const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch clangd release info")
- return
+ const ext = process.platform === "win32" ? ".exe" : ""
+ const direct = path.join(Global.Path.bin, "clangd" + ext)
+ if (await Bun.file(direct).exists()) {
+ return {
+ process: spawn(direct, args, {
+ cwd: root,
+ }),
}
+ }
- const release = (await releaseResponse.json()) as any
+ const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue
+ if (!entry.name.startsWith("clangd_")) continue
+ const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
+ if (await Bun.file(candidate).exists()) {
+ return {
+ process: spawn(candidate, args, {
+ cwd: root,
+ }),
+ }
+ }
+ }
- const platform = process.platform
- let assetName = ""
+ if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
+ log.info("downloading clangd from GitHub releases")
- if (platform === "darwin") {
- assetName = "clangd-mac-"
- } else if (platform === "linux") {
- assetName = "clangd-linux-"
- } else if (platform === "win32") {
- assetName = "clangd-windows-"
- } else {
- log.error(`Platform ${platform} is not supported by clangd auto-download`)
- return
- }
+ const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
+ if (!releaseResponse.ok) {
+ log.error("Failed to fetch clangd release info")
+ return
+ }
- assetName += release.tag_name + ".zip"
+ const release: {
+ tag_name?: string
+ assets?: { name?: string; browser_download_url?: string }[]
+ } = await releaseResponse.json()
- const asset = release.assets.find((a: any) => a.name === assetName)
- if (!asset) {
- log.error(`Could not find asset ${assetName} in latest clangd release`)
- return
- }
+ const tag = release.tag_name
+ if (!tag) {
+ log.error("clangd release did not include a tag name")
+ return
+ }
+ const platform = process.platform
+ const tokens: Record<string, string> = {
+ darwin: "mac",
+ linux: "linux",
+ win32: "windows",
+ }
+ const token = tokens[platform]
+ if (!token) {
+ log.error(`Platform ${platform} is not supported by clangd auto-download`)
+ return
+ }
- const downloadUrl = asset.browser_download_url
- const downloadResponse = await fetch(downloadUrl)
- if (!downloadResponse.ok) {
- log.error("Failed to download clangd")
- return
- }
+ const assets = release.assets ?? []
+ const valid = (item: { name?: string; browser_download_url?: string }) => {
+ if (!item.name) return false
+ if (!item.browser_download_url) return false
+ if (!item.name.includes(token)) return false
+ return item.name.includes(tag)
+ }
- const zipPath = path.join(Global.Path.bin, "clangd.zip")
- await Bun.file(zipPath).write(downloadResponse)
+ const asset =
+ assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
+ assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
+ assets.find((item) => valid(item))
+ if (!asset?.name || !asset.browser_download_url) {
+ log.error("clangd could not match release asset", { tag, platform })
+ return
+ }
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
- await fs.rm(zipPath, { force: true })
+ const name = asset.name
+ const downloadResponse = await fetch(asset.browser_download_url)
+ if (!downloadResponse.ok) {
+ log.error("Failed to download clangd")
+ return
+ }
+
+ const archive = path.join(Global.Path.bin, name)
+ const buf = await downloadResponse.arrayBuffer()
+ if (buf.byteLength === 0) {
+ log.error("Failed to write clangd archive")
+ return
+ }
+ await Bun.write(archive, buf)
- const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
- bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
+ const zip = name.endsWith(".zip")
+ const tar = name.endsWith(".tar.xz")
+ if (!zip && !tar) {
+ log.error("clangd encountered unsupported asset", { asset: name })
+ return
+ }
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract clangd binary")
- return
- }
+ if (zip) {
+ await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
+ }
+ if (tar) {
+ await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
+ }
+ await fs.rm(archive, { force: true })
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.nothrow()
- }
+ const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
+ if (!(await Bun.file(bin).exists())) {
+ log.error("Failed to extract clangd binary")
+ return
+ }
- log.info(`installed clangd`, { bin })
+ if (platform !== "win32") {
+ await $`chmod +x ${bin}`.nothrow()
}
+ await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
+ await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
+
+ log.info(`installed clangd`, { bin })
+
return {
- process: spawn(bin, ["--background-index", "--clang-tidy"], {
+ process: spawn(bin, args, {
cwd: root,
}),
}
diff --git a/packages/plugin/package.json b/packages/plugin/package.json
index 56269395b..a9be085b9 100644
--- a/packages/plugin/package.json
+++ b/packages/plugin/package.json
@@ -24,4 +24,4 @@
"typescript": "catalog:",
"@typescript/native-preview": "catalog:"
}
-} \ No newline at end of file
+}
diff --git a/packages/sdk/js/package.json b/packages/sdk/js/package.json
index 8739e74e0..25de57553 100644
--- a/packages/sdk/js/package.json
+++ b/packages/sdk/js/package.json
@@ -26,4 +26,4 @@
"publishConfig": {
"directory": "dist"
}
-} \ No newline at end of file
+}