Browse Source

make LSP lazy again

Dax Raad 7 months ago
parent
commit
ba5be6b625

+ 0 - 1
packages/opencode/src/lsp/client.ts

@@ -184,7 +184,6 @@ export namespace LSPClient {
       },
       },
     }
     }
 
 
-    if (input.server.onInitialized) input.server.onInitialized(result)
     l.info("initialized")
     l.info("initialized")
 
 
     return result
     return result

+ 37 - 35
packages/opencode/src/lsp/index.ts

@@ -4,7 +4,6 @@ import { LSPClient } from "./client"
 import path from "path"
 import path from "path"
 import { LSPServer } from "./server"
 import { LSPServer } from "./server"
 import { z } from "zod"
 import { z } from "zod"
-import { Filesystem } from "../util/filesystem"
 
 
 export namespace LSP {
 export namespace LSP {
   const log = Log.create({ service: "lsp" })
   const log = Log.create({ service: "lsp" })
@@ -54,37 +53,10 @@ export namespace LSP {
 
 
   const state = App.state(
   const state = App.state(
     "lsp",
     "lsp",
-    async (app) => {
-      log.info("initializing")
+    async () => {
       const clients: LSPClient.Info[] = []
       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 {
       return {
+        broken: new Set<string>(),
         clients,
         clients,
       }
       }
     },
     },
@@ -99,13 +71,43 @@ export namespace LSP {
     return state()
     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) {
   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) => {
     await run(async (client) => {
-      if (!matches.includes(client.serverID)) return
+      if (!clients.includes(client)) return
       const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
       const wait = waitForDiagnostics ? client.waitForDiagnostics({ path: input }) : Promise.resolve()
       await client.notify.open({ path: input })
       await client.notify.open({ path: input })
       return wait
       return wait

+ 22 - 47
packages/opencode/src/lsp/server.ts

@@ -6,10 +6,7 @@ import { Log } from "../util/log"
 import { BunProc } from "../bun"
 import { BunProc } from "../bun"
 import { $ } from "bun"
 import { $ } from "bun"
 import fs from "fs/promises"
 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 {
 export namespace LSPServer {
   const log = Log.create({ service: "lsp.server" })
   const log = Log.create({ service: "lsp.server" })
@@ -17,19 +14,21 @@ export namespace LSPServer {
   export interface Handle {
   export interface Handle {
     process: ChildProcessWithoutNullStreams
     process: ChildProcessWithoutNullStreams
     initialization?: Record<string, any>
     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
     id: string
     extensions: string[]
     extensions: string[]
     global?: boolean
     global?: boolean
-    roots: (app: App.Info) => Promise<string[]>
+    root: RootFunction
     spawn(app: App.Info, root: string): Promise<Handle | undefined>
     spawn(app: App.Info, root: string): Promise<Handle | undefined>
   }
   }
 
 
   export const Typescript: Info = {
   export const Typescript: Info = {
     id: "typescript",
     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"],
     extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
     async spawn(app, root) {
     async spawn(app, root) {
       const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
       const tsserver = await Bun.resolve("typescript/lib/tsserver.js", app.path.cwd).catch(() => {})
@@ -62,33 +61,16 @@ export namespace LSPServer {
             path: tsserver,
             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 = {
   export const Gopls: Info = {
     id: "golang",
     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"],
     extensions: [".go"],
     async spawn(_, root) {
     async spawn(_, root) {
@@ -125,7 +107,7 @@ export namespace LSPServer {
 
 
   export const RubyLsp: Info = {
   export const RubyLsp: Info = {
     id: "ruby-lsp",
     id: "ruby-lsp",
-    roots: SimpleRoots(["Gemfile"]),
+    root: NearestRoot(["Gemfile"]),
     extensions: [".rb", ".rake", ".gemspec", ".ru"],
     extensions: [".rb", ".rake", ".gemspec", ".ru"],
     async spawn(_, root) {
     async spawn(_, root) {
       let bin = Bun.which("ruby-lsp", {
       let bin = Bun.which("ruby-lsp", {
@@ -166,14 +148,7 @@ export namespace LSPServer {
   export const Pyright: Info = {
   export const Pyright: Info = {
     id: "pyright",
     id: "pyright",
     extensions: [".py", ".pyi"],
     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) {
     async spawn(_, root) {
       const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
       const proc = spawn(BunProc.which(), ["x", "pyright-langserver", "--stdio"], {
         cwd: root,
         cwd: root,
@@ -191,7 +166,7 @@ export namespace LSPServer {
   export const ElixirLS: Info = {
   export const ElixirLS: Info = {
     id: "elixir-ls",
     id: "elixir-ls",
     extensions: [".ex", ".exs"],
     extensions: [".ex", ".exs"],
-    roots: SimpleRoots(["mix.exs", "mix.lock"]),
+    root: NearestRoot(["mix.exs", "mix.lock"]),
     async spawn(_, root) {
     async spawn(_, root) {
       let binary = Bun.which("elixir-ls")
       let binary = Bun.which("elixir-ls")
       if (!binary) {
       if (!binary) {
@@ -246,7 +221,7 @@ export namespace LSPServer {
   export const Zls: Info = {
   export const Zls: Info = {
     id: "zls",
     id: "zls",
     extensions: [".zig", ".zon"],
     extensions: [".zig", ".zon"],
-    roots: SimpleRoots(["build.zig"]),
+    root: NearestRoot(["build.zig"]),
     async spawn(_, root) {
     async spawn(_, root) {
       let bin = Bun.which("zls", {
       let bin = Bun.which("zls", {
         PATH: process.env["PATH"] + ":" + Global.Path.bin,
         PATH: process.env["PATH"] + ":" + Global.Path.bin,

+ 15 - 0
packages/opencode/src/util/filesystem.ts

@@ -26,6 +26,21 @@ export namespace Filesystem {
     return result
     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) {
   export async function globUp(pattern: string, start: string, stop?: string) {
     let current = start
     let current = start
     const result = []
     const result = []