Merge remote-tracking branch 'origin/farm/ba7d8cf5/lsp-register-capability'
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
|
||||
- Fixed advisor dependent settings staying visible while `advisor.enabled` is off ([#3027](https://github.com/can1357/oh-my-pi/issues/3027)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed LSP servers that dynamically register capabilities, including Expert, hanging semantic requests after `client/registerCapability` was rejected ([#3029](https://github.com/can1357/oh-my-pi/issues/3029)).
|
||||
|
||||
## [16.1.0] - 2026-06-19
|
||||
|
||||
### Added
|
||||
|
||||
@@ -5,6 +5,7 @@ import { applyWorkspaceEdit } from "./edits";
|
||||
import { getLspmuxCommand, isLspmuxSupported } from "./lspmux";
|
||||
import type {
|
||||
LspClient,
|
||||
LspJsonRpcId,
|
||||
LspJsonRpcNotification,
|
||||
LspJsonRpcRequest,
|
||||
LspJsonRpcResponse,
|
||||
@@ -416,7 +417,6 @@ function currentWorkspaceFolders(client: LspClient): Array<{ uri: string; name:
|
||||
* Handle workspace/workspaceFolders requests from the server.
|
||||
*/
|
||||
async function handleWorkspaceFoldersRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
||||
if (typeof message.id !== "number") return;
|
||||
await sendResponse(client, message.id, currentWorkspaceFolders(client), "workspace/workspaceFolders");
|
||||
}
|
||||
|
||||
@@ -424,7 +424,6 @@ async function handleWorkspaceFoldersRequest(client: LspClient, message: LspJson
|
||||
* Handle workspace/configuration requests from the server.
|
||||
*/
|
||||
async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
||||
if (typeof message.id !== "number") return;
|
||||
const params = message.params as { items?: Array<{ section?: string }> };
|
||||
const items = params?.items ?? [];
|
||||
const result = items.map(item => {
|
||||
@@ -438,7 +437,6 @@ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpc
|
||||
* Handle workspace/applyEdit requests from the server.
|
||||
*/
|
||||
async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
||||
if (typeof message.id !== "number") return;
|
||||
const params = message.params as { edit?: WorkspaceEdit };
|
||||
if (!params?.edit) {
|
||||
await sendResponse(
|
||||
@@ -475,13 +473,15 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
||||
return;
|
||||
}
|
||||
if (message.method === "window/workDoneProgress/create") {
|
||||
// Accept progress token registration from the server
|
||||
if (typeof message.id === "number") {
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
}
|
||||
// Accept progress token registration from the server.
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
return;
|
||||
}
|
||||
if (message.method === "client/registerCapability" || message.method === "client/unregisterCapability") {
|
||||
// Some servers block semantic requests until dynamic registration succeeds.
|
||||
await sendResponse(client, message.id, null, message.method);
|
||||
return;
|
||||
}
|
||||
if (typeof message.id !== "number") return;
|
||||
await sendResponse(client, message.id, null, message.method, {
|
||||
code: -32601,
|
||||
message: `Method not found: ${message.method}`,
|
||||
@@ -493,7 +493,7 @@ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest
|
||||
*/
|
||||
async function sendResponse(
|
||||
client: LspClient,
|
||||
id: number,
|
||||
id: LspJsonRpcId,
|
||||
result: unknown,
|
||||
method: string,
|
||||
error?: { code: number; message: string; data?: unknown },
|
||||
|
||||
@@ -399,7 +399,7 @@ export interface LspClient {
|
||||
diagnostics: Map<string, PublishedDiagnostics>;
|
||||
diagnosticsVersion: number;
|
||||
openFiles: Map<string, OpenFile>;
|
||||
pendingRequests: Map<number, PendingRequest>;
|
||||
pendingRequests: Map<number | string, PendingRequest>;
|
||||
messageBuffer: Uint8Array;
|
||||
isReading: boolean;
|
||||
/** Lifecycle state: "connecting" until initialize completes, then "ready"; "error" on init failure or reader death. */
|
||||
@@ -420,16 +420,19 @@ export interface LspClient {
|
||||
// JSON-RPC Protocol Types
|
||||
// =============================================================================
|
||||
|
||||
/** JSON-RPC request/response identifier accepted by LSP peers. */
|
||||
export type LspJsonRpcId = number | string;
|
||||
|
||||
export interface LspJsonRpcRequest {
|
||||
jsonrpc: "2.0";
|
||||
id: number;
|
||||
id: LspJsonRpcId;
|
||||
method: string;
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface LspJsonRpcResponse {
|
||||
jsonrpc: "2.0";
|
||||
id?: number;
|
||||
id?: LspJsonRpcId;
|
||||
result?: unknown;
|
||||
error?: { code: number; message: string; data?: unknown };
|
||||
}
|
||||
|
||||
@@ -308,6 +308,78 @@ describe("lsp regressions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts dynamic capability registration before semantic requests", async () => {
|
||||
const tempDir = TempDir.createSync("@omp-lsp-dynamic-registration-");
|
||||
try {
|
||||
let dynamicRegistrationAccepted = false;
|
||||
const server = installFakeLsp((message, srv) => {
|
||||
if (message.method === "initialize") {
|
||||
srv.send({ jsonrpc: "2.0", id: message.id, result: { capabilities: { hoverProvider: true } } });
|
||||
} else if (message.method === "initialized") {
|
||||
srv.send({
|
||||
jsonrpc: "2.0",
|
||||
id: 9002,
|
||||
method: "client/registerCapability",
|
||||
params: {
|
||||
registrations: [
|
||||
{
|
||||
id: "-42",
|
||||
method: "workspace/didChangeWatchedFiles",
|
||||
registerOptions: {
|
||||
watchers: [{ globPattern: "**/mix.lock" }, { globPattern: "**/*.{ex,exs}" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
srv.send({
|
||||
jsonrpc: "2.0",
|
||||
id: "expert-unregister-1",
|
||||
method: "client/unregisterCapability",
|
||||
params: { unregisterations: [{ id: "-42", method: "workspace/didChangeWatchedFiles" }] },
|
||||
});
|
||||
} else if (message.id === 9002 && message.method === undefined) {
|
||||
dynamicRegistrationAccepted = message.error === undefined;
|
||||
} else if (message.method === "textDocument/hover" && dynamicRegistrationAccepted) {
|
||||
srv.send({ jsonrpc: "2.0", id: message.id, result: { contents: "Atas.version()" } });
|
||||
} 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: ["ex"],
|
||||
rootMarkers: [],
|
||||
};
|
||||
|
||||
const client = await lspClient.getOrCreateClient(config, tempDir.path(), 1_000);
|
||||
const registerResponse = await server.waitFor(message => message.id === 9002 && message.method === undefined);
|
||||
const unregisterResponse = await server.waitFor(
|
||||
message => message.id === "expert-unregister-1" && message.method === undefined,
|
||||
);
|
||||
expect(registerResponse.error).toBeUndefined();
|
||||
expect(unregisterResponse.error).toBeUndefined();
|
||||
const result = await lspClient.sendRequest(
|
||||
client,
|
||||
"textDocument/hover",
|
||||
{
|
||||
textDocument: { uri: fileToUri(path.join(tempDir.path(), "lib", "atas.ex")) },
|
||||
position: { line: 0, character: 0 },
|
||||
},
|
||||
undefined,
|
||||
50,
|
||||
);
|
||||
|
||||
expect(result).toEqual({ contents: "Atas.version()" });
|
||||
} 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