Просмотр исходного кода

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 <[email protected]>
Co-authored-by: Eric Wheeler <[email protected]>
KJ7LNW 9 месяцев назад
Родитель
Сommit
9fadd3dbe2
1 измененных файлов с 63 добавлено и 54 удалено
  1. 63 54
      src/services/ripgrep/index.ts

+ 63 - 54
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
+	lines: SearchLineResult[]
+}
+
+interface SearchLineResult {
 	line: number
-	column: number
 	text: string
-	beforeContext: ContextResult[]
-	afterContext: ContextResult[]
+	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,
-						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 = {
+				} 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,
-						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`
-
-		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"
+		output += `# ${filePath.toPosix()}\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()