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/3083.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3083
affected:
- src/CodeIndex/Database/DbSearchReader.cs
- tests/CodeIndex.Tests/DbSearchReaderIssueTests.cs
---

## English

- **Guarded search precomputes primary match terms and scans candidate lines lazily (#3083)** — guard evaluation no longer rebuilds stable primary query terms or splits every candidate chunk into a full line array before choosing focus lines.

## 日本語

- **guard付き検索がprimary match語を事前計算し、候補行を遅延スキャンするようになりました (#3083)** — guard評価は安定したprimary query語を候補ごとに再構築せず、focus line選択前に候補chunk全体を行配列へ分割しなくなりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3084.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3084
affected:
- src/CodeIndex/Database/DbSearchReader.cs
- tests/CodeIndex.Tests/DbSearchReaderIssueTests.cs
---

## English

- **Guarded search caches line-window reads within each request (#3084)** — guard evaluation now reuses bounded path/window reads for repeated filters or nearby candidates while keeping the cache scoped and capped.

## 日本語

- **guard付き検索がリクエスト内でline-window読み取りをcacheするようになりました (#3084)** — guard評価は、繰り返しfilterや近接候補で同じpath/windowの読み取りを再利用し、cacheはリクエスト内かつ上限付きに保ちます。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3085.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3085
affected:
- src/CodeIndex/Database/DbSearchReader.cs
- tests/CodeIndex.Tests/DbSearchReaderIssueTests.cs
---

## English

- **Guarded search reads only requested line windows from matching chunks (#3085)** — guard context extraction now materializes only the focused line range instead of splitting every overlapping chunk into all lines.

## 日本語

- **guard付き検索が一致chunkから要求された行windowだけを読み取るようになりました (#3085)** — guard文脈抽出は、重なったchunk全体を全行分割せず、focusされた行範囲だけをmaterializeします。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3086.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3086
affected:
- src/CodeIndex/Database/DbSearchReader.cs
- tests/CodeIndex.Tests/DbReaderTests.cs
---

## English

- **Search enclosing-symbol lookup reuses prepared match-line context (#3086)** — search enrichment now caches normalized query terms per language and streams candidate content to the first matching line instead of rebuilding normalized line arrays per result.

## 日本語

- **検索のenclosing symbol lookupが準備済みmatch-line contextを再利用するようになりました (#3086)** — 検索結果の補強は、言語ごとの正規化query語をcacheし、候補本文を最初の一致行までstreamして、結果ごとに正規化済み行配列を再構築しなくなりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3087.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3087
affected:
- src/CodeIndex/Cli/SearchSnippetFormatter.cs
- tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs
---

## English

- **Search snippets avoid full-content line splitting for bounded excerpts (#3087)** — snippet formatting now scans content lines without materializing the entire split array before building the requested snippet window.

## 日本語

- **検索スニペットが範囲限定excerptのために本文全体を行分割しないようになりました (#3087)** — スニペット整形は、要求されたsnippet windowを構築する前に本文全体の分割配列を作らず、行をスキャンするようになりました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/3088.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 3088
affected:
- src/CodeIndex/Cli/SearchSnippetFormatter.cs
- tests/CodeIndex.Tests/SearchSnippetFormatterTests.cs
---

## English

- **Search snippets cap tracked match indexes for repetitive content (#3088)** — snippet matching now keeps only match-line indexes needed for the selected snippet window while still reporting the total dropped match-line count.

## 日本語

- **反復の多い本文で検索スニペットが保持する一致indexを制限しました (#3088)** — スニペット照合は選択済みsnippet windowに必要な一致行indexだけを保持しつつ、dropされた一致行数の合計は引き続き報告します。
160 changes: 116 additions & 44 deletions src/CodeIndex/Cli/SearchSnippetFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,36 +110,26 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
maxLines = ClampSnippetLines(maxLines);
maxLineWidth = LineWidthFormatter.ClampMaxLineWidth(maxLineWidth);

var lines = content.Replace("\r\n", "\n").Split('\n');
if (lines.Length == 0)
{
return new SearchSnippetExcerpt
{
StartLine = absoluteStartLine,
EndLine = absoluteStartLine,
};
}

var queryForLanguage = queryContext.ForLanguage(lang);
var normalizedQuery = queryForLanguage.NormalizedQuery;
var tokens = queryForLanguage.Tokens;
var normalizeCSharpVerbatimNames = queryForLanguage.NormalizeCSharpVerbatimNames;

string[]? normalizedLines = null;
int[][]? rawIndexMaps = null;
if (normalizeCSharpVerbatimNames)
var matchScan = FindMatchingLineIndexes(content, normalizedQuery, tokens, caseSensitive, normalizeCSharpVerbatimNames, maxLines);
var lineCount = matchScan.LineCount;
if (lineCount == 0)
{
normalizedLines = new string[lines.Length];
rawIndexMaps = new int[lines.Length][];
for (int i = 0; i < lines.Length; i++)
normalizedLines[i] = CSharpVerbatimNameNormalizer.Normalize(lines[i], out rawIndexMaps[i]);
return new SearchSnippetExcerpt
{
StartLine = absoluteStartLine,
EndLine = absoluteStartLine,
};
}

var matchLinesSource = normalizedLines ?? lines;
var matchIndexes = FindMatchingLineIndexes(matchLinesSource, normalizedQuery, tokens, caseSensitive);
var matchIndexes = matchScan.MatchIndexes;
var focusStart = matchIndexes.Count > 0 ? matchIndexes[0] : 0;
var focusEnd = focusStart;
var includedMatchLineCount = Math.Min(1, matchIndexes.Count);
var includedMatchLineCount = matchIndexes.Count > 0 ? 1 : 0;
foreach (var matchIndex in matchIndexes.Skip(1))
{
if ((matchIndex - focusStart) + 1 > maxLines)
Expand All @@ -148,15 +138,15 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
focusEnd = matchIndex;
includedMatchLineCount++;
}
var droppedMatchLineCount = Math.Max(0, matchIndexes.Count - includedMatchLineCount);
var droppedMatchLineCount = Math.Max(0, matchScan.TotalMatchCount - includedMatchLineCount);

var focusLength = Math.Max(1, (focusEnd - focusStart) + 1);
var remaining = Math.Max(0, maxLines - focusLength);
var before = remaining / 2;
var after = remaining - before;

var start = Math.Max(0, focusStart - before);
var end = Math.Min(lines.Length - 1, focusEnd + after);
var end = Math.Min(lineCount - 1, focusEnd + after);
while ((end - start) + 1 < maxLines)
{
if (start > 0)
Expand All @@ -165,7 +155,7 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
continue;
}

if (end < lines.Length - 1)
if (end < lineCount - 1)
{
end++;
continue;
Expand All @@ -180,18 +170,21 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
var clampedLines = new List<string>((end - start) + 1);
var truncatedCharCounts = new List<int>();
var truncatedLineCount = 0;
var snippetLines = ReadSnippetLines(content, start, end, normalizeCSharpVerbatimNames);

for (int i = start; i <= end; i++)
foreach (var snippetLine in snippetLines)
{
var originalLine = lines[i];
var i = snippetLine.Index;
var originalLine = snippetLine.Text;
var isMatch = matchSet.Contains(i);
ClampedTextResult clamped;
if (normalizeCSharpVerbatimNames && matchSet.Contains(i) && normalizedLines != null && rawIndexMaps != null)
if (normalizeCSharpVerbatimNames && isMatch && snippetLine.NormalizedText != null && snippetLine.RawIndexMap != null)
{
clamped = ClampNormalizedSnippetLine(originalLine, normalizedLines[i], rawIndexMaps[i], maxLineWidth, normalizedQuery, tokens, caseSensitive, focusMode);
clamped = ClampNormalizedSnippetLine(originalLine, snippetLine.NormalizedText, snippetLine.RawIndexMap, maxLineWidth, normalizedQuery, tokens, caseSensitive, focusMode);
}
else
{
clamped = ClampSnippetLine(originalLine, maxLineWidth, matchSet.Contains(i) ? normalizedQuery : null, tokens, caseSensitive, focusMode);
clamped = ClampSnippetLine(originalLine, maxLineWidth, isMatch ? normalizedQuery : null, tokens, caseSensitive, focusMode);
}
clampedLines.Add(clamped.Text);
if (clamped.Truncated)
Expand All @@ -200,18 +193,18 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
truncatedCharCounts.Add(clamped.TruncatedCharCount);
}

if (!matchSet.Contains(i))
if (!isMatch)
continue;

var absoluteLine = absoluteStartLine + i;
matchLines.Add(absoluteLine);
var matchLineForTerms = normalizeCSharpVerbatimNames && normalizedLines != null ? normalizedLines[i] : originalLine;
var termOccurrences = normalizeCSharpVerbatimNames && normalizedLines != null && rawIndexMaps != null
? GetMatchedTermOccurrences(normalizedLines[i], absoluteLine, normalizedQuery, tokens, caseSensitive, originalLine, rawIndexMaps[i])
var matchLineForTerms = normalizeCSharpVerbatimNames && snippetLine.NormalizedText != null ? snippetLine.NormalizedText : originalLine;
var termOccurrences = normalizeCSharpVerbatimNames && snippetLine.NormalizedText != null && snippetLine.RawIndexMap != null
? GetMatchedTermOccurrences(snippetLine.NormalizedText, absoluteLine, normalizedQuery, tokens, caseSensitive, originalLine, snippetLine.RawIndexMap)
: GetMatchedTermOccurrences(originalLine, absoluteLine, normalizedQuery, tokens, caseSensitive);
var literalTermOccurrences = exposeLiteralHighlights
? normalizeCSharpVerbatimNames && normalizedLines != null && rawIndexMaps != null
? GetMatchedTermOccurrences(normalizedLines[i], absoluteLine, normalizedQuery, [], caseSensitive, originalLine, rawIndexMaps[i])
? normalizeCSharpVerbatimNames && snippetLine.NormalizedText != null && snippetLine.RawIndexMap != null
? GetMatchedTermOccurrences(snippetLine.NormalizedText, absoluteLine, normalizedQuery, [], caseSensitive, originalLine, snippetLine.RawIndexMap)
: GetMatchedTermOccurrences(originalLine, absoluteLine, normalizedQuery, [], caseSensitive)
: null;
highlights.Add(new SearchHighlight
Expand All @@ -238,7 +231,7 @@ public static SearchSnippetExcerpt BuildExcerpt(string content, SearchSnippetQue
ContextBefore = focusStart - start,
ContextAfter = end - focusEnd,
TruncatedBefore = start > 0,
TruncatedAfter = end < lines.Length - 1,
TruncatedAfter = end < lineCount - 1,
TruncatedLineCount = truncatedLineCount,
DroppedMatchLineCount = droppedMatchLineCount,
TruncationContext = new SearchTruncationContext
Expand Down Expand Up @@ -432,32 +425,111 @@ private static string[] BuildQueryTokens(string query, bool normalizeCSharpVerba
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();

private static List<int> FindMatchingLineIndexes(string[] lines, string query, string[] tokens, bool caseSensitive = false)
private static SearchSnippetLineMatchScan FindMatchingLineIndexes(string content, string query, string[] tokens, bool caseSensitive, bool normalizeCSharpVerbatimNames, int maxTrackedWindowLines)
{
var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
var matches = new List<int>();
var lineCount = 0;
var totalMatchCount = 0;
int? focusStart = null;

if (!string.IsNullOrWhiteSpace(query))
{
for (int i = 0; i < lines.Length; i++)
foreach (var (i, rawLine) in EnumerateContentLines(content))
{
if (lines[i].Contains(query, comparison))
matches.Add(i);
lineCount++;
var line = normalizeCSharpVerbatimNames ? CSharpVerbatimNameNormalizer.Normalize(rawLine) : rawLine;
if (line.Contains(query, comparison))
AddTrackedMatchIndex(matches, i, maxTrackedWindowLines, ref focusStart, ref totalMatchCount);
}
}
else
{
lineCount = CountContentLines(content);
}

if (matches.Count > 0 || tokens.Length == 0)
return matches;
return new SearchSnippetLineMatchScan(matches, lineCount, totalMatchCount);

for (int i = 0; i < lines.Length; i++)
matches.Clear();
lineCount = 0;
totalMatchCount = 0;
focusStart = null;
foreach (var (i, rawLine) in EnumerateContentLines(content))
{
if (tokens.Any(token => lines[i].Contains(token, comparison)))
matches.Add(i);
lineCount++;
var line = normalizeCSharpVerbatimNames ? CSharpVerbatimNameNormalizer.Normalize(rawLine) : rawLine;
if (tokens.Any(token => line.Contains(token, comparison)))
AddTrackedMatchIndex(matches, i, maxTrackedWindowLines, ref focusStart, ref totalMatchCount);
}

return matches;
return new SearchSnippetLineMatchScan(matches, lineCount, totalMatchCount);
}

private static void AddTrackedMatchIndex(List<int> matches, int lineIndex, int maxTrackedWindowLines, ref int? focusStart, ref int totalMatchCount)
{
totalMatchCount++;
focusStart ??= lineIndex;
if ((lineIndex - focusStart.Value) + 1 <= maxTrackedWindowLines)
matches.Add(lineIndex);
}

private static List<SearchSnippetLine> ReadSnippetLines(string content, int start, int end, bool normalizeCSharpVerbatimNames)
{
var lines = new List<SearchSnippetLine>((end - start) + 1);
foreach (var (index, rawLine) in EnumerateContentLines(content))
{
if (index < start)
continue;
if (index > end)
break;

if (normalizeCSharpVerbatimNames)
{
var normalized = CSharpVerbatimNameNormalizer.Normalize(rawLine, out var rawIndexMap);
lines.Add(new SearchSnippetLine(index, rawLine, normalized, rawIndexMap));
}
else
{
lines.Add(new SearchSnippetLine(index, rawLine, null, null));
}
}

return lines;
}

private static int CountContentLines(string content)
{
var count = 0;
foreach (var _ in EnumerateContentLines(content))
count++;
return count;
}

private static IEnumerable<(int Index, string Text)> EnumerateContentLines(string content)
{
var lineStart = 0;
var lineIndex = 0;
for (var i = 0; i < content.Length; i++)
{
if (content[i] != '\n')
continue;

var lineEnd = i;
if (lineEnd > lineStart && content[lineEnd - 1] == '\r')
lineEnd--;
yield return (lineIndex, content[lineStart..lineEnd]);
lineIndex++;
lineStart = i + 1;
}

yield return (lineIndex, content[lineStart..]);
}

private sealed record SearchSnippetLineMatchScan(List<int> MatchIndexes, int LineCount, int TotalMatchCount);

private sealed record SearchSnippetLine(int Index, string Text, string? NormalizedText, int[]? RawIndexMap);

private static List<SearchTermOccurrence> GetMatchedTermOccurrences(string line, int absoluteLine, string query, string[] tokens, bool caseSensitive = false, string? rawLine = null, int[]? rawIndexMap = null)
{
var comparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
Expand Down
Loading
Loading