feat(language-server): add offset-preserving virtual HTML document
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import type { TextDocument } from "./index.ts";
|
||||
|
||||
export interface HtmlRegion {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Byte ranges of the markup inside each `view { }` block.
|
||||
*
|
||||
* This is a tolerant scanner rather than the parser: completion fires while
|
||||
* the document is being typed, which is exactly when it does not parse.
|
||||
*/
|
||||
export function viewRegions(text: string): HtmlRegion[] {
|
||||
const regions: HtmlRegion[] = [];
|
||||
const pattern = /\bview\s*\{/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = pattern.exec(text))) {
|
||||
const bodyStart = match.index + match[0].length;
|
||||
const end = matchingBrace(text, bodyStart);
|
||||
regions.push({ start: bodyStart, end });
|
||||
pattern.lastIndex = end;
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset of the brace closing the block that starts at `from`, or the end of
|
||||
* the text when it is never closed (an unterminated block is normal mid-edit).
|
||||
*
|
||||
* Quotes are only tracked inside a tag, never in text content: `<p>it's</p>`
|
||||
* would otherwise open a string that never closes and swallow the rest of the
|
||||
* file.
|
||||
*/
|
||||
function matchingBrace(text: string, from: number): number {
|
||||
let depth = 1;
|
||||
let inTag = false;
|
||||
let quote: string | null = null;
|
||||
|
||||
for (let index = from; index < text.length; index += 1) {
|
||||
const char = text[index]!;
|
||||
|
||||
if (quote) {
|
||||
if (char === quote) quote = null;
|
||||
continue;
|
||||
}
|
||||
if (inTag && (char === '"' || char === "'")) {
|
||||
quote = char;
|
||||
continue;
|
||||
}
|
||||
if (char === "<") inTag = true;
|
||||
else if (char === ">") inTag = false;
|
||||
else if (char === "{") depth += 1;
|
||||
else if (char === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return index;
|
||||
}
|
||||
}
|
||||
return text.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* A parallel document containing only the markup.
|
||||
*
|
||||
* Everything outside a view block becomes whitespace of the same length, and
|
||||
* newlines are preserved, so an offset in the source is the same offset here.
|
||||
* That removes the need for a mapping table entirely.
|
||||
*/
|
||||
export function virtualHtmlDocument(document: TextDocument): {
|
||||
uri: string;
|
||||
languageId: "html";
|
||||
text: string;
|
||||
} {
|
||||
const source = document.text;
|
||||
const keep = new Array<boolean>(source.length).fill(false);
|
||||
for (const region of viewRegions(source)) {
|
||||
for (let index = region.start; index < region.end; index += 1) keep[index] = true;
|
||||
}
|
||||
|
||||
let text = "";
|
||||
for (let index = 0; index < source.length; index += 1) {
|
||||
const char = source[index]!;
|
||||
text += keep[index] || char === "\n" ? char : char === "\r" ? "\r" : " ";
|
||||
}
|
||||
|
||||
return { uri: `${document.uri}.html`, languageId: "html", text };
|
||||
}
|
||||
|
||||
export function isInsideHtml(document: TextDocument, offset: number): boolean {
|
||||
return regionsFor(document).some((region) => offset >= region.start && offset <= region.end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regions for a document, cached by uri and version.
|
||||
*
|
||||
* A single keystroke produces a burst of completion, hover, and tag-close
|
||||
* requests; without this each one rescans the file.
|
||||
*/
|
||||
const regionCache = new Map<string, { version: number; regions: HtmlRegion[] }>();
|
||||
|
||||
function regionsFor(document: TextDocument): HtmlRegion[] {
|
||||
const version = document.version ?? -1;
|
||||
const cached = regionCache.get(document.uri);
|
||||
if (cached && cached.version === version) return cached.regions;
|
||||
|
||||
const regions = viewRegions(document.text);
|
||||
regionCache.set(document.uri, { version, regions });
|
||||
return regions;
|
||||
}
|
||||
|
||||
/** Drops a document's cached regions. Call when a document closes. */
|
||||
export function clearHtmlRegionCache(uri?: string): void {
|
||||
if (uri) regionCache.delete(uri);
|
||||
else regionCache.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user