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" }) const pathExists = async (p: string) => fs .stat(p) .then(() => true) .catch(() => false) 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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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,Lines 1–500 of 2046. Load more
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" }) const pathExists = async (p: string) => fs .stat(p) .then(() => true) .catch(() => false) 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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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.OPENSCIENCE_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,