server.ts 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187
  1. import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
  2. import path from "path"
  3. import os from "os"
  4. import { Global } from "../global"
  5. import { Log } from "../util/log"
  6. import { BunProc } from "../bun"
  7. import { $ } from "bun"
  8. import fs from "fs/promises"
  9. import { Filesystem } from "../util/filesystem"
  10. import { Instance } from "../project/instance"
  11. import { Flag } from "../flag/flag"
  12. export namespace LSPServer {
  13. const log = Log.create({ service: "lsp.server" })
  14. export interface Handle {
  15. process: ChildProcessWithoutNullStreams
  16. initialization?: Record<string, any>
  17. }
  18. type RootFunction = (file: string) => Promise<string | undefined>
  19. const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
  20. return async (file) => {
  21. if (excludePatterns) {
  22. const excludedFiles = Filesystem.up({
  23. targets: excludePatterns,
  24. start: path.dirname(file),
  25. stop: Instance.directory,
  26. })
  27. const excluded = await excludedFiles.next()
  28. await excludedFiles.return()
  29. if (excluded.value) return undefined
  30. }
  31. const files = Filesystem.up({
  32. targets: includePatterns,
  33. start: path.dirname(file),
  34. stop: Instance.directory,
  35. })
  36. const first = await files.next()
  37. await files.return()
  38. if (!first.value) return Instance.directory
  39. return path.dirname(first.value)
  40. }
  41. }
  42. export interface Info {
  43. id: string
  44. extensions: string[]
  45. global?: boolean
  46. root: RootFunction
  47. spawn(root: string): Promise<Handle | undefined>
  48. }
  49. export const Deno: Info = {
  50. id: "deno",
  51. root: async (file) => {
  52. const files = Filesystem.up({
  53. targets: ["deno.json", "deno.jsonc"],
  54. start: path.dirname(file),
  55. stop: Instance.directory,
  56. })
  57. const first = await files.next()
  58. await files.return()
  59. if (!first.value) return undefined
  60. return path.dirname(first.value)
  61. },
  62. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
  63. async spawn(root) {
  64. const deno = Bun.which("deno")
  65. if (!deno) {
  66. log.info("deno not found, please install deno first")
  67. return
  68. }
  69. return {
  70. process: spawn(deno, ["lsp"], {
  71. cwd: root,
  72. }),
  73. }
  74. },
  75. }
  76. export const Typescript: Info = {
  77. id: "typescript",
  78. root: NearestRoot(
  79. ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
  80. ["deno.json", "deno.jsonc"],
  81. ),
  82. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
  83. async spawn(root) {
  84. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  85. log.info("typescript server", { tsserver })
  86. if (!tsserver) return
  87. const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
  88. cwd: root,
  89. env: {
  90. ...process.env,
  91. BUN_BE_BUN: "1",
  92. },
  93. })
  94. return {
  95. process: proc,
  96. initialization: {
  97. tsserver: {
  98. path: tsserver,
  99. },
  100. },
  101. }
  102. },
  103. }
  104. export const Vue: Info = {
  105. id: "vue",
  106. extensions: [".vue"],
  107. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  108. async spawn(root) {
  109. let binary = Bun.which("vue-language-server")
  110. const args: string[] = []
  111. if (!binary) {
  112. const js = path.join(
  113. Global.Path.bin,
  114. "node_modules",
  115. "@vue",
  116. "language-server",
  117. "bin",
  118. "vue-language-server.js",
  119. )
  120. if (!(await Bun.file(js).exists())) {
  121. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  122. await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
  123. cwd: Global.Path.bin,
  124. env: {
  125. ...process.env,
  126. BUN_BE_BUN: "1",
  127. },
  128. stdout: "pipe",
  129. stderr: "pipe",
  130. stdin: "pipe",
  131. }).exited
  132. }
  133. binary = BunProc.which()
  134. args.push("run", js)
  135. }
  136. args.push("--stdio")
  137. const proc = spawn(binary, args, {
  138. cwd: root,
  139. env: {
  140. ...process.env,
  141. BUN_BE_BUN: "1",
  142. },
  143. })
  144. return {
  145. process: proc,
  146. initialization: {
  147. // Leave empty; the server will auto-detect workspace TypeScript.
  148. },
  149. }
  150. },
  151. }
  152. export const ESLint: Info = {
  153. id: "eslint",
  154. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  155. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
  156. async spawn(root) {
  157. const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
  158. if (!eslint) return
  159. log.info("spawning eslint server")
  160. const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
  161. if (!(await Bun.file(serverPath).exists())) {
  162. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  163. log.info("downloading and building VS Code ESLint server")
  164. const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
  165. if (!response.ok) return
  166. const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
  167. await Bun.file(zipPath).write(response)
  168. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  169. await fs.rm(zipPath, { force: true })
  170. const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
  171. const finalPath = path.join(Global.Path.bin, "vscode-eslint")
  172. const stats = await fs.stat(finalPath).catch(() => undefined)
  173. if (stats) {
  174. log.info("removing old eslint installation", { path: finalPath })
  175. await fs.rm(finalPath, { force: true, recursive: true })
  176. }
  177. await fs.rename(extractedPath, finalPath)
  178. await $`npm install`.cwd(finalPath).quiet()
  179. await $`npm run compile`.cwd(finalPath).quiet()
  180. log.info("installed VS Code ESLint server", { serverPath })
  181. }
  182. const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
  183. cwd: root,
  184. env: {
  185. ...process.env,
  186. BUN_BE_BUN: "1",
  187. },
  188. })
  189. return {
  190. process: proc,
  191. }
  192. },
  193. }
  194. export const Gopls: Info = {
  195. id: "gopls",
  196. root: async (file) => {
  197. const work = await NearestRoot(["go.work"])(file)
  198. if (work) return work
  199. return NearestRoot(["go.mod", "go.sum"])(file)
  200. },
  201. extensions: [".go"],
  202. async spawn(root) {
  203. let bin = Bun.which("gopls", {
  204. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  205. })
  206. if (!bin) {
  207. if (!Bun.which("go")) return
  208. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  209. log.info("installing gopls")
  210. const proc = Bun.spawn({
  211. cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
  212. env: { ...process.env, GOBIN: Global.Path.bin },
  213. stdout: "pipe",
  214. stderr: "pipe",
  215. stdin: "pipe",
  216. })
  217. const exit = await proc.exited
  218. if (exit !== 0) {
  219. log.error("Failed to install gopls")
  220. return
  221. }
  222. bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
  223. log.info(`installed gopls`, {
  224. bin,
  225. })
  226. }
  227. return {
  228. process: spawn(bin!, {
  229. cwd: root,
  230. }),
  231. }
  232. },
  233. }
  234. export const Rubocop: Info = {
  235. id: "ruby-lsp",
  236. root: NearestRoot(["Gemfile"]),
  237. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  238. async spawn(root) {
  239. let bin = Bun.which("rubocop", {
  240. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  241. })
  242. if (!bin) {
  243. const ruby = Bun.which("ruby")
  244. const gem = Bun.which("gem")
  245. if (!ruby || !gem) {
  246. log.info("Ruby not found, please install Ruby first")
  247. return
  248. }
  249. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  250. log.info("installing rubocop")
  251. const proc = Bun.spawn({
  252. cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
  253. stdout: "pipe",
  254. stderr: "pipe",
  255. stdin: "pipe",
  256. })
  257. const exit = await proc.exited
  258. if (exit !== 0) {
  259. log.error("Failed to install rubocop")
  260. return
  261. }
  262. bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
  263. log.info(`installed rubocop`, {
  264. bin,
  265. })
  266. }
  267. return {
  268. process: spawn(bin!, ["--lsp"], {
  269. cwd: root,
  270. }),
  271. }
  272. },
  273. }
  274. export const Pyright: Info = {
  275. id: "pyright",
  276. extensions: [".py", ".pyi"],
  277. root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
  278. async spawn(root) {
  279. let binary = Bun.which("pyright-langserver")
  280. const args = []
  281. if (!binary) {
  282. const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
  283. if (!(await Bun.file(js).exists())) {
  284. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  285. await Bun.spawn([BunProc.which(), "install", "pyright"], {
  286. cwd: Global.Path.bin,
  287. env: {
  288. ...process.env,
  289. BUN_BE_BUN: "1",
  290. },
  291. }).exited
  292. }
  293. binary = BunProc.which()
  294. args.push(...["run", js])
  295. }
  296. args.push("--stdio")
  297. const initialization: Record<string, string> = {}
  298. const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
  299. (p): p is string => p !== undefined,
  300. )
  301. for (const venvPath of potentialVenvPaths) {
  302. const isWindows = process.platform === "win32"
  303. const potentialPythonPath = isWindows
  304. ? path.join(venvPath, "Scripts", "python.exe")
  305. : path.join(venvPath, "bin", "python")
  306. if (await Bun.file(potentialPythonPath).exists()) {
  307. initialization["pythonPath"] = potentialPythonPath
  308. break
  309. }
  310. }
  311. const proc = spawn(binary, args, {
  312. cwd: root,
  313. env: {
  314. ...process.env,
  315. BUN_BE_BUN: "1",
  316. },
  317. })
  318. return {
  319. process: proc,
  320. initialization,
  321. }
  322. },
  323. }
  324. export const ElixirLS: Info = {
  325. id: "elixir-ls",
  326. extensions: [".ex", ".exs"],
  327. root: NearestRoot(["mix.exs", "mix.lock"]),
  328. async spawn(root) {
  329. let binary = Bun.which("elixir-ls")
  330. if (!binary) {
  331. const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
  332. binary = path.join(
  333. Global.Path.bin,
  334. "elixir-ls-master",
  335. "release",
  336. process.platform === "win32" ? "language_server.bar" : "language_server.sh",
  337. )
  338. if (!(await Bun.file(binary).exists())) {
  339. const elixir = Bun.which("elixir")
  340. if (!elixir) {
  341. log.error("elixir is required to run elixir-ls")
  342. return
  343. }
  344. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  345. log.info("downloading elixir-ls from GitHub releases")
  346. const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
  347. if (!response.ok) return
  348. const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
  349. await Bun.file(zipPath).write(response)
  350. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  351. await fs.rm(zipPath, {
  352. force: true,
  353. recursive: true,
  354. })
  355. await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
  356. .quiet()
  357. .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
  358. .env({ MIX_ENV: "prod", ...process.env })
  359. log.info(`installed elixir-ls`, {
  360. path: elixirLsPath,
  361. })
  362. }
  363. }
  364. return {
  365. process: spawn(binary, {
  366. cwd: root,
  367. }),
  368. }
  369. },
  370. }
  371. export const Zls: Info = {
  372. id: "zls",
  373. extensions: [".zig", ".zon"],
  374. root: NearestRoot(["build.zig"]),
  375. async spawn(root) {
  376. let bin = Bun.which("zls", {
  377. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  378. })
  379. if (!bin) {
  380. const zig = Bun.which("zig")
  381. if (!zig) {
  382. log.error("Zig is required to use zls. Please install Zig first.")
  383. return
  384. }
  385. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  386. log.info("downloading zls from GitHub releases")
  387. const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
  388. if (!releaseResponse.ok) {
  389. log.error("Failed to fetch zls release info")
  390. return
  391. }
  392. const release = (await releaseResponse.json()) as any
  393. const platform = process.platform
  394. const arch = process.arch
  395. let assetName = ""
  396. let zlsArch: string = arch
  397. if (arch === "arm64") zlsArch = "aarch64"
  398. else if (arch === "x64") zlsArch = "x86_64"
  399. else if (arch === "ia32") zlsArch = "x86"
  400. let zlsPlatform: string = platform
  401. if (platform === "darwin") zlsPlatform = "macos"
  402. else if (platform === "win32") zlsPlatform = "windows"
  403. const ext = platform === "win32" ? "zip" : "tar.xz"
  404. assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
  405. const supportedCombos = [
  406. "zls-x86_64-linux.tar.xz",
  407. "zls-x86_64-macos.tar.xz",
  408. "zls-x86_64-windows.zip",
  409. "zls-aarch64-linux.tar.xz",
  410. "zls-aarch64-macos.tar.xz",
  411. "zls-aarch64-windows.zip",
  412. "zls-x86-linux.tar.xz",
  413. "zls-x86-windows.zip",
  414. ]
  415. if (!supportedCombos.includes(assetName)) {
  416. log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
  417. return
  418. }
  419. const asset = release.assets.find((a: any) => a.name === assetName)
  420. if (!asset) {
  421. log.error(`Could not find asset ${assetName} in latest zls release`)
  422. return
  423. }
  424. const downloadUrl = asset.browser_download_url
  425. const downloadResponse = await fetch(downloadUrl)
  426. if (!downloadResponse.ok) {
  427. log.error("Failed to download zls")
  428. return
  429. }
  430. const tempPath = path.join(Global.Path.bin, assetName)
  431. await Bun.file(tempPath).write(downloadResponse)
  432. if (ext === "zip") {
  433. await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
  434. } else {
  435. await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
  436. }
  437. await fs.rm(tempPath, { force: true })
  438. bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
  439. if (!(await Bun.file(bin).exists())) {
  440. log.error("Failed to extract zls binary")
  441. return
  442. }
  443. if (platform !== "win32") {
  444. await $`chmod +x ${bin}`.nothrow()
  445. }
  446. log.info(`installed zls`, { bin })
  447. }
  448. return {
  449. process: spawn(bin, {
  450. cwd: root,
  451. }),
  452. }
  453. },
  454. }
  455. export const CSharp: Info = {
  456. id: "csharp",
  457. root: NearestRoot([".sln", ".csproj", "global.json"]),
  458. extensions: [".cs"],
  459. async spawn(root) {
  460. let bin = Bun.which("csharp-ls", {
  461. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  462. })
  463. if (!bin) {
  464. if (!Bun.which("dotnet")) {
  465. log.error(".NET SDK is required to install csharp-ls")
  466. return
  467. }
  468. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  469. log.info("installing csharp-ls via dotnet tool")
  470. const proc = Bun.spawn({
  471. cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
  472. stdout: "pipe",
  473. stderr: "pipe",
  474. stdin: "pipe",
  475. })
  476. const exit = await proc.exited
  477. if (exit !== 0) {
  478. log.error("Failed to install csharp-ls")
  479. return
  480. }
  481. bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
  482. log.info(`installed csharp-ls`, { bin })
  483. }
  484. return {
  485. process: spawn(bin, {
  486. cwd: root,
  487. }),
  488. }
  489. },
  490. }
  491. export const SourceKit: Info = {
  492. id: "sourcekit-lsp",
  493. extensions: [".swift", ".objc", "objcpp"],
  494. root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
  495. async spawn(root) {
  496. // Check if sourcekit-lsp is available in the PATH
  497. // This is installed with the Swift toolchain
  498. const sourcekit = Bun.which("sourcekit-lsp")
  499. if (sourcekit) {
  500. return {
  501. process: spawn(sourcekit, {
  502. cwd: root,
  503. }),
  504. }
  505. }
  506. // If sourcekit-lsp not found, check if xcrun is available
  507. // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
  508. if (!Bun.which("xcrun")) return
  509. const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
  510. if (lspLoc.exitCode !== 0) return
  511. const bin = lspLoc.text().trim()
  512. return {
  513. process: spawn(bin, {
  514. cwd: root,
  515. }),
  516. }
  517. },
  518. }
  519. export const RustAnalyzer: Info = {
  520. id: "rust",
  521. root: async (root) => {
  522. const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
  523. if (crateRoot === undefined) {
  524. return undefined
  525. }
  526. let currentDir = crateRoot
  527. while (currentDir !== path.dirname(currentDir)) {
  528. // Stop at filesystem root
  529. const cargoTomlPath = path.join(currentDir, "Cargo.toml")
  530. try {
  531. const cargoTomlContent = await Bun.file(cargoTomlPath).text()
  532. if (cargoTomlContent.includes("[workspace]")) {
  533. return currentDir
  534. }
  535. } catch (err) {
  536. // File doesn't exist or can't be read, continue searching up
  537. }
  538. const parentDir = path.dirname(currentDir)
  539. if (parentDir === currentDir) break // Reached filesystem root
  540. currentDir = parentDir
  541. // Stop if we've gone above the app root
  542. if (!currentDir.startsWith(Instance.worktree)) break
  543. }
  544. return crateRoot
  545. },
  546. extensions: [".rs"],
  547. async spawn(root) {
  548. const bin = Bun.which("rust-analyzer")
  549. if (!bin) {
  550. log.info("rust-analyzer not found in path, please install it")
  551. return
  552. }
  553. return {
  554. process: spawn(bin, {
  555. cwd: root,
  556. }),
  557. }
  558. },
  559. }
  560. export const Clangd: Info = {
  561. id: "clangd",
  562. root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
  563. extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
  564. async spawn(root) {
  565. const args = ["--background-index", "--clang-tidy"]
  566. const fromPath = Bun.which("clangd")
  567. if (fromPath) {
  568. return {
  569. process: spawn(fromPath, args, {
  570. cwd: root,
  571. }),
  572. }
  573. }
  574. const ext = process.platform === "win32" ? ".exe" : ""
  575. const direct = path.join(Global.Path.bin, "clangd" + ext)
  576. if (await Bun.file(direct).exists()) {
  577. return {
  578. process: spawn(direct, args, {
  579. cwd: root,
  580. }),
  581. }
  582. }
  583. const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
  584. for (const entry of entries) {
  585. if (!entry.isDirectory()) continue
  586. if (!entry.name.startsWith("clangd_")) continue
  587. const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
  588. if (await Bun.file(candidate).exists()) {
  589. return {
  590. process: spawn(candidate, args, {
  591. cwd: root,
  592. }),
  593. }
  594. }
  595. }
  596. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  597. log.info("downloading clangd from GitHub releases")
  598. const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
  599. if (!releaseResponse.ok) {
  600. log.error("Failed to fetch clangd release info")
  601. return
  602. }
  603. const release: {
  604. tag_name?: string
  605. assets?: { name?: string; browser_download_url?: string }[]
  606. } = await releaseResponse.json()
  607. const tag = release.tag_name
  608. if (!tag) {
  609. log.error("clangd release did not include a tag name")
  610. return
  611. }
  612. const platform = process.platform
  613. const tokens: Record<string, string> = {
  614. darwin: "mac",
  615. linux: "linux",
  616. win32: "windows",
  617. }
  618. const token = tokens[platform]
  619. if (!token) {
  620. log.error(`Platform ${platform} is not supported by clangd auto-download`)
  621. return
  622. }
  623. const assets = release.assets ?? []
  624. const valid = (item: { name?: string; browser_download_url?: string }) => {
  625. if (!item.name) return false
  626. if (!item.browser_download_url) return false
  627. if (!item.name.includes(token)) return false
  628. return item.name.includes(tag)
  629. }
  630. const asset =
  631. assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
  632. assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
  633. assets.find((item) => valid(item))
  634. if (!asset?.name || !asset.browser_download_url) {
  635. log.error("clangd could not match release asset", { tag, platform })
  636. return
  637. }
  638. const name = asset.name
  639. const downloadResponse = await fetch(asset.browser_download_url)
  640. if (!downloadResponse.ok) {
  641. log.error("Failed to download clangd")
  642. return
  643. }
  644. const archive = path.join(Global.Path.bin, name)
  645. const buf = await downloadResponse.arrayBuffer()
  646. if (buf.byteLength === 0) {
  647. log.error("Failed to write clangd archive")
  648. return
  649. }
  650. await Bun.write(archive, buf)
  651. const zip = name.endsWith(".zip")
  652. const tar = name.endsWith(".tar.xz")
  653. if (!zip && !tar) {
  654. log.error("clangd encountered unsupported asset", { asset: name })
  655. return
  656. }
  657. if (zip) {
  658. await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
  659. }
  660. if (tar) {
  661. await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
  662. }
  663. await fs.rm(archive, { force: true })
  664. const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
  665. if (!(await Bun.file(bin).exists())) {
  666. log.error("Failed to extract clangd binary")
  667. return
  668. }
  669. if (platform !== "win32") {
  670. await $`chmod +x ${bin}`.nothrow()
  671. }
  672. await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
  673. await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
  674. log.info(`installed clangd`, { bin })
  675. return {
  676. process: spawn(bin, args, {
  677. cwd: root,
  678. }),
  679. }
  680. },
  681. }
  682. export const Svelte: Info = {
  683. id: "svelte",
  684. extensions: [".svelte"],
  685. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  686. async spawn(root) {
  687. let binary = Bun.which("svelteserver")
  688. const args: string[] = []
  689. if (!binary) {
  690. const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
  691. if (!(await Bun.file(js).exists())) {
  692. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  693. await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
  694. cwd: Global.Path.bin,
  695. env: {
  696. ...process.env,
  697. BUN_BE_BUN: "1",
  698. },
  699. stdout: "pipe",
  700. stderr: "pipe",
  701. stdin: "pipe",
  702. }).exited
  703. }
  704. binary = BunProc.which()
  705. args.push("run", js)
  706. }
  707. args.push("--stdio")
  708. const proc = spawn(binary, args, {
  709. cwd: root,
  710. env: {
  711. ...process.env,
  712. BUN_BE_BUN: "1",
  713. },
  714. })
  715. return {
  716. process: proc,
  717. initialization: {},
  718. }
  719. },
  720. }
  721. export const Astro: Info = {
  722. id: "astro",
  723. extensions: [".astro"],
  724. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  725. async spawn(root) {
  726. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  727. if (!tsserver) {
  728. log.info("typescript not found, required for Astro language server")
  729. return
  730. }
  731. const tsdk = path.dirname(tsserver)
  732. let binary = Bun.which("astro-ls")
  733. const args: string[] = []
  734. if (!binary) {
  735. const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
  736. if (!(await Bun.file(js).exists())) {
  737. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  738. await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
  739. cwd: Global.Path.bin,
  740. env: {
  741. ...process.env,
  742. BUN_BE_BUN: "1",
  743. },
  744. stdout: "pipe",
  745. stderr: "pipe",
  746. stdin: "pipe",
  747. }).exited
  748. }
  749. binary = BunProc.which()
  750. args.push("run", js)
  751. }
  752. args.push("--stdio")
  753. const proc = spawn(binary, args, {
  754. cwd: root,
  755. env: {
  756. ...process.env,
  757. BUN_BE_BUN: "1",
  758. },
  759. })
  760. return {
  761. process: proc,
  762. initialization: {
  763. typescript: {
  764. tsdk,
  765. },
  766. },
  767. }
  768. },
  769. }
  770. export const JDTLS: Info = {
  771. id: "jdtls",
  772. root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
  773. extensions: [".java"],
  774. async spawn(root) {
  775. const java = Bun.which("java")
  776. if (!java) {
  777. log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
  778. return
  779. }
  780. const javaMajorVersion = await $`java -version`
  781. .quiet()
  782. .nothrow()
  783. .then(({ stderr }) => {
  784. const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
  785. return !m ? undefined : parseInt(m[1])
  786. })
  787. if (javaMajorVersion == null || javaMajorVersion < 21) {
  788. log.error("JDTLS requires at least Java 21.")
  789. return
  790. }
  791. const distPath = path.join(Global.Path.bin, "jdtls")
  792. const launcherDir = path.join(distPath, "plugins")
  793. const installed = await fs.exists(launcherDir)
  794. if (!installed) {
  795. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  796. log.info("Downloading JDTLS LSP server.")
  797. await fs.mkdir(distPath, { recursive: true })
  798. const releaseURL =
  799. "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
  800. const archivePath = path.join(distPath, "release.tar.gz")
  801. await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
  802. await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
  803. await fs.rm(archivePath, { force: true })
  804. }
  805. const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
  806. .cwd(launcherDir)
  807. .quiet()
  808. .nothrow()
  809. .then(({ stdout }) => stdout.toString().trim())
  810. const launcherJar = path.join(launcherDir, jarFileName)
  811. if (!(await fs.exists(launcherJar))) {
  812. log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
  813. return
  814. }
  815. const configFile = path.join(
  816. distPath,
  817. (() => {
  818. switch (process.platform) {
  819. case "darwin":
  820. return "config_mac"
  821. case "linux":
  822. return "config_linux"
  823. case "win32":
  824. return "config_windows"
  825. default:
  826. return "config_linux"
  827. }
  828. })(),
  829. )
  830. const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
  831. return {
  832. process: spawn(
  833. java,
  834. [
  835. "-jar",
  836. launcherJar,
  837. "-configuration",
  838. configFile,
  839. "-data",
  840. dataDir,
  841. "-Declipse.application=org.eclipse.jdt.ls.core.id1",
  842. "-Dosgi.bundles.defaultStartLevel=4",
  843. "-Declipse.product=org.eclipse.jdt.ls.core.product",
  844. "-Dlog.level=ALL",
  845. "--add-modules=ALL-SYSTEM",
  846. "--add-opens java.base/java.util=ALL-UNNAMED",
  847. "--add-opens java.base/java.lang=ALL-UNNAMED",
  848. ],
  849. {
  850. cwd: root,
  851. },
  852. ),
  853. }
  854. },
  855. }
  856. export const YamlLS: Info = {
  857. id: "yaml-ls",
  858. extensions: [".yaml", ".yml"],
  859. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  860. async spawn(root) {
  861. let binary = Bun.which("yaml-language-server")
  862. const args: string[] = []
  863. if (!binary) {
  864. const js = path.join(
  865. Global.Path.bin,
  866. "node_modules",
  867. "yaml-language-server",
  868. "out",
  869. "server",
  870. "src",
  871. "server.js",
  872. )
  873. const exists = await Bun.file(js).exists()
  874. if (!exists) {
  875. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  876. await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
  877. cwd: Global.Path.bin,
  878. env: {
  879. ...process.env,
  880. BUN_BE_BUN: "1",
  881. },
  882. stdout: "pipe",
  883. stderr: "pipe",
  884. stdin: "pipe",
  885. }).exited
  886. }
  887. binary = BunProc.which()
  888. args.push("run", js)
  889. }
  890. args.push("--stdio")
  891. const proc = spawn(binary, args, {
  892. cwd: root,
  893. env: {
  894. ...process.env,
  895. BUN_BE_BUN: "1",
  896. },
  897. })
  898. return {
  899. process: proc,
  900. }
  901. },
  902. }
  903. export const LuaLS: Info = {
  904. id: "lua-ls",
  905. root: NearestRoot([
  906. ".luarc.json",
  907. ".luarc.jsonc",
  908. ".luacheckrc",
  909. ".stylua.toml",
  910. "stylua.toml",
  911. "selene.toml",
  912. "selene.yml",
  913. ]),
  914. extensions: [".lua"],
  915. async spawn(root) {
  916. let bin = Bun.which("lua-language-server", {
  917. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  918. })
  919. if (!bin) {
  920. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  921. log.info("downloading lua-language-server from GitHub releases")
  922. const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
  923. if (!releaseResponse.ok) {
  924. log.error("Failed to fetch lua-language-server release info")
  925. return
  926. }
  927. const release = await releaseResponse.json()
  928. const platform = process.platform
  929. const arch = process.arch
  930. let assetName = ""
  931. let lualsArch: string = arch
  932. if (arch === "arm64") lualsArch = "arm64"
  933. else if (arch === "x64") lualsArch = "x64"
  934. else if (arch === "ia32") lualsArch = "ia32"
  935. let lualsPlatform: string = platform
  936. if (platform === "darwin") lualsPlatform = "darwin"
  937. else if (platform === "linux") lualsPlatform = "linux"
  938. else if (platform === "win32") lualsPlatform = "win32"
  939. const ext = platform === "win32" ? "zip" : "tar.gz"
  940. assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
  941. const supportedCombos = [
  942. "darwin-arm64.tar.gz",
  943. "darwin-x64.tar.gz",
  944. "linux-x64.tar.gz",
  945. "linux-arm64.tar.gz",
  946. "win32-x64.zip",
  947. "win32-ia32.zip",
  948. ]
  949. const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
  950. if (!supportedCombos.includes(assetSuffix)) {
  951. log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
  952. return
  953. }
  954. const asset = release.assets.find((a: any) => a.name === assetName)
  955. if (!asset) {
  956. log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
  957. return
  958. }
  959. const downloadUrl = asset.browser_download_url
  960. const downloadResponse = await fetch(downloadUrl)
  961. if (!downloadResponse.ok) {
  962. log.error("Failed to download lua-language-server")
  963. return
  964. }
  965. const tempPath = path.join(Global.Path.bin, assetName)
  966. await Bun.file(tempPath).write(downloadResponse)
  967. // Unlike zls which is a single self-contained binary,
  968. // lua-language-server needs supporting files (meta/, locale/, etc.)
  969. // Extract entire archive to dedicated directory to preserve all files
  970. const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
  971. // Remove old installation if exists
  972. const stats = await fs.stat(installDir).catch(() => undefined)
  973. if (stats) {
  974. await fs.rm(installDir, { force: true, recursive: true })
  975. }
  976. await fs.mkdir(installDir, { recursive: true })
  977. if (ext === "zip") {
  978. const ok = await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => {
  979. log.error("Failed to extract lua-language-server archive", { error })
  980. })
  981. if (!ok) return
  982. } else {
  983. const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => {
  984. log.error("Failed to extract lua-language-server archive", { error })
  985. })
  986. if (!ok) return
  987. }
  988. await fs.rm(tempPath, { force: true })
  989. // Binary is located in bin/ subdirectory within the extracted archive
  990. bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
  991. if (!(await Bun.file(bin).exists())) {
  992. log.error("Failed to extract lua-language-server binary")
  993. return
  994. }
  995. if (platform !== "win32") {
  996. const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
  997. log.error("Failed to set executable permission for lua-language-server binary", {
  998. error,
  999. })
  1000. })
  1001. if (!ok) return
  1002. }
  1003. log.info(`installed lua-language-server`, { bin })
  1004. }
  1005. return {
  1006. process: spawn(bin, {
  1007. cwd: root,
  1008. }),
  1009. }
  1010. },
  1011. }
  1012. export const PHPIntelephense: Info = {
  1013. id: "php intelephense",
  1014. extensions: [".php"],
  1015. root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
  1016. async spawn(root) {
  1017. let binary = Bun.which("intelephense")
  1018. const args: string[] = []
  1019. if (!binary) {
  1020. const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
  1021. if (!(await Bun.file(js).exists())) {
  1022. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1023. await Bun.spawn([BunProc.which(), "install", "intelephense"], {
  1024. cwd: Global.Path.bin,
  1025. env: {
  1026. ...process.env,
  1027. BUN_BE_BUN: "1",
  1028. },
  1029. stdout: "pipe",
  1030. stderr: "pipe",
  1031. stdin: "pipe",
  1032. }).exited
  1033. }
  1034. binary = BunProc.which()
  1035. args.push("run", js)
  1036. }
  1037. args.push("--stdio")
  1038. const proc = spawn(binary, args, {
  1039. cwd: root,
  1040. env: {
  1041. ...process.env,
  1042. BUN_BE_BUN: "1",
  1043. },
  1044. })
  1045. return {
  1046. process: proc,
  1047. initialization: {},
  1048. }
  1049. },
  1050. }
  1051. export const Dart: Info = {
  1052. id: "dart",
  1053. extensions: [".dart"],
  1054. root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
  1055. async spawn(root) {
  1056. const dart = Bun.which("dart")
  1057. if (!dart) {
  1058. log.info("dart not found, please install dart first")
  1059. return
  1060. }
  1061. return {
  1062. process: spawn(dart, ["language-server", "--lsp"], {
  1063. cwd: root,
  1064. }),
  1065. }
  1066. },
  1067. }
  1068. }