fix: clangd hanging fixed (#3611)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Filip
2025-11-12 07:21:55 +01:00
committed by GitHub
parent 7c2d4ee79a
commit aa2e2c76c0
4 changed files with 191 additions and 92 deletions

View File

@@ -103,6 +103,7 @@ export namespace LSP {
broken: new Set<string>(), broken: new Set<string>(),
servers, servers,
clients, clients,
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
} }
}, },
async (state) => { async (state) => {
@@ -145,6 +146,49 @@ export namespace LSP {
const s = await state() const s = await state()
const extension = path.parse(file).ext || file const extension = path.parse(file).ext || file
const result: LSPClient.Info[] = [] const result: LSPClient.Info[] = []
async function schedule(server: LSPServer.Info, root: string, key: string) {
const handle = await server
.spawn(root)
.then((value) => {
if (!value) s.broken.add(key)
return value
})
.catch((err) => {
s.broken.add(key)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) return undefined
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({
serverID: server.id,
server: handle,
root,
}).catch((err) => {
s.broken.add(key)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})
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)) { for (const server of Object.values(s.servers)) {
if (server.extensions.length && !server.extensions.includes(extension)) continue if (server.extensions.length && !server.extensions.includes(extension)) continue
const root = await server.root(file) const root = await server.root(file)
@@ -156,39 +200,31 @@ export namespace LSP {
result.push(match) result.push(match)
continue continue
} }
const handle = await server
.spawn(root)
.then((h) => {
if (h === undefined) {
s.broken.add(root + server.id)
}
return h
})
.catch((err) => {
s.broken.add(root + server.id)
log.error(`Failed to spawn LSP server ${server.id}`, { error: err })
return undefined
})
if (!handle) continue
log.info("spawned lsp server", { serverID: server.id })
const client = await LSPClient.create({ const inflight = s.spawning.get(root + server.id)
serverID: server.id, if (inflight) {
server: handle, const client = await inflight
root,
}).catch((err) => {
s.broken.add(root + server.id)
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, {
error: err,
})
return undefined
})
if (!client) continue if (!client) continue
s.clients.push(client) 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) result.push(client)
Bus.publish(Event.Updated, {}) Bus.publish(Event.Updated, {})
} }
return result return result
} }
@@ -199,6 +235,7 @@ export namespace LSP {
if (!clients.includes(client)) return if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve() const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input }) await client.notify.open({ path: input })
return wait return wait
}).catch((err) => { }).catch((err) => {
log.error("failed to touch file", { err, file: input }) log.error("failed to touch file", { err, file: input })

View File

@@ -632,10 +632,40 @@ export namespace LSPServer {
root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]), root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"], extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
async spawn(root) { async spawn(root) {
let bin = Bun.which("clangd", { const args = ["--background-index", "--clang-tidy"]
PATH: process.env["PATH"] + ":" + Global.Path.bin, const fromPath = Bun.which("clangd")
}) if (fromPath) {
if (!bin) { return {
process: spawn(fromPath, args, {
cwd: root,
}),
}
}
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 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,
}),
}
}
}
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases") log.info("downloading clangd from GitHub releases")
@@ -645,46 +675,76 @@ export namespace LSPServer {
return return
} }
const release = (await releaseResponse.json()) as any const release: {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
} = await releaseResponse.json()
const tag = release.tag_name
if (!tag) {
log.error("clangd release did not include a tag name")
return
}
const platform = process.platform const platform = process.platform
let assetName = "" const tokens: Record<string, string> = {
darwin: "mac",
if (platform === "darwin") { linux: "linux",
assetName = "clangd-mac-" win32: "windows",
} else if (platform === "linux") { }
assetName = "clangd-linux-" const token = tokens[platform]
} else if (platform === "win32") { if (!token) {
assetName = "clangd-windows-"
} else {
log.error(`Platform ${platform} is not supported by clangd auto-download`) log.error(`Platform ${platform} is not supported by clangd auto-download`)
return return
} }
assetName += release.tag_name + ".zip" 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 asset = release.assets.find((a: any) => a.name === assetName) const asset =
if (!asset) { assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
log.error(`Could not find asset ${assetName} in latest clangd release`) 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 return
} }
const downloadUrl = asset.browser_download_url const name = asset.name
const downloadResponse = await fetch(downloadUrl) const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) { if (!downloadResponse.ok) {
log.error("Failed to download clangd") log.error("Failed to download clangd")
return return
} }
const zipPath = path.join(Global.Path.bin, "clangd.zip") const archive = path.join(Global.Path.bin, name)
await Bun.file(zipPath).write(downloadResponse) const buf = await downloadResponse.arrayBuffer()
if (buf.byteLength === 0) {
log.error("Failed to write clangd archive")
return
}
await Bun.write(archive, buf)
await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow() const zip = name.endsWith(".zip")
await fs.rm(zipPath, { force: true }) const tar = name.endsWith(".tar.xz")
if (!zip && !tar) {
log.error("clangd encountered unsupported asset", { asset: name })
return
}
const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", "")) if (zip) {
bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : "")) 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 })
const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
if (!(await Bun.file(bin).exists())) { if (!(await Bun.file(bin).exists())) {
log.error("Failed to extract clangd binary") log.error("Failed to extract clangd binary")
return return
@@ -694,11 +754,13 @@ export namespace LSPServer {
await $`chmod +x ${bin}`.nothrow() 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 }) log.info(`installed clangd`, { bin })
}
return { return {
process: spawn(bin, ["--background-index", "--clang-tidy"], { process: spawn(bin, args, {
cwd: root, cwd: root,
}), }),
} }