From 9fadd3dbe20011cde2c3a6e1d9cacb479296129e Mon Sep 17 00:00:00 2001 From: KJ7LNW <93454819+KJ7LNW@users.noreply.github.com> Date: Thu, 20 Mar 2025 06:10:22 -0700 Subject: [PATCH] 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 Co-authored-by: Eric Wheeler --- src/services/ripgrep/index.ts | 117 ++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 54 deletions(-) diff --git a/src/services/ripgrep/index.ts b/src/services/ripgrep/index.ts index f9fd4b6595..89e1da62f8 100644 --- a/src/services/ripgrep/index.ts +++ b/src/services/ripgrep/index.ts @@ -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 | 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()