diff --git a/changelog.d/unreleased/2107.fixed.md b/changelog.d/unreleased/2107.fixed.md new file mode 100644 index 0000000000..523ba601a2 --- /dev/null +++ b/changelog.d/unreleased/2107.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2107 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **HTML slot declarations and projections are now indexed (#2107)** — `` declarations are emitted as `property` symbols, unnamed default slots use `"(default)"`, and `slot="..."` projections are emitted as `reference` symbols. + +## 日本語 + +- **HTML の slot 宣言と投影参照を index するようになりました (#2107)** — `` 宣言は `property` symbol、名前なしの default slot は `"(default)"`、`slot="..."` の投影先は `reference` symbol として出力します。 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs index 13b8a09544..9ec6e7e760 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs @@ -9,6 +9,8 @@ public static partial class SymbolExtractor { private static List ExtractHtmlSymbols(long fileId, string[] lines) { + const string defaultSlotSymbolName = "(default)"; + // HTML needs proper tag-structure awareness so attribute lookalikes inside // other attributes' quoted values (e.g. ``) // don't leak phantom imports AND real attributes on the same tag aren't @@ -73,6 +75,7 @@ private static List ExtractHtmlSymbols(long fileId, string[] lines var tagName = maskedText[tagNameStart..tagNameEnd]; var tagNameLower = tagName.ToLowerInvariant(); + var sawNamedSlotDeclaration = false; // Emit custom Web Components (hyphenated opening tag) at the `<` position, // but skip the standard HTML/SVG/MathML tags that happen to contain a hyphen @@ -221,6 +224,25 @@ private static List ExtractHtmlSymbols(long fileId, string[] lines emitKind = "property"; emittedNames = [attrValue.Trim()]; } + else if (attrNameLower == "name" && tagNameLower == "slot") + { + var slotName = attrValue.Trim(); + if (slotName.Length > 0) + { + emitKind = "property"; + emittedNames = [slotName]; + sawNamedSlotDeclaration = true; + } + } + else if (attrNameLower == "slot") + { + var slotName = attrValue.Trim(); + if (slotName.Length > 0) + { + emitKind = "reference"; + emittedNames = [slotName]; + } + } if (emitKind == null || emittedNames == null || emittedNames.Count == 0) continue; @@ -248,6 +270,22 @@ private static List ExtractHtmlSymbols(long fileId, string[] lines } } + if (tagNameLower == "slot" && !sawNamedSlotDeclaration) + { + var startLine = FindHtmlLineNumber(lineStarts, pos); + var signatureIndex = Math.Clamp(startLine - 1, 0, lines.Length - 1); + symbols.Add(new SymbolRecord + { + FileId = fileId, + Kind = "property", + Name = defaultSlotSymbolName, + Line = startLine, + StartLine = startLine, + EndLine = startLine, + Signature = lines[signatureIndex].Trim(), + }); + } + pos = cursor < maskedText.Length ? cursor + 1 : cursor; } diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 0795b52a7c..24ac0610dc 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -22296,6 +22296,35 @@ public void Extract_Html_CapturesCustomWebComponentTagsAsClasses() Assert.DoesNotContain(symbols, s => s.Kind == "class" && s.Name == "div"); } + [Fact] + public void Extract_Html_CapturesSlotDeclarationsAndProjectionReferences() + { + var content = """ + +
+

Title

+

Default content

+ Actions + +
+ """; + + var symbols = SymbolExtractor.Extract(1, "html", content); + + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "header"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "(default)"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "footer"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "nested"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "forwarded"); + Assert.Equal(2, symbols.Count(s => s.Kind == "reference" && s.Name == "footer")); + Assert.Contains(symbols, s => s.Kind == "reference" && s.Name == "header"); + Assert.DoesNotContain(symbols, s => s.Kind == "class" && s.Name == "slot"); + } + [Fact] public void Extract_Html_CapturesAllSymbolsOnSameLine() {