import type { XmlAttribute, XmlDocument, XmlElement } from "./xmlParser"; export type ContextKind = | "element-name" | "attribute-name" | "attribute-value" | "content" | "none"; export interface CompletionContext { kind: ContextKind; /** The element whose start tag the cursor is in (or whose content). */ element: XmlElement | null; /** Set when the cursor is inside a closing tag name ("= el.start && offset <= el.end) { if (!container || el.depth > container.depth) container = el; } } if (!container) return empty("none"); // Inside the start tag of the element. if (offset >= container.start && offset <= container.startTagEnd) { return analyzeStartTag(container, text, offset); } // Otherwise the cursor is in element content. return { kind: "content", element: container, closing: false, attr: null, valuePrefix: "", existingAttrs: [], }; } function analyzeStartTag( el: XmlElement, text: string, offset: number, ): CompletionContext { const closing = text.startsWith(" a.name); if (offset <= nameEnd) { return { kind: "element-name", element: el, closing, attr: null, valuePrefix: "", existingAttrs, }; } // Inside an attribute value? for (const attr of el.attrs) { // 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 { kind: "attribute-value", element: el, closing: false, attr, valuePrefix: prefix, existingAttrs, }; } } // Right after "=" (no quotes yet) or between attributes. const before = text.slice(el.start, offset); const trimmed = before.replace(/\s+$/, ""); if (trimmed.endsWith("=")) { return { kind: "attribute-value", element: el, closing: false, attr: lastAttrOf(el), valuePrefix: "", existingAttrs, }; } return { kind: "attribute-name", element: el, closing: false, attr: null, valuePrefix: "", existingAttrs, }; } function lastAttrOf(el: XmlElement): XmlAttribute | null { return el.attrs.length ? el.attrs[el.attrs.length - 1] : null; } function empty(kind: ContextKind): CompletionContext { return { kind, element: null, closing: false, attr: null, valuePrefix: "", 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 }; }