summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChristopher Sacca <[email protected]>2025-11-08 17:31:39 -0500
committerGitHub <[email protected]>2025-11-08 16:31:39 -0600
commit271b679058473537099b9b333aa25fa45863efd1 (patch)
tree68204e2b273279743e3ae6f9f52c31cd23860078
parent83b16cb18ea9a72f318f2cdb783bdeefd5d02700 (diff)
downloadopencode-271b679058473537099b9b333aa25fa45863efd1.tar.gz
opencode-271b679058473537099b9b333aa25fa45863efd1.zip
fix(lsp): handle optional requests to avoid MethodNotFound (-32601) with MATLAB Language Server (#4007)
-rw-r--r--packages/opencode/src/lsp/client.ts33
-rw-r--r--packages/opencode/test/fixture/lsp/fake-lsp-server.js77
-rw-r--r--packages/opencode/test/lsp/client.test.ts95
3 files changed, 200 insertions, 5 deletions
diff --git a/packages/opencode/src/lsp/client.ts b/packages/opencode/src/lsp/client.ts
index 1a6e2cb71..9ab2fee6b 100644
--- a/packages/opencode/src/lsp/client.ts
+++ b/packages/opencode/src/lsp/client.ts
@@ -1,5 +1,9 @@
import path from "path"
-import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
+import {
+ createMessageConnection,
+ StreamMessageReader,
+ StreamMessageWriter,
+} from "vscode-jsonrpc/node"
import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { Log } from "../util/log"
import { LANGUAGE_EXTENSIONS } from "./language"
@@ -34,7 +38,11 @@ export namespace LSPClient {
),
}
- export async function create(input: { serverID: string; server: LSPServer.Handle; root: string }) {
+ export async function create(input: {
+ serverID: string
+ server: LSPServer.Handle
+ root: string
+ }) {
const l = log.clone().tag("serverID", input.serverID)
l.info("starting client")
@@ -62,6 +70,14 @@ export namespace LSPClient {
// Return server initialization options
return [input.server.initialization ?? {}]
})
+ connection.onRequest("client/registerCapability", async () => {})
+ connection.onRequest("client/unregisterCapability", async () => {})
+ connection.onRequest("workspace/workspaceFolders", async () => [
+ {
+ name: "workspace",
+ uri: "file://" + input.root,
+ },
+ ])
connection.listen()
l.info("sending initialize")
@@ -129,7 +145,9 @@ export namespace LSPClient {
},
notify: {
async open(input: { path: string }) {
- input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
+ input.path = path.isAbsolute(input.path)
+ ? input.path
+ : path.resolve(Instance.directory, input.path)
const file = Bun.file(input.path)
const text = await file.text()
const extension = path.extname(input.path)
@@ -171,13 +189,18 @@ export namespace LSPClient {
return diagnostics
},
async waitForDiagnostics(input: { path: string }) {
- input.path = path.isAbsolute(input.path) ? input.path : path.resolve(Instance.directory, input.path)
+ input.path = path.isAbsolute(input.path)
+ ? input.path
+ : path.resolve(Instance.directory, input.path)
log.info("waiting for diagnostics", input)
let unsub: () => void
return await withTimeout(
new Promise<void>((resolve) => {
unsub = Bus.subscribe(Event.Diagnostics, (event) => {
- if (event.properties.path === input.path && event.properties.serverID === result.serverID) {
+ if (
+ event.properties.path === input.path &&
+ event.properties.serverID === result.serverID
+ ) {
log.info("got diagnostics", input)
unsub?.()
resolve()
diff --git a/packages/opencode/test/fixture/lsp/fake-lsp-server.js b/packages/opencode/test/fixture/lsp/fake-lsp-server.js
new file mode 100644
index 000000000..39e578801
--- /dev/null
+++ b/packages/opencode/test/fixture/lsp/fake-lsp-server.js
@@ -0,0 +1,77 @@
+// Simple JSON-RPC 2.0 LSP-like fake server over stdio
+// Implements a minimal LSP handshake and triggers a request upon notification
+
+const net = require("net")
+
+let nextId = 1
+
+function encode(message) {
+ const json = JSON.stringify(message)
+ const header = `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n`
+ return Buffer.concat([Buffer.from(header, "utf8"), Buffer.from(json, "utf8")])
+}
+
+function decodeFrames(buffer) {
+ const results = []
+ let idx
+ while ((idx = buffer.indexOf("\r\n\r\n")) !== -1) {
+ const header = buffer.slice(0, idx).toString("utf8")
+ const m = /Content-Length:\s*(\d+)/i.exec(header)
+ const len = m ? parseInt(m[1], 10) : 0
+ const bodyStart = idx + 4
+ const bodyEnd = bodyStart + len
+ if (buffer.length < bodyEnd) break
+ const body = buffer.slice(bodyStart, bodyEnd).toString("utf8")
+ results.push(body)
+ buffer = buffer.slice(bodyEnd)
+ }
+ return { messages: results, rest: buffer }
+}
+
+let readBuffer = Buffer.alloc(0)
+
+process.stdin.on("data", (chunk) => {
+ readBuffer = Buffer.concat([readBuffer, chunk])
+ const { messages, rest } = decodeFrames(readBuffer)
+ readBuffer = rest
+ for (const m of messages) handle(m)
+})
+
+function send(msg) {
+ process.stdout.write(encode(msg))
+}
+
+function sendRequest(method, params) {
+ const id = nextId++
+ send({ jsonrpc: "2.0", id, method, params })
+ return id
+}
+
+function handle(raw) {
+ let data
+ try {
+ data = JSON.parse(raw)
+ } catch {
+ return
+ }
+ if (data.method === "initialize") {
+ send({ jsonrpc: "2.0", id: data.id, result: { capabilities: {} } })
+ return
+ }
+ if (data.method === "initialized") {
+ return
+ }
+ if (data.method === "workspace/didChangeConfiguration") {
+ return
+ }
+ if (data.method === "test/trigger") {
+ const method = data.params && data.params.method
+ if (method) sendRequest(method, {})
+ return
+ }
+ if (typeof data.id !== "undefined") {
+ // Respond OK to any request from client to keep transport flowing
+ send({ jsonrpc: "2.0", id: data.id, result: null })
+ return
+ }
+}
diff --git a/packages/opencode/test/lsp/client.test.ts b/packages/opencode/test/lsp/client.test.ts
new file mode 100644
index 000000000..c2ba3ac5b
--- /dev/null
+++ b/packages/opencode/test/lsp/client.test.ts
@@ -0,0 +1,95 @@
+import { describe, expect, test, beforeEach } from "bun:test"
+import path from "path"
+import { LSPClient } from "../../src/lsp/client"
+import { LSPServer } from "../../src/lsp/server"
+import { Instance } from "../../src/project/instance"
+import { Log } from "../../src/util/log"
+
+// Minimal fake LSP server that speaks JSON-RPC over stdio
+function spawnFakeServer() {
+ const { spawn } = require("child_process")
+ const serverPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
+ return {
+ process: spawn(process.execPath, [serverPath], {
+ stdio: "pipe",
+ }),
+ }
+}
+
+describe("LSPClient interop", () => {
+ beforeEach(async () => {
+ await Log.init({ print: true })
+ })
+
+ test("handles workspace/workspaceFolders request", async () => {
+ const handle = spawnFakeServer() as any
+
+ const client = await Instance.provide({
+ directory: process.cwd(),
+ fn: () =>
+ LSPClient.create({
+ serverID: "fake",
+ server: handle as unknown as LSPServer.Handle,
+ root: process.cwd(),
+ }),
+ })
+
+ await client.connection.sendNotification("test/trigger", {
+ method: "workspace/workspaceFolders",
+ })
+
+ await new Promise((r) => setTimeout(r, 100))
+
+ expect(client.connection).toBeDefined()
+
+ await client.shutdown()
+ })
+
+ test("handles client/registerCapability request", async () => {
+ const handle = spawnFakeServer() as any
+
+ const client = await Instance.provide({
+ directory: process.cwd(),
+ fn: () =>
+ LSPClient.create({
+ serverID: "fake",
+ server: handle as unknown as LSPServer.Handle,
+ root: process.cwd(),
+ }),
+ })
+
+ await client.connection.sendNotification("test/trigger", {
+ method: "client/registerCapability",
+ })
+
+ await new Promise((r) => setTimeout(r, 100))
+
+ expect(client.connection).toBeDefined()
+
+ await client.shutdown()
+ })
+
+ test("handles client/unregisterCapability request", async () => {
+ const handle = spawnFakeServer() as any
+
+ const client = await Instance.provide({
+ directory: process.cwd(),
+ fn: () =>
+ LSPClient.create({
+ serverID: "fake",
+ server: handle as unknown as LSPServer.Handle,
+ root: process.cwd(),
+ }),
+ })
+
+ await client.connection.sendNotification("test/trigger", {
+ method: "client/unregisterCapability",
+ })
+
+ await new Promise((r) => setTimeout(r, 100))
+
+ expect(client.connection).toBeDefined()
+
+ await client.shutdown()
+ })
+})