Dax Raad 8 месяцев назад
Родитель
Сommit
32e6a552c0

+ 11 - 53
packages/opencode/src/lsp/client.ts

@@ -1,4 +1,3 @@
-import { Readable, Writable } from "stream"
 import path from "path"
 import {
   createMessageConnection,
@@ -11,11 +10,12 @@ import { Log } from "../util/log"
 import { LANGUAGE_EXTENSIONS } from "./language"
 import { Bus } from "../bus"
 import z from "zod"
+import type { LSPServer } from "./server"
 
 export namespace LSPClient {
   const log = Log.create({ service: "lsp.client" })
 
-  export type Info = Awaited<ReturnType<typeof create>>
+  export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
 
   export type Diagnostic = VSCodeDiagnostic
 
@@ -29,60 +29,18 @@ export namespace LSPClient {
     ),
   }
 
-  export async function create(input: {
-    cmd: string[]
-    serverID: string
-    initialization?: any
-  }) {
+  export async function create(input: LSPServer.Info) {
     const app = App.info()
     log.info("starting client", {
-      ...input,
-      cwd: app.path.cwd,
+      id: input.id,
     })
 
-    const server = Bun.spawn({
-      cmd: input.cmd,
-      stdin: "pipe",
-      stdout: "pipe",
-      stderr: "pipe",
-      cwd: app.path.cwd,
-    })
-
-    const stdout = new Readable({
-      read() {},
-      construct(callback) {
-        const reader = server.stdout.getReader()
-        const pump = async () => {
-          try {
-            while (true) {
-              const { done, value } = await reader.read()
-              if (done) {
-                this.push(null)
-                break
-              }
-              this.push(Buffer.from(value))
-            }
-          } catch (error) {
-            this.destroy(
-              error instanceof Error ? error : new Error(String(error)),
-            )
-          }
-        }
-        pump()
-        callback()
-      },
-    })
-
-    const stdin = new Writable({
-      write(chunk, _encoding, callback) {
-        server.stdin.write(chunk)
-        callback()
-      },
-    })
+    const server = await input.spawn(app)
+    if (!server) return
 
     const connection = createMessageConnection(
-      new StreamMessageReader(stdout),
-      new StreamMessageWriter(stdin),
+      new StreamMessageReader(server.stdout),
+      new StreamMessageWriter(server.stdin),
     )
 
     const diagnostics = new Map<string, Diagnostic[]>()
@@ -92,14 +50,14 @@ export namespace LSPClient {
         path,
       })
       diagnostics.set(path, params.diagnostics)
-      Bus.publish(Event.Diagnostics, { path, serverID: input.serverID })
+      Bus.publish(Event.Diagnostics, { path, serverID: input.id })
     })
     connection.onRequest("workspace/configuration", async () => {
       return [{}]
     })
     connection.listen()
 
-    const response = await connection.sendRequest("initialize", {
+    await connection.sendRequest("initialize", {
       processId: server.pid,
       workspaceFolders: [
         {
@@ -134,7 +92,7 @@ export namespace LSPClient {
 
     const result = {
       get clientID() {
-        return input.serverID
+        return input.id
       },
       get connection() {
         return connection

+ 6 - 44
packages/opencode/src/lsp/index.ts

@@ -2,6 +2,7 @@ import { App } from "../app/app"
 import { Log } from "../util/log"
 import { LSPClient } from "./client"
 import path from "path"
+import { LSPServer } from "./server"
 
 export namespace LSP {
   const log = Log.create({ service: "lsp" })
@@ -26,18 +27,14 @@ export namespace LSP {
   export async function touchFile(input: string, waitForDiagnostics?: boolean) {
     const extension = path.parse(input).ext
     const s = await state()
-    const matches = AUTO.filter((x) => x.extensions.includes(extension))
+    const matches = LSPServer.All.filter((x) =>
+      x.extensions.includes(extension),
+    )
     for (const match of matches) {
       const existing = s.clients.get(match.id)
       if (existing) continue
-      const [binary] = match.command
-      const bin = Bun.which(binary)
-      if (!bin) continue
-      const client = await LSPClient.create({
-        cmd: match.command,
-        serverID: match.id,
-        initialization: match.initialization,
-      })
+      const client = await LSPClient.create(match)
+      if (!client) continue
       s.clients.set(match.id, client)
     }
     if (waitForDiagnostics) {
@@ -87,41 +84,6 @@ export namespace LSP {
     return Promise.all(tasks)
   }
 
-  const AUTO: {
-    id: string
-    command: string[]
-    initialization?: any
-    extensions: string[]
-    install?: () => Promise<void>
-  }[] = [
-    {
-      id: "typescript",
-      command: ["bun", "x", "typescript-language-server", "--stdio"],
-      extensions: [
-        ".ts",
-        ".tsx",
-        ".js",
-        ".jsx",
-        ".mjs",
-        ".cjs",
-        ".mts",
-        ".cts",
-        ".mtsx",
-        ".ctsx",
-      ],
-      initialization: {
-        tsserver: {
-          path: require.resolve("typescript/lib/tsserver.js"),
-        },
-      },
-    },
-    {
-      id: "golang",
-      command: ["gopls" /*"-logfile", "gopls.log", "-rpc.trace", "-vv"*/],
-      extensions: [".go"],
-    },
-  ]
-
   export namespace Diagnostic {
     export function pretty(diagnostic: LSPClient.Diagnostic) {
       const severityMap = {

+ 70 - 0
packages/opencode/src/lsp/server.ts

@@ -0,0 +1,70 @@
+import { spawn, type ChildProcessWithoutNullStreams } from "child_process"
+import type { App } from "../app/app"
+import path from "path"
+import { Global } from "../global"
+import { Log } from "../util/log"
+
+export namespace LSPServer {
+  const log = Log.create({ service: "lsp.server" })
+
+  export interface Info {
+    id: string
+    extensions: string[]
+    initialization?: Record<string, any>
+    spawn(app: App.Info): Promise<ChildProcessWithoutNullStreams | undefined>
+  }
+
+  export const All: Info[] = [
+    {
+      id: "typescript",
+      extensions: [
+        ".ts",
+        ".tsx",
+        ".js",
+        ".jsx",
+        ".mjs",
+        ".cjs",
+        ".mts",
+        ".cts",
+      ],
+      async spawn() {
+        const root =
+          process.argv0 !== "bun"
+            ? path.resolve(process.cwd(), process.argv0)
+            : process.argv0
+        return spawn(root + " x typescript-language-server --stdio", {
+          argv0: "bun",
+        })
+      },
+    },
+    {
+      id: "golang",
+      extensions: [".go"],
+      async spawn() {
+        let bin = Bun.which("gopls", {
+          PATH: process.env["PATH"] + ":" + Global.Path.bin,
+        })
+        if (!bin) {
+          log.info("installing gopls")
+          const proc = Bun.spawn({
+            cmd: ["go", "install", "golang.org/x/tools/gopls@latest"],
+            env: { ...process.env, GOBIN: Global.Path.bin },
+          })
+          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 spawn(bin!)
+      },
+    },
+  ]
+}