mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-20 01:04:22 +01:00
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:
@@ -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 })
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user