2
0

server.ts 60 KB

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