fix(lsp): replied to defined server requests with spec no-ops
handleServerRequest fell through to a JSON-RPC -32601 Method not found for several defined server -> client requests (window/showMessageRequest, window/showDocument, workspace/{semanticTokens,inlayHint,codeLens,codeAction,diagnostic}/refresh). Servers that block on a real reply -- the same failure class as the client/registerCapability hang fixed in #3029 -- could stall waiting for an acknowledgement that never came.
Reply with the spec no-op result instead: null for showMessageRequest / *Refresh, { success: false } for showDocument. Headless omp cannot honour the UI surface, but it still owes a defined response.
Fixes #3044
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed LSP client returning `-32601 Method not found` for defined server→client requests (`window/showMessageRequest`, `window/showDocument`, `workspace/{semanticTokens,inlayHint,codeLens,codeAction,diagnostic}/refresh`). Servers that stall waiting for a real reply (same failure mode as #3029) now receive the spec no-op result ([#3044](https://github.com/can1357/oh-my-pi/issues/3044)).
|
||||
|
||||
## [16.1.1] - 2026-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -482,6 +482,28 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
return;
|
||||
}
|
||||
if (message.method === "window/showMessageRequest") {
|
||||
// Headless: no UI to surface the prompt. Spec says null = "no action selected".
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
return;
|
||||
}
|
||||
if (message.method === "window/showDocument") {
|
||||
// Headless: nothing to display. Spec result is `{ success: boolean }`.
|
||||
await sendResponse(client, message.id, { success: false }, message.method);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
message.method === "workspace/semanticTokens/refresh" ||
|
||||
message.method === "workspace/inlayHint/refresh" ||
|
||||
message.method === "workspace/codeLens/refresh" ||
|
||||
message.method === "workspace/codeAction/refresh" ||
|
||||
message.method === "workspace/diagnostic/refresh"
|
||||
) {
|
||||
// Void acknowledgement per spec; servers that stall waiting for a reply
|
||||
// (same failure mode as the dynamic-registration hang in #3029) move on.
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
return;
|
||||
}
|
||||
await sendResponse(client, message.id, null, message.method, {
|
||||
code: -32601,
|
||||
message: `Method not found: ${message.method}`,
|
||||
|
||||
@@ -380,6 +380,67 @@ describe("lsp regressions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("answers defined server→client requests with spec no-op results", async () => {
|
||||
// Same failure class as #3029: a defined server→client request
|
||||
// (window/showMessage{Request}, window/showDocument, workspace/*/refresh)
|
||||
// must receive a spec-shaped reply, not a -32601. Headless omp can't
|
||||
// surface UI prompts but still owes a defined no-op.
|
||||
const tempDir = TempDir.createSync("@omp-lsp-server-requests-");
|
||||
try {
|
||||
const server = installFakeLsp((message, srv) => {
|
||||
if (message.method === "initialize") {
|
||||
srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: {} } });
|
||||
} else if (message.method === "initialized") {
|
||||
srv.send({
|
||||
jsonrpc: "2.0",
|
||||
id: 9101,
|
||||
method: "window/showMessageRequest",
|
||||
params: { type: 1, message: "x", actions: [{ title: "Cancel" }] },
|
||||
});
|
||||
srv.send({
|
||||
jsonrpc: "2.0",
|
||||
id: 9102,
|
||||
method: "window/showDocument",
|
||||
params: { uri: "file:///tmp/a.md" },
|
||||
});
|
||||
srv.send({ jsonrpc: "2.0", id: 9103, method: "workspace/semanticTokens/refresh" });
|
||||
srv.send({ jsonrpc: "2.0", id: 9104, method: "workspace/inlayHint/refresh" });
|
||||
srv.send({ jsonrpc: "2.0", id: 9105, method: "workspace/codeLens/refresh" });
|
||||
srv.send({ jsonrpc: "2.0", id: 9106, method: "workspace/diagnostic/refresh" });
|
||||
} else if (message.method === "shutdown") {
|
||||
srv.send({ jsonrpc: "2.0", id: message.id, result: null });
|
||||
} else if (message.method === "exit") {
|
||||
srv.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
const config: ServerConfig = {
|
||||
command: "fake-lsp",
|
||||
fileTypes: ["rs"],
|
||||
rootMarkers: [],
|
||||
};
|
||||
|
||||
await lspClient.getOrCreateClient(config, tempDir.path(), 1_000);
|
||||
|
||||
const showMessage = await server.waitFor(message => message.id === 9101 && message.method === undefined);
|
||||
expect(showMessage.error).toBeUndefined();
|
||||
expect(showMessage.result).toBeNull();
|
||||
|
||||
const showDocument = await server.waitFor(message => message.id === 9102 && message.method === undefined);
|
||||
expect(showDocument.error).toBeUndefined();
|
||||
expect(showDocument.result).toEqual({ success: false });
|
||||
|
||||
for (const id of [9103, 9104, 9105, 9106]) {
|
||||
const refresh = await server.waitFor(message => message.id === id && message.method === undefined);
|
||||
expect(refresh.error).toBeUndefined();
|
||||
expect(refresh.result).toBeNull();
|
||||
}
|
||||
} finally {
|
||||
await lspClient.shutdownAll();
|
||||
tempDir.removeSync();
|
||||
}
|
||||
});
|
||||
|
||||
it("opens rust-analyzer Cargo workspace files before polling workspace readiness", async () => {
|
||||
const tempDir = TempDir.createSync("@omp-lsp-rust-workspace-");
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user