server.ts 31 KB

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