fix(lsp): handle optional requests to avoid MethodNotFound (-32601) with MATLAB Language Server (#4007)

This commit is contained in:
Christopher Sacca
2025-11-08 17:31:39 -05:00
committed by GitHub
parent 83b16cb18e
commit 271b679058
3 changed files with 200 additions and 5 deletions

View File

@@ -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
}
}

View File

@@ -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()
})
})