diff --git a/packages/language-server/src/html-service.ts b/packages/language-server/src/html-service.ts index 76681db1..7705a42b 100644 --- a/packages/language-server/src/html-service.ts +++ b/packages/language-server/src/html-service.ts @@ -77,12 +77,37 @@ export function htmlFoldingRanges( * start tag, since plain HTML has no such elements outside its fixed void-element * list. WRNexus components (``) are exactly that case, so we complete * it ourselves rather than relying on the library. + * + * The scan tracks quote state from the tag's opening `<` up to `offset` (quotes + * are only meaningful inside a tag) so a `/` inside an attribute value — e.g. the + * first slash of `href="https://..."` — never misfires as a self-close: the + * library already returns `null` there on purpose, because the cursor sits in an + * attribute value, not a tag-close position. */ function selfClosingTagCompletion(text: string, offset: number): string | null { if (text.charAt(offset - 1) !== "/") return null; if (text.charAt(offset) === ">") return null; - const before = text.slice(0, offset); - return /<[A-Za-z][\w-]*(?:\s[^<>]*)?\/$/.test(before) ? ">" : null; + + const tagStart = text.lastIndexOf("<", offset - 1); + if (tagStart < 0) return null; + if (!/^<[A-Za-z][\w-]*/.test(text.slice(tagStart))) return null; + + let quote: '"' | "'" | null = null; + for (let i = tagStart + 1; i < offset - 1; i++) { + const ch = text[i]; + if (quote) { + if (ch === quote) quote = null; + continue; + } + if (ch === '"' || ch === "'") { + quote = ch; + continue; + } + if (ch === "<" || ch === ">") return null; + } + if (quote) return null; + + return ">"; } /** diff --git a/packages/language-server/test/html-service.test.ts b/packages/language-server/test/html-service.test.ts index b191088d..3e43d93a 100644 --- a/packages/language-server/test/html-service.test.ts +++ b/packages/language-server/test/html-service.test.ts @@ -96,6 +96,36 @@ test("completes a self-closing component tag", () => { expect(htmlTagComplete(doc(text), positionOf(text, ""); }); +test("does not misfire inside a quoted attribute value containing a slash", () => { + const doubleQuoted = `page A { + view { + { + const text = `page A { + view { + "); +}); + test("returns no tag completion outside a view block", () => { const text = `page A { functions {