feat(language-server): understand the api binding attribute
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user