diff --git a/changelog.d/unreleased/2105.fixed.md b/changelog.d/unreleased/2105.fixed.md new file mode 100644 index 0000000000..62eb63e7fb --- /dev/null +++ b/changelog.d/unreleased/2105.fixed.md @@ -0,0 +1,17 @@ +--- +category: fixed +issues: + - 2105 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Css.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **CSS media features are now searchable as symbols (#2105)** — `@media` feature names such as `min-width`, `prefers-color-scheme`, `orientation`, range-style `width`, and custom media feature names are indexed as `property` symbols while literal values and boolean operators are skipped. + +## 日本語 + +- **CSS media feature を symbol として検索できるようにしました (#2105)** — `@media` 内の `min-width`、`prefers-color-scheme`、`orientation`、range 形式の `width`、custom media feature 名を `property` symbol として index し、値リテラルや boolean operator は除外します。 diff --git a/changelog.d/unreleased/2611.fixed.md b/changelog.d/unreleased/2611.fixed.md new file mode 100644 index 0000000000..9aff899450 --- /dev/null +++ b/changelog.d/unreleased/2611.fixed.md @@ -0,0 +1,16 @@ +--- +category: fixed +issues: + - 2611 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - tests/CodeIndex.Tests/SymbolExtractorTests.cs +--- + +## English + +- **Rust associated type defaults are indexed under trait containers again (#2611)** — the post-processing pass now handles Rust traits stored as `protocol` symbols, so defaults such as `type Output = ();` produce property symbols with the correct trait container. + +## 日本語 + +- **Rust associated type default を trait container 配下で再び index するようにしました (#2611)** — 後処理が `protocol` symbol として保存された Rust trait も対象にするようになり、`type Output = ();` のような default が正しい trait container を持つ property symbol になります。 diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Css.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Css.cs index 29fa3a4cc0..e4525fb1f6 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Css.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Css.cs @@ -276,6 +276,7 @@ private static int FindCssSameLineBraceEndColumn(string line, int startColumn) private static readonly Regex CssFontFaceDeclarationRegex = new(@"(?:^|[;{])\s*font-family\s*:", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static readonly Regex CssInlineCustomPropertyRegex = new(@"(?--[\w-]+)\s*:", RegexOptions.Compiled); + private static readonly Regex CssMediaFeatureNameRegex = new(@"^\s*(?:not\s+)?(?--[\w-]+|[A-Za-z_][\w-]*)(?=\s*(?::|[<>]=?|=|$))|[<>]=?\s*(?--[\w-]+|[A-Za-z_][\w-]*)(?=\s*(?:[<>]=?|$))", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); private static string ResolveCssSymbolName(string matchLine, string name, string[] lines, int startIndex, int endLine) { @@ -287,6 +288,79 @@ private static string ResolveCssSymbolName(string matchLine, string name, string : string.Empty; } + private static void TryAddCssMediaFeatureSymbols( + long fileId, + string rawLine, + string maskedLine, + int lineIndex, + List symbols, + HashSet? cssSeenSymbols) + { + var trimmedMaskedLine = maskedLine.TrimStart(); + if (!trimmedMaskedLine.StartsWith("@media", StringComparison.OrdinalIgnoreCase)) + return; + + var blockStart = maskedLine.IndexOf('{'); + if (blockStart < 0) + blockStart = maskedLine.Length; + + var query = maskedLine[..blockStart]; + for (var index = 0; index < query.Length; index++) + { + if (query[index] != '(') + continue; + + var featureStart = index + 1; + var depth = 1; + index++; + while (index < query.Length && depth > 0) + { + if (query[index] == '(') + depth++; + else if (query[index] == ')') + depth--; + + index++; + } + + if (depth != 0) + break; + + var featureText = query[featureStart..(index - 1)]; + if (string.IsNullOrWhiteSpace(featureText)) + continue; + + var match = CssMediaFeatureNameRegex.Match(featureText); + if (match.Success) + { + var name = match.Groups["name"].Value; + if (string.Equals(name, "and", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "or", StringComparison.OrdinalIgnoreCase) + || string.Equals(name, "not", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var featureColumn = featureStart + match.Groups["name"].Index; + AddSymbolRecord( + symbols, + cssSeenSymbols, + lineIndex + 1, + new SymbolRecord + { + FileId = fileId, + Kind = "property", + Name = name, + Line = lineIndex + 1, + StartLine = lineIndex + 1, + StartColumn = featureColumn, + EndLine = lineIndex + 1, + Signature = rawLine.Trim(), + }); + } + } + } + private static bool TryGetCssFontFaceFamilyName(string[] lines, int startIndex, int endLine, out string fontFamily) { fontFamily = string.Empty; diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index e93e36a2fd..e2effa2aa3 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -3460,6 +3460,20 @@ public static List Extract(long fileId, string? lang, string conte } } + if (lang == "css" + && pattern.Kind == "namespace" + && pattern.BodyStyle == BodyStyle.Brace + && cssScannerLines != null) + { + TryAddCssMediaFeatureSymbols( + fileId, + line, + cssScannerLines[i], + i, + symbols, + cssSeenSymbols); + } + if (lang == "css" && pattern.Kind == "class" && pattern.BodyStyle == BodyStyle.Brace @@ -10038,7 +10052,7 @@ private static bool IsCppTemplateSpecializationSymbol( private static void ExtractRustAssociatedTypeDefaultSymbols(long fileId, string[] lines, string[] structuralLines, List symbols) { var traits = symbols - .Where(symbol => symbol.Kind == "interface" + .Where(symbol => symbol.Kind is "interface" or "protocol" && symbol.BodyStartLine is > 0 && symbol.BodyEndLine is > 0) .OrderBy(symbol => symbol.StartLine) diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 638d450497..8dc484c535 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -14382,13 +14382,13 @@ fn build(&self) { Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "Output" - && s.ContainerKind == "interface" + && s.ContainerKind == "protocol" && s.ContainerName == "Builder" && s.ReturnType == "()"); Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "Error" - && s.ContainerKind == "interface" + && s.ContainerKind == "protocol" && s.ContainerName == "Builder" && s.ReturnType == "String"); Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "Pending"); @@ -21508,6 +21508,37 @@ @media screen { Assert.DoesNotContain(symbols, s => s.Kind == "class" && s.Name == ".nested-child"); } + [Fact] + public void Extract_CSS_CapturesMediaFeatureNamesButNotValuesOrOperators() + { + var content = """ + @media (min-width: 768px) and (prefers-color-scheme: dark), not screen and (orientation: landscape) { + .responsive { + color: red; + } + } + + @supports (display: grid) { + @media (width >= 40rem) and (--narrow) { + .nested-media { + display: grid; + } + } + } + """; + + var symbols = SymbolExtractor.Extract(1, "css", content); + + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "min-width"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "prefers-color-scheme"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "orientation"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "width"); + Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "--narrow"); + Assert.DoesNotContain(symbols, s => + s.Kind == "property" + && s.Name is "768px" or "dark" or "landscape" or "and" or "not" or "or"); + } + [Fact] public void Extract_CSS_DoesNotLeakNestedSelectorsAfterSameLineGroupingAndQualifiedRule() {