| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011 |
- import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
- import path from "path"
- import os from "os"
- import { Global } from "../global"
- import { Log } from "../util/log"
- import { BunProc } from "../bun"
- import { $, readableStreamToText } from "bun"
- import fs from "fs/promises"
- import { Filesystem } from "../util/filesystem"
- import { Instance } from "../project/instance"
- import { Flag } from "../flag/flag"
- import { Archive } from "../util/archive"
- export namespace LSPServer {
- const log = Log.create({ service: "lsp.server" })
- export interface Handle {
- process: ChildProcessWithoutNullStreams
- initialization?: Record<string, any>
- }
- type RootFunction = (file: string) => Promise<string | undefined>
- const NearestRoot = (includePatterns: string[], excludePatterns?: string[]): RootFunction => {
- return async (file) => {
- if (excludePatterns) {
- const excludedFiles = Filesystem.up({
- targets: excludePatterns,
- start: path.dirname(file),
- stop: Instance.directory,
- })
- const excluded = await excludedFiles.next()
- await excludedFiles.return()
- if (excluded.value) return undefined
- }
- const files = Filesystem.up({
- targets: includePatterns,
- start: path.dirname(file),
- stop: Instance.directory,
- })
- const first = await files.next()
- await files.return()
- if (!first.value) return Instance.directory
- return path.dirname(first.value)
- }
- }
- export interface Info {
- id: string
- extensions: string[]
- global?: boolean
- root: RootFunction
- spawn(root: string): Promise<Handle | undefined>
- }
- export const Deno: Info = {
- id: "deno",
- root: async (file) => {
- const files = Filesystem.up({
- targets: ["deno.json", "deno.jsonc"],
- start: path.dirname(file),
- stop: Instance.directory,
- })
- const first = await files.next()
- await files.return()
- if (!first.value) return undefined
- return path.dirname(first.value)
- },
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs"],
- async spawn(root) {
- const deno = Bun.which("deno")
- if (!deno) {
- log.info("deno not found, please install deno first")
- return
- }
- return {
- process: spawn(deno, ["lsp"], {
- cwd: root,
- }),
- }
- },
- }
- export const Typescript: Info = {
- id: "typescript",
- root: NearestRoot(
- ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"],
- ["deno.json", "deno.jsonc"],
- ),
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
- async spawn(root) {
- const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
- log.info("typescript server", { tsserver })
- if (!tsserver) return
- const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization: {
- tsserver: {
- path: tsserver,
- },
- },
- }
- },
- }
- export const Vue: Info = {
- id: "vue",
- extensions: [".vue"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
- async spawn(root) {
- let binary = Bun.which("vue-language-server")
- const args: string[] = []
- if (!binary) {
- const js = path.join(
- Global.Path.bin,
- "node_modules",
- "@vue",
- "language-server",
- "bin",
- "vue-language-server.js",
- )
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "@vue/language-server"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization: {
- // Leave empty; the server will auto-detect workspace TypeScript.
- },
- }
- },
- }
- export const ESLint: Info = {
- id: "eslint",
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue"],
- async spawn(root) {
- const eslint = await Bun.resolve("eslint", Instance.directory).catch(() => {})
- if (!eslint) return
- log.info("spawning eslint server")
- const serverPath = path.join(Global.Path.bin, "vscode-eslint", "server", "out", "eslintServer.js")
- if (!(await Bun.file(serverPath).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading and building VS Code ESLint server")
- const response = await fetch("https://github.com/microsoft/vscode-eslint/archive/refs/heads/main.zip")
- if (!response.ok) return
- const zipPath = path.join(Global.Path.bin, "vscode-eslint.zip")
- await Bun.file(zipPath).write(response)
- const ok = await Archive.extractZip(zipPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract vscode-eslint archive", { error })
- return false
- })
- if (!ok) return
- await fs.rm(zipPath, { force: true })
- const extractedPath = path.join(Global.Path.bin, "vscode-eslint-main")
- const finalPath = path.join(Global.Path.bin, "vscode-eslint")
- const stats = await fs.stat(finalPath).catch(() => undefined)
- if (stats) {
- log.info("removing old eslint installation", { path: finalPath })
- await fs.rm(finalPath, { force: true, recursive: true })
- }
- await fs.rename(extractedPath, finalPath)
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
- await $`${npmCmd} install`.cwd(finalPath).quiet()
- await $`${npmCmd} run compile`.cwd(finalPath).quiet()
- log.info("installed VS Code ESLint server", { serverPath })
- }
- const proc = spawn(BunProc.which(), [serverPath, "--stdio"], {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- }
- },
- }
- export const Oxlint: Info = {
- id: "oxlint",
- root: NearestRoot([
- ".oxlintrc.json",
- "package-lock.json",
- "bun.lockb",
- "bun.lock",
- "pnpm-lock.yaml",
- "yarn.lock",
- "package.json",
- ]),
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".vue", ".astro", ".svelte"],
- async spawn(root) {
- const ext = process.platform === "win32" ? ".cmd" : ""
- const serverTarget = path.join("node_modules", ".bin", "oxc_language_server" + ext)
- const lintTarget = path.join("node_modules", ".bin", "oxlint" + ext)
- const resolveBin = async (target: string) => {
- const localBin = path.join(root, target)
- if (await Bun.file(localBin).exists()) return localBin
- const candidates = Filesystem.up({
- targets: [target],
- start: root,
- stop: Instance.worktree,
- })
- const first = await candidates.next()
- await candidates.return()
- if (first.value) return first.value
- return undefined
- }
- let lintBin = await resolveBin(lintTarget)
- if (!lintBin) {
- const found = Bun.which("oxlint")
- if (found) lintBin = found
- }
- if (lintBin) {
- const proc = Bun.spawn([lintBin, "--help"], { stdout: "pipe" })
- await proc.exited
- const help = await readableStreamToText(proc.stdout)
- if (help.includes("--lsp")) {
- return {
- process: spawn(lintBin, ["--lsp"], {
- cwd: root,
- }),
- }
- }
- }
- let serverBin = await resolveBin(serverTarget)
- if (!serverBin) {
- const found = Bun.which("oxc_language_server")
- if (found) serverBin = found
- }
- if (serverBin) {
- return {
- process: spawn(serverBin, [], {
- cwd: root,
- }),
- }
- }
- log.info("oxlint not found, please install oxlint")
- return
- },
- }
- export const Biome: Info = {
- id: "biome",
- root: NearestRoot([
- "biome.json",
- "biome.jsonc",
- "package-lock.json",
- "bun.lockb",
- "bun.lock",
- "pnpm-lock.yaml",
- "yarn.lock",
- ]),
- extensions: [
- ".ts",
- ".tsx",
- ".js",
- ".jsx",
- ".mjs",
- ".cjs",
- ".mts",
- ".cts",
- ".json",
- ".jsonc",
- ".vue",
- ".astro",
- ".svelte",
- ".css",
- ".graphql",
- ".gql",
- ".html",
- ],
- async spawn(root) {
- const localBin = path.join(root, "node_modules", ".bin", "biome")
- let bin: string | undefined
- if (await Bun.file(localBin).exists()) bin = localBin
- if (!bin) {
- const found = Bun.which("biome")
- if (found) bin = found
- }
- let args = ["lsp-proxy", "--stdio"]
- if (!bin) {
- const resolved = await Bun.resolve("biome", root).catch(() => undefined)
- if (!resolved) return
- bin = BunProc.which()
- args = ["x", "biome", "lsp-proxy", "--stdio"]
- }
- const proc = spawn(bin, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- }
- },
- }
- export const Gopls: Info = {
- id: "gopls",
- root: async (file) => {
- const work = await NearestRoot(["go.work"])(file)
- if (work) return work
- return NearestRoot(["go.mod", "go.sum"])(file)
- },
- extensions: [".go"],
- async spawn(root) {
- let bin = Bun.which("gopls", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (!Bun.which("go")) return
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("installing gopls")
- const proc = Bun.spawn({
- cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
- env: { ...process.env, GOBIN: Global.Path.bin },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- log.error("Failed to install gopls")
- return
- }
- bin = path.join(Global.Path.bin, "gopls" + (process.platform === "win32" ? ".exe" : ""))
- log.info(`installed gopls`, {
- bin,
- })
- }
- return {
- process: spawn(bin!, {
- cwd: root,
- }),
- }
- },
- }
- export const Rubocop: Info = {
- id: "ruby-lsp",
- root: NearestRoot(["Gemfile"]),
- extensions: [".rb", ".rake", ".gemspec", ".ru"],
- async spawn(root) {
- let bin = Bun.which("rubocop", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- const ruby = Bun.which("ruby")
- const gem = Bun.which("gem")
- if (!ruby || !gem) {
- log.info("Ruby not found, please install Ruby first")
- return
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("installing rubocop")
- const proc = Bun.spawn({
- cmd: ["gem", "install", "rubocop", "--bindir", Global.Path.bin],
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- log.error("Failed to install rubocop")
- return
- }
- bin = path.join(Global.Path.bin, "rubocop" + (process.platform === "win32" ? ".exe" : ""))
- log.info(`installed rubocop`, {
- bin,
- })
- }
- return {
- process: spawn(bin!, ["--lsp"], {
- cwd: root,
- }),
- }
- },
- }
- export const Ty: Info = {
- id: "ty",
- extensions: [".py", ".pyi"],
- root: NearestRoot([
- "pyproject.toml",
- "ty.toml",
- "setup.py",
- "setup.cfg",
- "requirements.txt",
- "Pipfile",
- "pyrightconfig.json",
- ]),
- async spawn(root) {
- if (!Flag.OPENCODE_EXPERIMENTAL_LSP_TY) {
- return undefined
- }
- let binary = Bun.which("ty")
- const initialization: Record<string, string> = {}
- const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
- (p): p is string => p !== undefined,
- )
- for (const venvPath of potentialVenvPaths) {
- const isWindows = process.platform === "win32"
- const potentialPythonPath = isWindows
- ? path.join(venvPath, "Scripts", "python.exe")
- : path.join(venvPath, "bin", "python")
- if (await Bun.file(potentialPythonPath).exists()) {
- initialization["pythonPath"] = potentialPythonPath
- break
- }
- }
- if (!binary) {
- for (const venvPath of potentialVenvPaths) {
- const isWindows = process.platform === "win32"
- const potentialTyPath = isWindows
- ? path.join(venvPath, "Scripts", "ty.exe")
- : path.join(venvPath, "bin", "ty")
- if (await Bun.file(potentialTyPath).exists()) {
- binary = potentialTyPath
- break
- }
- }
- }
- if (!binary) {
- log.error("ty not found, please install ty first")
- return
- }
- const proc = spawn(binary, ["server"], {
- cwd: root,
- })
- return {
- process: proc,
- initialization,
- }
- },
- }
- export const Pyright: Info = {
- id: "pyright",
- extensions: [".py", ".pyi"],
- root: NearestRoot(["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"]),
- async spawn(root) {
- let binary = Bun.which("pyright-langserver")
- const args = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "pyright", "dist", "pyright-langserver.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "pyright"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- }).exited
- }
- binary = BunProc.which()
- args.push(...["run", js])
- }
- args.push("--stdio")
- const initialization: Record<string, string> = {}
- const potentialVenvPaths = [process.env["VIRTUAL_ENV"], path.join(root, ".venv"), path.join(root, "venv")].filter(
- (p): p is string => p !== undefined,
- )
- for (const venvPath of potentialVenvPaths) {
- const isWindows = process.platform === "win32"
- const potentialPythonPath = isWindows
- ? path.join(venvPath, "Scripts", "python.exe")
- : path.join(venvPath, "bin", "python")
- if (await Bun.file(potentialPythonPath).exists()) {
- initialization["pythonPath"] = potentialPythonPath
- break
- }
- }
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization,
- }
- },
- }
- export const ElixirLS: Info = {
- id: "elixir-ls",
- extensions: [".ex", ".exs"],
- root: NearestRoot(["mix.exs", "mix.lock"]),
- async spawn(root) {
- let binary = Bun.which("elixir-ls")
- if (!binary) {
- const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
- binary = path.join(
- Global.Path.bin,
- "elixir-ls-master",
- "release",
- process.platform === "win32" ? "language_server.bat" : "language_server.sh",
- )
- if (!(await Bun.file(binary).exists())) {
- const elixir = Bun.which("elixir")
- if (!elixir) {
- log.error("elixir is required to run elixir-ls")
- return
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading elixir-ls from GitHub releases")
- const response = await fetch("https://github.com/elixir-lsp/elixir-ls/archive/refs/heads/master.zip")
- if (!response.ok) return
- const zipPath = path.join(Global.Path.bin, "elixir-ls.zip")
- await Bun.file(zipPath).write(response)
- const ok = await Archive.extractZip(zipPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract elixir-ls archive", { error })
- return false
- })
- if (!ok) return
- await fs.rm(zipPath, {
- force: true,
- recursive: true,
- })
- await $`mix deps.get && mix compile && mix elixir_ls.release2 -o release`
- .quiet()
- .cwd(path.join(Global.Path.bin, "elixir-ls-master"))
- .env({ MIX_ENV: "prod", ...process.env })
- log.info(`installed elixir-ls`, {
- path: elixirLsPath,
- })
- }
- }
- return {
- process: spawn(binary, {
- cwd: root,
- }),
- }
- },
- }
- export const Zls: Info = {
- id: "zls",
- extensions: [".zig", ".zon"],
- root: NearestRoot(["build.zig"]),
- async spawn(root) {
- let bin = Bun.which("zls", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- const zig = Bun.which("zig")
- if (!zig) {
- log.error("Zig is required to use zls. Please install Zig first.")
- return
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading zls from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/zigtools/zls/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch zls release info")
- return
- }
- const release = (await releaseResponse.json()) as any
- const platform = process.platform
- const arch = process.arch
- let assetName = ""
- let zlsArch: string = arch
- if (arch === "arm64") zlsArch = "aarch64"
- else if (arch === "x64") zlsArch = "x86_64"
- else if (arch === "ia32") zlsArch = "x86"
- let zlsPlatform: string = platform
- if (platform === "darwin") zlsPlatform = "macos"
- else if (platform === "win32") zlsPlatform = "windows"
- const ext = platform === "win32" ? "zip" : "tar.xz"
- assetName = `zls-${zlsArch}-${zlsPlatform}.${ext}`
- const supportedCombos = [
- "zls-x86_64-linux.tar.xz",
- "zls-x86_64-macos.tar.xz",
- "zls-x86_64-windows.zip",
- "zls-aarch64-linux.tar.xz",
- "zls-aarch64-macos.tar.xz",
- "zls-aarch64-windows.zip",
- "zls-x86-linux.tar.xz",
- "zls-x86-windows.zip",
- ]
- if (!supportedCombos.includes(assetName)) {
- log.error(`Platform ${platform} and architecture ${arch} is not supported by zls`)
- return
- }
- const asset = release.assets.find((a: any) => a.name === assetName)
- if (!asset) {
- log.error(`Could not find asset ${assetName} in latest zls release`)
- return
- }
- const downloadUrl = asset.browser_download_url
- const downloadResponse = await fetch(downloadUrl)
- if (!downloadResponse.ok) {
- log.error("Failed to download zls")
- return
- }
- const tempPath = path.join(Global.Path.bin, assetName)
- await Bun.file(tempPath).write(downloadResponse)
- if (ext === "zip") {
- const ok = await Archive.extractZip(tempPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract zls archive", { error })
- return false
- })
- if (!ok) return
- } else {
- await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
- }
- await fs.rm(tempPath, { force: true })
- bin = path.join(Global.Path.bin, "zls" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract zls binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.quiet().nothrow()
- }
- log.info(`installed zls`, { bin })
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const CSharp: Info = {
- id: "csharp",
- root: NearestRoot([".sln", ".csproj", "global.json"]),
- extensions: [".cs"],
- async spawn(root) {
- let bin = Bun.which("csharp-ls", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (!Bun.which("dotnet")) {
- log.error(".NET SDK is required to install csharp-ls")
- return
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("installing csharp-ls via dotnet tool")
- const proc = Bun.spawn({
- cmd: ["dotnet", "tool", "install", "csharp-ls", "--tool-path", Global.Path.bin],
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- log.error("Failed to install csharp-ls")
- return
- }
- bin = path.join(Global.Path.bin, "csharp-ls" + (process.platform === "win32" ? ".exe" : ""))
- log.info(`installed csharp-ls`, { bin })
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const FSharp: Info = {
- id: "fsharp",
- root: NearestRoot([".sln", ".fsproj", "global.json"]),
- extensions: [".fs", ".fsi", ".fsx", ".fsscript"],
- async spawn(root) {
- let bin = Bun.which("fsautocomplete", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (!Bun.which("dotnet")) {
- log.error(".NET SDK is required to install fsautocomplete")
- return
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("installing fsautocomplete via dotnet tool")
- const proc = Bun.spawn({
- cmd: ["dotnet", "tool", "install", "fsautocomplete", "--tool-path", Global.Path.bin],
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- log.error("Failed to install fsautocomplete")
- return
- }
- bin = path.join(Global.Path.bin, "fsautocomplete" + (process.platform === "win32" ? ".exe" : ""))
- log.info(`installed fsautocomplete`, { bin })
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const SourceKit: Info = {
- id: "sourcekit-lsp",
- extensions: [".swift", ".objc", "objcpp"],
- root: NearestRoot(["Package.swift", "*.xcodeproj", "*.xcworkspace"]),
- async spawn(root) {
- // Check if sourcekit-lsp is available in the PATH
- // This is installed with the Swift toolchain
- const sourcekit = Bun.which("sourcekit-lsp")
- if (sourcekit) {
- return {
- process: spawn(sourcekit, {
- cwd: root,
- }),
- }
- }
- // If sourcekit-lsp not found, check if xcrun is available
- // This is specific to macOS where sourcekit-lsp is typically installed with Xcode
- if (!Bun.which("xcrun")) return
- const lspLoc = await $`xcrun --find sourcekit-lsp`.quiet().nothrow()
- if (lspLoc.exitCode !== 0) return
- const bin = lspLoc.text().trim()
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const RustAnalyzer: Info = {
- id: "rust",
- root: async (root) => {
- const crateRoot = await NearestRoot(["Cargo.toml", "Cargo.lock"])(root)
- if (crateRoot === undefined) {
- return undefined
- }
- let currentDir = crateRoot
- while (currentDir !== path.dirname(currentDir)) {
- // Stop at filesystem root
- const cargoTomlPath = path.join(currentDir, "Cargo.toml")
- try {
- const cargoTomlContent = await Bun.file(cargoTomlPath).text()
- if (cargoTomlContent.includes("[workspace]")) {
- return currentDir
- }
- } catch (err) {
- // File doesn't exist or can't be read, continue searching up
- }
- const parentDir = path.dirname(currentDir)
- if (parentDir === currentDir) break // Reached filesystem root
- currentDir = parentDir
- // Stop if we've gone above the app root
- if (!currentDir.startsWith(Instance.worktree)) break
- }
- return crateRoot
- },
- extensions: [".rs"],
- async spawn(root) {
- const bin = Bun.which("rust-analyzer")
- if (!bin) {
- log.info("rust-analyzer not found in path, please install it")
- return
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const Clangd: Info = {
- id: "clangd",
- root: NearestRoot(["compile_commands.json", "compile_flags.txt", ".clangd", "CMakeLists.txt", "Makefile"]),
- extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
- async spawn(root) {
- const args = ["--background-index", "--clang-tidy"]
- const fromPath = Bun.which("clangd")
- if (fromPath) {
- return {
- process: spawn(fromPath, args, {
- cwd: root,
- }),
- }
- }
- const ext = process.platform === "win32" ? ".exe" : ""
- const direct = path.join(Global.Path.bin, "clangd" + ext)
- if (await Bun.file(direct).exists()) {
- return {
- process: spawn(direct, args, {
- cwd: root,
- }),
- }
- }
- const entries = await fs.readdir(Global.Path.bin, { withFileTypes: true }).catch(() => [])
- for (const entry of entries) {
- if (!entry.isDirectory()) continue
- if (!entry.name.startsWith("clangd_")) continue
- const candidate = path.join(Global.Path.bin, entry.name, "bin", "clangd" + ext)
- if (await Bun.file(candidate).exists()) {
- return {
- process: spawn(candidate, args, {
- cwd: root,
- }),
- }
- }
- }
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading clangd from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/clangd/clangd/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch clangd release info")
- return
- }
- const release: {
- tag_name?: string
- assets?: { name?: string; browser_download_url?: string }[]
- } = await releaseResponse.json()
- const tag = release.tag_name
- if (!tag) {
- log.error("clangd release did not include a tag name")
- return
- }
- const platform = process.platform
- const tokens: Record<string, string> = {
- darwin: "mac",
- linux: "linux",
- win32: "windows",
- }
- const token = tokens[platform]
- if (!token) {
- log.error(`Platform ${platform} is not supported by clangd auto-download`)
- return
- }
- const assets = release.assets ?? []
- const valid = (item: { name?: string; browser_download_url?: string }) => {
- if (!item.name) return false
- if (!item.browser_download_url) return false
- if (!item.name.includes(token)) return false
- return item.name.includes(tag)
- }
- const asset =
- assets.find((item) => valid(item) && item.name?.endsWith(".zip")) ??
- assets.find((item) => valid(item) && item.name?.endsWith(".tar.xz")) ??
- assets.find((item) => valid(item))
- if (!asset?.name || !asset.browser_download_url) {
- log.error("clangd could not match release asset", { tag, platform })
- return
- }
- const name = asset.name
- const downloadResponse = await fetch(asset.browser_download_url)
- if (!downloadResponse.ok) {
- log.error("Failed to download clangd")
- return
- }
- const archive = path.join(Global.Path.bin, name)
- const buf = await downloadResponse.arrayBuffer()
- if (buf.byteLength === 0) {
- log.error("Failed to write clangd archive")
- return
- }
- await Bun.write(archive, buf)
- const zip = name.endsWith(".zip")
- const tar = name.endsWith(".tar.xz")
- if (!zip && !tar) {
- log.error("clangd encountered unsupported asset", { asset: name })
- return
- }
- if (zip) {
- const ok = await Archive.extractZip(archive, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract clangd archive", { error })
- return false
- })
- if (!ok) return
- }
- if (tar) {
- await $`tar -xf ${archive}`.cwd(Global.Path.bin).quiet().nothrow()
- }
- await fs.rm(archive, { force: true })
- const bin = path.join(Global.Path.bin, "clangd_" + tag, "bin", "clangd" + ext)
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract clangd binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.quiet().nothrow()
- }
- await fs.unlink(path.join(Global.Path.bin, "clangd")).catch(() => {})
- await fs.symlink(bin, path.join(Global.Path.bin, "clangd")).catch(() => {})
- log.info(`installed clangd`, { bin })
- return {
- process: spawn(bin, args, {
- cwd: root,
- }),
- }
- },
- }
- export const Svelte: Info = {
- id: "svelte",
- extensions: [".svelte"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
- async spawn(root) {
- let binary = Bun.which("svelteserver")
- const args: string[] = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "svelte-language-server", "bin", "server.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "svelte-language-server"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization: {},
- }
- },
- }
- export const Astro: Info = {
- id: "astro",
- extensions: [".astro"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
- async spawn(root) {
- const tsserver = await Bun.resolve("typescript/lib/tsserver.js", Instance.directory).catch(() => {})
- if (!tsserver) {
- log.info("typescript not found, required for Astro language server")
- return
- }
- const tsdk = path.dirname(tsserver)
- let binary = Bun.which("astro-ls")
- const args: string[] = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "@astrojs", "language-server", "bin", "nodeServer.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "@astrojs/language-server"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization: {
- typescript: {
- tsdk,
- },
- },
- }
- },
- }
- export const JDTLS: Info = {
- id: "jdtls",
- root: NearestRoot(["pom.xml", "build.gradle", "build.gradle.kts", ".project", ".classpath"]),
- extensions: [".java"],
- async spawn(root) {
- const java = Bun.which("java")
- if (!java) {
- log.error("Java 21 or newer is required to run the JDTLS. Please install it first.")
- return
- }
- const javaMajorVersion = await $`java -version`
- .quiet()
- .nothrow()
- .then(({ stderr }) => {
- const m = /"(\d+)\.\d+\.\d+"/.exec(stderr.toString())
- return !m ? undefined : parseInt(m[1])
- })
- if (javaMajorVersion == null || javaMajorVersion < 21) {
- log.error("JDTLS requires at least Java 21.")
- return
- }
- const distPath = path.join(Global.Path.bin, "jdtls")
- const launcherDir = path.join(distPath, "plugins")
- const installed = await fs.exists(launcherDir)
- if (!installed) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("Downloading JDTLS LSP server.")
- await fs.mkdir(distPath, { recursive: true })
- const releaseURL =
- "https://www.eclipse.org/downloads/download.php?file=/jdtls/snapshots/jdt-language-server-latest.tar.gz"
- const archivePath = path.join(distPath, "release.tar.gz")
- await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
- await $`tar -xzf ${archivePath}`.cwd(distPath).quiet().nothrow()
- await fs.rm(archivePath, { force: true })
- }
- const jarFileName = await $`ls org.eclipse.equinox.launcher_*.jar`
- .cwd(launcherDir)
- .quiet()
- .nothrow()
- .then(({ stdout }) => stdout.toString().trim())
- const launcherJar = path.join(launcherDir, jarFileName)
- if (!(await fs.exists(launcherJar))) {
- log.error(`Failed to locate the JDTLS launcher module in the installed directory: ${distPath}.`)
- return
- }
- const configFile = path.join(
- distPath,
- (() => {
- switch (process.platform) {
- case "darwin":
- return "config_mac"
- case "linux":
- return "config_linux"
- case "win32":
- return "config_win"
- default:
- return "config_linux"
- }
- })(),
- )
- const dataDir = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-jdtls-data"))
- return {
- process: spawn(
- java,
- [
- "-jar",
- launcherJar,
- "-configuration",
- configFile,
- "-data",
- dataDir,
- "-Declipse.application=org.eclipse.jdt.ls.core.id1",
- "-Dosgi.bundles.defaultStartLevel=4",
- "-Declipse.product=org.eclipse.jdt.ls.core.product",
- "-Dlog.level=ALL",
- "--add-modules=ALL-SYSTEM",
- "--add-opens java.base/java.util=ALL-UNNAMED",
- "--add-opens java.base/java.lang=ALL-UNNAMED",
- ],
- {
- cwd: root,
- },
- ),
- }
- },
- }
- export const KotlinLS: Info = {
- id: "kotlin-ls",
- extensions: [".kt", ".kts"],
- root: NearestRoot(["build.gradle", "build.gradle.kts", "settings.gradle.kts", "pom.xml"]),
- async spawn(root) {
- const distPath = path.join(Global.Path.bin, "kotlin-ls")
- const launcherScript =
- process.platform === "win32" ? path.join(distPath, "kotlin-lsp.cmd") : path.join(distPath, "kotlin-lsp.sh")
- const installed = await Bun.file(launcherScript).exists()
- if (!installed) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("Downloading Kotlin Language Server from GitHub.")
- const releaseResponse = await fetch("https://api.github.com/repos/Kotlin/kotlin-lsp/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch kotlin-lsp release info")
- return
- }
- const release = await releaseResponse.json()
- const version = release.name?.replace(/^v/, "")
- if (!version) {
- log.error("Could not determine Kotlin LSP version from release")
- return
- }
- const platform = process.platform
- const arch = process.arch
- let kotlinArch: string = arch
- if (arch === "arm64") kotlinArch = "aarch64"
- else if (arch === "x64") kotlinArch = "x64"
- let kotlinPlatform: string = platform
- if (platform === "darwin") kotlinPlatform = "mac"
- else if (platform === "linux") kotlinPlatform = "linux"
- else if (platform === "win32") kotlinPlatform = "win"
- const supportedCombos = ["mac-x64", "mac-aarch64", "linux-x64", "linux-aarch64", "win-x64", "win-aarch64"]
- const combo = `${kotlinPlatform}-${kotlinArch}`
- if (!supportedCombos.includes(combo)) {
- log.error(`Platform ${platform}/${arch} is not supported by Kotlin LSP`)
- return
- }
- const assetName = `kotlin-lsp-${version}-${kotlinPlatform}-${kotlinArch}.zip`
- const releaseURL = `https://download-cdn.jetbrains.com/kotlin-lsp/${version}/${assetName}`
- await fs.mkdir(distPath, { recursive: true })
- const archivePath = path.join(distPath, "kotlin-ls.zip")
- await $`curl -L -o '${archivePath}' '${releaseURL}'`.quiet().nothrow()
- const ok = await Archive.extractZip(archivePath, distPath)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract Kotlin LS archive", { error })
- return false
- })
- if (!ok) return
- await fs.rm(archivePath, { force: true })
- if (process.platform !== "win32") {
- await $`chmod +x ${launcherScript}`.quiet().nothrow()
- }
- log.info("Installed Kotlin Language Server", { path: launcherScript })
- }
- if (!(await Bun.file(launcherScript).exists())) {
- log.error(`Failed to locate the Kotlin LS launcher script in the installed directory: ${distPath}.`)
- return
- }
- return {
- process: spawn(launcherScript, ["--stdio"], {
- cwd: root,
- }),
- }
- },
- }
- export const YamlLS: Info = {
- id: "yaml-ls",
- extensions: [".yaml", ".yml"],
- root: NearestRoot(["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock"]),
- async spawn(root) {
- let binary = Bun.which("yaml-language-server")
- const args: string[] = []
- if (!binary) {
- const js = path.join(
- Global.Path.bin,
- "node_modules",
- "yaml-language-server",
- "out",
- "server",
- "src",
- "server.js",
- )
- const exists = await Bun.file(js).exists()
- if (!exists) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "yaml-language-server"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- }
- },
- }
- export const LuaLS: Info = {
- id: "lua-ls",
- root: NearestRoot([
- ".luarc.json",
- ".luarc.jsonc",
- ".luacheckrc",
- ".stylua.toml",
- "stylua.toml",
- "selene.toml",
- "selene.yml",
- ]),
- extensions: [".lua"],
- async spawn(root) {
- let bin = Bun.which("lua-language-server", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading lua-language-server from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/LuaLS/lua-language-server/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch lua-language-server release info")
- return
- }
- const release = await releaseResponse.json()
- const platform = process.platform
- const arch = process.arch
- let assetName = ""
- let lualsArch: string = arch
- if (arch === "arm64") lualsArch = "arm64"
- else if (arch === "x64") lualsArch = "x64"
- else if (arch === "ia32") lualsArch = "ia32"
- let lualsPlatform: string = platform
- if (platform === "darwin") lualsPlatform = "darwin"
- else if (platform === "linux") lualsPlatform = "linux"
- else if (platform === "win32") lualsPlatform = "win32"
- const ext = platform === "win32" ? "zip" : "tar.gz"
- assetName = `lua-language-server-${release.tag_name}-${lualsPlatform}-${lualsArch}.${ext}`
- const supportedCombos = [
- "darwin-arm64.tar.gz",
- "darwin-x64.tar.gz",
- "linux-x64.tar.gz",
- "linux-arm64.tar.gz",
- "win32-x64.zip",
- "win32-ia32.zip",
- ]
- const assetSuffix = `${lualsPlatform}-${lualsArch}.${ext}`
- if (!supportedCombos.includes(assetSuffix)) {
- log.error(`Platform ${platform} and architecture ${arch} is not supported by lua-language-server`)
- return
- }
- const asset = release.assets.find((a: any) => a.name === assetName)
- if (!asset) {
- log.error(`Could not find asset ${assetName} in latest lua-language-server release`)
- return
- }
- const downloadUrl = asset.browser_download_url
- const downloadResponse = await fetch(downloadUrl)
- if (!downloadResponse.ok) {
- log.error("Failed to download lua-language-server")
- return
- }
- const tempPath = path.join(Global.Path.bin, assetName)
- await Bun.file(tempPath).write(downloadResponse)
- // Unlike zls which is a single self-contained binary,
- // lua-language-server needs supporting files (meta/, locale/, etc.)
- // Extract entire archive to dedicated directory to preserve all files
- const installDir = path.join(Global.Path.bin, `lua-language-server-${lualsArch}-${lualsPlatform}`)
- // Remove old installation if exists
- const stats = await fs.stat(installDir).catch(() => undefined)
- if (stats) {
- await fs.rm(installDir, { force: true, recursive: true })
- }
- await fs.mkdir(installDir, { recursive: true })
- if (ext === "zip") {
- const ok = await Archive.extractZip(tempPath, installDir)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- return false
- })
- if (!ok) return
- } else {
- const ok = await $`tar -xzf ${tempPath} -C ${installDir}`
- .quiet()
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- return false
- })
- if (!ok) return
- }
- await fs.rm(tempPath, { force: true })
- // Binary is located in bin/ subdirectory within the extracted archive
- bin = path.join(installDir, "bin", "lua-language-server" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract lua-language-server binary")
- return
- }
- if (platform !== "win32") {
- const ok = await $`chmod +x ${bin}`.quiet().catch((error) => {
- log.error("Failed to set executable permission for lua-language-server binary", {
- error,
- })
- })
- if (!ok) return
- }
- log.info(`installed lua-language-server`, { bin })
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const PHPIntelephense: Info = {
- id: "php intelephense",
- extensions: [".php"],
- root: NearestRoot(["composer.json", "composer.lock", ".php-version"]),
- async spawn(root) {
- let binary = Bun.which("intelephense")
- const args: string[] = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "intelephense", "lib", "intelephense.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "intelephense"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- initialization: {},
- }
- },
- }
- export const Prisma: Info = {
- id: "prisma",
- extensions: [".prisma"],
- root: NearestRoot(["schema.prisma", "prisma/schema.prisma", "prisma"], ["package.json"]),
- async spawn(root) {
- const prisma = Bun.which("prisma")
- if (!prisma) {
- log.info("prisma not found, please install prisma")
- return
- }
- return {
- process: spawn(prisma, ["language-server"], {
- cwd: root,
- }),
- }
- },
- }
- export const Dart: Info = {
- id: "dart",
- extensions: [".dart"],
- root: NearestRoot(["pubspec.yaml", "analysis_options.yaml"]),
- async spawn(root) {
- const dart = Bun.which("dart")
- if (!dart) {
- log.info("dart not found, please install dart first")
- return
- }
- return {
- process: spawn(dart, ["language-server", "--lsp"], {
- cwd: root,
- }),
- }
- },
- }
- export const Ocaml: Info = {
- id: "ocaml-lsp",
- extensions: [".ml", ".mli"],
- root: NearestRoot(["dune-project", "dune-workspace", ".merlin", "opam"]),
- async spawn(root) {
- const bin = Bun.which("ocamllsp")
- if (!bin) {
- log.info("ocamllsp not found, please install ocaml-lsp-server")
- return
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const BashLS: Info = {
- id: "bash",
- extensions: [".sh", ".bash", ".zsh", ".ksh"],
- root: async () => Instance.directory,
- async spawn(root) {
- let binary = Bun.which("bash-language-server")
- const args: string[] = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "bash-language-server", "out", "cli.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "bash-language-server"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("start")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- }
- },
- }
- export const TerraformLS: Info = {
- id: "terraform",
- extensions: [".tf", ".tfvars"],
- root: NearestRoot([".terraform.lock.hcl", "terraform.tfstate", "*.tf"]),
- async spawn(root) {
- let bin = Bun.which("terraform-ls", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading terraform-ls from GitHub releases")
- const releaseResponse = await fetch("https://api.github.com/repos/hashicorp/terraform-ls/releases/latest")
- if (!releaseResponse.ok) {
- log.error("Failed to fetch terraform-ls release info")
- return
- }
- const release = (await releaseResponse.json()) as {
- tag_name?: string
- assets?: { name?: string; browser_download_url?: string }[]
- }
- const version = release.tag_name?.replace("v", "")
- if (!version) {
- log.error("terraform-ls release did not include a version tag")
- return
- }
- const platform = process.platform
- const arch = process.arch
- const tfArch = arch === "arm64" ? "arm64" : "amd64"
- const tfPlatform = platform === "win32" ? "windows" : platform
- const assetName = `terraform-ls_${version}_${tfPlatform}_${tfArch}.zip`
- const assets = release.assets ?? []
- const asset = assets.find((a) => a.name === assetName)
- if (!asset?.browser_download_url) {
- log.error(`Could not find asset ${assetName} in terraform-ls release`)
- return
- }
- const downloadResponse = await fetch(asset.browser_download_url)
- if (!downloadResponse.ok) {
- log.error("Failed to download terraform-ls")
- return
- }
- const tempPath = path.join(Global.Path.bin, assetName)
- await Bun.file(tempPath).write(downloadResponse)
- const ok = await Archive.extractZip(tempPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract terraform-ls archive", { error })
- return false
- })
- if (!ok) return
- await fs.rm(tempPath, { force: true })
- bin = path.join(Global.Path.bin, "terraform-ls" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract terraform-ls binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.quiet().nothrow()
- }
- log.info(`installed terraform-ls`, { bin })
- }
- return {
- process: spawn(bin, ["serve"], {
- cwd: root,
- }),
- initialization: {
- experimentalFeatures: {
- prefillRequiredFields: true,
- validateOnSave: true,
- },
- },
- }
- },
- }
- export const TexLab: Info = {
- id: "texlab",
- extensions: [".tex", ".bib"],
- root: NearestRoot([".latexmkrc", "latexmkrc", ".texlabroot", "texlabroot"]),
- async spawn(root) {
- let bin = Bun.which("texlab", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading texlab from GitHub releases")
- const response = await fetch("https://api.github.com/repos/latex-lsp/texlab/releases/latest")
- if (!response.ok) {
- log.error("Failed to fetch texlab release info")
- return
- }
- const release = (await response.json()) as {
- tag_name?: string
- assets?: { name?: string; browser_download_url?: string }[]
- }
- const version = release.tag_name?.replace("v", "")
- if (!version) {
- log.error("texlab release did not include a version tag")
- return
- }
- const platform = process.platform
- const arch = process.arch
- const texArch = arch === "arm64" ? "aarch64" : "x86_64"
- const texPlatform = platform === "darwin" ? "macos" : platform === "win32" ? "windows" : "linux"
- const ext = platform === "win32" ? "zip" : "tar.gz"
- const assetName = `texlab-${texArch}-${texPlatform}.${ext}`
- const assets = release.assets ?? []
- const asset = assets.find((a) => a.name === assetName)
- if (!asset?.browser_download_url) {
- log.error(`Could not find asset ${assetName} in texlab release`)
- return
- }
- const downloadResponse = await fetch(asset.browser_download_url)
- if (!downloadResponse.ok) {
- log.error("Failed to download texlab")
- return
- }
- const tempPath = path.join(Global.Path.bin, assetName)
- await Bun.file(tempPath).write(downloadResponse)
- if (ext === "zip") {
- const ok = await Archive.extractZip(tempPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract texlab archive", { error })
- return false
- })
- if (!ok) return
- }
- if (ext === "tar.gz") {
- await $`tar -xzf ${tempPath}`.cwd(Global.Path.bin).quiet().nothrow()
- }
- await fs.rm(tempPath, { force: true })
- bin = path.join(Global.Path.bin, "texlab" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract texlab binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.quiet().nothrow()
- }
- log.info("installed texlab", { bin })
- }
- return {
- process: spawn(bin, {
- cwd: root,
- }),
- }
- },
- }
- export const DockerfileLS: Info = {
- id: "dockerfile",
- extensions: [".dockerfile", "Dockerfile"],
- root: async () => Instance.directory,
- async spawn(root) {
- let binary = Bun.which("docker-langserver")
- const args: string[] = []
- if (!binary) {
- const js = path.join(Global.Path.bin, "node_modules", "dockerfile-language-server-nodejs", "lib", "server.js")
- if (!(await Bun.file(js).exists())) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- await Bun.spawn([BunProc.which(), "install", "dockerfile-language-server-nodejs"], {
- cwd: Global.Path.bin,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- }).exited
- }
- binary = BunProc.which()
- args.push("run", js)
- }
- args.push("--stdio")
- const proc = spawn(binary, args, {
- cwd: root,
- env: {
- ...process.env,
- BUN_BE_BUN: "1",
- },
- })
- return {
- process: proc,
- }
- },
- }
- export const Gleam: Info = {
- id: "gleam",
- extensions: [".gleam"],
- root: NearestRoot(["gleam.toml"]),
- async spawn(root) {
- const gleam = Bun.which("gleam")
- if (!gleam) {
- log.info("gleam not found, please install gleam first")
- return
- }
- return {
- process: spawn(gleam, ["lsp"], {
- cwd: root,
- }),
- }
- },
- }
- export const Clojure: Info = {
- id: "clojure-lsp",
- extensions: [".clj", ".cljs", ".cljc", ".edn"],
- root: NearestRoot(["deps.edn", "project.clj", "shadow-cljs.edn", "bb.edn", "build.boot"]),
- async spawn(root) {
- let bin = Bun.which("clojure-lsp")
- if (!bin && process.platform === "win32") {
- bin = Bun.which("clojure-lsp.exe")
- }
- if (!bin) {
- log.info("clojure-lsp not found, please install clojure-lsp first")
- return
- }
- return {
- process: spawn(bin, ["listen"], {
- cwd: root,
- }),
- }
- },
- }
- export const Nixd: Info = {
- id: "nixd",
- extensions: [".nix"],
- root: async (file) => {
- // First, look for flake.nix - the most reliable Nix project root indicator
- const flakeRoot = await NearestRoot(["flake.nix"])(file)
- if (flakeRoot && flakeRoot !== Instance.directory) return flakeRoot
- // If no flake.nix, fall back to git repository root
- if (Instance.worktree && Instance.worktree !== Instance.directory) return Instance.worktree
- // Finally, use the instance directory as fallback
- return Instance.directory
- },
- async spawn(root) {
- const nixd = Bun.which("nixd")
- if (!nixd) {
- log.info("nixd not found, please install nixd first")
- return
- }
- return {
- process: spawn(nixd, [], {
- cwd: root,
- env: {
- ...process.env,
- },
- }),
- }
- },
- }
- export const Tinymist: Info = {
- id: "tinymist",
- extensions: [".typ", ".typc"],
- root: NearestRoot(["typst.toml"]),
- async spawn(root) {
- let bin = Bun.which("tinymist", {
- PATH: process.env["PATH"] + path.delimiter + Global.Path.bin,
- })
- if (!bin) {
- if (Flag.OPENCODE_DISABLE_LSP_DOWNLOAD) return
- log.info("downloading tinymist from GitHub releases")
- const response = await fetch("https://api.github.com/repos/Myriad-Dreamin/tinymist/releases/latest")
- if (!response.ok) {
- log.error("Failed to fetch tinymist release info")
- return
- }
- const release = (await response.json()) as {
- tag_name?: string
- assets?: { name?: string; browser_download_url?: string }[]
- }
- const platform = process.platform
- const arch = process.arch
- const tinymistArch = arch === "arm64" ? "aarch64" : "x86_64"
- let tinymistPlatform: string
- let ext: string
- if (platform === "darwin") {
- tinymistPlatform = "apple-darwin"
- ext = "tar.gz"
- } else if (platform === "win32") {
- tinymistPlatform = "pc-windows-msvc"
- ext = "zip"
- } else {
- tinymistPlatform = "unknown-linux-gnu"
- ext = "tar.gz"
- }
- const assetName = `tinymist-${tinymistArch}-${tinymistPlatform}.${ext}`
- const assets = release.assets ?? []
- const asset = assets.find((a) => a.name === assetName)
- if (!asset?.browser_download_url) {
- log.error(`Could not find asset ${assetName} in tinymist release`)
- return
- }
- const downloadResponse = await fetch(asset.browser_download_url)
- if (!downloadResponse.ok) {
- log.error("Failed to download tinymist")
- return
- }
- const tempPath = path.join(Global.Path.bin, assetName)
- await Bun.file(tempPath).write(downloadResponse)
- if (ext === "zip") {
- const ok = await Archive.extractZip(tempPath, Global.Path.bin)
- .then(() => true)
- .catch((error) => {
- log.error("Failed to extract tinymist archive", { error })
- return false
- })
- if (!ok) return
- } else {
- await $`tar -xzf ${tempPath} --strip-components=1`.cwd(Global.Path.bin).quiet().nothrow()
- }
- await fs.rm(tempPath, { force: true })
- bin = path.join(Global.Path.bin, "tinymist" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract tinymist binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.quiet().nothrow()
- }
- log.info("installed tinymist", { bin })
- }
- return {
- process: spawn(bin, { cwd: root }),
- }
- },
- }
- export const HLS: Info = {
- id: "haskell-language-server",
- extensions: [".hs", ".lhs"],
- root: NearestRoot(["stack.yaml", "cabal.project", "hie.yaml", "*.cabal"]),
- async spawn(root) {
- const bin = Bun.which("haskell-language-server-wrapper")
- if (!bin) {
- log.info("haskell-language-server-wrapper not found, please install haskell-language-server")
- return
- }
- return {
- process: spawn(bin, ["--lsp"], {
- cwd: root,
- }),
- }
- },
- }
- }
|