Просмотр исходного кода

fix: clangd hanging fixed (#3611)

Co-authored-by: Aiden Cline <[email protected]>
Co-authored-by: GitHub Action <[email protected]>
Filip 3 месяцев назад
Родитель
Сommit
aa2e2c76c0

+ 59 - 22
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 })

+ 112 - 50
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,
         }),
       }

+ 1 - 1
packages/plugin/package.json

@@ -24,4 +24,4 @@
     "typescript": "catalog:",
     "@typescript/native-preview": "catalog:"
   }
-}
+}

+ 1 - 1
packages/sdk/js/package.json

@@ -26,4 +26,4 @@
   "publishConfig": {
     "directory": "dist"
   }
-}
+}