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:
roboomp
2026-06-19 10:11:54 +00:00
parent 492fe5e896
commit ef901ecb0d
3 changed files with 87 additions and 0 deletions
+4
View File
@@ -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
+22
View File
@@ -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 {