feat(language-server): understand the api binding attribute

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 16:49:06 +05:30
co-authored by Claude Opus 5
parent cd0dffa87d
commit 29febb2e0c
4 changed files with 187 additions and 3 deletions
+32 -1
View File
@@ -5,6 +5,7 @@
// cleanly, so import it explicitly.
import {
getLanguageService,
newHTMLDataProvider,
TextDocument as HtmlTextDocument,
} from "vscode-html-languageservice/lib/esm/htmlLanguageService.js";
import { isInsideHtml, virtualHtmlDocument } from "./html-regions.ts";
@@ -19,7 +20,21 @@ export interface HtmlCompletionItem {
insertText?: string;
}
const service = getLanguageService();
/**
* `api="<name>"` / `api="<name>()"` / `api="<name>({ field: value })"` is a
* WRNexus data-api binding, not arbitrary markup — declaring it here keeps
* the HTML service from treating it as an unrecognized attribute on any
* element, without inventing an attribute-value grammar the library would
* try to spell-check as prose.
*/
const wrnexusDataProvider = newHTMLDataProvider("wrnexus", {
version: 1,
globalAttributes: [
{ name: "api", description: "Binds this element to a declared `apis { }` entry." },
],
});
const service = getLanguageService({ customDataProviders: [wrnexusDataProvider] });
/** The virtual document as the HTML service's own document type. */
function htmlDocument(document: TextDocument) {
@@ -41,8 +56,24 @@ function markdown(value: unknown): string | undefined {
* Every item carries the `1` sortText prefix so the server can rank WRNexus
* entries above these without filtering either list.
*/
/**
* True when `offset` sits inside the quotes of an `api="…"` attribute value.
*
* The value is a call expression (`name`, `name()`, `name({ field: value })`),
* not prose, so completion there is routed to `apiCallCompletions` instead of
* the HTML service's own (text-oriented) attribute-value completion.
*/
export function isApiAttributeValuePosition(document: TextDocument, position: Position): boolean {
const offset = offsetAt(document.text, position);
if (!isInsideHtml(document, offset)) return false;
const before = document.text.slice(0, offset);
return /\bapi\s*=\s*(["'])(?:(?!\1)[^\n])*$/.test(before);
}
export function htmlCompletions(document: TextDocument, position: Position): HtmlCompletionItem[] {
if (!isInsideHtml(document, offsetAt(document.text, position))) return [];
if (isApiAttributeValuePosition(document, position)) return [];
const virtual = htmlDocument(document);
const parsed = service.parseHTMLDocument(virtual);
+5
View File
@@ -26,6 +26,7 @@ import {
htmlHover,
htmlLinkedEditingRanges,
htmlTagComplete,
isApiAttributeValuePosition,
mergeCompletions,
} from "./html-service.ts";
import { clearHtmlRegionCache } from "./html-regions.ts";
@@ -288,6 +289,10 @@ async function handle(message: JsonRpc): Promise<void> {
result(message.id, apiCallCompletions(document.text));
break;
}
if (document && isApiAttributeValuePosition(document, params.position)) {
result(message.id, apiCallCompletions(document.text));
break;
}
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
const html = document ? htmlCompletions(document, params.position) : [];
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);