|
|
@@ -6,6 +6,9 @@ import { Log } from "../util/log"
|
|
|
import { BunProc } from "../bun"
|
|
|
import { $ } from "bun"
|
|
|
import fs from "fs/promises"
|
|
|
+import { unique } from "remeda"
|
|
|
+import { Ripgrep } from "../file/ripgrep"
|
|
|
+import type { LSPClient } from "./client"
|
|
|
|
|
|
export namespace LSPServer {
|
|
|
const log = Log.create({ service: "lsp.server" })
|
|
|
@@ -13,21 +16,40 @@ export namespace LSPServer {
|
|
|
export interface Handle {
|
|
|
process: ChildProcessWithoutNullStreams
|
|
|
initialization?: Record<string, any>
|
|
|
+ onInitialized?: (lsp: LSPClient.Info) => Promise<void>
|
|
|
+ }
|
|
|
+
|
|
|
+ type RootsFunction = (app: App.Info) => Promise<string[]>
|
|
|
+
|
|
|
+ const SimpleRoots = (patterns: string[]): RootsFunction => {
|
|
|
+ return async (app) => {
|
|
|
+ const glob = `**/*/{${patterns.join(",")}}`
|
|
|
+ const files = await Ripgrep.files({
|
|
|
+ glob: [glob],
|
|
|
+ cwd: app.path.root,
|
|
|
+ })
|
|
|
+ const dirs = files.map((file) => path.dirname(file))
|
|
|
+ return unique(dirs).map((dir) => path.join(app.path.root, dir))
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
export interface Info {
|
|
|
id: string
|
|
|
extensions: string[]
|
|
|
- spawn(app: App.Info): Promise<Handle | undefined>
|
|
|
+ global?: boolean
|
|
|
+ roots: (app: App.Info) => Promise<string[]>
|
|
|
+ spawn(app: App.Info, root: string): Promise<Handle | undefined>
|
|
|
}
|
|
|
|
|
|
export const Typescript: Info = {
|
|
|
id: "typescript",
|
|
|
+ roots: SimpleRoots(["tsconfig.json", "jsconfig.json", "package.json"]),
|
|
|
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
|
- async spawn(app) {
|
|
|
+ async spawn(app, root) {
|
|
|
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
|
|
|
if (!tsserver) return
|
|
|
const proc = spawn(BunProc.which(), ["x", "typescript-language-server", "--stdio"], {
|
|
|
+ cwd: root,
|
|
|
env: {
|
|
|
...process.env,
|
|
|
BUN_BE_BUN: "1",
|
|
|
@@ -40,14 +62,31 @@ export namespace LSPServer {
|
|
|
path: tsserver,
|
|
|
},
|
|
|
},
|
|
|
+ // tsserver sucks and won't start processing codebase until you open a file
|
|
|
+ onInitialized: async (lsp) => {
|
|
|
+ const [hint] = await Ripgrep.files({
|
|
|
+ cwd: lsp.root,
|
|
|
+ glob: ["*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "*.cjs", "*.mts", "*.cts"],
|
|
|
+ limit: 1,
|
|
|
+ })
|
|
|
+ await new Promise<void>(async (resolve) => {
|
|
|
+ const notif = lsp.connection.onNotification("$/progress", (params) => {
|
|
|
+ if (params.value.kind !== "end") return
|
|
|
+ notif.dispose()
|
|
|
+ resolve()
|
|
|
+ })
|
|
|
+ await lsp.notify.open({ path: hint })
|
|
|
+ })
|
|
|
+ },
|
|
|
}
|
|
|
},
|
|
|
}
|
|
|
|
|
|
export const Gopls: Info = {
|
|
|
id: "golang",
|
|
|
+ roots: SimpleRoots(["go.mod", "go.sum"]),
|
|
|
extensions: [".go"],
|
|
|
- async spawn() {
|
|
|
+ async spawn(_, root) {
|
|
|
let bin = Bun.which("gopls", {
|
|
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
|
|
})
|
|
|
@@ -72,15 +111,18 @@ export namespace LSPServer {
|
|
|
})
|
|
|
}
|
|
|
return {
|
|
|
- process: spawn(bin!),
|
|
|
+ process: spawn(bin!, {
|
|
|
+ cwd: root,
|
|
|
+ }),
|
|
|
}
|
|
|
},
|
|
|
}
|
|
|
|
|
|
export const RubyLsp: Info = {
|
|
|
id: "ruby-lsp",
|
|
|
+ roots: SimpleRoots(["Gemfile"]),
|
|
|
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
|
|
- async spawn() {
|
|
|
+ async spawn(_, root) {
|
|
|
let bin = Bun.which("ruby-lsp", {
|
|
|
PATH: process.env["PATH"] + ":" + Global.Path.bin,
|
|
|
})
|
|
|
@@ -109,7 +151,9 @@ export namespace LSPServer {
|
|
|
})
|
|
|
}
|
|
|
return {
|
|
|
- process: spawn(bin!, ["--stdio"]),
|
|
|
+ process: spawn(bin!, ["--stdio"], {
|
|
|
+ cwd: root,
|
|
|
+ }),
|
|
|
}
|
|
|
},
|
|
|
}
|
|
|
@@ -117,8 +161,17 @@ export namespace LSPServer {
|
|
|
export const Pyright: Info = {
|
|
|
id: "pyright",
|
|
|
extensions: [".py", ".pyi"],
|
|
|
- async spawn() {
|
|
|
+ roots: SimpleRoots([
|
|
|
+ "pyproject.toml",
|
|
|
+ "setup.py",
|
|
|
+ "setup.cfg",
|
|
|
+ "requirements.txt",
|
|
|
+ "Pipfile",
|
|
|
+ "pyrightconfig.json",
|
|
|
+ ]),
|
|
|
+ async spawn(_, root) {
|
|
|
const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
|
|
|
+ cwd: root,
|
|
|
env: {
|
|
|
...process.env,
|
|
|
BUN_BE_BUN: "1",
|
|
|
@@ -133,7 +186,8 @@ export namespace LSPServer {
|
|
|
export const ElixirLS: Info = {
|
|
|
id: "elixir-ls",
|
|
|
extensions: [".ex", ".exs"],
|
|
|
- async spawn() {
|
|
|
+ roots: SimpleRoots(["mix.exs", "mix.lock"]),
|
|
|
+ async spawn(_, root) {
|
|
|
let binary = Bun.which("elixir-ls")
|
|
|
if (!binary) {
|
|
|
const elixirLsPath = path.join(Global.Path.bin, "elixir-ls")
|
|
|
@@ -177,109 +231,9 @@ export namespace LSPServer {
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
- process: spawn(binary),
|
|
|
- }
|
|
|
- },
|
|
|
- }
|
|
|
-
|
|
|
- export const Zls: Info = {
|
|
|
- id: "zls",
|
|
|
- extensions: [".zig", ".zon"],
|
|
|
- async spawn() {
|
|
|
- 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
|
|
|
- }
|
|
|
-
|
|
|
- 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()
|
|
|
-
|
|
|
- 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("Unsupported platform/architecture for zls", { platform, arch, assetName })
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const asset = release.assets?.find((a: any) => a.name === assetName)
|
|
|
-
|
|
|
- if (!asset) {
|
|
|
- log.error("Could not find zls download for platform", { platform, arch, assetName })
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const downloadUrl = asset.browser_download_url
|
|
|
- log.info("downloading zls", { url: downloadUrl })
|
|
|
-
|
|
|
- const response = await fetch(downloadUrl)
|
|
|
- if (!response.ok) {
|
|
|
- log.error("Failed to download zls")
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const isZip = assetName.endsWith(".zip")
|
|
|
- const archivePath = path.join(Global.Path.bin, isZip ? "zls.zip" : "zls.tar.xz")
|
|
|
- await Bun.file(archivePath).write(response)
|
|
|
-
|
|
|
- if (isZip) {
|
|
|
- await $`unzip -o -q ${archivePath} -d ${Global.Path.bin}`.nothrow()
|
|
|
- } else {
|
|
|
- await $`tar -xf ${archivePath} -C ${Global.Path.bin}`.quiet()
|
|
|
- }
|
|
|
-
|
|
|
- await fs.rm(archivePath, { force: true })
|
|
|
-
|
|
|
- if (platform !== "win32") {
|
|
|
- bin = path.join(Global.Path.bin, "zls")
|
|
|
- await $`chmod +x ${bin}`.quiet()
|
|
|
- } else {
|
|
|
- bin = path.join(Global.Path.bin, "zls.exe")
|
|
|
- }
|
|
|
-
|
|
|
- log.info("installed zls", { bin })
|
|
|
- }
|
|
|
-
|
|
|
- return {
|
|
|
- process: spawn(bin!),
|
|
|
+ process: spawn(binary, {
|
|
|
+ cwd: root,
|
|
|
+ }),
|
|
|
}
|
|
|
},
|
|
|
}
|