Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 45 additions & 26 deletions packages/opencode/src/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export namespace LSP {
broken: new Set<string>(),
servers,
clients,
spawning: new Map<string, Promise<LSPClient.Info | undefined>>(),
}
},
async (state) => {
Expand All @@ -124,35 +125,53 @@ export namespace LSP {
result.push(match)
continue
}
const handle = await server
.spawn(root)
.then((h) => {
if (h === undefined) {

const spawnKey = root + server.id
let spawnPromise = s.spawning.get(spawnKey)

if (!spawnPromise) {
spawnPromise = (async () => {
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) 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(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
handle.process.kill()
log.error(`Failed to initialize LSP client ${server.id}`, { error: err })
return undefined
})
if (!client) return undefined
s.clients.push(client)
return client
})()

s.spawning.set(spawnKey, spawnPromise)
spawnPromise.finally(() => {
s.spawning.delete(spawnKey)
})
if (!handle) continue
log.info("spawned lsp server", { serverID: server.id })
}

const client = await LSPClient.create({
serverID: server.id,
server: handle,
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
s.clients.push(client)
result.push(client)
const client = await spawnPromise
if (client) {
result.push(client)
}
}
return result
}
Expand Down
174 changes: 123 additions & 51 deletions packages/opencode/src/lsp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,75 +645,147 @@ export namespace LSPServer {
]),
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",
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 (!releaseResponse.ok) {
log.error("Failed to fetch clangd release info")
return
if (await Bun.file(candidate).exists()) {
return {
process: spawn(candidate, args, {
cwd: root,
}),
}
}
}

const release = await releaseResponse.json()
if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
log.info("downloading clangd from GitHub releases")

const platform = process.platform
let assetName = ""
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
}

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 release: {
tag_name?: string
assets?: { name?: string; browser_download_url?: string }[]
} = await releaseResponse.json()

assetName += release.tag_name + ".zip"
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 asset = release.assets.find((a: any) => a.name === assetName)
if (!asset) {
log.error(`Could not find asset ${assetName} in latest clangd release`)
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 downloadUrl = asset.browser_download_url
const downloadResponse = await fetch(downloadUrl)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}
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
}

const zipPath = path.join(Global.Path.bin, "clangd.zip")
await Bun.file(zipPath).write(downloadResponse)
const name = asset.name
const downloadResponse = await fetch(asset.browser_download_url)
if (!downloadResponse.ok) {
log.error("Failed to download clangd")
return
}

await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
await fs.rm(zipPath, { force: true })
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,
}),
}
Expand Down