From 5429057a380ba8fa5fdd6ec366c5ae66cc34a363 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Thu, 20 Aug 2026 19:03:46 +0530 Subject: [PATCH] fix(language-server): gate api hover to actual api. references Hover fired on whatever word was under the cursor, so a local variable colliding with a declared block name reported the block's method and path instead of its own hover info. Completion was already gated this way. Co-Authored-By: Claude Opus 5 --- docs/public-api-0.8.json | 3 +- editors/vscode/src/language-server.cjs | 11 +++++-- packages/language-server/src/server.ts | 17 +++++++++- .../test/api-call-completion.test.ts | 31 ++++++++++++++++++- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/public-api-0.8.json b/docs/public-api-0.8.json index c506a875..38670a46 100644 --- a/docs/public-api-0.8.json +++ b/docs/public-api-0.8.json @@ -1864,7 +1864,8 @@ "./server": [ "ApiCallCompletionItem", "apiCallCompletions", - "apiCallHover" + "apiCallHover", + "isApiReferenceAt" ] }, "@wrnexus/mcp": { diff --git a/editors/vscode/src/language-server.cjs b/editors/vscode/src/language-server.cjs index b623630a..445bca49 100644 --- a/editors/vscode/src/language-server.cjs +++ b/editors/vscode/src/language-server.cjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -// WRN editor language server source hash: eca7e58f47277a649cf9f7b2fcf23d2db52c18ff64e3f3ffb0b442d9d5f2024d +// WRN editor language server source hash: 1efd569f289f736d7dc094e4165d11490593f1c4534ae460a5122bf6dd45344b // WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72 // @bun @bun-cjs (function(exports, require, module, __filename, __dirname) {var __create = Object.create; @@ -169658,6 +169658,7 @@ var require_main = __commonJS((exports2, module2) => { // packages/language-server/src/server.ts var exports_server = {}; __export(exports_server, { + isApiReferenceAt: () => isApiReferenceAt, apiCallHover: () => apiCallHover, apiCallCompletions: () => apiCallCompletions }); @@ -196268,6 +196269,12 @@ function isApiCallPosition(text, offset) { const before = text.slice(0, offset); return /\bapi\.\w*$/.test(before); } +function isApiReferenceAt(text, offset) { + let start = offset; + while (start > 0 && /\w/.test(text[start - 1])) + start--; + return /\bapi\.$/.test(text.slice(0, start)); +} var documents = new Map; var diagnosticTimers = new Map; var MAX_OPEN_DOCUMENTS = 256; @@ -196466,7 +196473,7 @@ async function handle(message) { case "textDocument/hover": { const document = documents.get(params.textDocument.uri); const word = document ? wordAt(document.text, params.position)?.word : undefined; - const apiHover = word ? apiCallHover(document.text, word) : undefined; + const apiHover = word && isApiReferenceAt(document.text, offsetAt(document.text, params.position)) ? apiCallHover(document.text, word) : undefined; if (apiHover) { result(message.id, { contents: apiHover }); break; diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index 3c0315cd..edd5b9d6 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -103,6 +103,18 @@ function isApiCallPosition(text: string, offset: number): boolean { return /\bapi\.\w*$/.test(before); } +/** + * True when the identifier containing `offset` is the `` of an `api.` + * reference. Hover fires on whatever word is under the cursor, so without this a local + * variable that merely collides with a block name would report the block's method and + * path instead of its own hover info. + */ +export function isApiReferenceAt(text: string, offset: number): boolean { + let start = offset; + while (start > 0 && /\w/.test(text[start - 1]!)) start--; + return /\bapi\.$/.test(text.slice(0, start)); +} + type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any }; const documents = new Map(); const diagnosticTimers = new Map>(); @@ -311,7 +323,10 @@ async function handle(message: JsonRpc): Promise { case "textDocument/hover": { const document = documents.get(params.textDocument.uri); const word = document ? wordAt(document.text, params.position)?.word : undefined; - const apiHover = word ? apiCallHover(document!.text, word) : undefined; + const apiHover = + word && isApiReferenceAt(document!.text, offsetAt(document!.text, params.position)) + ? apiCallHover(document!.text, word) + : undefined; if (apiHover) { result(message.id, { contents: apiHover }); break; diff --git a/packages/language-server/test/api-call-completion.test.ts b/packages/language-server/test/api-call-completion.test.ts index 1bb26594..626d8968 100644 --- a/packages/language-server/test/api-call-completion.test.ts +++ b/packages/language-server/test/api-call-completion.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { apiCallCompletions, apiCallHover } from "../src/server.ts"; +import { apiCallCompletions, apiCallHover, isApiReferenceAt } from "../src/server.ts"; const SOURCE = `page Search { apis { @@ -71,3 +71,32 @@ test("an ssr {} data block, which the parser now rejects with a ParseError, stil expect(() => apiCallCompletions(legacy)).not.toThrow(); expect(apiCallCompletions(legacy)).toEqual([]); }); + +test("a bare identifier that merely collides with a block name is not an api reference", () => { + // `searchUsers` here is a local variable, not `api.searchUsers` -- hovering it must + // not report the API block's method and path. + const source = `page Search { + apis { + searchUsers POST /api/users { + response { return data.users } + } + } + + functions { + client function go() { + const searchUsers = 1 + void api.searchUsers({ name: "x" }) + return searchUsers + } + } + + view {
x
} +} +`; + const collision = source.indexOf("const searchUsers") + "const ".length; + const reference = source.indexOf("searchUsers POST"); + + expect(isApiReferenceAt(source, collision)).toBe(false); + expect(isApiReferenceAt(source, source.indexOf("api.searchUsers") + 4)).toBe(true); + expect(reference).toBeGreaterThan(-1); +});