修代码补全

This commit is contained in:
2026-08-01 15:47:51 +02:00
parent 429ea367f3
commit 45fa2b9a27
18 changed files with 802 additions and 37 deletions
+26 -1
View File
@@ -76,7 +76,15 @@ function analyzeStartTag(
// Inside an attribute value?
for (const attr of el.attrs) {
if (attr.hasValue && offset >= attr.quoteStart && offset <= attr.quoteEnd) {
// An unterminated value (quoteEnd < 0) happens while the user is typing
// the opening quote of a new attribute value; it must still be treated as
// an attribute-value context so enum/ref/define completions show up.
if (
attr.hasValue &&
attr.quoteStart >= 0 &&
offset >= attr.quoteStart &&
(attr.quoteEnd < 0 || offset <= attr.quoteEnd)
) {
const start = attr.valueStart;
const prefix = offset > start ? text.slice(start, offset) : "";
return {
@@ -128,3 +136,20 @@ function empty(kind: ContextKind): CompletionContext {
existingAttrs: [],
};
}
/**
* Splits the typed prefix of a whitespace-separated list value (xs:list, e.g.
* bit flags such as Surfaces="GROUND WATER") into the token being edited and
* the offset of that token inside the prefix.
*
* "GROUND WA" -> { token: "WA", start: 7 }
* "GROUND " -> { token: "", start: 7 }
* "WA" -> { token: "WA", start: 0 }
*/
export function splitListValuePrefix(prefix: string): { token: string; start: number } {
let start = prefix.length;
while (start > 0 && !/\s/.test(prefix[start - 1])) {
start--;
}
return { token: prefix.slice(start), start };
}
+63
View File
@@ -0,0 +1,63 @@
import { LineMap, type XmlDocument } from "./xmlParser";
/**
* Semantic token types used by the highlighting fallback. They are standard
* vscode token types, so every theme already has colors for them.
*/
export type SemanticTokenType = "type" | "property" | "string";
export interface SemanticTokenRange {
line: number;
startChar: number;
length: number;
tokenType: SemanticTokenType;
}
/**
* Builds semantic token ranges from a tolerant parse tree.
*
* This is the highlighting fallback for malformed XML: while the TextMate
* grammar loses structure (e.g. an attribute value whose closing quote has
* not been typed yet turns the rest of the file into one string), semantic
* tokens keep element names, attribute names and attribute values colored.
* The ranges are sorted by position for the vscode encoder.
*/
export function buildSemanticTokenRanges(
doc: XmlDocument,
text: string,
): SemanticTokenRange[] {
const lineMap = new LineMap(text);
const out: SemanticTokenRange[] = [];
const push = (offset: number, length: number, tokenType: SemanticTokenType) => {
if (length <= 0 || offset < 0 || offset + length > text.length) return;
const pos = lineMap.positionAt(offset);
out.push({
line: pos.line,
startChar: pos.character,
length,
tokenType,
});
};
for (const el of doc.elements) {
// Element name in the start tag.
push(el.start + 1, el.name.length, "type");
// Element name in the closing tag (when present).
if (el.closeTagStart >= 0) {
push(el.closeTagStart + 2, el.name.length, "type");
}
for (const attr of el.attrs) {
push(attr.nameStart, attr.name.length, "property");
if (!attr.hasValue) continue;
// Include the surrounding quotes when available; for an unterminated
// value quoteEnd is -1 and the token ends at the recovered value end.
const start = attr.quoteStart >= 0 ? attr.quoteStart : attr.valueStart;
const end = attr.quoteEnd >= 0 ? attr.quoteEnd : attr.valueEnd;
push(start, end - start, "string");
}
}
out.sort((a, b) => a.line - b.line || a.startChar - b.startChar);
return out;
}
+12 -2
View File
@@ -336,7 +336,16 @@ export function parseXml(text: string): XmlDocument {
const gt = findTagEnd(text, i + 1);
if (gt < 0) {
err("Unterminated start tag", i);
const content = text.slice(i + 1);
// Recovery while typing: an attribute value whose closing quote has not
// been typed yet makes the scanner run to EOF. End the malformed start
// tag at the first line break (or EOF) so the rest of the document is
// still parsed and completion/hover keep working for the elements after
// the broken tag. The missing quote/tag end is still reported above.
let recoverTo = i + 1;
while (recoverTo < text.length && text[recoverTo] !== "\n" && text[recoverTo] !== "\r") {
recoverTo++;
}
const content = text.slice(i + 1, recoverTo);
const raw = parseTag(content, i + 1);
if (raw.name) {
const el = buildElement(raw, stack.length);
@@ -344,7 +353,8 @@ export function parseXml(text: string): XmlDocument {
root = root ?? el;
stack.push(el);
}
break;
i = recoverTo + 1;
continue;
}
const content = text.slice(i + 1, gt);
const raw = parseTag(content, i + 1);