summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-07-10 09:37:28 -0400
committerDax Raad <[email protected]>2025-07-10 09:37:40 -0400
commitba5be6b6257ea06302db70e3f706e0e29359a77d (patch)
tree326c8a42d24cc33c7183e74789dc054fff103581
parentf95c3f4177fc2558005628ed458431d884444125 (diff)
downloadopencode-ba5be6b6257ea06302db70e3f706e0e29359a77d.tar.gz
opencode-ba5be6b6257ea06302db70e3f706e0e29359a77d.zip
make LSP lazy again
-rw-r--r--packages/opencode/src/lsp/client.ts1
-rw-r--r--packages/opencode/src/lsp/index.ts72
-rw-r--r--packages/opencode/src/lsp/server.ts69
-rw-r--r--packages/opencode/src/util/filesystem.ts15
4 files changed, 74 insertions, 83 deletions
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
index 51e6cbd39..c63e02592 100644
--- a/packages/opencode/src/lsp/client.ts
+++ b/packages/opencode/src/lsp/client.ts
@@ -184,7 +184,6 @@ export namespace LSPClient {
},
}
- if (input.server.onInitialized) input.server.onInitialized(result)
l.info("initialized")
return result
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index b91ca9e02..8f6c1c9cb 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/packages/opencode/src/lsp/index.ts
@@ -4,7 +4,6 @@ import { LSPClient } from "./client"
import path from "path"
import { LSPServer } from "./server"
import { z } from "zod"
-import { Filesystem } from "../util/filesystem"
export namespace LSP {
const log = Log.create({ service: "lsp" })
@@ -54,37 +53,10 @@ export namespace LSP {
const state = App.state(
"lsp",
- async (app) => {
- log.info("initializing")
+ async () => {
const clients: LSPClient.Info[] = []
- if (!app.git) return { clients }
-
- for (const server of Object.values(LSPServer)) {
- const roots = await server.roots(app)
-
- for (const root of roots) {
- if (!Filesystem.overlaps(app.path.cwd, root)) continue
- log.info("", {
- root,
- serverID: server.id,
- })
- const handle = await server.spawn(App.info(), root)
- if (!handle) break
- const client = await LSPClient.create({
- serverID: server.id,
- server: handle,
- root,
- }).catch((err) => {
- handle.process.kill()
- log.error("", { error: err })
- })
- if (!client) break
- clients.push(client)
- }
- }
-
- log.info("initialized")
return {
+ broken: new Set<string>(),
clients,
}
},
@@ -99,13 +71,43 @@ export namespace LSP {
return state()
}
+ async function getClients(file: string) {
+ const s = await state()
+ const extension = path.parse(file).ext
+ const result: LSPClient.Info[] = []
+ for (const server of Object.values(LSPServer)) {
+ if (!server.extensions.includes(extension)) continue
+ const root = await server.root(file, App.info())
+ if (!root) continue
+ if (s.broken.has(root + server.id)) continue
+
+ const match = s.clients.find((x) => x.root === root && x.serverID === server.id)
+ if (match) {
+ result.push(match)
+ continue
+ }
+ const handle = await server.spawn(App.info(), root)
+ if (!handle) continue
+ const client = await LSPClient.create({
+ serverID: server.id,
+ server: handle,
+ root,
+ }).catch((err) => {
+ s.broken.add(root + server.id)
+ handle.process.kill()
+ log.error("", { error: err })
+ })
+ if (!client) continue
+ s.clients.push(client)
+ result.push(client)
+ }
+ return result
+ }
+
export async function touchFile(input: string, waitForDiagnostics?: boolean) {
- const extension = path.parse(input).ext
- const matches = Object.values(LSPServer)
- .filter((x) => x.extensions.includes(extension))
- .map((x) => x.id)
+ const clients = await getClients(input)
await run(async (client) => {
- if (!matches.includes(client.serverID)) return
+ if (!clients.includes(client)) return
const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
await client.notify.open({ path: input })
return wait
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
index 76c377772..8c843fea1 100644
--- a/packages/opencode/src/lsp/server.ts
+++ b/packages/opencode/src/lsp/server.ts
@@ -6,10 +6,7 @@ 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"
-import { withTimeout } from "../util/timeout"
+import { Filesystem } from "../util/filesystem"
export namespace LSPServer {
const log = Log.create({ service: "lsp.server" })
@@ -17,19 +14,21 @@ 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[]>
+ type RootFunction = (file: string, app: App.Info) => Promise<string | undefined>
- const SimpleRoots = (patterns: string[]): RootsFunction => {
- return async (app) => {
- const files = await Ripgrep.files({
- glob: patterns.map((p) => `**/${p}`),
- cwd: app.path.root,
+ const NearestRoot = (patterns: string[]): RootFunction => {
+ return async (file, app) => {
+ const files = Filesystem.up({
+ targets: patterns,
+ start: path.dirname(file),
+ stop: app.path.root,
})
- const dirs = files.map((file) => path.dirname(file))
- return unique(dirs).map((dir) => path.join(app.path.root, dir))
+ const first = await files.next()
+ await files.return()
+ if (!first.value) return app.path.root
+ return path.dirname(first.value)
}
}
@@ -37,13 +36,13 @@ export namespace LSPServer {
id: string
extensions: string[]
global?: boolean
- roots: (app: App.Info) => Promise<string[]>
+ root: RootFunction
spawn(app: App.Info, root: string): Promise<Handle | undefined>
}
export const Typescript: Info = {
id: "typescript",
- roots: async (app) => [app.path.root],
+ root: NearestRoot(["tsconfig.json", "package.json", "jsconfig.json"]),
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
async spawn(app, root) {
const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
@@ -62,33 +61,16 @@ 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,
- })
- const wait = 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: path.join(lsp.root, hint) })
- })
- await withTimeout(wait, 5_000)
- },
}
},
}
export const Gopls: Info = {
id: "golang",
- roots: async (app) => {
- const work = await SimpleRoots(["go.work"])(app)
- if (work.length > 0) return work
- return SimpleRoots(["go.mod", "go.sum"])(app)
+ root: async (file, app) => {
+ const work = await NearestRoot(["go.work"])(file, app)
+ if (work) return work
+ return NearestRoot(["go.mod", "go.sum"])(file, app)
},
extensions: [".go"],
async spawn(_, root) {
@@ -125,7 +107,7 @@ export namespace LSPServer {
export const RubyLsp: Info = {
id: "ruby-lsp",
- roots: SimpleRoots(["Gemfile"]),
+ root: NearestRoot(["Gemfile"]),
extensions: [".rb", ".rake", ".gemspec", ".ru"],
async spawn(_, root) {
let bin = Bun.which("ruby-lsp", {
@@ -166,14 +148,7 @@ export namespace LSPServer {
export const Pyright: Info = {
id: "pyright",
extensions: [".py", ".pyi"],
- roots: SimpleRoots([
- "pyproject.toml",
- "setup.py",
- "setup.cfg",
- "requirements.txt",
- "Pipfile",
- "pyrightconfig.json",
- ]),
+ root: NearestRoot(["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,
@@ -191,7 +166,7 @@ export namespace LSPServer {
export const ElixirLS: Info = {
id: "elixir-ls",
extensions: [".ex", ".exs"],
- roots: SimpleRoots(["mix.exs", "mix.lock"]),
+ root: NearestRoot(["mix.exs", "mix.lock"]),
async spawn(_, root) {
let binary = Bun.which("elixir-ls")
if (!binary) {
@@ -246,7 +221,7 @@ export namespace LSPServer {
export const Zls: Info = {
id: "zls",
extensions: [".zig", ".zon"],
- roots: SimpleRoots(["build.zig"]),
+ root: NearestRoot(["build.zig"]),
async spawn(_, root) {
let bin = Bun.which("zls", {
PATH: process.env["PATH"] + ":" + Global.Path.bin,
diff --git a/packages/opencode/src/util/filesystem.ts b/packages/opencode/src/util/filesystem.ts
index 318f60b93..d5149cf39 100644
--- a/packages/opencode/src/util/filesystem.ts
+++ b/packages/opencode/src/util/filesystem.ts
@@ -26,6 +26,21 @@ export namespace Filesystem {
return result
}
+ export async function* up(options: { targets: string[]; start: string; stop?: string }) {
+ const { targets, start, stop } = options
+ let current = start
+ while (true) {
+ for (const target of targets) {
+ const search = join(current, target)
+ if (await exists(search)) yield search
+ }
+ if (stop === current) break
+ const parent = dirname(current)
+ if (parent === current) break
+ current = parent
+ }
+ }
+
export async function globUp(pattern: string, start: string, stop?: string) {
let current = start
const result = []