2046 lines · 61.1 KB · TypeScript
RawDownload
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.