summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-06-06 23:21:57 -0400
committerDax Raad <[email protected]>2025-06-06 23:21:57 -0400
commit32e6a552c0ded8946126c969083ae53b733be0d8 (patch)
treedfc96d2cbb0f12bec645996a30656ee49a6e282e
parentd6afebf22a66958393f25caa33236c345584c79d (diff)
downloadopencode-32e6a552c0ded8946126c969083ae53b733be0d8.tar.gz
opencode-32e6a552c0ded8946126c969083ae53b733be0d8.zip
autodownload lsp
-rw-r--r--packages/opencode/src/lsp/client.ts64
-rw-r--r--packages/opencode/src/lsp/index.ts50
-rw-r--r--packages/opencode/src/lsp/server.ts70
3 files changed, 87 insertions, 97 deletions
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
index eea989e0c..e2f02216c 100644
--- a/packages/opencode/src/lsp/client.ts
+++ b/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
diff --git a/packages/opencode/src/lsp/index.ts b/packages/opencode/src/lsp/index.ts
index e6cee0a4d..522be73af 100644
--- a/packages/opencode/src/lsp/index.ts
+++ b/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 = {
diff --git a/packages/opencode/src/lsp/server.ts b/packages/opencode/src/lsp/server.ts
new file mode 100644
index 000000000..80236e4df
--- /dev/null
+++ b/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!)
+ },
+ },
+ ]
+}