Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions changelog.d/unreleased/2107.fixed.md
Original file line number Diff line number Diff line change
@@ -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)** — `<slot name="...">` declarations are emitted as `property` symbols, unnamed default slots use `"(default)"`, and `slot="..."` projections are emitted as `reference` symbols.

## 日本語

- **HTML の slot 宣言と投影参照を index するようになりました (#2107)** — `<slot name="...">` 宣言は `property` symbol、名前なしの default slot は `"(default)"`、`slot="..."` の投影先は `reference` symbol として出力します。
38 changes: 38 additions & 0 deletions src/CodeIndex/Indexer/Symbols/SymbolExtractor.Markup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public static partial class SymbolExtractor
{
private static List<SymbolRecord> 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. `<link title="href=evil.css" href="/real.css">`)
// don't leak phantom imports AND real attributes on the same tag aren't
Expand Down Expand Up @@ -73,6 +75,7 @@ private static List<SymbolRecord> 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
Expand Down Expand Up @@ -221,6 +224,25 @@ private static List<SymbolRecord> 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;
Expand Down Expand Up @@ -248,6 +270,22 @@ private static List<SymbolRecord> 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;
}

Expand Down
29 changes: 29 additions & 0 deletions tests/CodeIndex.Tests/SymbolExtractorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<template id="card-template">
<slot name="header">Untitled</slot>
<slot></slot>
<slot name='footer'><slot name="nested"></slot></slot>
</template>
<article>
<h2 slot="header">Title</h2>
<p>Default content</p>
<span slot='footer'>Actions</span>
<slot slot="footer" name="forwarded"></slot>
</article>
""";

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()
{
Expand Down
Loading