mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 11:14:23 +01:00
fix(lsp): handle optional requests to avoid MethodNotFound (-32601) with MATLAB Language Server (#4007)
This commit is contained in:
committed by
GitHub
parent
83b16cb18e
commit
271b679058
@@ -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()
|
||||
|
||||
77
packages/opencode/test/fixture/lsp/fake-lsp-server.js
Normal file
77
packages/opencode/test/fixture/lsp/fake-lsp-server.js
Normal 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
|
||||
}
|
||||
}
|
||||
95
packages/opencode/test/lsp/client.test.ts
Normal file
95
packages/opencode/test/lsp/client.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user