| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073 |
- 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 { $ } from "bun"
- import fs from "fs/promises"
- import { Filesystem } from "../util/filesystem"
- import { Instance } from "../project/instance"
- import { Flag } from "../flag/flag"
- 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(
- () => {},
- )
- 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)
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
- 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)
- await $`npm install`.cwd(finalPath).quiet()
- await $`npm run compile`.cwd(finalPath).quiet()
- log.info("installed VS Code ESLint server", { serverPath })
- }
- const proc = spawn(BunProc.which(), ["--max-old-space-size=8192", serverPath, "--stdio"], {
- 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"] + ":" + 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 RubyLsp: Info = {
- id: "ruby-lsp",
- root: NearestRoot(["Gemfile"]),
- extensions: [".rb", ".rake", ".gemspec", ".ru"],
- async spawn(root) {
- let bin = Bun.which("ruby-lsp", {
- PATH: process.env["PATH"] + ":" + 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 ruby-lsp")
- const proc = Bun.spawn({
- cmd: ["gem", "install", "ruby-lsp", "--bindir", Global.Path.bin],
- stdout: "pipe",
- stderr: "pipe",
- stdin: "pipe",
- })
- const exit = await proc.exited
- if (exit !== 0) {
- log.error("Failed to install ruby-lsp")
- return
- }
- bin = path.join(Global.Path.bin, "ruby-lsp" + (process.platform === "win32" ? ".exe" : ""))
- log.info(`installed ruby-lsp`, {
- bin,
- })
- }
- return {
- process: spawn(bin!, ["--stdio"], {
- cwd: root,
- }),
- }
- },
- }
- 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.bar" : "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)
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
- 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"] + ":" + 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") {
- await $`unzip -o -q ${tempPath}`.quiet().cwd(Global.Path.bin).nothrow()
- } else {
- await $`tar -xf ${tempPath}`.cwd(Global.Path.bin).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}`.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"] + ":" + 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 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) {
- let bin = Bun.which("clangd", {
- PATH: process.env["PATH"] + ":" + Global.Path.bin,
- })
- if (!bin) {
- 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 = (await releaseResponse.json()) as any
- const platform = process.platform
- let assetName = ""
- if (platform === "darwin") {
- assetName = "clangd-mac-"
- } else if (platform === "linux") {
- assetName = "clangd-linux-"
- } else if (platform === "win32") {
- assetName = "clangd-windows-"
- } else {
- log.error(`Platform ${platform} is not supported by clangd auto-download`)
- return
- }
- assetName += release.tag_name + ".zip"
- const asset = release.assets.find((a: any) => a.name === assetName)
- if (!asset) {
- log.error(`Could not find asset ${assetName} in latest clangd release`)
- return
- }
- const downloadUrl = asset.browser_download_url
- const downloadResponse = await fetch(downloadUrl)
- if (!downloadResponse.ok) {
- log.error("Failed to download clangd")
- return
- }
- const zipPath = path.join(Global.Path.bin, "clangd.zip")
- await Bun.file(zipPath).write(downloadResponse)
- await $`unzip -o -q ${zipPath}`.quiet().cwd(Global.Path.bin).nothrow()
- await fs.rm(zipPath, { force: true })
- const extractedDir = path.join(Global.Path.bin, assetName.replace(".zip", ""))
- bin = path.join(extractedDir, "bin", "clangd" + (platform === "win32" ? ".exe" : ""))
- if (!(await Bun.file(bin).exists())) {
- log.error("Failed to extract clangd binary")
- return
- }
- if (platform !== "win32") {
- await $`chmod +x ${bin}`.nothrow()
- }
- log.info(`installed clangd`, { bin })
- }
- return {
- process: spawn(bin, ["--background-index", "--clang-tidy"], {
- 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_windows"
- 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 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"] + ":" + 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 $`unzip -o -q ${tempPath} -d ${installDir}`.quiet().catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- })
- if (!ok) return
- } else {
- const ok = await $`tar -xzf ${tempPath} -C ${installDir}`.quiet().catch((error) => {
- log.error("Failed to extract lua-language-server archive", { error })
- })
- 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,
- }),
- }
- },
- }
- }
|