server.ts 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570
  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 Biome: Info = {
  195. id: "biome",
  196. root: NearestRoot([
  197. "biome.json",
  198. "biome.jsonc",
  199. "package-lock.json",
  200. "bun.lockb",
  201. "bun.lock",
  202. "pnpm-lock.yaml",
  203. "yarn.lock",
  204. ]),
  205. extensions: [
  206. ".ts",
  207. ".tsx",
  208. ".js",
  209. ".jsx",
  210. ".mjs",
  211. ".cjs",
  212. ".mts",
  213. ".cts",
  214. ".json",
  215. ".jsonc",
  216. ".vue",
  217. ".astro",
  218. ".svelte",
  219. ".css",
  220. ".graphql",
  221. ".gql",
  222. ".html",
  223. ],
  224. async spawn(root) {
  225. const localBin = path.join(root, "node_modules", ".bin", "biome")
  226. let bin: string | undefined
  227. if (await Bun.file(localBin).exists()) bin = localBin
  228. if (!bin) {
  229. const found = Bun.which("biome")
  230. if (found) bin = found
  231. }
  232. let args = ["lsp-proxy", "--stdio"]
  233. if (!bin) {
  234. const resolved = await Bun.resolve("biome", root).catch(() => undefined)
  235. if (!resolved) return
  236. bin = BunProc.which()
  237. args = ["x", "biome", "lsp-proxy", "--stdio"]
  238. }
  239. const proc = spawn(bin, args, {
  240. cwd: root,
  241. env: {
  242. ...process.env,
  243. BUN_BE_BUN: "1",
  244. },
  245. })
  246. return {
  247. process: proc,
  248. }
  249. },
  250. }
  251. export const Gopls: Info = {
  252. id: "gopls",
  253. root: async (file) => {
  254. const work = await NearestRoot(["go.work"])(file)
  255. if (work) return work
  256. return NearestRoot(["go.mod", "go.sum"])(file)
  257. },
  258. extensions: [".go"],
  259. async spawn(root) {
  260. let bin = Bun.which("gopls", {
  261. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  262. })
  263. if (!bin) {
  264. if (!Bun.which("go")) return
  265. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  266. log.info("installing gopls")
  267. const proc = Bun.spawn({
  268. cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
  269. env: { ...process.env, GOBIN: Global.Path.bin },
  270. stdout: "pipe",
  271. stderr: "pipe",
  272. stdin: "pipe",
  273. })
  274. const exit = await proc.exited
  275. if (exit !== 0) {
  276. log.error("Failed to install gopls")
  277. return
  278. }
  279. bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
  280. log.info(`installed gopls`, {
  281. bin,
  282. })
  283. }
  284. return {
  285. process: spawn(bin!, {
  286. cwd: root,
  287. }),
  288. }
  289. },
  290. }
  291. export const Rubocop: Info = {
  292. id: "ruby-lsp",
  293. root: NearestRoot(["Gemfile"]),
  294. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  295. async spawn(root) {
  296. let bin = Bun.which("rubocop", {
  297. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  298. })
  299. if (!bin) {
  300. const ruby = Bun.which("ruby")
  301. const gem = Bun.which("gem")
  302. if (!ruby || !gem) {
  303. log.info("Ruby not found, please install Ruby first")
  304. return
  305. }
  306. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  307. log.info("installing rubocop")
  308. const proc = Bun.spawn({
  309. cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
  310. stdout: "pipe",
  311. stderr: "pipe",
  312. stdin: "pipe",
  313. })
  314. const exit = await proc.exited
  315. if (exit !== 0) {
  316. log.error("Failed to install rubocop")
  317. return
  318. }
  319. bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
  320. log.info(`installed rubocop`, {
  321. bin,
  322. })
  323. }
  324. return {
  325. process: spawn(bin!, ["--lsp"], {
  326. cwd: root,
  327. }),
  328. }
  329. },
  330. }
  331. export const Pyright: Info = {
  332. id: "pyright",
  333. extensions: [".py", ".pyi"],
  334. root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
  335. async spawn(root) {
  336. let binary = Bun.which("pyright-langserver")
  337. const args = []
  338. if (!binary) {
  339. const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
  340. if (!(await Bun.file(js).exists())) {
  341. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  342. await Bun.spawn([BunProc.which(), "install", "pyright"], {
  343. cwd: Global.Path.bin,
  344. env: {
  345. ...process.env,
  346. BUN_BE_BUN: "1",
  347. },
  348. }).exited
  349. }
  350. binary = BunProc.which()
  351. args.push(...["run", js])
  352. }
  353. args.push("--stdio")
  354. const initialization: Record<string, string> = {}
  355. const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
  356. (p): p is string => p !== undefined,
  357. )
  358. for (const venvPath of potentialVenvPaths) {
  359. const isWindows = process.platform === "win32"
  360. const potentialPythonPath = isWindows
  361. ? path.join(venvPath, "Scripts", "python.exe")
  362. : path.join(venvPath, "bin", "python")
  363. if (await Bun.file(potentialPythonPath).exists()) {
  364. initialization["pythonPath"] = potentialPythonPath
  365. break
  366. }
  367. }
  368. const proc = spawn(binary, args, {
  369. cwd: root,
  370. env: {
  371. ...process.env,
  372. BUN_BE_BUN: "1",
  373. },
  374. })
  375. return {
  376. process: proc,
  377. initialization,
  378. }
  379. },
  380. }
  381. export const ElixirLS: Info = {
  382. id: "elixir-ls",
  383. extensions: [".ex", ".exs"],
  384. root: NearestRoot(["mix.exs", "mix.lock"]),
  385. async spawn(root) {
  386. let binary = Bun.which("elixir-ls")
  387. if (!binary) {
  388. const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
  389. binary = path.join(
  390. Global.Path.bin,
  391. "elixir-ls-master",
  392. "release",
  393. process.platform === "win32" ? "language_server.bar" : "language_server.sh",
  394. )
  395. if (!(await Bun.file(binary).exists())) {
  396. const elixir = Bun.which("elixir")
  397. if (!elixir) {
  398. log.error("elixir is required to run elixir-ls")
  399. return
  400. }
  401. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  402. log.info("downloading elixir-ls from GitHub releases")
  403. const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
  404. if (!response.ok) return
  405. const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
  406. await Bun.file(zipPath).write(response)
  407. await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
  408. await fs.rm(zipPath, {
  409. force: true,
  410. recursive: true,
  411. })
  412. await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
  413. .quiet()
  414. .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
  415. .env({ MIX_ENV: "prod", ...process.env })
  416. log.info(`installed elixir-ls`, {
  417. path: elixirLsPath,
  418. })
  419. }
  420. }
  421. return {
  422. process: spawn(binary, {
  423. cwd: root,
  424. }),
  425. }
  426. },
  427. }
  428. export const Zls: Info = {
  429. id: "zls",
  430. extensions: [".zig", ".zon"],
  431. root: NearestRoot(["build.zig"]),
  432. async spawn(root) {
  433. let bin = Bun.which("zls", {
  434. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  435. })
  436. if (!bin) {
  437. const zig = Bun.which("zig")
  438. if (!zig) {
  439. log.error("Zig is required to use zls. Please install Zig first.")
  440. return
  441. }
  442. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  443. log.info("downloading zls from GitHub releases")
  444. const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
  445. if (!releaseResponse.ok) {
  446. log.error("Failed to fetch zls release info")
  447. return
  448. }
  449. const release = (await releaseResponse.json()) as any
  450. const platform = process.platform
  451. const arch = process.arch
  452. let assetName = ""
  453. let zlsArch: string = arch
  454. if (arch === "arm64") zlsArch = "aarch64"
  455. else if (arch === "x64") zlsArch = "x86_64"
  456. else if (arch === "ia32") zlsArch = "x86"
  457. let zlsPlatform: string = platform
  458. if (platform === "darwin") zlsPlatform = "macos"
  459. else if (platform === "win32") zlsPlatform = "windows"
  460. const ext = platform === "win32" ? "zip" : "tar.xz"
  461. assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
  462. const supportedCombos = [
  463. "zls-x86_64-linux.tar.xz",
  464. "zls-x86_64-macos.tar.xz",
  465. "zls-x86_64-windows.zip",
  466. "zls-aarch64-linux.tar.xz",
  467. "zls-aarch64-macos.tar.xz",
  468. "zls-aarch64-windows.zip",
  469. "zls-x86-linux.tar.xz",
  470. "zls-x86-windows.zip",
  471. ]
  472. if (!supportedCombos.includes(assetName)) {
  473. log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
  474. return
  475. }
  476. const asset = release.assets.find((a: any) => a.name === assetName)
  477. if (!asset) {
  478. log.error(`Could not find asset ${assetName} in latest zls release`)
  479. return
  480. }
  481. const downloadUrl = asset.browser_download_url
  482. const downloadResponse = await fetch(downloadUrl)
  483. if (!downloadResponse.ok) {
  484. log.error("Failed to download zls")
  485. return
  486. }
  487. const tempPath = path.join(Global.Path.bin, assetName)
  488. await Bun.file(tempPath).write(downloadResponse)
  489. if (ext === "zip") {
  490. await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
  491. } else {
  492. await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).nothrow()
  493. }
  494. await fs.rm(tempPath, { force: true })
  495. bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
  496. if (!(await Bun.file(bin).exists())) {
  497. log.error("Failed to extract zls binary")
  498. return
  499. }
  500. if (platform !== "win32") {
  501. await $`chmod +x ${bin}`.nothrow()
  502. }
  503. log.info(`installed zls`, { bin })
  504. }
  505. return {
  506. process: spawn(bin, {
  507. cwd: root,
  508. }),
  509. }
  510. },
  511. }
  512. export const CSharp: Info = {
  513. id: "csharp",
  514. root: NearestRoot([".sln", ".csproj", "global.json"]),
  515. extensions: [".cs"],
  516. async spawn(root) {
  517. let bin = Bun.which("csharp-ls", {
  518. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  519. })
  520. if (!bin) {
  521. if (!Bun.which("dotnet")) {
  522. log.error(".NET SDK is required to install csharp-ls")
  523. return
  524. }
  525. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  526. log.info("installing csharp-ls via dotnet tool")
  527. const proc = Bun.spawn({
  528. cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
  529. stdout: "pipe",
  530. stderr: "pipe",
  531. stdin: "pipe",
  532. })
  533. const exit = await proc.exited
  534. if (exit !== 0) {
  535. log.error("Failed to install csharp-ls")
  536. return
  537. }
  538. bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
  539. log.info(`installed csharp-ls`, { bin })
  540. }
  541. return {
  542. process: spawn(bin, {
  543. cwd: root,
  544. }),
  545. }
  546. },
  547. }
  548. export const FSharp: Info = {
  549. id: "fsharp",
  550. root: NearestRoot([".sln", ".fsproj", "global.json"]),
  551. extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
  552. async spawn(root) {
  553. let bin = Bun.which("fsautocomplete", {
  554. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  555. })
  556. if (!bin) {
  557. if (!Bun.which("dotnet")) {
  558. log.error(".NET SDK is required to install fsautocomplete")
  559. return
  560. }
  561. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  562. log.info("installing fsautocomplete via dotnet tool")
  563. const proc = Bun.spawn({
  564. cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin],
  565. stdout: "pipe",
  566. stderr: "pipe",
  567. stdin: "pipe",
  568. })
  569. const exit = await proc.exited
  570. if (exit !== 0) {
  571. log.error("Failed to install fsautocomplete")
  572. return
  573. }
  574. bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
  575. log.info(`installed fsautocomplete`, { bin })
  576. }
  577. return {
  578. process: spawn(bin, {
  579. cwd: root,
  580. }),
  581. }
  582. },
  583. }
  584. export const SourceKit: Info = {
  585. id: "sourcekit-lsp",
  586. extensions: [".swift", ".objc", "objcpp"],
  587. root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
  588. async spawn(root) {
  589. // Check if sourcekit-lsp is available in the PATH
  590. // This is installed with the Swift toolchain
  591. const sourcekit = Bun.which("sourcekit-lsp")
  592. if (sourcekit) {
  593. return {
  594. process: spawn(sourcekit, {
  595. cwd: root,
  596. }),
  597. }
  598. }
  599. // If sourcekit-lsp not found, check if xcrun is available
  600. // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
  601. if (!Bun.which("xcrun")) return
  602. const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
  603. if (lspLoc.exitCode !== 0) return
  604. const bin = lspLoc.text().trim()
  605. return {
  606. process: spawn(bin, {
  607. cwd: root,
  608. }),
  609. }
  610. },
  611. }
  612. export const RustAnalyzer: Info = {
  613. id: "rust",
  614. root: async (root) => {
  615. const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
  616. if (crateRoot === undefined) {
  617. return undefined
  618. }
  619. let currentDir = crateRoot
  620. while (currentDir !== path.dirname(currentDir)) {
  621. // Stop at filesystem root
  622. const cargoTomlPath = path.join(currentDir, "Cargo.toml")
  623. try {
  624. const cargoTomlContent = await Bun.file(cargoTomlPath).text()
  625. if (cargoTomlContent.includes("[workspace]")) {
  626. return currentDir
  627. }
  628. } catch (err) {
  629. // File doesn't exist or can't be read, continue searching up
  630. }
  631. const parentDir = path.dirname(currentDir)
  632. if (parentDir === currentDir) break // Reached filesystem root
  633. currentDir = parentDir
  634. // Stop if we've gone above the app root
  635. if (!currentDir.startsWith(Instance.worktree)) break
  636. }
  637. return crateRoot
  638. },
  639. extensions: [".rs"],
  640. async spawn(root) {
  641. const bin = Bun.which("rust-analyzer")
  642. if (!bin) {
  643. log.info("rust-analyzer not found in path, please install it")
  644. return
  645. }
  646. return {
  647. process: spawn(bin, {
  648. cwd: root,
  649. }),
  650. }
  651. },
  652. }
  653. export const Clangd: Info = {
  654. id: "clangd",
  655. root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
  656. extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
  657. async spawn(root) {
  658. const args = ["--background-index", "--clang-tidy"]
  659. const fromPath = Bun.which("clangd")
  660. if (fromPath) {
  661. return {
  662. process: spawn(fromPath, args, {
  663. cwd: root,
  664. }),
  665. }
  666. }
  667. const ext = process.platform === "win32" ? ".exe" : ""
  668. const direct = path.join(Global.Path.bin, "clangd" + ext)
  669. if (await Bun.file(direct).exists()) {
  670. return {
  671. process: spawn(direct, args, {
  672. cwd: root,
  673. }),
  674. }
  675. }
  676. const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
  677. for (const entry of entries) {
  678. if (!entry.isDirectory()) continue
  679. if (!entry.name.startsWith("clangd_")) continue
  680. const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
  681. if (await Bun.file(candidate).exists()) {
  682. return {
  683. process: spawn(candidate, args, {
  684. cwd: root,
  685. }),
  686. }
  687. }
  688. }
  689. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  690. log.info("downloading clangd from GitHub releases")
  691. const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
  692. if (!releaseResponse.ok) {
  693. log.error("Failed to fetch clangd release info")
  694. return
  695. }
  696. const release: {
  697. tag_name?: string
  698. assets?: { name?: string; browser_download_url?: string }[]
  699. } = await releaseResponse.json()
  700. const tag = release.tag_name
  701. if (!tag) {
  702. log.error("clangd release did not include a tag name")
  703. return
  704. }
  705. const platform = process.platform
  706. const tokens: Record<string, string> = {
  707. darwin: "mac",
  708. linux: "linux",
  709. win32: "windows",
  710. }
  711. const token = tokens[platform]
  712. if (!token) {
  713. log.error(`Platform ${platform} is not supported by clangd auto-download`)
  714. return
  715. }
  716. const assets = release.assets ?? []
  717. const valid = (item: { name?: string; browser_download_url?: string }) => {
  718. if (!item.name) return false
  719. if (!item.browser_download_url) return false
  720. if (!item.name.includes(token)) return false
  721. return item.name.includes(tag)
  722. }
  723. const asset =
  724. assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
  725. assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
  726. assets.find((item) => valid(item))
  727. if (!asset?.name || !asset.browser_download_url) {
  728. log.error("clangd could not match release asset", { tag, platform })
  729. return
  730. }
  731. const name = asset.name
  732. const downloadResponse = await fetch(asset.browser_download_url)
  733. if (!downloadResponse.ok) {
  734. log.error("Failed to download clangd")
  735. return
  736. }
  737. const archive = path.join(Global.Path.bin, name)
  738. const buf = await downloadResponse.arrayBuffer()
  739. if (buf.byteLength === 0) {
  740. log.error("Failed to write clangd archive")
  741. return
  742. }
  743. await Bun.write(archive, buf)
  744. const zip = name.endsWith(".zip")
  745. const tar = name.endsWith(".tar.xz")
  746. if (!zip && !tar) {
  747. log.error("clangd encountered unsupported asset", { asset: name })
  748. return
  749. }
  750. if (zip) {
  751. await $`unzip -o -q ${archive}`.quiet().cwd(Global.Path.bin).nothrow()
  752. }
  753. if (tar) {
  754. await $`tar -xf ${archive}`.cwd(Global.Path.bin).nothrow()
  755. }
  756. await fs.rm(archive, { force: true })
  757. const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
  758. if (!(await Bun.file(bin).exists())) {
  759. log.error("Failed to extract clangd binary")
  760. return
  761. }
  762. if (platform !== "win32") {
  763. await $`chmod +x ${bin}`.nothrow()
  764. }
  765. await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
  766. await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
  767. log.info(`installed clangd`, { bin })
  768. return {
  769. process: spawn(bin, args, {
  770. cwd: root,
  771. }),
  772. }
  773. },
  774. }
  775. export const Svelte: Info = {
  776. id: "svelte",
  777. extensions: [".svelte"],
  778. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  779. async spawn(root) {
  780. let binary = Bun.which("svelteserver")
  781. const args: string[] = []
  782. if (!binary) {
  783. const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
  784. if (!(await Bun.file(js).exists())) {
  785. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  786. await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
  787. cwd: Global.Path.bin,
  788. env: {
  789. ...process.env,
  790. BUN_BE_BUN: "1",
  791. },
  792. stdout: "pipe",
  793. stderr: "pipe",
  794. stdin: "pipe",
  795. }).exited
  796. }
  797. binary = BunProc.which()
  798. args.push("run", js)
  799. }
  800. args.push("--stdio")
  801. const proc = spawn(binary, args, {
  802. cwd: root,
  803. env: {
  804. ...process.env,
  805. BUN_BE_BUN: "1",
  806. },
  807. })
  808. return {
  809. process: proc,
  810. initialization: {},
  811. }
  812. },
  813. }
  814. export const Astro: Info = {
  815. id: "astro",
  816. extensions: [".astro"],
  817. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  818. async spawn(root) {
  819. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  820. if (!tsserver) {
  821. log.info("typescript not found, required for Astro language server")
  822. return
  823. }
  824. const tsdk = path.dirname(tsserver)
  825. let binary = Bun.which("astro-ls")
  826. const args: string[] = []
  827. if (!binary) {
  828. const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
  829. if (!(await Bun.file(js).exists())) {
  830. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  831. await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
  832. cwd: Global.Path.bin,
  833. env: {
  834. ...process.env,
  835. BUN_BE_BUN: "1",
  836. },
  837. stdout: "pipe",
  838. stderr: "pipe",
  839. stdin: "pipe",
  840. }).exited
  841. }
  842. binary = BunProc.which()
  843. args.push("run", js)
  844. }
  845. args.push("--stdio")
  846. const proc = spawn(binary, args, {
  847. cwd: root,
  848. env: {
  849. ...process.env,
  850. BUN_BE_BUN: "1",
  851. },
  852. })
  853. return {
  854. process: proc,
  855. initialization: {
  856. typescript: {
  857. tsdk,
  858. },
  859. },
  860. }
  861. },
  862. }
  863. export const JDTLS: Info = {
  864. id: "jdtls",
  865. root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
  866. extensions: [".java"],
  867. async spawn(root) {
  868. const java = Bun.which("java")
  869. if (!java) {
  870. log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
  871. return
  872. }
  873. const javaMajorVersion = await $`java -version`
  874. .quiet()
  875. .nothrow()
  876. .then(({ stderr }) => {
  877. const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
  878. return !m ? undefined : parseInt(m[1])
  879. })
  880. if (javaMajorVersion == null || javaMajorVersion < 21) {
  881. log.error("JDTLS requires at least Java 21.")
  882. return
  883. }
  884. const distPath = path.join(Global.Path.bin, "jdtls")
  885. const launcherDir = path.join(distPath, "plugins")
  886. const installed = await fs.exists(launcherDir)
  887. if (!installed) {
  888. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  889. log.info("Downloading JDTLS LSP server.")
  890. await fs.mkdir(distPath, { recursive: true })
  891. const releaseURL =
  892. "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
  893. const archivePath = path.join(distPath, "release.tar.gz")
  894. await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
  895. await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
  896. await fs.rm(archivePath, { force: true })
  897. }
  898. const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
  899. .cwd(launcherDir)
  900. .quiet()
  901. .nothrow()
  902. .then(({ stdout }) => stdout.toString().trim())
  903. const launcherJar = path.join(launcherDir, jarFileName)
  904. if (!(await fs.exists(launcherJar))) {
  905. log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
  906. return
  907. }
  908. const configFile = path.join(
  909. distPath,
  910. (() => {
  911. switch (process.platform) {
  912. case "darwin":
  913. return "config_mac"
  914. case "linux":
  915. return "config_linux"
  916. case "win32":
  917. return "config_windows"
  918. default:
  919. return "config_linux"
  920. }
  921. })(),
  922. )
  923. const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
  924. return {
  925. process: spawn(
  926. java,
  927. [
  928. "-jar",
  929. launcherJar,
  930. "-configuration",
  931. configFile,
  932. "-data",
  933. dataDir,
  934. "-Declipse.application=org.eclipse.jdt.ls.core.id1",
  935. "-Dosgi.bundles.defaultStartLevel=4",
  936. "-Declipse.product=org.eclipse.jdt.ls.core.product",
  937. "-Dlog.level=ALL",
  938. "--add-modules=ALL-SYSTEM",
  939. "--add-opens java.base/java.util=ALL-UNNAMED",
  940. "--add-opens java.base/java.lang=ALL-UNNAMED",
  941. ],
  942. {
  943. cwd: root,
  944. },
  945. ),
  946. }
  947. },
  948. }
  949. export const YamlLS: Info = {
  950. id: "yaml-ls",
  951. extensions: [".yaml", ".yml"],
  952. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  953. async spawn(root) {
  954. let binary = Bun.which("yaml-language-server")
  955. const args: string[] = []
  956. if (!binary) {
  957. const js = path.join(
  958. Global.Path.bin,
  959. "node_modules",
  960. "yaml-language-server",
  961. "out",
  962. "server",
  963. "src",
  964. "server.js",
  965. )
  966. const exists = await Bun.file(js).exists()
  967. if (!exists) {
  968. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  969. await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
  970. cwd: Global.Path.bin,
  971. env: {
  972. ...process.env,
  973. BUN_BE_BUN: "1",
  974. },
  975. stdout: "pipe",
  976. stderr: "pipe",
  977. stdin: "pipe",
  978. }).exited
  979. }
  980. binary = BunProc.which()
  981. args.push("run", js)
  982. }
  983. args.push("--stdio")
  984. const proc = spawn(binary, args, {
  985. cwd: root,
  986. env: {
  987. ...process.env,
  988. BUN_BE_BUN: "1",
  989. },
  990. })
  991. return {
  992. process: proc,
  993. }
  994. },
  995. }
  996. export const LuaLS: Info = {
  997. id: "lua-ls",
  998. root: NearestRoot([
  999. ".luarc.json",
  1000. ".luarc.jsonc",
  1001. ".luacheckrc",
  1002. ".stylua.toml",
  1003. "stylua.toml",
  1004. "selene.toml",
  1005. "selene.yml",
  1006. ]),
  1007. extensions: [".lua"],
  1008. async spawn(root) {
  1009. let bin = Bun.which("lua-language-server", {
  1010. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  1011. })
  1012. if (!bin) {
  1013. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1014. log.info("downloading lua-language-server from GitHub releases")
  1015. const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
  1016. if (!releaseResponse.ok) {
  1017. log.error("Failed to fetch lua-language-server release info")
  1018. return
  1019. }
  1020. const release = await releaseResponse.json()
  1021. const platform = process.platform
  1022. const arch = process.arch
  1023. let assetName = ""
  1024. let lualsArch: string = arch
  1025. if (arch === "arm64") lualsArch = "arm64"
  1026. else if (arch === "x64") lualsArch = "x64"
  1027. else if (arch === "ia32") lualsArch = "ia32"
  1028. let lualsPlatform: string = platform
  1029. if (platform === "darwin") lualsPlatform = "darwin"
  1030. else if (platform === "linux") lualsPlatform = "linux"
  1031. else if (platform === "win32") lualsPlatform = "win32"
  1032. const ext = platform === "win32" ? "zip" : "tar.gz"
  1033. assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
  1034. const supportedCombos = [
  1035. "darwin-arm64.tar.gz",
  1036. "darwin-x64.tar.gz",
  1037. "linux-x64.tar.gz",
  1038. "linux-arm64.tar.gz",
  1039. "win32-x64.zip",
  1040. "win32-ia32.zip",
  1041. ]
  1042. const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
  1043. if (!supportedCombos.includes(assetSuffix)) {
  1044. log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
  1045. return
  1046. }
  1047. const asset = release.assets.find((a: any) => a.name === assetName)
  1048. if (!asset) {
  1049. log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
  1050. return
  1051. }
  1052. const downloadUrl = asset.browser_download_url
  1053. const downloadResponse = await fetch(downloadUrl)
  1054. if (!downloadResponse.ok) {
  1055. log.error("Failed to download lua-language-server")
  1056. return
  1057. }
  1058. const tempPath = path.join(Global.Path.bin, assetName)
  1059. await Bun.file(tempPath).write(downloadResponse)
  1060. // Unlike zls which is a single self-contained binary,
  1061. // lua-language-server needs supporting files (meta/, locale/, etc.)
  1062. // Extract entire archive to dedicated directory to preserve all files
  1063. const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
  1064. // Remove old installation if exists
  1065. const stats = await fs.stat(installDir).catch(() => undefined)
  1066. if (stats) {
  1067. await fs.rm(installDir, { force: true, recursive: true })
  1068. }
  1069. await fs.mkdir(installDir, { recursive: true })
  1070. if (ext === "zip") {
  1071. const ok = await $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => {
  1072. log.error("Failed to extract lua-language-server archive", { error })
  1073. })
  1074. if (!ok) return
  1075. } else {
  1076. const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => {
  1077. log.error("Failed to extract lua-language-server archive", { error })
  1078. })
  1079. if (!ok) return
  1080. }
  1081. await fs.rm(tempPath, { force: true })
  1082. // Binary is located in bin/ subdirectory within the extracted archive
  1083. bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
  1084. if (!(await Bun.file(bin).exists())) {
  1085. log.error("Failed to extract lua-language-server binary")
  1086. return
  1087. }
  1088. if (platform !== "win32") {
  1089. const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
  1090. log.error("Failed to set executable permission for lua-language-server binary", {
  1091. error,
  1092. })
  1093. })
  1094. if (!ok) return
  1095. }
  1096. log.info(`installed lua-language-server`, { bin })
  1097. }
  1098. return {
  1099. process: spawn(bin, {
  1100. cwd: root,
  1101. }),
  1102. }
  1103. },
  1104. }
  1105. export const PHPIntelephense: Info = {
  1106. id: "php intelephense",
  1107. extensions: [".php"],
  1108. root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
  1109. async spawn(root) {
  1110. let binary = Bun.which("intelephense")
  1111. const args: string[] = []
  1112. if (!binary) {
  1113. const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
  1114. if (!(await Bun.file(js).exists())) {
  1115. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1116. await Bun.spawn([BunProc.which(), "install", "intelephense"], {
  1117. cwd: Global.Path.bin,
  1118. env: {
  1119. ...process.env,
  1120. BUN_BE_BUN: "1",
  1121. },
  1122. stdout: "pipe",
  1123. stderr: "pipe",
  1124. stdin: "pipe",
  1125. }).exited
  1126. }
  1127. binary = BunProc.which()
  1128. args.push("run", js)
  1129. }
  1130. args.push("--stdio")
  1131. const proc = spawn(binary, args, {
  1132. cwd: root,
  1133. env: {
  1134. ...process.env,
  1135. BUN_BE_BUN: "1",
  1136. },
  1137. })
  1138. return {
  1139. process: proc,
  1140. initialization: {},
  1141. }
  1142. },
  1143. }
  1144. export const Dart: Info = {
  1145. id: "dart",
  1146. extensions: [".dart"],
  1147. root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
  1148. async spawn(root) {
  1149. const dart = Bun.which("dart")
  1150. if (!dart) {
  1151. log.info("dart not found, please install dart first")
  1152. return
  1153. }
  1154. return {
  1155. process: spawn(dart, ["language-server", "--lsp"], {
  1156. cwd: root,
  1157. }),
  1158. }
  1159. },
  1160. }
  1161. export const Ocaml: Info = {
  1162. id: "ocaml-lsp",
  1163. extensions: [".ml", ".mli"],
  1164. root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
  1165. async spawn(root) {
  1166. const bin = Bun.which("ocamllsp")
  1167. if (!bin) {
  1168. log.info("ocamllsp not found, please install ocaml-lsp-server")
  1169. return
  1170. }
  1171. return {
  1172. process: spawn(bin, {
  1173. cwd: root,
  1174. }),
  1175. }
  1176. },
  1177. }
  1178. export const BashLS: Info = {
  1179. id: "bash",
  1180. extensions: [".sh", ".bash", ".zsh", ".ksh"],
  1181. root: async () => Instance.directory,
  1182. async spawn(root) {
  1183. let binary = Bun.which("bash-language-server")
  1184. const args: string[] = []
  1185. if (!binary) {
  1186. const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
  1187. if (!(await Bun.file(js).exists())) {
  1188. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1189. await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
  1190. cwd: Global.Path.bin,
  1191. env: {
  1192. ...process.env,
  1193. BUN_BE_BUN: "1",
  1194. },
  1195. stdout: "pipe",
  1196. stderr: "pipe",
  1197. stdin: "pipe",
  1198. }).exited
  1199. }
  1200. binary = BunProc.which()
  1201. args.push("run", js)
  1202. }
  1203. args.push("start")
  1204. const proc = spawn(binary, args, {
  1205. cwd: root,
  1206. env: {
  1207. ...process.env,
  1208. BUN_BE_BUN: "1",
  1209. },
  1210. })
  1211. return {
  1212. process: proc,
  1213. }
  1214. },
  1215. }
  1216. export const TerraformLS: Info = {
  1217. id: "terraform",
  1218. extensions: [".tf", ".tfvars"],
  1219. root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
  1220. async spawn(root) {
  1221. let bin = Bun.which("terraform-ls", {
  1222. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  1223. })
  1224. if (!bin) {
  1225. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1226. log.info("downloading terraform-ls from GitHub releases")
  1227. const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
  1228. if (!releaseResponse.ok) {
  1229. log.error("Failed to fetch terraform-ls release info")
  1230. return
  1231. }
  1232. const release = (await releaseResponse.json()) as {
  1233. tag_name?: string
  1234. assets?: { name?: string; browser_download_url?: string }[]
  1235. }
  1236. const version = release.tag_name?.replace("v", "")
  1237. if (!version) {
  1238. log.error("terraform-ls release did not include a version tag")
  1239. return
  1240. }
  1241. const platform = process.platform
  1242. const arch = process.arch
  1243. const tfArch = arch === "arm64" ? "arm64" : "amd64"
  1244. const tfPlatform = platform === "win32" ? "windows" : platform
  1245. const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
  1246. const assets = release.assets ?? []
  1247. const asset = assets.find((a) => a.name === assetName)
  1248. if (!asset?.browser_download_url) {
  1249. log.error(`Could not find asset ${assetName} in terraform-ls release`)
  1250. return
  1251. }
  1252. const downloadResponse = await fetch(asset.browser_download_url)
  1253. if (!downloadResponse.ok) {
  1254. log.error("Failed to download terraform-ls")
  1255. return
  1256. }
  1257. const tempPath = path.join(Global.Path.bin, assetName)
  1258. await Bun.file(tempPath).write(downloadResponse)
  1259. await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
  1260. await fs.rm(tempPath, { force: true })
  1261. bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
  1262. if (!(await Bun.file(bin).exists())) {
  1263. log.error("Failed to extract terraform-ls binary")
  1264. return
  1265. }
  1266. if (platform !== "win32") {
  1267. await $`chmod +x ${bin}`.nothrow()
  1268. }
  1269. log.info(`installed terraform-ls`, { bin })
  1270. }
  1271. return {
  1272. process: spawn(bin, ["serve"], {
  1273. cwd: root,
  1274. }),
  1275. initialization: {
  1276. experimentalFeatures: {
  1277. prefillRequiredFields: true,
  1278. validateOnSave: true,
  1279. },
  1280. },
  1281. }
  1282. },
  1283. }
  1284. export const TexLab: Info = {
  1285. id: "texlab",
  1286. extensions: [".tex", ".bib"],
  1287. root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
  1288. async spawn(root) {
  1289. let bin = Bun.which("texlab", {
  1290. PATH: process.env["PATH"] + ":" + Global.Path.bin,
  1291. })
  1292. if (!bin) {
  1293. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1294. log.info("downloading texlab from GitHub releases")
  1295. const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
  1296. if (!response.ok) {
  1297. log.error("Failed to fetch texlab release info")
  1298. return
  1299. }
  1300. const release = (await response.json()) as {
  1301. tag_name?: string
  1302. assets?: { name?: string; browser_download_url?: string }[]
  1303. }
  1304. const version = release.tag_name?.replace("v", "")
  1305. if (!version) {
  1306. log.error("texlab release did not include a version tag")
  1307. return
  1308. }
  1309. const platform = process.platform
  1310. const arch = process.arch
  1311. const texArch = arch === "arm64" ? "aarch64" : "x86_64"
  1312. const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
  1313. const ext = platform === "win32" ? "zip" : "tar.gz"
  1314. const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
  1315. const assets = release.assets ?? []
  1316. const asset = assets.find((a) => a.name === assetName)
  1317. if (!asset?.browser_download_url) {
  1318. log.error(`Could not find asset ${assetName} in texlab release`)
  1319. return
  1320. }
  1321. const downloadResponse = await fetch(asset.browser_download_url)
  1322. if (!downloadResponse.ok) {
  1323. log.error("Failed to download texlab")
  1324. return
  1325. }
  1326. const tempPath = path.join(Global.Path.bin, assetName)
  1327. await Bun.file(tempPath).write(downloadResponse)
  1328. if (ext === "zip") {
  1329. await $`unzip -o -q ${tempPath}`.cwd(Global.Path.bin).nothrow()
  1330. }
  1331. if (ext === "tar.gz") {
  1332. await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).nothrow()
  1333. }
  1334. await fs.rm(tempPath, { force: true })
  1335. bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
  1336. if (!(await Bun.file(bin).exists())) {
  1337. log.error("Failed to extract texlab binary")
  1338. return
  1339. }
  1340. if (platform !== "win32") {
  1341. await $`chmod +x ${bin}`.nothrow()
  1342. }
  1343. log.info("installed texlab", { bin })
  1344. }
  1345. return {
  1346. process: spawn(bin, {
  1347. cwd: root,
  1348. }),
  1349. }
  1350. },
  1351. }
  1352. export const DockerfileLS: Info = {
  1353. id: "dockerfile",
  1354. extensions: [".dockerfile", "Dockerfile"],
  1355. root: async () => Instance.directory,
  1356. async spawn(root) {
  1357. let binary = Bun.which("docker-langserver")
  1358. const args: string[] = []
  1359. if (!binary) {
  1360. const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
  1361. if (!(await Bun.file(js).exists())) {
  1362. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1363. await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
  1364. cwd: Global.Path.bin,
  1365. env: {
  1366. ...process.env,
  1367. BUN_BE_BUN: "1",
  1368. },
  1369. stdout: "pipe",
  1370. stderr: "pipe",
  1371. stdin: "pipe",
  1372. }).exited
  1373. }
  1374. binary = BunProc.which()
  1375. args.push("run", js)
  1376. }
  1377. args.push("--stdio")
  1378. const proc = spawn(binary, args, {
  1379. cwd: root,
  1380. env: {
  1381. ...process.env,
  1382. BUN_BE_BUN: "1",
  1383. },
  1384. })
  1385. return {
  1386. process: proc,
  1387. }
  1388. },
  1389. }
  1390. export const Gleam: Info = {
  1391. id: "gleam",
  1392. extensions: [".gleam"],
  1393. root: NearestRoot(["gleam.toml"]),
  1394. async spawn(root) {
  1395. const gleam = Bun.which("gleam")
  1396. if (!gleam) {
  1397. log.info("gleam not found, please install gleam first")
  1398. return
  1399. }
  1400. return {
  1401. process: spawn(gleam, ["lsp"], {
  1402. cwd: root,
  1403. }),
  1404. }
  1405. },
  1406. }
  1407. }