feat(language-server): add offset-preserving virtual HTML document
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { isInsideHtml, viewRegions, virtualHtmlDocument } from "../src/html-regions.ts";
|
||||
|
||||
function doc(text: string): { uri: string; text: string; version?: number } {
|
||||
return { uri: `file:///${Math.random()}.wrn`, text };
|
||||
}
|
||||
|
||||
const PAGE = `page Home {
|
||||
view {
|
||||
<div class="card">hello</div>
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
test("the virtual document preserves length and newline offsets", () => {
|
||||
// This is what makes position mapping unnecessary. If it breaks, every
|
||||
// feature reports positions off by some amount instead of failing loudly.
|
||||
const source = doc(PAGE);
|
||||
const virtual = virtualHtmlDocument(source);
|
||||
|
||||
expect(virtual.text.length).toBe(source.text.length);
|
||||
expect(virtual.languageId).toBe("html");
|
||||
for (let i = 0; i < source.text.length; i += 1) {
|
||||
if (source.text[i] === "\n") expect(virtual.text[i]).toBe("\n");
|
||||
}
|
||||
});
|
||||
|
||||
test("markup survives into the virtual document and everything else is blanked", () => {
|
||||
const virtual = virtualHtmlDocument(doc(PAGE));
|
||||
expect(virtual.text).toContain('<div class="card">hello</div>');
|
||||
expect(virtual.text).not.toContain("page Home");
|
||||
expect(virtual.text).not.toContain("view");
|
||||
});
|
||||
|
||||
test("an apostrophe in text content does not swallow later regions", () => {
|
||||
// A scanner treating ' as a string delimiter anywhere considers the rest of
|
||||
// the file one open string and loses every later region.
|
||||
const source = `page A {
|
||||
view {
|
||||
<p>it's fine</p>
|
||||
}
|
||||
}
|
||||
component B {
|
||||
view {
|
||||
<span>second</span>
|
||||
}
|
||||
}
|
||||
`;
|
||||
expect(viewRegions(source)).toHaveLength(2);
|
||||
expect(virtualHtmlDocument(doc(source)).text).toContain("<span>second</span>");
|
||||
});
|
||||
|
||||
test("interpolation braces nest without ending the region early", () => {
|
||||
const source = `page A {
|
||||
view {
|
||||
<div class={cond ? "a" : "b"} data-x={{ a: 1 }}>after</div>
|
||||
}
|
||||
}
|
||||
`;
|
||||
const regions = viewRegions(source);
|
||||
expect(regions).toHaveLength(1);
|
||||
expect(virtualHtmlDocument(doc(source)).text).toContain("after</div>");
|
||||
});
|
||||
|
||||
test("unparseable mid-edit markup still yields a region", () => {
|
||||
// Completion fires exactly when the document does not parse.
|
||||
const source = `page A {
|
||||
view {
|
||||
<div class="
|
||||
}
|
||||
}
|
||||
`;
|
||||
expect(viewRegions(source).length).toBe(1);
|
||||
});
|
||||
|
||||
test("a file with no view block yields no regions and a fully blank document", () => {
|
||||
const source = `page A {
|
||||
functions {
|
||||
function go() {}
|
||||
}
|
||||
}
|
||||
`;
|
||||
expect(viewRegions(source)).toEqual([]);
|
||||
expect(virtualHtmlDocument(doc(source)).text.trim()).toBe("");
|
||||
});
|
||||
|
||||
test("regions are cached per document version", () => {
|
||||
// One keystroke fans out into completion, hover, and tag-close requests.
|
||||
const first = doc(PAGE);
|
||||
first.version = 1;
|
||||
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(true);
|
||||
|
||||
// Same version, mutated text: the cached regions are reused, proving the
|
||||
// scan did not run again.
|
||||
first.text = "page A { }";
|
||||
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(true);
|
||||
|
||||
first.version = 2;
|
||||
expect(isInsideHtml(first, PAGE.indexOf("<div"))).toBe(false);
|
||||
});
|
||||
|
||||
test("isInsideHtml distinguishes markup from surrounding code", () => {
|
||||
const source = doc(PAGE);
|
||||
const markupOffset = PAGE.indexOf("<div");
|
||||
const keywordOffset = PAGE.indexOf("page");
|
||||
|
||||
expect(isInsideHtml(source, markupOffset)).toBe(true);
|
||||
expect(isInsideHtml(source, keywordOffset)).toBe(false);
|
||||
});
|
||||
Reference in New Issue
Block a user