server.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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 = (patterns: string[]): RootFunction => {
  20. return async (file) => {
  21. const files = Filesystem.up({
  22. targets: patterns,
  23. start: path.dirname(file),
  24. stop: Instance.directory,
  25. })
  26. const first = await files.next()
  27. await files.return()
  28. if (!first.value) return Instance.directory
  29. return path.dirname(first.value)
  30. }
  31. }
  32. export interface Info {
  33. id: string
  34. extensions: string[]
  35. global?: boolean
  36. root: RootFunction
  37. spawn(root: string): Promise<Handle | undefined>
  38. }
  39. export const Typescript: Info = {
  40. id: "typescript",
  41. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  42. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
  43. async spawn(root) {
  44. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  45. if (!tsserver) return
  46. const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
  47. cwd: root,
  48. env: {
  49. ...process.env,
  50. BUN_BE_BUN: "1",
  51. },
  52. })
  53. return {
  54. process: proc,
  55. initialization: {
  56. tsserver: {
  57. path: tsserver,
  58. },
  59. },
  60. }
  61. },
  62. }
  63. export const Vue: Info = {
  64. id: "vue",
  65. extensions: [".vue"],
  66. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  67. async spawn(root) {
  68. let binary = Bun.which("vue-language-server")
  69. const args: string[] = []
  70. if (!binary) {
  71. const js = path.join(
  72. Global.Path.bin,
  73. "node_modules",
  74. "@vue",
  75. "language-server",
  76. "bin",
  77. "vue-language-server.js",
  78. )
  79. if (!(await Bun.file(js).exists())) {
  80. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  81. await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
  82. cwd: Global.Path.bin,
  83. env: {
  84. ...process.env,
  85. BUN_BE_BUN: "1",
  86. },
  87. stdout: "pipe",
  88. stderr: "pipe",
  89. stdin: "pipe",
  90. }).exited
  91. }
  92. binary = BunProc.which()
  93. args.push("run", js)
  94. }
  95. args.push("--stdio")
  96. const proc = spawn(binary, args, {
  97. cwd: root,
  98. env: {
  99. ...process.env,
  100. BUN_BE_BUN: "1",
  101. },
  102. })
  103. return {
  104. process: proc,
  105. initialization: {
  106. // Leave empty; the server will auto-detect workspace TypeScript.
  107. },
  108. }
  109. },
  110. }
  111. export const ESLint: Info = {
  112. id: "eslint",
  113. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  114. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
  115. async spawn(root) {
  116. const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
  117. if (!eslint) return
  118. log.info("spawning eslint server")
  119. const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
  120. if (!(await Bun.file(serverPath).exists())) {
  121. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  122. log.info("downloading and building VS Code ESLint server")
  123. const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
  124. if (!response.ok) return
  125. const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
  126. await Bun.file(zipPath).write(response)
  127. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  128. await fs.rm(zipPath, { force: true })
  129. const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
  130. const finalPath = path.join(Global.Path.bin, "vscode-eslint")
  131. const stats = await fs.stat(finalPath).catch(() => undefined)
  132. if (stats) {
  133. log.info("removing old eslint installation", { path: finalPath })
  134. await fs.rm(finalPath, { force: true, recursive: true })
  135. }
  136. await fs.rename(extractedPath, finalPath)
  137. await $`npm install`.cwd(finalPath).quiet()
  138. await $`npm run compile`.cwd(finalPath).quiet()
  139. log.info("installed VS Code ESLint server", { serverPath })
  140. }
  141. const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
  142. cwd: root,
  143. env: {
  144. ...process.env,
  145. BUN_BE_BUN: "1",
  146. },
  147. })
  148. return {
  149. process: proc,
  150. }
  151. },
  152. }
  153. export const Gopls: Info = {
  154. id: "gopls",
  155. root: async (file) => {
  156. const work = await NearestRoot(["go.work"])(file)
  157. if (work) return work
  158. return NearestRoot(["go.mod", "go.sum"])(file)
  159. },
  160. extensions: [".go"],
  161. async spawn(root) {
  162. let bin = Bun.which("gopls", {
  163. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  164. })
  165. if (!bin) {
  166. if (!Bun.which("go")) return
  167. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  168. log.info("installing gopls")
  169. const proc = Bun.spawn({
  170. cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
  171. env: { ...process.env, GOBIN: Global.Path.bin },
  172. stdout: "pipe",
  173. stderr: "pipe",
  174. stdin: "pipe",
  175. })
  176. const exit = await proc.exited
  177. if (exit !== 0) {
  178. log.error("Failed to install gopls")
  179. return
  180. }
  181. bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
  182. log.info(`installed gopls`, {
  183. bin,
  184. })
  185. }
  186. return {
  187. process: spawn(bin!, {
  188. cwd: root,
  189. }),
  190. }
  191. },
  192. }
  193. export const RubyLsp: Info = {
  194. id: "ruby-lsp",
  195. root: NearestRoot(["Gemfile"]),
  196. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  197. async spawn(root) {
  198. let bin = Bun.which("ruby-lsp", {
  199. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  200. })
  201. if (!bin) {
  202. const ruby = Bun.which("ruby")
  203. const gem = Bun.which("gem")
  204. if (!ruby || !gem) {
  205. log.info("Ruby not found, please install Ruby first")
  206. return
  207. }
  208. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  209. log.info("installing ruby-lsp")
  210. const proc = Bun.spawn({
  211. cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
  212. stdout: "pipe",
  213. stderr: "pipe",
  214. stdin: "pipe",
  215. })
  216. const exit = await proc.exited
  217. if (exit !== 0) {
  218. log.error("Failed to install ruby-lsp")
  219. return
  220. }
  221. bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
  222. log.info(`installed ruby-lsp`, {
  223. bin,
  224. })
  225. }
  226. return {
  227. process: spawn(bin!, ["--stdio"], {
  228. cwd: root,
  229. }),
  230. }
  231. },
  232. }
  233. export const Pyright: Info = {
  234. id: "pyright",
  235. extensions: [".py", ".pyi"],
  236. root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
  237. async spawn(root) {
  238. let binary = Bun.which("pyright-langserver")
  239. const args = []
  240. if (!binary) {
  241. const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
  242. if (!(await Bun.file(js).exists())) {
  243. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  244. await Bun.spawn([BunProc.which(), "install", "pyright"], {
  245. cwd: Global.Path.bin,
  246. env: {
  247. ...process.env,
  248. BUN_BE_BUN: "1",
  249. },
  250. }).exited
  251. }
  252. binary = BunProc.which()
  253. args.push(...["run", js])
  254. }
  255. args.push("--stdio")
  256. const initialization: Record<string, string> = {}
  257. const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
  258. (p): p is string => p !== undefined,
  259. )
  260. for (const venvPath of potentialVenvPaths) {
  261. const isWindows = process.platform === "win32"
  262. const potentialPythonPath = isWindows
  263. ? path.join(venvPath, "Scripts", "python.exe")
  264. : path.join(venvPath, "bin", "python")
  265. if (await Bun.file(potentialPythonPath).exists()) {
  266. initialization["pythonPath"] = potentialPythonPath
  267. break
  268. }
  269. }
  270. const proc = spawn(binary, args, {
  271. cwd: root,
  272. env: {
  273. ...process.env,
  274. BUN_BE_BUN: "1",
  275. },
  276. })
  277. return {
  278. process: proc,
  279. initialization,
  280. }
  281. },
  282. }
  283. export const ElixirLS: Info = {
  284. id: "elixir-ls",
  285. extensions: [".ex", ".exs"],
  286. root: NearestRoot(["mix.exs", "mix.lock"]),
  287. async spawn(root) {
  288. let binary = Bun.which("elixir-ls")
  289. if (!binary) {
  290. const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
  291. binary = path.join(
  292. Global.Path.bin,
  293. "elixir-ls-master",
  294. "release",
  295. process.platform === "win32" ? "language_server.bar" : "language_server.sh",
  296. )
  297. if (!(await Bun.file(binary).exists())) {
  298. const elixir = Bun.which("elixir")
  299. if (!elixir) {
  300. log.error("elixir is required to run elixir-ls")
  301. return
  302. }
  303. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  304. log.info("downloading elixir-ls from GitHub releases")
  305. const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
  306. if (!response.ok) return
  307. const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
  308. await Bun.file(zipPath).write(response)
  309. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  310. await fs.rm(zipPath, {
  311. force: true,
  312. recursive: true,
  313. })
  314. await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
  315. .quiet()
  316. .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
  317. .env({ MIX_ENV: "prod", ...process.env })
  318. log.info(`installed elixir-ls`, {
  319. path: elixirLsPath,
  320. })
  321. }
  322. }
  323. return {
  324. process: spawn(binary, {
  325. cwd: root,
  326. }),
  327. }
  328. },
  329. }
  330. export const Zls: Info = {
  331. id: "zls",
  332. extensions: [".zig", ".zon"],
  333. root: NearestRoot(["build.zig"]),
  334. async spawn(root) {
  335. let bin = Bun.which("zls", {
  336. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  337. })
  338. if (!bin) {
  339. const zig = Bun.which("zig")
  340. if (!zig) {
  341. log.error("Zig is required to use zls. Please install Zig first.")
  342. return
  343. }
  344. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  345. log.info("downloading zls from GitHub releases")
  346. const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
  347. if (!releaseResponse.ok) {
  348. log.error("Failed to fetch zls release info")
  349. return
  350. }
  351. const release = await releaseResponse.json()
  352. const platform = process.platform
  353. const arch = process.arch
  354. let assetName = ""
  355. let zlsArch: string = arch
  356. if (arch === "arm64") zlsArch = "aarch64"
  357. else if (arch === "x64") zlsArch = "x86_64"
  358. else if (arch === "ia32") zlsArch = "x86"
  359. let zlsPlatform: string = platform
  360. if (platform === "darwin") zlsPlatform = "macos"
  361. else if (platform === "win32") zlsPlatform = "windows"
  362. const ext = platform === "win32" ? "zip" : "tar.xz"
  363. assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
  364. const supportedCombos = [
  365. "zls-x86_64-linux.tar.xz",
  366. "zls-x86_64-macos.tar.xz",
  367. "zls-x86_64-windows.zip",
  368. "zls-aarch64-linux.tar.xz",
  369. "zls-aarch64-macos.tar.xz",
  370. "zls-aarch64-windows.zip",
  371. "zls-x86-linux.tar.xz",
  372. "zls-x86-windows.zip",
  373. ]
  374. if (!supportedCombos.includes(assetName)) {
  375. log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
  376. return
  377. }
  378. const asset = release.assets.find((a: any) => a.name === assetName)
  379. if (!asset) {
  380. log.error(`Could not find asset ${assetName} in latest zls release`)
  381. return
  382. }
  383. const downloadUrl = asset.browser_download_url
  384. const downloadResponse = await fetch(downloadUrl)
  385. if (!downloadResponse.ok) {
  386. log.error("Failed to download zls")
  387. return
  388. }
  389. const tempPath = path.join(Global.Path.bin, assetName)
  390. await Bun.file(tempPath).write(downloadResponse)
  391. if (ext === "zip") {
  392. await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
  393. } else {
  394. await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
  395. }
  396. await fs.rm(tempPath, { force: true })
  397. bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
  398. if (!(await Bun.file(bin).exists())) {
  399. log.error("Failed to extract zls binary")
  400. return
  401. }
  402. if (platform !== "win32") {
  403. await $`chmod +x ${bin}`.nothrow()
  404. }
  405. log.info(`installed zls`, { bin })
  406. }
  407. return {
  408. process: spawn(bin, {
  409. cwd: root,
  410. }),
  411. }
  412. },
  413. }
  414. export const CSharp: Info = {
  415. id: "csharp",
  416. root: NearestRoot([".sln", ".csproj", "global.json"]),
  417. extensions: [".cs"],
  418. async spawn(root) {
  419. let bin = Bun.which("csharp-ls", {
  420. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  421. })
  422. if (!bin) {
  423. if (!Bun.which("dotnet")) {
  424. log.error(".NET SDK is required to install csharp-ls")
  425. return
  426. }
  427. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  428. log.info("installing csharp-ls via dotnet tool")
  429. const proc = Bun.spawn({
  430. cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
  431. stdout: "pipe",
  432. stderr: "pipe",
  433. stdin: "pipe",
  434. })
  435. const exit = await proc.exited
  436. if (exit !== 0) {
  437. log.error("Failed to install csharp-ls")
  438. return
  439. }
  440. bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
  441. log.info(`installed csharp-ls`, { bin })
  442. }
  443. return {
  444. process: spawn(bin, {
  445. cwd: root,
  446. }),
  447. }
  448. },
  449. }
  450. export const RustAnalyzer: Info = {
  451. id: "rust",
  452. root: async (root) => {
  453. const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
  454. if (crateRoot === undefined) {
  455. return undefined
  456. }
  457. let currentDir = crateRoot
  458. while (currentDir !== path.dirname(currentDir)) {
  459. // Stop at filesystem root
  460. const cargoTomlPath = path.join(currentDir, "Cargo.toml")
  461. try {
  462. const cargoTomlContent = await Bun.file(cargoTomlPath).text()
  463. if (cargoTomlContent.includes("[workspace]")) {
  464. return currentDir
  465. }
  466. } catch (err) {
  467. // File doesn't exist or can't be read, continue searching up
  468. }
  469. const parentDir = path.dirname(currentDir)
  470. if (parentDir === currentDir) break // Reached filesystem root
  471. currentDir = parentDir
  472. // Stop if we've gone above the app root
  473. if (!currentDir.startsWith(Instance.worktree)) break
  474. }
  475. return crateRoot
  476. },
  477. extensions: [".rs"],
  478. async spawn(root) {
  479. const bin = Bun.which("rust-analyzer")
  480. if (!bin) {
  481. log.info("rust-analyzer not found in path, please install it")
  482. return
  483. }
  484. return {
  485. process: spawn(bin, {
  486. cwd: root,
  487. }),
  488. }
  489. },
  490. }
  491. export const Clangd: Info = {
  492. id: "clangd",
  493. root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
  494. extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
  495. async spawn(root) {
  496. let bin = Bun.which("clangd", {
  497. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  498. })
  499. if (!bin) {
  500. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  501. log.info("downloading clangd from GitHub releases")
  502. const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
  503. if (!releaseResponse.ok) {
  504. log.error("Failed to fetch clangd release info")
  505. return
  506. }
  507. const release = await releaseResponse.json()
  508. const platform = process.platform
  509. let assetName = ""
  510. if (platform === "darwin") {
  511. assetName = "clangd-mac-"
  512. } else if (platform === "linux") {
  513. assetName = "clangd-linux-"
  514. } else if (platform === "win32") {
  515. assetName = "clangd-windows-"
  516. } else {
  517. log.error(`Platform ${platform} is not supported by clangd auto-download`)
  518. return
  519. }
  520. assetName += release.tag_name + ".zip"
  521. const asset = release.assets.find((a: any) => a.name === assetName)
  522. if (!asset) {
  523. log.error(`Could not find asset ${assetName} in latest clangd release`)
  524. return
  525. }
  526. const downloadUrl = asset.browser_download_url
  527. const downloadResponse = await fetch(downloadUrl)
  528. if (!downloadResponse.ok) {
  529. log.error("Failed to download clangd")
  530. return
  531. }
  532. const zipPath = path.join(Global.Path.bin, "clangd.zip")
  533. await Bun.file(zipPath).write(downloadResponse)
  534. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  535. await fs.rm(zipPath, { force: true })
  536. const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
  537. bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
  538. if (!(await Bun.file(bin).exists())) {
  539. log.error("Failed to extract clangd binary")
  540. return
  541. }
  542. if (platform !== "win32") {
  543. await $`chmod +x ${bin}`.nothrow()
  544. }
  545. log.info(`installed clangd`, { bin })
  546. }
  547. return {
  548. process: spawn(bin, ["--background-index", "--clang-tidy"], {
  549. cwd: root,
  550. }),
  551. }
  552. },
  553. }
  554. export const Svelte: Info = {
  555. id: "svelte",
  556. extensions: [".svelte"],
  557. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  558. async spawn(root) {
  559. let binary = Bun.which("svelteserver")
  560. const args: string[] = []
  561. if (!binary) {
  562. const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
  563. if (!(await Bun.file(js).exists())) {
  564. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  565. await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
  566. cwd: Global.Path.bin,
  567. env: {
  568. ...process.env,
  569. BUN_BE_BUN: "1",
  570. },
  571. stdout: "pipe",
  572. stderr: "pipe",
  573. stdin: "pipe",
  574. }).exited
  575. }
  576. binary = BunProc.which()
  577. args.push("run", js)
  578. }
  579. args.push("--stdio")
  580. const proc = spawn(binary, args, {
  581. cwd: root,
  582. env: {
  583. ...process.env,
  584. BUN_BE_BUN: "1",
  585. },
  586. })
  587. return {
  588. process: proc,
  589. initialization: {},
  590. }
  591. },
  592. }
  593. export const JDTLS: Info = {
  594. id: "jdtls",
  595. root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
  596. extensions: [".java"],
  597. async spawn(root) {
  598. const java = Bun.which("java")
  599. if (!java) {
  600. log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
  601. return
  602. }
  603. const javaMajorVersion = await $`java -version`
  604. .quiet()
  605. .nothrow()
  606. .then(({ stderr }) => {
  607. const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
  608. return !m ? undefined : parseInt(m[1])
  609. })
  610. if (javaMajorVersion == null || javaMajorVersion < 21) {
  611. log.error("JDTLS requires at least Java 21.")
  612. return
  613. }
  614. const distPath = path.join(Global.Path.bin, "jdtls")
  615. const launcherDir = path.join(distPath, "plugins")
  616. const installed = await fs.exists(launcherDir)
  617. if (!installed) {
  618. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  619. log.info("Downloading JDTLS LSP server.")
  620. await fs.mkdir(distPath, { recursive: true })
  621. const releaseURL =
  622. "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
  623. const archivePath = path.join(distPath, "release.tar.gz")
  624. await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
  625. await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
  626. await fs.rm(archivePath, { force: true })
  627. }
  628. const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
  629. .cwd(launcherDir)
  630. .quiet()
  631. .nothrow()
  632. .then(({ stdout }) => stdout.toString().trim())
  633. const launcherJar = path.join(launcherDir, jarFileName)
  634. if (!(await fs.exists(launcherJar))) {
  635. log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
  636. return
  637. }
  638. const configFile = path.join(
  639. distPath,
  640. (() => {
  641. switch (process.platform) {
  642. case "darwin":
  643. return "config_mac"
  644. case "linux":
  645. return "config_linux"
  646. case "win32":
  647. return "config_windows"
  648. default:
  649. return "config_linux"
  650. }
  651. })(),
  652. )
  653. const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
  654. return {
  655. process: spawn(
  656. java,
  657. [
  658. "-jar",
  659. launcherJar,
  660. "-configuration",
  661. configFile,
  662. "-data",
  663. dataDir,
  664. "-Declipse.application=org.eclipse.jdt.ls.core.id1",
  665. "-Dosgi.bundles.defaultStartLevel=4",
  666. "-Declipse.product=org.eclipse.jdt.ls.core.product",
  667. "-Dlog.level=ALL",
  668. "--add-modules=ALL-SYSTEM",
  669. "--add-opens java.base/java.util=ALL-UNNAMED",
  670. "--add-opens java.base/java.lang=ALL-UNNAMED",
  671. ],
  672. {
  673. cwd: root,
  674. },
  675. ),
  676. }
  677. },
  678. }
  679. }