Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit aa2e2c7

Browse files
neriousyrekram1-nodeactions-user
authored
fix: clangd hanging fixed (anomalyco#3611)
Co-authored-by: Aiden Cline <aidenpcline@gmail.com> Co-authored-by: GitHub Action <action@github.com>
1 parent 7c2d4ee commit aa2e2c7

4 files changed

Lines changed: 173 additions & 74 deletions

File tree

packages/opencode/src/lsp/index.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export namespace LSP {
103103
broken: new Set<string>(),
104104
servers,
105105
clients,
106+
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
106107
}
107108
},
108109
async (state) => {
@@ -145,50 +146,85 @@ export namespace LSP {
145146
const s = await state()
146147
const extension = path.parse(file).ext || file
147148
const result: LSPClient.Info[] = []
148-
for (const server of Object.values(s.servers)) {
149-
if (server.extensions.length && !server.extensions.includes(extension)) continue
150-
const root = await server.root(file)
151-
if (!root) continue
152-
if (s.broken.has(root + server.id)) continue
153149

154-
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
155-
if (match) {
156-
result.push(match)
157-
continue
158-
}
150+
async function schedule(server: LSPServer.Info, root: string, key: string) {
159151
const handle = await server
160152
.spawn(root)
161-
.then((h) => {
162-
if (h === undefined) {
163-
s.broken.add(root + server.id)
164-
}
165-
return h
153+
.then((value) => {
154+
if (!value) s.broken.add(key)
155+
return value
166156
})
167157
.catch((err) => {
168-
s.broken.add(root + server.id)
158+
s.broken.add(key)
169159
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
170160
return undefined
171161
})
172-
if (!handle) continue
162+
163+
if (!handle) return undefined
173164
log.info("spawned lsp server", { serverID: server.id })
174165

175166
const client = await LSPClient.create({
176167
serverID: server.id,
177168
server: handle,
178169
root,
179170
}).catch((err) => {
180-
s.broken.add(root + server.id)
171+
s.broken.add(key)
181172
handle.process.kill()
182-
log.error(`Failed to initialize LSP client ${server.id}`, {
183-
error: err,
184-
})
173+
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
185174
return undefined
186175
})
187-
if (!client) continue
176+
177+
if (!client) {
178+
handle.process.kill()
179+
return undefined
180+
}
181+
182+
const existing = s.clients.find((x) => x.root === root && x.serverID === server.id)
183+
if (existing) {
184+
handle.process.kill()
185+
return existing
186+
}
187+
188188
s.clients.push(client)
189+
return client
190+
}
191+
192+
for (const server of Object.values(s.servers)) {
193+
if (server.extensions.length && !server.extensions.includes(extension)) continue
194+
const root = await server.root(file)
195+
if (!root) continue
196+
if (s.broken.has(root + server.id)) continue
197+
198+
const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
199+
if (match) {
200+
result.push(match)
201+
continue
202+
}
203+
204+
const inflight = s.spawning.get(root + server.id)
205+
if (inflight) {
206+
const client = await inflight
207+
if (!client) continue
208+
result.push(client)
209+
continue
210+
}
211+
212+
const task = schedule(server, root, root + server.id)
213+
s.spawning.set(root + server.id, task)
214+
215+
task.finally(() => {
216+
if (s.spawning.get(root + server.id) === task) {
217+
s.spawning.delete(root + server.id)
218+
}
219+
})
220+
221+
const client = await task
222+
if (!client) continue
223+
189224
result.push(client)
190225
Bus.publish(Event.Updated, {})
191226
}
227+
192228
return result
193229
}
194230

@@ -199,6 +235,7 @@ export namespace LSP {
199235
if (!clients.includes(client)) return
200236
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
201237
await client.notify.open({ path: input })
238+
202239
return wait
203240
}).catch((err) => {
204241
log.error("failed to touch file", { err, file: input })

packages/opencode/src/lsp/server.ts

Lines changed: 112 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -632,73 +632,135 @@ export namespace LSPServer {
632632
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
633633
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
634634
async spawn(root) {
635-
let bin = Bun.which("clangd", {
636-
PATH: process.env["PATH"] + ":" + Global.Path.bin,
637-
})
638-
if (!bin) {
639-
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
640-
log.info("downloading clangd from GitHub releases")
635+
const args = ["--background-index", "--clang-tidy"]
636+
const fromPath = Bun.which("clangd")
637+
if (fromPath) {
638+
return {
639+
process: spawn(fromPath, args, {
640+
cwd: root,
641+
}),
642+
}
643+
}
641644

642-
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
643-
if (!releaseResponse.ok) {
644-
log.error("Failed to fetch clangd release info")
645-
return
645+
const ext = process.platform === "win32" ? ".exe" : ""
646+
const direct = path.join(Global.Path.bin, "clangd" + ext)
647+
if (await Bun.file(direct).exists()) {
648+
return {
649+
process: spawn(direct, args, {
650+
cwd: root,
651+
}),
646652
}
653+
}
647654

648-
const release = (await releaseResponse.json()) as any
655+
const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
656+
for (const entry of entries) {
657+
if (!entry.isDirectory()) continue
658+
if (!entry.name.startsWith("clangd_")) continue
659+
const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
660+
if (await Bun.file(candidate).exists()) {
661+
return {
662+
process: spawn(candidate, args, {
663+
cwd: root,
664+
}),
665+
}
666+
}
667+
}
649668

650-
const platform = process.platform
651-
let assetName = ""
669+
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
670+
log.info("downloading clangd from GitHub releases")
652671

653-
if (platform === "darwin") {
654-
assetName = "clangd-mac-"
655-
} else if (platform === "linux") {
656-
assetName = "clangd-linux-"
657-
} else if (platform === "win32") {
658-
assetName = "clangd-windows-"
659-
} else {
660-
log.error(`Platform ${platform} is not supported by clangd auto-download`)
661-
return
662-
}
672+
const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
673+
if (!releaseResponse.ok) {
674+
log.error("Failed to fetch clangd release info")
675+
return
676+
}
663677

664-
assetName += release.tag_name + ".zip"
678+
const release: {
679+
tag_name?: string
680+
assets?: { name?: string; browser_download_url?: string }[]
681+
} = await releaseResponse.json()
665682

666-
const asset = release.assets.find((a: any) => a.name === assetName)
667-
if (!asset) {
668-
log.error(`Could not find asset ${assetName} in latest clangd release`)
669-
return
670-
}
683+
const tag = release.tag_name
684+
if (!tag) {
685+
log.error("clangd release did not include a tag name")
686+
return
687+
}
688+
const platform = process.platform
689+
const tokens: Record<string, string> = {
690+
darwin: "mac",
691+
linux: "linux",
692+
win32: "windows",
693+
}
694+
const token = tokens[platform]
695+
if (!token) {
696+
log.error(`Platform ${platform} is not supported by clangd auto-download`)
697+
return
698+
}
671699

672-
const downloadUrl = asset.browser_download_url
673-
const downloadResponse = await fetch(downloadUrl)
674-
if (!downloadResponse.ok) {
675-
log.error("Failed to download clangd")
676-
return
677-
}
700+
const assets = release.assets ?? []
701+
const valid = (item: { name?: string; browser_download_url?: string }) => {
702+
if (!item.name) return false
703+
if (!item.browser_download_url) return false
704+
if (!item.name.includes(token)) return false
705+
return item.name.includes(tag)
706+
}
678707

679-
const zipPath = path.join(Global.Path.bin, "clangd.zip")
680-
await Bun.file(zipPath).write(downloadResponse)
708+
const asset =
709+
assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
710+
assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
711+
assets.find((item) => valid(item))
712+
if (!asset?.name || !asset.browser_download_url) {
713+
log.error("clangd could not match release asset", { tag, platform })
714+
return
715+
}
681716

682-
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
683-
await fs.rm(zipPath, { force: true })
717+
const name = asset.name
718+
const downloadResponse = await fetch(asset.browser_download_url)
719+
if (!downloadResponse.ok) {
720+
log.error("Failed to download clangd")
721+
return
722+
}
723+
724+
const archive = path.join(Global.Path.bin, name)
725+
const buf = await downloadResponse.arrayBuffer()
726+
if (buf.byteLength === 0) {
727+
log.error("Failed to write clangd archive")
728+
return
729+
}
730+
await Bun.write(archive, buf)
684731

685-
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
686-
bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
732+
const zip = name.endsWith(".zip")
733+
const tar = name.endsWith(".tar.xz")
734+
if (!zip && !tar) {
735+
log.error("clangd encountered unsupported asset", { asset: name })
736+
return
737+
}
687738

688-
if (!(await Bun.file(bin).exists())) {
689-
log.error("Failed to extract clangd binary")
690-
return
691-
}
739+
if (zip) {
740+
await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
741+
}
742+
if (tar) {
743+
await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
744+
}
745+
await fs.rm(archive, { force: true })
692746

693-
if (platform !== "win32") {
694-
await $`chmod +x ${bin}`.nothrow()
695-
}
747+
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
748+
if (!(await Bun.file(bin).exists())) {
749+
log.error("Failed to extract clangd binary")
750+
return
751+
}
696752

697-
log.info(`installed clangd`, { bin })
753+
if (platform !== "win32") {
754+
await $`chmod +x ${bin}`.nothrow()
698755
}
699756

757+
await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
758+
await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
759+
760+
log.info(`installed clangd`, { bin })
761+
700762
return {
701-
process: spawn(bin, ["--background-index", "--clang-tidy"], {
763+
process: spawn(bin, args, {
702764
cwd: root,
703765
}),
704766
}

packages/plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@
2424
"typescript": "catalog:",
2525
"@typescript/native-preview": "catalog:"
2626
}
27-
}
27+
}

packages/sdk/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626
"publishConfig": {
2727
"directory": "dist"
2828
}
29-
}
29+
}

0 commit comments

Comments
 (0)