0.1.13 improve completion

This commit is contained in:
2026-08-04 17:53:11 +02:00
parent 6794896ac7
commit f8187da611
12 changed files with 844 additions and 36 deletions
+38 -1
View File
@@ -1,4 +1,5 @@
import type { XmlAttribute, XmlDocument, XmlElement } from "./xmlParser";
import { parseTag } from "./xmlParser";
export type ContextKind =
| "element-name"
@@ -42,6 +43,15 @@ export function analyzeContext(
return analyzeStartTag(container, text, offset);
}
// A start tag that had to be recovered (e.g. an attribute quote is still
// open) is truncated at the first line break, so attributes typed on later
// lines of the same tag are not part of the parsed element. Re-parse the
// partial tag up to the cursor so value/attribute completion keeps working
// while typing in the malformed tag.
if (container.recoveredStartTag) {
return analyzeRecoveredStartTag(container, text, offset);
}
// Otherwise the cursor is in element content.
return {
kind: "content",
@@ -53,6 +63,33 @@ export function analyzeContext(
};
}
/**
* Classifies the cursor inside a recovered (truncated) start tag by re-parsing
* the raw tag content up to the cursor. The original element only kept the
* attributes that fit on its first line; re-parsing the partial text recovers
* attributes typed on later lines without affecting the rest of the document.
*/
function analyzeRecoveredStartTag(
el: XmlElement,
text: string,
offset: number,
): CompletionContext {
const raw = parseTag(text.slice(el.start + 1, offset), el.start + 1);
const partial: XmlElement = {
name: raw.name,
attrs: raw.attrs,
children: [],
parent: el.parent,
start: raw.start,
startTagEnd: offset,
end: offset,
selfClosing: raw.selfClosing,
closeTagStart: -1,
depth: el.depth,
};
return analyzeStartTag(partial, text, offset);
}
function analyzeStartTag(
el: XmlElement,
text: string,
@@ -83,7 +120,7 @@ function analyzeStartTag(
attr.hasValue &&
attr.quoteStart >= 0 &&
offset >= attr.quoteStart &&
(attr.quoteEnd < 0 || offset <= attr.quoteEnd)
(attr.quoteEnd < 0 || offset < attr.quoteEnd)
) {
const start = attr.valueStart;
const prefix = offset > start ? text.slice(start, offset) : "";
+15 -2
View File
@@ -43,6 +43,12 @@ export interface XmlElement {
/** Offset of "</" of the closing tag, or -1 when self-closing. */
closeTagStart: number;
depth: number;
/**
* True when the start tag was unterminated and recovered at a line break.
* Attributes typed on later lines of the same tag are not part of the
* parsed element; completion re-parses the partial tag up to the cursor.
*/
recoveredStartTag?: boolean;
}
export interface XmlParseError {
@@ -119,7 +125,7 @@ interface RawTag {
const NAME_RE = /[A-Za-z_][\w:.-]*/y;
function parseTag(content: string, contentStart: number): RawTag {
export function parseTag(content: string, contentStart: number): RawTag {
const base = contentStart;
let j = 0;
while (j < content.length && /\s/.test(content[j])) {
@@ -357,8 +363,15 @@ export function parseXml(text: string): XmlDocument {
const raw = parseTag(content, i + 1);
if (raw.name) {
const el = buildElement(raw, stack.length);
el.recoveredStartTag = true;
elements.push(el);
root = root ?? el;
if (stack.length === 0) {
root = root ?? el;
} else {
const parent = stack[stack.length - 1];
parent.children.push(el);
el.parent = parent;
}
stack.push(el);
}
i = recoverTo + 1;