server.ts 56 KB

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