fix: ripgrep search result handling and formatting (#1831)

Problem:
- Previous implementation incorrectly grouped context lines with matches
- Context lines were split into beforeContext/afterContext arrays, making it difficult to maintain proper line order
- Output format was inconsistent with read_file output
- Non-contiguous search results were sometimes broken or incorrectly grouped
- Example of incorrect output:
  # bench/bundle-test/rollup.config.mjs
   16 |       file: 'dist/index-wasm.min.mjs',
   17 |       format: 'es',
   24 |     output: {
  ----
   25 |       file: 'dist/index-lite.min.mjs',
   26 |       format: 'es',
  ----

  # packages/rehype/package.json
  ----
   10 | it('run', async () => {
   27 |   "files": [
   28 |     "dist"
  ----

Solution:
- Replace separate context arrays with a single 'lines' array containing both matches and context
- Add isMatch flag to distinguish between match and context lines
- Improve contiguity detection by checking if line numbers are sequential
- Standardize output format to match read_file output
- Properly handle non-contiguous search results by creating new result groups
- Example of correct output:
  # bench/bundle-test/rollup.config.mjs
   16 |       file: 'dist/index-wasm.min.mjs',
   17 |       format: 'es',
  ----
   24 |     output: {
   25 |       file: 'dist/index-lite.min.mjs',
   26 |       format: 'es',
  ----

This change ensures search results are properly grouped and displayed with the correct context.

Signed-off-by: Eric Wheeler <roo-code@z.ewheeler.org>
Co-authored-by: Eric Wheeler <roo-code@z.ewheeler.org>
This commit is contained in:
KJ7LNW 2025-03-20 06:10:22 -07:00 committed by GitHub
parent 4a0c0b1116
commit 9fadd3dbe2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -50,20 +50,21 @@ rel/path/to/helper.ts
const isWindows = /^win/.test(process.platform)
const binName = isWindows ? "rg.exe" : "rg"
interface ContextResult {
line: number
text: string
interface SearchFileResult {
file: string
searchResults: SearchResult[]
}
interface SearchResult {
file: string
line: number
column: number
text: string
beforeContext: ContextResult[]
afterContext: ContextResult[]
lines: SearchLineResult[]
}
interface SearchLineResult {
line: number
text: string
isMatch: boolean
column?: number
}
// Constants
const MAX_RESULTS = 300
const MAX_LINE_LENGTH = 500
@ -157,43 +158,50 @@ export async function regexSearchFiles(
console.error("Error executing ripgrep:", error)
return "No results found"
}
const results: SearchResult[] = []
const results: SearchFileResult[] = []
let currentResult: Partial<SearchResult> | null = null
let currentFile: SearchFileResult | null = null
output.split("\n").forEach((line) => {
if (line) {
try {
const parsed = JSON.parse(line)
if (parsed.type === "match") {
if (currentResult) {
results.push(currentResult as SearchResult)
if (parsed.type === "begin") {
currentFile = {
file: parsed.data.path.text.toString(),
searchResults: [],
}
// Safety check: truncate extremely long lines to prevent excessive output
const matchText = parsed.data.lines.text
const truncatedMatch = truncateLine(matchText)
currentResult = {
file: parsed.data.path.text,
} else if (parsed.type === "end") {
// Reset the current result when a new file is encountered
results.push(currentFile as SearchFileResult)
currentFile = null
} else if ((parsed.type === "match" || parsed.type === "context") && currentFile) {
const line = {
line: parsed.data.line_number,
column: parsed.data.submatches[0].start,
text: truncatedMatch,
beforeContext: [],
afterContext: [],
}
} else if (parsed.type === "context" && currentResult) {
// Apply the same truncation logic to context lines
const contextText = parsed.data.lines.text
const truncatedContext = truncateLine(contextText)
let contextResult: ContextResult = {
line: parsed.data.line_number,
text: truncatedContext,
text: truncateLine(parsed.data.lines.text),
isMatch: parsed.type === "match",
...(parsed.type === "match" && { column: parsed.data.absolute_offset }),
}
if (parsed.data.line_number < currentResult.line!) {
currentResult.beforeContext!.push(contextResult)
const lastResult = currentFile.searchResults[currentFile.searchResults.length - 1]
if (lastResult?.lines.length > 0) {
const lastLine = lastResult.lines[lastResult.lines.length - 1]
// If this line is contiguous with the last result, add to it
if (parsed.data.line_number <= lastLine.line + 1) {
lastResult.lines.push(line)
} else {
// Otherwise create a new result
currentFile.searchResults.push({
lines: [line],
})
}
} else {
currentResult.afterContext!.push(contextResult)
// First line in file
currentFile.searchResults.push({
lines: [line],
})
}
}
} catch (error) {
@ -202,9 +210,7 @@ export async function regexSearchFiles(
}
})
if (currentResult) {
results.push(currentResult as SearchResult)
}
// console.log(results)
// Filter results using RooIgnoreController if provided
const filteredResults = rooIgnoreController
@ -214,40 +220,43 @@ export async function regexSearchFiles(
return formatResults(filteredResults, cwd)
}
function formatResults(results: SearchResult[], cwd: string): string {
function formatResults(fileResults: SearchFileResult[], cwd: string): string {
const groupedResults: { [key: string]: SearchResult[] } = {}
let totalResults = fileResults.reduce((sum, file) => sum + file.searchResults.length, 0)
let output = ""
if (results.length >= MAX_RESULTS) {
if (totalResults >= MAX_RESULTS) {
output += `Showing first ${MAX_RESULTS} of ${MAX_RESULTS}+ results. Use a more specific search if necessary.\n\n`
} else {
output += `Found ${results.length === 1 ? "1 result" : `${results.length.toLocaleString()} results`}.\n\n`
output += `Found ${totalResults === 1 ? "1 result" : `${totalResults.toLocaleString()} results`}.\n\n`
}
// Group results by file name
results.slice(0, MAX_RESULTS).forEach((result) => {
const relativeFilePath = path.relative(cwd, result.file)
fileResults.slice(0, MAX_RESULTS).forEach((file) => {
const relativeFilePath = path.relative(cwd, file.file)
if (!groupedResults[relativeFilePath]) {
groupedResults[relativeFilePath] = []
groupedResults[relativeFilePath].push(...file.searchResults)
}
groupedResults[relativeFilePath].push(result)
})
for (const [filePath, fileResults] of Object.entries(groupedResults)) {
output += `${filePath.toPosix()}\n││ ││----\n`
output += `# ${filePath.toPosix()}\n`
fileResults.forEach((result, index) => {
const allLines = [...result.beforeContext, result, ...result.afterContext]
allLines.forEach((line) => {
output += `││ ${line.line} ││${line.text?.trimEnd() ?? ""}\n`
})
if (index < fileResults.length - 1) {
output += "││ ││----\n"
fileResults.forEach((result) => {
// Only show results with at least one line
if (result.lines.length > 0) {
// Show all lines in the result
result.lines.forEach((line) => {
const lineNumber = String(line.line).padStart(3, " ")
output += `${lineNumber} | ${line.text.trimEnd()}\n`
})
output += "----\n"
}
})
output += "││ ││----\n\n"
output += "\n"
}
return output.trim()