server.ts 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011
  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 { $, readableStreamToText } 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. import { Archive } from "../util/archive"
  13. export namespace LSPServer {
  14. const log = Log.create({ service: "lsp.server" })
  15. export interface Handle {
  16. process: ChildProcessWithoutNullStreams
  17. initialization?: Record<string, any>
  18. }
  19. type RootFunction = (file: string) => Promise<string | undefined>
  20. const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
  21. return async (file) => {
  22. if (excludePatterns) {
  23. const excludedFiles = Filesystem.up({
  24. targets: excludePatterns,
  25. start: path.dirname(file),
  26. stop: Instance.directory,
  27. })
  28. const excluded = await excludedFiles.next()
  29. await excludedFiles.return()
  30. if (excluded.value) return undefined
  31. }
  32. const files = Filesystem.up({
  33. targets: includePatterns,
  34. start: path.dirname(file),
  35. stop: Instance.directory,
  36. })
  37. const first = await files.next()
  38. await files.return()
  39. if (!first.value) return Instance.directory
  40. return path.dirname(first.value)
  41. }
  42. }
  43. export interface Info {
  44. id: string
  45. extensions: string[]
  46. global?: boolean
  47. root: RootFunction
  48. spawn(root: string): Promise<Handle | undefined>
  49. }
  50. export const Deno: Info = {
  51. id: "deno",
  52. root: async (file) => {
  53. const files = Filesystem.up({
  54. targets: ["deno.json", "deno.jsonc"],
  55. start: path.dirname(file),
  56. stop: Instance.directory,
  57. })
  58. const first = await files.next()
  59. await files.return()
  60. if (!first.value) return undefined
  61. return path.dirname(first.value)
  62. },
  63. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
  64. async spawn(root) {
  65. const deno = Bun.which("deno")
  66. if (!deno) {
  67. log.info("deno not found, please install deno first")
  68. return
  69. }
  70. return {
  71. process: spawn(deno, ["lsp"], {
  72. cwd: root,
  73. }),
  74. }
  75. },
  76. }
  77. export const Typescript: Info = {
  78. id: "typescript",
  79. root: NearestRoot(
  80. ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
  81. ["deno.json", "deno.jsonc"],
  82. ),
  83. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
  84. async spawn(root) {
  85. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  86. log.info("typescript server", { tsserver })
  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(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  109. async spawn(root) {
  110. let binary = Bun.which("vue-language-server")
  111. const args: string[] = []
  112. if (!binary) {
  113. const js = path.join(
  114. Global.Path.bin,
  115. "node_modules",
  116. "@vue",
  117. "language-server",
  118. "bin",
  119. "vue-language-server.js",
  120. )
  121. if (!(await Bun.file(js).exists())) {
  122. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  123. await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
  124. cwd: Global.Path.bin,
  125. env: {
  126. ...process.env,
  127. BUN_BE_BUN: "1",
  128. },
  129. stdout: "pipe",
  130. stderr: "pipe",
  131. stdin: "pipe",
  132. }).exited
  133. }
  134. binary = BunProc.which()
  135. args.push("run", js)
  136. }
  137. args.push("--stdio")
  138. const proc = spawn(binary, args, {
  139. cwd: root,
  140. env: {
  141. ...process.env,
  142. BUN_BE_BUN: "1",
  143. },
  144. })
  145. return {
  146. process: proc,
  147. initialization: {
  148. // Leave empty; the server will auto-detect workspace TypeScript.
  149. },
  150. }
  151. },
  152. }
  153. export const ESLint: Info = {
  154. id: "eslint",
  155. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  156. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
  157. async spawn(root) {
  158. const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
  159. if (!eslint) return
  160. log.info("spawning eslint server")
  161. const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
  162. if (!(await Bun.file(serverPath).exists())) {
  163. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  164. log.info("downloading and building VS Code ESLint server")
  165. const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
  166. if (!response.ok) return
  167. const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
  168. await Bun.file(zipPath).write(response)
  169. const ok = await Archive.extractZip(zipPath, Global.Path.bin)
  170. .then(() => true)
  171. .catch((error) => {
  172. log.error("Failed to extract vscode-eslint archive", { error })
  173. return false
  174. })
  175. if (!ok) return
  176. await fs.rm(zipPath, { force: true })
  177. const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
  178. const finalPath = path.join(Global.Path.bin, "vscode-eslint")
  179. const stats = await fs.stat(finalPath).catch(() => undefined)
  180. if (stats) {
  181. log.info("removing old eslint installation", { path: finalPath })
  182. await fs.rm(finalPath, { force: true, recursive: true })
  183. }
  184. await fs.rename(extractedPath, finalPath)
  185. const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
  186. await $`${npmCmd} install`.cwd(finalPath).quiet()
  187. await $`${npmCmd} run compile`.cwd(finalPath).quiet()
  188. log.info("installed VS Code ESLint server", { serverPath })
  189. }
  190. const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
  191. cwd: root,
  192. env: {
  193. ...process.env,
  194. BUN_BE_BUN: "1",
  195. },
  196. })
  197. return {
  198. process: proc,
  199. }
  200. },
  201. }
  202. export const Oxlint: Info = {
  203. id: "oxlint",
  204. root: NearestRoot([
  205. ".oxlintrc.json",
  206. "package-lock.json",
  207. "bun.lockb",
  208. "bun.lock",
  209. "pnpm-lock.yaml",
  210. "yarn.lock",
  211. "package.json",
  212. ]),
  213. extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
  214. async spawn(root) {
  215. const ext = process.platform === "win32" ? ".cmd" : ""
  216. const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
  217. const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext)
  218. const resolveBin = async (target: string) => {
  219. const localBin = path.join(root, target)
  220. if (await Bun.file(localBin).exists()) return localBin
  221. const candidates = Filesystem.up({
  222. targets: [target],
  223. start: root,
  224. stop: Instance.worktree,
  225. })
  226. const first = await candidates.next()
  227. await candidates.return()
  228. if (first.value) return first.value
  229. return undefined
  230. }
  231. let lintBin = await resolveBin(lintTarget)
  232. if (!lintBin) {
  233. const found = Bun.which("oxlint")
  234. if (found) lintBin = found
  235. }
  236. if (lintBin) {
  237. const proc = Bun.spawn([lintBin, "--help"], { stdout: "pipe" })
  238. await proc.exited
  239. const help = await readableStreamToText(proc.stdout)
  240. if (help.includes("--lsp")) {
  241. return {
  242. process: spawn(lintBin, ["--lsp"], {
  243. cwd: root,
  244. }),
  245. }
  246. }
  247. }
  248. let serverBin = await resolveBin(serverTarget)
  249. if (!serverBin) {
  250. const found = Bun.which("oxc_language_server")
  251. if (found) serverBin = found
  252. }
  253. if (serverBin) {
  254. return {
  255. process: spawn(serverBin, [], {
  256. cwd: root,
  257. }),
  258. }
  259. }
  260. log.info("oxlint not found, please install oxlint")
  261. return
  262. },
  263. }
  264. export const Biome: Info = {
  265. id: "biome",
  266. root: NearestRoot([
  267. "biome.json",
  268. "biome.jsonc",
  269. "package-lock.json",
  270. "bun.lockb",
  271. "bun.lock",
  272. "pnpm-lock.yaml",
  273. "yarn.lock",
  274. ]),
  275. extensions: [
  276. ".ts",
  277. ".tsx",
  278. ".js",
  279. ".jsx",
  280. ".mjs",
  281. ".cjs",
  282. ".mts",
  283. ".cts",
  284. ".json",
  285. ".jsonc",
  286. ".vue",
  287. ".astro",
  288. ".svelte",
  289. ".css",
  290. ".graphql",
  291. ".gql",
  292. ".html",
  293. ],
  294. async spawn(root) {
  295. const localBin = path.join(root, "node_modules", ".bin", "biome")
  296. let bin: string | undefined
  297. if (await Bun.file(localBin).exists()) bin = localBin
  298. if (!bin) {
  299. const found = Bun.which("biome")
  300. if (found) bin = found
  301. }
  302. let args = ["lsp-proxy", "--stdio"]
  303. if (!bin) {
  304. const resolved = await Bun.resolve("biome", root).catch(() => undefined)
  305. if (!resolved) return
  306. bin = BunProc.which()
  307. args = ["x", "biome", "lsp-proxy", "--stdio"]
  308. }
  309. const proc = spawn(bin, args, {
  310. cwd: root,
  311. env: {
  312. ...process.env,
  313. BUN_BE_BUN: "1",
  314. },
  315. })
  316. return {
  317. process: proc,
  318. }
  319. },
  320. }
  321. export const Gopls: Info = {
  322. id: "gopls",
  323. root: async (file) => {
  324. const work = await NearestRoot(["go.work"])(file)
  325. if (work) return work
  326. return NearestRoot(["go.mod", "go.sum"])(file)
  327. },
  328. extensions: [".go"],
  329. async spawn(root) {
  330. let bin = Bun.which("gopls", {
  331. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  332. })
  333. if (!bin) {
  334. if (!Bun.which("go")) return
  335. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  336. log.info("installing gopls")
  337. const proc = Bun.spawn({
  338. cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
  339. env: { ...process.env, GOBIN: Global.Path.bin },
  340. stdout: "pipe",
  341. stderr: "pipe",
  342. stdin: "pipe",
  343. })
  344. const exit = await proc.exited
  345. if (exit !== 0) {
  346. log.error("Failed to install gopls")
  347. return
  348. }
  349. bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
  350. log.info(`installed gopls`, {
  351. bin,
  352. })
  353. }
  354. return {
  355. process: spawn(bin!, {
  356. cwd: root,
  357. }),
  358. }
  359. },
  360. }
  361. export const Rubocop: Info = {
  362. id: "ruby-lsp",
  363. root: NearestRoot(["Gemfile"]),
  364. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  365. async spawn(root) {
  366. let bin = Bun.which("rubocop", {
  367. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  368. })
  369. if (!bin) {
  370. const ruby = Bun.which("ruby")
  371. const gem = Bun.which("gem")
  372. if (!ruby || !gem) {
  373. log.info("Ruby not found, please install Ruby first")
  374. return
  375. }
  376. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  377. log.info("installing rubocop")
  378. const proc = Bun.spawn({
  379. cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
  380. stdout: "pipe",
  381. stderr: "pipe",
  382. stdin: "pipe",
  383. })
  384. const exit = await proc.exited
  385. if (exit !== 0) {
  386. log.error("Failed to install rubocop")
  387. return
  388. }
  389. bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
  390. log.info(`installed rubocop`, {
  391. bin,
  392. })
  393. }
  394. return {
  395. process: spawn(bin!, ["--lsp"], {
  396. cwd: root,
  397. }),
  398. }
  399. },
  400. }
  401. export const Ty: Info = {
  402. id: "ty",
  403. extensions: [".py", ".pyi"],
  404. root: NearestRoot([
  405. "pyproject.toml",
  406. "ty.toml",
  407. "setup.py",
  408. "setup.cfg",
  409. "requirements.txt",
  410. "Pipfile",
  411. "pyrightconfig.json",
  412. ]),
  413. async spawn(root) {
  414. if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
  415. return undefined
  416. }
  417. let binary = Bun.which("ty")
  418. const initialization: Record<string, string> = {}
  419. const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
  420. (p): p is string => p !== undefined,
  421. )
  422. for (const venvPath of potentialVenvPaths) {
  423. const isWindows = process.platform === "win32"
  424. const potentialPythonPath = isWindows
  425. ? path.join(venvPath, "Scripts", "python.exe")
  426. : path.join(venvPath, "bin", "python")
  427. if (await Bun.file(potentialPythonPath).exists()) {
  428. initialization["pythonPath"] = potentialPythonPath
  429. break
  430. }
  431. }
  432. if (!binary) {
  433. for (const venvPath of potentialVenvPaths) {
  434. const isWindows = process.platform === "win32"
  435. const potentialTyPath = isWindows
  436. ? path.join(venvPath, "Scripts", "ty.exe")
  437. : path.join(venvPath, "bin", "ty")
  438. if (await Bun.file(potentialTyPath).exists()) {
  439. binary = potentialTyPath
  440. break
  441. }
  442. }
  443. }
  444. if (!binary) {
  445. log.error("ty not found, please install ty first")
  446. return
  447. }
  448. const proc = spawn(binary, ["server"], {
  449. cwd: root,
  450. })
  451. return {
  452. process: proc,
  453. initialization,
  454. }
  455. },
  456. }
  457. export const Pyright: Info = {
  458. id: "pyright",
  459. extensions: [".py", ".pyi"],
  460. root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
  461. async spawn(root) {
  462. let binary = Bun.which("pyright-langserver")
  463. const args = []
  464. if (!binary) {
  465. const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
  466. if (!(await Bun.file(js).exists())) {
  467. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  468. await Bun.spawn([BunProc.which(), "install", "pyright"], {
  469. cwd: Global.Path.bin,
  470. env: {
  471. ...process.env,
  472. BUN_BE_BUN: "1",
  473. },
  474. }).exited
  475. }
  476. binary = BunProc.which()
  477. args.push(...["run", js])
  478. }
  479. args.push("--stdio")
  480. const initialization: Record<string, string> = {}
  481. const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
  482. (p): p is string => p !== undefined,
  483. )
  484. for (const venvPath of potentialVenvPaths) {
  485. const isWindows = process.platform === "win32"
  486. const potentialPythonPath = isWindows
  487. ? path.join(venvPath, "Scripts", "python.exe")
  488. : path.join(venvPath, "bin", "python")
  489. if (await Bun.file(potentialPythonPath).exists()) {
  490. initialization["pythonPath"] = potentialPythonPath
  491. break
  492. }
  493. }
  494. const proc = spawn(binary, args, {
  495. cwd: root,
  496. env: {
  497. ...process.env,
  498. BUN_BE_BUN: "1",
  499. },
  500. })
  501. return {
  502. process: proc,
  503. initialization,
  504. }
  505. },
  506. }
  507. export const ElixirLS: Info = {
  508. id: "elixir-ls",
  509. extensions: [".ex", ".exs"],
  510. root: NearestRoot(["mix.exs", "mix.lock"]),
  511. async spawn(root) {
  512. let binary = Bun.which("elixir-ls")
  513. if (!binary) {
  514. const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
  515. binary = path.join(
  516. Global.Path.bin,
  517. "elixir-ls-master",
  518. "release",
  519. process.platform === "win32" ? "language_server.bat" : "language_server.sh",
  520. )
  521. if (!(await Bun.file(binary).exists())) {
  522. const elixir = Bun.which("elixir")
  523. if (!elixir) {
  524. log.error("elixir is required to run elixir-ls")
  525. return
  526. }
  527. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  528. log.info("downloading elixir-ls from GitHub releases")
  529. const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
  530. if (!response.ok) return
  531. const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
  532. await Bun.file(zipPath).write(response)
  533. const ok = await Archive.extractZip(zipPath, Global.Path.bin)
  534. .then(() => true)
  535. .catch((error) => {
  536. log.error("Failed to extract elixir-ls archive", { error })
  537. return false
  538. })
  539. if (!ok) return
  540. await fs.rm(zipPath, {
  541. force: true,
  542. recursive: true,
  543. })
  544. await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
  545. .quiet()
  546. .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
  547. .env({ MIX_ENV: "prod", ...process.env })
  548. log.info(`installed elixir-ls`, {
  549. path: elixirLsPath,
  550. })
  551. }
  552. }
  553. return {
  554. process: spawn(binary, {
  555. cwd: root,
  556. }),
  557. }
  558. },
  559. }
  560. export const Zls: Info = {
  561. id: "zls",
  562. extensions: [".zig", ".zon"],
  563. root: NearestRoot(["build.zig"]),
  564. async spawn(root) {
  565. let bin = Bun.which("zls", {
  566. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  567. })
  568. if (!bin) {
  569. const zig = Bun.which("zig")
  570. if (!zig) {
  571. log.error("Zig is required to use zls. Please install Zig first.")
  572. return
  573. }
  574. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  575. log.info("downloading zls from GitHub releases")
  576. const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
  577. if (!releaseResponse.ok) {
  578. log.error("Failed to fetch zls release info")
  579. return
  580. }
  581. const release = (await releaseResponse.json()) as any
  582. const platform = process.platform
  583. const arch = process.arch
  584. let assetName = ""
  585. let zlsArch: string = arch
  586. if (arch === "arm64") zlsArch = "aarch64"
  587. else if (arch === "x64") zlsArch = "x86_64"
  588. else if (arch === "ia32") zlsArch = "x86"
  589. let zlsPlatform: string = platform
  590. if (platform === "darwin") zlsPlatform = "macos"
  591. else if (platform === "win32") zlsPlatform = "windows"
  592. const ext = platform === "win32" ? "zip" : "tar.xz"
  593. assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
  594. const supportedCombos = [
  595. "zls-x86_64-linux.tar.xz",
  596. "zls-x86_64-macos.tar.xz",
  597. "zls-x86_64-windows.zip",
  598. "zls-aarch64-linux.tar.xz",
  599. "zls-aarch64-macos.tar.xz",
  600. "zls-aarch64-windows.zip",
  601. "zls-x86-linux.tar.xz",
  602. "zls-x86-windows.zip",
  603. ]
  604. if (!supportedCombos.includes(assetName)) {
  605. log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
  606. return
  607. }
  608. const asset = release.assets.find((a: any) => a.name === assetName)
  609. if (!asset) {
  610. log.error(`Could not find asset ${assetName} in latest zls release`)
  611. return
  612. }
  613. const downloadUrl = asset.browser_download_url
  614. const downloadResponse = await fetch(downloadUrl)
  615. if (!downloadResponse.ok) {
  616. log.error("Failed to download zls")
  617. return
  618. }
  619. const tempPath = path.join(Global.Path.bin, assetName)
  620. await Bun.file(tempPath).write(downloadResponse)
  621. if (ext === "zip") {
  622. const ok = await Archive.extractZip(tempPath, Global.Path.bin)
  623. .then(() => true)
  624. .catch((error) => {
  625. log.error("Failed to extract zls archive", { error })
  626. return false
  627. })
  628. if (!ok) return
  629. } else {
  630. await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
  631. }
  632. await fs.rm(tempPath, { force: true })
  633. bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
  634. if (!(await Bun.file(bin).exists())) {
  635. log.error("Failed to extract zls binary")
  636. return
  637. }
  638. if (platform !== "win32") {
  639. await $`chmod +x ${bin}`.quiet().nothrow()
  640. }
  641. log.info(`installed zls`, { bin })
  642. }
  643. return {
  644. process: spawn(bin, {
  645. cwd: root,
  646. }),
  647. }
  648. },
  649. }
  650. export const CSharp: Info = {
  651. id: "csharp",
  652. root: NearestRoot([".sln", ".csproj", "global.json"]),
  653. extensions: [".cs"],
  654. async spawn(root) {
  655. let bin = Bun.which("csharp-ls", {
  656. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  657. })
  658. if (!bin) {
  659. if (!Bun.which("dotnet")) {
  660. log.error(".NET SDK is required to install csharp-ls")
  661. return
  662. }
  663. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  664. log.info("installing csharp-ls via dotnet tool")
  665. const proc = Bun.spawn({
  666. cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
  667. stdout: "pipe",
  668. stderr: "pipe",
  669. stdin: "pipe",
  670. })
  671. const exit = await proc.exited
  672. if (exit !== 0) {
  673. log.error("Failed to install csharp-ls")
  674. return
  675. }
  676. bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
  677. log.info(`installed csharp-ls`, { bin })
  678. }
  679. return {
  680. process: spawn(bin, {
  681. cwd: root,
  682. }),
  683. }
  684. },
  685. }
  686. export const FSharp: Info = {
  687. id: "fsharp",
  688. root: NearestRoot([".sln", ".fsproj", "global.json"]),
  689. extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
  690. async spawn(root) {
  691. let bin = Bun.which("fsautocomplete", {
  692. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  693. })
  694. if (!bin) {
  695. if (!Bun.which("dotnet")) {
  696. log.error(".NET SDK is required to install fsautocomplete")
  697. return
  698. }
  699. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  700. log.info("installing fsautocomplete via dotnet tool")
  701. const proc = Bun.spawn({
  702. cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin],
  703. stdout: "pipe",
  704. stderr: "pipe",
  705. stdin: "pipe",
  706. })
  707. const exit = await proc.exited
  708. if (exit !== 0) {
  709. log.error("Failed to install fsautocomplete")
  710. return
  711. }
  712. bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
  713. log.info(`installed fsautocomplete`, { bin })
  714. }
  715. return {
  716. process: spawn(bin, {
  717. cwd: root,
  718. }),
  719. }
  720. },
  721. }
  722. export const SourceKit: Info = {
  723. id: "sourcekit-lsp",
  724. extensions: [".swift", ".objc", "objcpp"],
  725. root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
  726. async spawn(root) {
  727. // Check if sourcekit-lsp is available in the PATH
  728. // This is installed with the Swift toolchain
  729. const sourcekit = Bun.which("sourcekit-lsp")
  730. if (sourcekit) {
  731. return {
  732. process: spawn(sourcekit, {
  733. cwd: root,
  734. }),
  735. }
  736. }
  737. // If sourcekit-lsp not found, check if xcrun is available
  738. // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
  739. if (!Bun.which("xcrun")) return
  740. const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
  741. if (lspLoc.exitCode !== 0) return
  742. const bin = lspLoc.text().trim()
  743. return {
  744. process: spawn(bin, {
  745. cwd: root,
  746. }),
  747. }
  748. },
  749. }
  750. export const RustAnalyzer: Info = {
  751. id: "rust",
  752. root: async (root) => {
  753. const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
  754. if (crateRoot === undefined) {
  755. return undefined
  756. }
  757. let currentDir = crateRoot
  758. while (currentDir !== path.dirname(currentDir)) {
  759. // Stop at filesystem root
  760. const cargoTomlPath = path.join(currentDir, "Cargo.toml")
  761. try {
  762. const cargoTomlContent = await Bun.file(cargoTomlPath).text()
  763. if (cargoTomlContent.includes("[workspace]")) {
  764. return currentDir
  765. }
  766. } catch (err) {
  767. // File doesn't exist or can't be read, continue searching up
  768. }
  769. const parentDir = path.dirname(currentDir)
  770. if (parentDir === currentDir) break // Reached filesystem root
  771. currentDir = parentDir
  772. // Stop if we've gone above the app root
  773. if (!currentDir.startsWith(Instance.worktree)) break
  774. }
  775. return crateRoot
  776. },
  777. extensions: [".rs"],
  778. async spawn(root) {
  779. const bin = Bun.which("rust-analyzer")
  780. if (!bin) {
  781. log.info("rust-analyzer not found in path, please install it")
  782. return
  783. }
  784. return {
  785. process: spawn(bin, {
  786. cwd: root,
  787. }),
  788. }
  789. },
  790. }
  791. export const Clangd: Info = {
  792. id: "clangd",
  793. root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
  794. extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
  795. async spawn(root) {
  796. const args = ["--background-index", "--clang-tidy"]
  797. const fromPath = Bun.which("clangd")
  798. if (fromPath) {
  799. return {
  800. process: spawn(fromPath, args, {
  801. cwd: root,
  802. }),
  803. }
  804. }
  805. const ext = process.platform === "win32" ? ".exe" : ""
  806. const direct = path.join(Global.Path.bin, "clangd" + ext)
  807. if (await Bun.file(direct).exists()) {
  808. return {
  809. process: spawn(direct, args, {
  810. cwd: root,
  811. }),
  812. }
  813. }
  814. const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
  815. for (const entry of entries) {
  816. if (!entry.isDirectory()) continue
  817. if (!entry.name.startsWith("clangd_")) continue
  818. const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
  819. if (await Bun.file(candidate).exists()) {
  820. return {
  821. process: spawn(candidate, args, {
  822. cwd: root,
  823. }),
  824. }
  825. }
  826. }
  827. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  828. log.info("downloading clangd from GitHub releases")
  829. const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
  830. if (!releaseResponse.ok) {
  831. log.error("Failed to fetch clangd release info")
  832. return
  833. }
  834. const release: {
  835. tag_name?: string
  836. assets?: { name?: string; browser_download_url?: string }[]
  837. } = await releaseResponse.json()
  838. const tag = release.tag_name
  839. if (!tag) {
  840. log.error("clangd release did not include a tag name")
  841. return
  842. }
  843. const platform = process.platform
  844. const tokens: Record<string, string> = {
  845. darwin: "mac",
  846. linux: "linux",
  847. win32: "windows",
  848. }
  849. const token = tokens[platform]
  850. if (!token) {
  851. log.error(`Platform ${platform} is not supported by clangd auto-download`)
  852. return
  853. }
  854. const assets = release.assets ?? []
  855. const valid = (item: { name?: string; browser_download_url?: string }) => {
  856. if (!item.name) return false
  857. if (!item.browser_download_url) return false
  858. if (!item.name.includes(token)) return false
  859. return item.name.includes(tag)
  860. }
  861. const asset =
  862. assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
  863. assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
  864. assets.find((item) => valid(item))
  865. if (!asset?.name || !asset.browser_download_url) {
  866. log.error("clangd could not match release asset", { tag, platform })
  867. return
  868. }
  869. const name = asset.name
  870. const downloadResponse = await fetch(asset.browser_download_url)
  871. if (!downloadResponse.ok) {
  872. log.error("Failed to download clangd")
  873. return
  874. }
  875. const archive = path.join(Global.Path.bin, name)
  876. const buf = await downloadResponse.arrayBuffer()
  877. if (buf.byteLength === 0) {
  878. log.error("Failed to write clangd archive")
  879. return
  880. }
  881. await Bun.write(archive, buf)
  882. const zip = name.endsWith(".zip")
  883. const tar = name.endsWith(".tar.xz")
  884. if (!zip && !tar) {
  885. log.error("clangd encountered unsupported asset", { asset: name })
  886. return
  887. }
  888. if (zip) {
  889. const ok = await Archive.extractZip(archive, Global.Path.bin)
  890. .then(() => true)
  891. .catch((error) => {
  892. log.error("Failed to extract clangd archive", { error })
  893. return false
  894. })
  895. if (!ok) return
  896. }
  897. if (tar) {
  898. await $`tar -xf ${archive}`.cwd(Global.Path.bin).quiet().nothrow()
  899. }
  900. await fs.rm(archive, { force: true })
  901. const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
  902. if (!(await Bun.file(bin).exists())) {
  903. log.error("Failed to extract clangd binary")
  904. return
  905. }
  906. if (platform !== "win32") {
  907. await $`chmod +x ${bin}`.quiet().nothrow()
  908. }
  909. await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
  910. await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
  911. log.info(`installed clangd`, { bin })
  912. return {
  913. process: spawn(bin, args, {
  914. cwd: root,
  915. }),
  916. }
  917. },
  918. }
  919. export const Svelte: Info = {
  920. id: "svelte",
  921. extensions: [".svelte"],
  922. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  923. async spawn(root) {
  924. let binary = Bun.which("svelteserver")
  925. const args: string[] = []
  926. if (!binary) {
  927. const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
  928. if (!(await Bun.file(js).exists())) {
  929. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  930. await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
  931. cwd: Global.Path.bin,
  932. env: {
  933. ...process.env,
  934. BUN_BE_BUN: "1",
  935. },
  936. stdout: "pipe",
  937. stderr: "pipe",
  938. stdin: "pipe",
  939. }).exited
  940. }
  941. binary = BunProc.which()
  942. args.push("run", js)
  943. }
  944. args.push("--stdio")
  945. const proc = spawn(binary, args, {
  946. cwd: root,
  947. env: {
  948. ...process.env,
  949. BUN_BE_BUN: "1",
  950. },
  951. })
  952. return {
  953. process: proc,
  954. initialization: {},
  955. }
  956. },
  957. }
  958. export const Astro: Info = {
  959. id: "astro",
  960. extensions: [".astro"],
  961. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  962. async spawn(root) {
  963. const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
  964. if (!tsserver) {
  965. log.info("typescript not found, required for Astro language server")
  966. return
  967. }
  968. const tsdk = path.dirname(tsserver)
  969. let binary = Bun.which("astro-ls")
  970. const args: string[] = []
  971. if (!binary) {
  972. const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
  973. if (!(await Bun.file(js).exists())) {
  974. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  975. await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
  976. cwd: Global.Path.bin,
  977. env: {
  978. ...process.env,
  979. BUN_BE_BUN: "1",
  980. },
  981. stdout: "pipe",
  982. stderr: "pipe",
  983. stdin: "pipe",
  984. }).exited
  985. }
  986. binary = BunProc.which()
  987. args.push("run", js)
  988. }
  989. args.push("--stdio")
  990. const proc = spawn(binary, args, {
  991. cwd: root,
  992. env: {
  993. ...process.env,
  994. BUN_BE_BUN: "1",
  995. },
  996. })
  997. return {
  998. process: proc,
  999. initialization: {
  1000. typescript: {
  1001. tsdk,
  1002. },
  1003. },
  1004. }
  1005. },
  1006. }
  1007. export const JDTLS: Info = {
  1008. id: "jdtls",
  1009. root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
  1010. extensions: [".java"],
  1011. async spawn(root) {
  1012. const java = Bun.which("java")
  1013. if (!java) {
  1014. log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
  1015. return
  1016. }
  1017. const javaMajorVersion = await $`java -version`
  1018. .quiet()
  1019. .nothrow()
  1020. .then(({ stderr }) => {
  1021. const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
  1022. return !m ? undefined : parseInt(m[1])
  1023. })
  1024. if (javaMajorVersion == null || javaMajorVersion < 21) {
  1025. log.error("JDTLS requires at least Java 21.")
  1026. return
  1027. }
  1028. const distPath = path.join(Global.Path.bin, "jdtls")
  1029. const launcherDir = path.join(distPath, "plugins")
  1030. const installed = await fs.exists(launcherDir)
  1031. if (!installed) {
  1032. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1033. log.info("Downloading JDTLS LSP server.")
  1034. await fs.mkdir(distPath, { recursive: true })
  1035. const releaseURL =
  1036. "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
  1037. const archivePath = path.join(distPath, "release.tar.gz")
  1038. await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
  1039. await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
  1040. await fs.rm(archivePath, { force: true })
  1041. }
  1042. const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
  1043. .cwd(launcherDir)
  1044. .quiet()
  1045. .nothrow()
  1046. .then(({ stdout }) => stdout.toString().trim())
  1047. const launcherJar = path.join(launcherDir, jarFileName)
  1048. if (!(await fs.exists(launcherJar))) {
  1049. log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
  1050. return
  1051. }
  1052. const configFile = path.join(
  1053. distPath,
  1054. (() => {
  1055. switch (process.platform) {
  1056. case "darwin":
  1057. return "config_mac"
  1058. case "linux":
  1059. return "config_linux"
  1060. case "win32":
  1061. return "config_win"
  1062. default:
  1063. return "config_linux"
  1064. }
  1065. })(),
  1066. )
  1067. const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
  1068. return {
  1069. process: spawn(
  1070. java,
  1071. [
  1072. "-jar",
  1073. launcherJar,
  1074. "-configuration",
  1075. configFile,
  1076. "-data",
  1077. dataDir,
  1078. "-Declipse.application=org.eclipse.jdt.ls.core.id1",
  1079. "-Dosgi.bundles.defaultStartLevel=4",
  1080. "-Declipse.product=org.eclipse.jdt.ls.core.product",
  1081. "-Dlog.level=ALL",
  1082. "--add-modules=ALL-SYSTEM",
  1083. "--add-opens java.base/java.util=ALL-UNNAMED",
  1084. "--add-opens java.base/java.lang=ALL-UNNAMED",
  1085. ],
  1086. {
  1087. cwd: root,
  1088. },
  1089. ),
  1090. }
  1091. },
  1092. }
  1093. export const KotlinLS: Info = {
  1094. id: "kotlin-ls",
  1095. extensions: [".kt", ".kts"],
  1096. root: NearestRoot(["build.gradle", "build.gradle.kts", "settings.gradle.kts", "pom.xml"]),
  1097. async spawn(root) {
  1098. const distPath = path.join(Global.Path.bin, "kotlin-ls")
  1099. const launcherScript =
  1100. process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
  1101. const installed = await Bun.file(launcherScript).exists()
  1102. if (!installed) {
  1103. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1104. log.info("Downloading Kotlin Language Server from GitHub.")
  1105. const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
  1106. if (!releaseResponse.ok) {
  1107. log.error("Failed to fetch kotlin-lsp release info")
  1108. return
  1109. }
  1110. const release = await releaseResponse.json()
  1111. const version = release.name?.replace(/^v/, "")
  1112. if (!version) {
  1113. log.error("Could not determine Kotlin LSP version from release")
  1114. return
  1115. }
  1116. const platform = process.platform
  1117. const arch = process.arch
  1118. let kotlinArch: string = arch
  1119. if (arch === "arm64") kotlinArch = "aarch64"
  1120. else if (arch === "x64") kotlinArch = "x64"
  1121. let kotlinPlatform: string = platform
  1122. if (platform === "darwin") kotlinPlatform = "mac"
  1123. else if (platform === "linux") kotlinPlatform = "linux"
  1124. else if (platform === "win32") kotlinPlatform = "win"
  1125. const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"]
  1126. const combo = `${kotlinPlatform}-${kotlinArch}`
  1127. if (!supportedCombos.includes(combo)) {
  1128. log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
  1129. return
  1130. }
  1131. const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip`
  1132. const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}`
  1133. await fs.mkdir(distPath, { recursive: true })
  1134. const archivePath = path.join(distPath, "kotlin-ls.zip")
  1135. await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
  1136. const ok = await Archive.extractZip(archivePath, distPath)
  1137. .then(() => true)
  1138. .catch((error) => {
  1139. log.error("Failed to extract Kotlin LS archive", { error })
  1140. return false
  1141. })
  1142. if (!ok) return
  1143. await fs.rm(archivePath, { force: true })
  1144. if (process.platform !== "win32") {
  1145. await $`chmod +x ${launcherScript}`.quiet().nothrow()
  1146. }
  1147. log.info("Installed Kotlin Language Server", { path: launcherScript })
  1148. }
  1149. if (!(await Bun.file(launcherScript).exists())) {
  1150. log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
  1151. return
  1152. }
  1153. return {
  1154. process: spawn(launcherScript, ["--stdio"], {
  1155. cwd: root,
  1156. }),
  1157. }
  1158. },
  1159. }
  1160. export const YamlLS: Info = {
  1161. id: "yaml-ls",
  1162. extensions: [".yaml", ".yml"],
  1163. root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
  1164. async spawn(root) {
  1165. let binary = Bun.which("yaml-language-server")
  1166. const args: string[] = []
  1167. if (!binary) {
  1168. const js = path.join(
  1169. Global.Path.bin,
  1170. "node_modules",
  1171. "yaml-language-server",
  1172. "out",
  1173. "server",
  1174. "src",
  1175. "server.js",
  1176. )
  1177. const exists = await Bun.file(js).exists()
  1178. if (!exists) {
  1179. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1180. await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
  1181. cwd: Global.Path.bin,
  1182. env: {
  1183. ...process.env,
  1184. BUN_BE_BUN: "1",
  1185. },
  1186. stdout: "pipe",
  1187. stderr: "pipe",
  1188. stdin: "pipe",
  1189. }).exited
  1190. }
  1191. binary = BunProc.which()
  1192. args.push("run", js)
  1193. }
  1194. args.push("--stdio")
  1195. const proc = spawn(binary, args, {
  1196. cwd: root,
  1197. env: {
  1198. ...process.env,
  1199. BUN_BE_BUN: "1",
  1200. },
  1201. })
  1202. return {
  1203. process: proc,
  1204. }
  1205. },
  1206. }
  1207. export const LuaLS: Info = {
  1208. id: "lua-ls",
  1209. root: NearestRoot([
  1210. ".luarc.json",
  1211. ".luarc.jsonc",
  1212. ".luacheckrc",
  1213. ".stylua.toml",
  1214. "stylua.toml",
  1215. "selene.toml",
  1216. "selene.yml",
  1217. ]),
  1218. extensions: [".lua"],
  1219. async spawn(root) {
  1220. let bin = Bun.which("lua-language-server", {
  1221. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  1222. })
  1223. if (!bin) {
  1224. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1225. log.info("downloading lua-language-server from GitHub releases")
  1226. const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
  1227. if (!releaseResponse.ok) {
  1228. log.error("Failed to fetch lua-language-server release info")
  1229. return
  1230. }
  1231. const release = await releaseResponse.json()
  1232. const platform = process.platform
  1233. const arch = process.arch
  1234. let assetName = ""
  1235. let lualsArch: string = arch
  1236. if (arch === "arm64") lualsArch = "arm64"
  1237. else if (arch === "x64") lualsArch = "x64"
  1238. else if (arch === "ia32") lualsArch = "ia32"
  1239. let lualsPlatform: string = platform
  1240. if (platform === "darwin") lualsPlatform = "darwin"
  1241. else if (platform === "linux") lualsPlatform = "linux"
  1242. else if (platform === "win32") lualsPlatform = "win32"
  1243. const ext = platform === "win32" ? "zip" : "tar.gz"
  1244. assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
  1245. const supportedCombos = [
  1246. "darwin-arm64.tar.gz",
  1247. "darwin-x64.tar.gz",
  1248. "linux-x64.tar.gz",
  1249. "linux-arm64.tar.gz",
  1250. "win32-x64.zip",
  1251. "win32-ia32.zip",
  1252. ]
  1253. const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
  1254. if (!supportedCombos.includes(assetSuffix)) {
  1255. log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
  1256. return
  1257. }
  1258. const asset = release.assets.find((a: any) => a.name === assetName)
  1259. if (!asset) {
  1260. log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
  1261. return
  1262. }
  1263. const downloadUrl = asset.browser_download_url
  1264. const downloadResponse = await fetch(downloadUrl)
  1265. if (!downloadResponse.ok) {
  1266. log.error("Failed to download lua-language-server")
  1267. return
  1268. }
  1269. const tempPath = path.join(Global.Path.bin, assetName)
  1270. await Bun.file(tempPath).write(downloadResponse)
  1271. // Unlike zls which is a single self-contained binary,
  1272. // lua-language-server needs supporting files (meta/, locale/, etc.)
  1273. // Extract entire archive to dedicated directory to preserve all files
  1274. const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
  1275. // Remove old installation if exists
  1276. const stats = await fs.stat(installDir).catch(() => undefined)
  1277. if (stats) {
  1278. await fs.rm(installDir, { force: true, recursive: true })
  1279. }
  1280. await fs.mkdir(installDir, { recursive: true })
  1281. if (ext === "zip") {
  1282. const ok = await Archive.extractZip(tempPath, installDir)
  1283. .then(() => true)
  1284. .catch((error) => {
  1285. log.error("Failed to extract lua-language-server archive", { error })
  1286. return false
  1287. })
  1288. if (!ok) return
  1289. } else {
  1290. const ok = await $`tar -xzf ${tempPath} -C ${installDir}`
  1291. .quiet()
  1292. .then(() => true)
  1293. .catch((error) => {
  1294. log.error("Failed to extract lua-language-server archive", { error })
  1295. return false
  1296. })
  1297. if (!ok) return
  1298. }
  1299. await fs.rm(tempPath, { force: true })
  1300. // Binary is located in bin/ subdirectory within the extracted archive
  1301. bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
  1302. if (!(await Bun.file(bin).exists())) {
  1303. log.error("Failed to extract lua-language-server binary")
  1304. return
  1305. }
  1306. if (platform !== "win32") {
  1307. const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
  1308. log.error("Failed to set executable permission for lua-language-server binary", {
  1309. error,
  1310. })
  1311. })
  1312. if (!ok) return
  1313. }
  1314. log.info(`installed lua-language-server`, { bin })
  1315. }
  1316. return {
  1317. process: spawn(bin, {
  1318. cwd: root,
  1319. }),
  1320. }
  1321. },
  1322. }
  1323. export const PHPIntelephense: Info = {
  1324. id: "php intelephense",
  1325. extensions: [".php"],
  1326. root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
  1327. async spawn(root) {
  1328. let binary = Bun.which("intelephense")
  1329. const args: string[] = []
  1330. if (!binary) {
  1331. const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
  1332. if (!(await Bun.file(js).exists())) {
  1333. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1334. await Bun.spawn([BunProc.which(), "install", "intelephense"], {
  1335. cwd: Global.Path.bin,
  1336. env: {
  1337. ...process.env,
  1338. BUN_BE_BUN: "1",
  1339. },
  1340. stdout: "pipe",
  1341. stderr: "pipe",
  1342. stdin: "pipe",
  1343. }).exited
  1344. }
  1345. binary = BunProc.which()
  1346. args.push("run", js)
  1347. }
  1348. args.push("--stdio")
  1349. const proc = spawn(binary, args, {
  1350. cwd: root,
  1351. env: {
  1352. ...process.env,
  1353. BUN_BE_BUN: "1",
  1354. },
  1355. })
  1356. return {
  1357. process: proc,
  1358. initialization: {},
  1359. }
  1360. },
  1361. }
  1362. export const Prisma: Info = {
  1363. id: "prisma",
  1364. extensions: [".prisma"],
  1365. root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
  1366. async spawn(root) {
  1367. const prisma = Bun.which("prisma")
  1368. if (!prisma) {
  1369. log.info("prisma not found, please install prisma")
  1370. return
  1371. }
  1372. return {
  1373. process: spawn(prisma, ["language-server"], {
  1374. cwd: root,
  1375. }),
  1376. }
  1377. },
  1378. }
  1379. export const Dart: Info = {
  1380. id: "dart",
  1381. extensions: [".dart"],
  1382. root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
  1383. async spawn(root) {
  1384. const dart = Bun.which("dart")
  1385. if (!dart) {
  1386. log.info("dart not found, please install dart first")
  1387. return
  1388. }
  1389. return {
  1390. process: spawn(dart, ["language-server", "--lsp"], {
  1391. cwd: root,
  1392. }),
  1393. }
  1394. },
  1395. }
  1396. export const Ocaml: Info = {
  1397. id: "ocaml-lsp",
  1398. extensions: [".ml", ".mli"],
  1399. root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
  1400. async spawn(root) {
  1401. const bin = Bun.which("ocamllsp")
  1402. if (!bin) {
  1403. log.info("ocamllsp not found, please install ocaml-lsp-server")
  1404. return
  1405. }
  1406. return {
  1407. process: spawn(bin, {
  1408. cwd: root,
  1409. }),
  1410. }
  1411. },
  1412. }
  1413. export const BashLS: Info = {
  1414. id: "bash",
  1415. extensions: [".sh", ".bash", ".zsh", ".ksh"],
  1416. root: async () => Instance.directory,
  1417. async spawn(root) {
  1418. let binary = Bun.which("bash-language-server")
  1419. const args: string[] = []
  1420. if (!binary) {
  1421. const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
  1422. if (!(await Bun.file(js).exists())) {
  1423. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1424. await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
  1425. cwd: Global.Path.bin,
  1426. env: {
  1427. ...process.env,
  1428. BUN_BE_BUN: "1",
  1429. },
  1430. stdout: "pipe",
  1431. stderr: "pipe",
  1432. stdin: "pipe",
  1433. }).exited
  1434. }
  1435. binary = BunProc.which()
  1436. args.push("run", js)
  1437. }
  1438. args.push("start")
  1439. const proc = spawn(binary, args, {
  1440. cwd: root,
  1441. env: {
  1442. ...process.env,
  1443. BUN_BE_BUN: "1",
  1444. },
  1445. })
  1446. return {
  1447. process: proc,
  1448. }
  1449. },
  1450. }
  1451. export const TerraformLS: Info = {
  1452. id: "terraform",
  1453. extensions: [".tf", ".tfvars"],
  1454. root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
  1455. async spawn(root) {
  1456. let bin = Bun.which("terraform-ls", {
  1457. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  1458. })
  1459. if (!bin) {
  1460. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1461. log.info("downloading terraform-ls from GitHub releases")
  1462. const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
  1463. if (!releaseResponse.ok) {
  1464. log.error("Failed to fetch terraform-ls release info")
  1465. return
  1466. }
  1467. const release = (await releaseResponse.json()) as {
  1468. tag_name?: string
  1469. assets?: { name?: string; browser_download_url?: string }[]
  1470. }
  1471. const version = release.tag_name?.replace("v", "")
  1472. if (!version) {
  1473. log.error("terraform-ls release did not include a version tag")
  1474. return
  1475. }
  1476. const platform = process.platform
  1477. const arch = process.arch
  1478. const tfArch = arch === "arm64" ? "arm64" : "amd64"
  1479. const tfPlatform = platform === "win32" ? "windows" : platform
  1480. const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
  1481. const assets = release.assets ?? []
  1482. const asset = assets.find((a) => a.name === assetName)
  1483. if (!asset?.browser_download_url) {
  1484. log.error(`Could not find asset ${assetName} in terraform-ls release`)
  1485. return
  1486. }
  1487. const downloadResponse = await fetch(asset.browser_download_url)
  1488. if (!downloadResponse.ok) {
  1489. log.error("Failed to download terraform-ls")
  1490. return
  1491. }
  1492. const tempPath = path.join(Global.Path.bin, assetName)
  1493. await Bun.file(tempPath).write(downloadResponse)
  1494. const ok = await Archive.extractZip(tempPath, Global.Path.bin)
  1495. .then(() => true)
  1496. .catch((error) => {
  1497. log.error("Failed to extract terraform-ls archive", { error })
  1498. return false
  1499. })
  1500. if (!ok) return
  1501. await fs.rm(tempPath, { force: true })
  1502. bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
  1503. if (!(await Bun.file(bin).exists())) {
  1504. log.error("Failed to extract terraform-ls binary")
  1505. return
  1506. }
  1507. if (platform !== "win32") {
  1508. await $`chmod +x ${bin}`.quiet().nothrow()
  1509. }
  1510. log.info(`installed terraform-ls`, { bin })
  1511. }
  1512. return {
  1513. process: spawn(bin, ["serve"], {
  1514. cwd: root,
  1515. }),
  1516. initialization: {
  1517. experimentalFeatures: {
  1518. prefillRequiredFields: true,
  1519. validateOnSave: true,
  1520. },
  1521. },
  1522. }
  1523. },
  1524. }
  1525. export const TexLab: Info = {
  1526. id: "texlab",
  1527. extensions: [".tex", ".bib"],
  1528. root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
  1529. async spawn(root) {
  1530. let bin = Bun.which("texlab", {
  1531. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  1532. })
  1533. if (!bin) {
  1534. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1535. log.info("downloading texlab from GitHub releases")
  1536. const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
  1537. if (!response.ok) {
  1538. log.error("Failed to fetch texlab release info")
  1539. return
  1540. }
  1541. const release = (await response.json()) as {
  1542. tag_name?: string
  1543. assets?: { name?: string; browser_download_url?: string }[]
  1544. }
  1545. const version = release.tag_name?.replace("v", "")
  1546. if (!version) {
  1547. log.error("texlab release did not include a version tag")
  1548. return
  1549. }
  1550. const platform = process.platform
  1551. const arch = process.arch
  1552. const texArch = arch === "arm64" ? "aarch64" : "x86_64"
  1553. const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
  1554. const ext = platform === "win32" ? "zip" : "tar.gz"
  1555. const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
  1556. const assets = release.assets ?? []
  1557. const asset = assets.find((a) => a.name === assetName)
  1558. if (!asset?.browser_download_url) {
  1559. log.error(`Could not find asset ${assetName} in texlab release`)
  1560. return
  1561. }
  1562. const downloadResponse = await fetch(asset.browser_download_url)
  1563. if (!downloadResponse.ok) {
  1564. log.error("Failed to download texlab")
  1565. return
  1566. }
  1567. const tempPath = path.join(Global.Path.bin, assetName)
  1568. await Bun.file(tempPath).write(downloadResponse)
  1569. if (ext === "zip") {
  1570. const ok = await Archive.extractZip(tempPath, Global.Path.bin)
  1571. .then(() => true)
  1572. .catch((error) => {
  1573. log.error("Failed to extract texlab archive", { error })
  1574. return false
  1575. })
  1576. if (!ok) return
  1577. }
  1578. if (ext === "tar.gz") {
  1579. await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
  1580. }
  1581. await fs.rm(tempPath, { force: true })
  1582. bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
  1583. if (!(await Bun.file(bin).exists())) {
  1584. log.error("Failed to extract texlab binary")
  1585. return
  1586. }
  1587. if (platform !== "win32") {
  1588. await $`chmod +x ${bin}`.quiet().nothrow()
  1589. }
  1590. log.info("installed texlab", { bin })
  1591. }
  1592. return {
  1593. process: spawn(bin, {
  1594. cwd: root,
  1595. }),
  1596. }
  1597. },
  1598. }
  1599. export const DockerfileLS: Info = {
  1600. id: "dockerfile",
  1601. extensions: [".dockerfile", "Dockerfile"],
  1602. root: async () => Instance.directory,
  1603. async spawn(root) {
  1604. let binary = Bun.which("docker-langserver")
  1605. const args: string[] = []
  1606. if (!binary) {
  1607. const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
  1608. if (!(await Bun.file(js).exists())) {
  1609. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1610. await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
  1611. cwd: Global.Path.bin,
  1612. env: {
  1613. ...process.env,
  1614. BUN_BE_BUN: "1",
  1615. },
  1616. stdout: "pipe",
  1617. stderr: "pipe",
  1618. stdin: "pipe",
  1619. }).exited
  1620. }
  1621. binary = BunProc.which()
  1622. args.push("run", js)
  1623. }
  1624. args.push("--stdio")
  1625. const proc = spawn(binary, args, {
  1626. cwd: root,
  1627. env: {
  1628. ...process.env,
  1629. BUN_BE_BUN: "1",
  1630. },
  1631. })
  1632. return {
  1633. process: proc,
  1634. }
  1635. },
  1636. }
  1637. export const Gleam: Info = {
  1638. id: "gleam",
  1639. extensions: [".gleam"],
  1640. root: NearestRoot(["gleam.toml"]),
  1641. async spawn(root) {
  1642. const gleam = Bun.which("gleam")
  1643. if (!gleam) {
  1644. log.info("gleam not found, please install gleam first")
  1645. return
  1646. }
  1647. return {
  1648. process: spawn(gleam, ["lsp"], {
  1649. cwd: root,
  1650. }),
  1651. }
  1652. },
  1653. }
  1654. export const Clojure: Info = {
  1655. id: "clojure-lsp",
  1656. extensions: [".clj", ".cljs", ".cljc", ".edn"],
  1657. root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
  1658. async spawn(root) {
  1659. let bin = Bun.which("clojure-lsp")
  1660. if (!bin && process.platform === "win32") {
  1661. bin = Bun.which("clojure-lsp.exe")
  1662. }
  1663. if (!bin) {
  1664. log.info("clojure-lsp not found, please install clojure-lsp first")
  1665. return
  1666. }
  1667. return {
  1668. process: spawn(bin, ["listen"], {
  1669. cwd: root,
  1670. }),
  1671. }
  1672. },
  1673. }
  1674. export const Nixd: Info = {
  1675. id: "nixd",
  1676. extensions: [".nix"],
  1677. root: async (file) => {
  1678. // First, look for flake.nix - the most reliable Nix project root indicator
  1679. const flakeRoot = await NearestRoot(["flake.nix"])(file)
  1680. if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot
  1681. // If no flake.nix, fall back to git repository root
  1682. if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree
  1683. // Finally, use the instance directory as fallback
  1684. return Instance.directory
  1685. },
  1686. async spawn(root) {
  1687. const nixd = Bun.which("nixd")
  1688. if (!nixd) {
  1689. log.info("nixd not found, please install nixd first")
  1690. return
  1691. }
  1692. return {
  1693. process: spawn(nixd, [], {
  1694. cwd: root,
  1695. env: {
  1696. ...process.env,
  1697. },
  1698. }),
  1699. }
  1700. },
  1701. }
  1702. export const Tinymist: Info = {
  1703. id: "tinymist",
  1704. extensions: [".typ", ".typc"],
  1705. root: NearestRoot(["typst.toml"]),
  1706. async spawn(root) {
  1707. let bin = Bun.which("tinymist", {
  1708. PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
  1709. })
  1710. if (!bin) {
  1711. if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
  1712. log.info("downloading tinymist from GitHub releases")
  1713. const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest")
  1714. if (!response.ok) {
  1715. log.error("Failed to fetch tinymist release info")
  1716. return
  1717. }
  1718. const release = (await response.json()) as {
  1719. tag_name?: string
  1720. assets?: { name?: string; browser_download_url?: string }[]
  1721. }
  1722. const platform = process.platform
  1723. const arch = process.arch
  1724. const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64"
  1725. let tinymistPlatform: string
  1726. let ext: string
  1727. if (platform === "darwin") {
  1728. tinymistPlatform = "apple-darwin"
  1729. ext = "tar.gz"
  1730. } else if (platform === "win32") {
  1731. tinymistPlatform = "pc-windows-msvc"
  1732. ext = "zip"
  1733. } else {
  1734. tinymistPlatform = "unknown-linux-gnu"
  1735. ext = "tar.gz"
  1736. }
  1737. const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}`
  1738. const assets = release.assets ?? []
  1739. const asset = assets.find((a) => a.name === assetName)
  1740. if (!asset?.browser_download_url) {
  1741. log.error(`Could not find asset ${assetName} in tinymist release`)
  1742. return
  1743. }
  1744. const downloadResponse = await fetch(asset.browser_download_url)
  1745. if (!downloadResponse.ok) {
  1746. log.error("Failed to download tinymist")
  1747. return
  1748. }
  1749. const tempPath = path.join(Global.Path.bin, assetName)
  1750. await Bun.file(tempPath).write(downloadResponse)
  1751. if (ext === "zip") {
  1752. const ok = await Archive.extractZip(tempPath, Global.Path.bin)
  1753. .then(() => true)
  1754. .catch((error) => {
  1755. log.error("Failed to extract tinymist archive", { error })
  1756. return false
  1757. })
  1758. if (!ok) return
  1759. } else {
  1760. await $`tar -xzf ${tempPath} --strip-components=1`.cwd(Global.Path.bin).quiet().nothrow()
  1761. }
  1762. await fs.rm(tempPath, { force: true })
  1763. bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
  1764. if (!(await Bun.file(bin).exists())) {
  1765. log.error("Failed to extract tinymist binary")
  1766. return
  1767. }
  1768. if (platform !== "win32") {
  1769. await $`chmod +x ${bin}`.quiet().nothrow()
  1770. }
  1771. log.info("installed tinymist", { bin })
  1772. }
  1773. return {
  1774. process: spawn(bin, { cwd: root }),
  1775. }
  1776. },
  1777. }
  1778. export const HLS: Info = {
  1779. id: "haskell-language-server",
  1780. extensions: [".hs", ".lhs"],
  1781. root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
  1782. async spawn(root) {
  1783. const bin = Bun.which("haskell-language-server-wrapper")
  1784. if (!bin) {
  1785. log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
  1786. return
  1787. }
  1788. return {
  1789. process: spawn(bin, ["--lsp"], {
  1790. cwd: root,
  1791. }),
  1792. }
  1793. },
  1794. }
  1795. }