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 {