diff options
| author | Filip <[email protected]> | 2025-11-12 07:21:55 +0100 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-11-12 00:21:55 -0600 |
| commit | aa2e2c76c0f44c3676d1b3d3ce03716e82408391 (patch) | |
| tree | 3d8a3303c60e5586b7299b4e1b1d76b732d9efb4 | |
| parent | 7c2d4ee79aed9d368f9684e4e9e88ffaf8de2d58 (diff) | |
| download | opencode-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.ts | 81 | ||||
| -rw-r--r-- | packages/opencode/src/lsp/server.ts | 162 | ||||
| -rw-r--r-- | packages/plugin/package.json | 2 | ||||
| -rw-r--r-- | packages/sdk/js/package.json | 2 |
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 +} |
