Add search_files tool

This commit is contained in:
Saoud Rizwan
2024-08-30 01:53:31 -04:00
parent fa763ad165
commit 2e438e5df5
7 changed files with 333 additions and 14 deletions

203
src/utils/ripgrep.ts Normal file
View File

@@ -0,0 +1,203 @@
import * as vscode from "vscode"
import * as childProcess from "child_process"
import * as path from "path"
import * as fs from "fs"
/*
This file provides functionality to perform regex searches on files using ripgrep.
Inspired by: https://github.com/DiscreteTom/vscode-ripgrep-utils
Key components:
1. getBinPath: Locates the ripgrep binary within the VSCode installation.
2. execRipgrep: Executes the ripgrep command and returns the output.
3. regexSearchFiles: The main function that performs regex searches on files.
- Parameters:
* cwd: The current working directory (for relative path calculation)
* directoryPath: The directory to search in
* regex: The regular expression to search for (Rust regex syntax)
* filePattern: Optional glob pattern to filter files (default: '*')
- Returns: A formatted string containing search results with context
The search results include:
- Relative file paths
- 2 lines of context before and after each match
- Matches formatted with pipe characters for easy reading
Usage example:
const results = await regexSearchFiles('/path/to/cwd', '/path/to/search', 'TODO:', '*.ts');
rel/path/to/app.ts
│----
│function processData(data: any) {
│ // Some processing logic here
│ // TODO: Implement error handling
│ return processedData;
│}
│----
rel/path/to/helper.ts
│----
│ let result = 0;
│ for (let i = 0; i < input; i++) {
│ // TODO: Optimize this function for performance
│ result += Math.pow(i, 2);
│ }
│----
*/
const isWindows = /^win/.test(process.platform)
const binName = isWindows ? "rg.exe" : "rg"
interface SearchResult {
file: string
line: number
column: number
match: string
beforeContext: string[]
afterContext: string[]
}
async function getBinPath(vscodeAppRoot: string): Promise<string | undefined> {
const checkPath = async (pkgFolder: string) => {
const fullPath = path.join(vscodeAppRoot, pkgFolder, binName)
return (await pathExists(fullPath)) ? fullPath : undefined
}
return (
(await checkPath("node_modules/@vscode/ripgrep/bin/")) ||
(await checkPath("node_modules/vscode-ripgrep/bin")) ||
(await checkPath("node_modules.asar.unpacked/vscode-ripgrep/bin/")) ||
(await checkPath("node_modules.asar.unpacked/@vscode/ripgrep/bin/"))
)
}
async function pathExists(path: string): Promise<boolean> {
return new Promise((resolve) => {
fs.access(path, (err) => {
resolve(err === null)
})
})
}
async function execRipgrep(bin: string, args: string[]): Promise<string> {
return new Promise((resolve, reject) => {
const process = childProcess.spawn(bin, args)
let output = ""
let errorOutput = ""
process.stdout.on("data", (data) => {
output += data.toString()
})
process.stderr.on("data", (data) => {
errorOutput += data.toString()
})
process.on("close", (code) => {
if (code === 0) {
resolve(output)
} else {
reject(new Error(`ripgrep process exited with code ${code}: ${errorOutput}`))
}
})
})
}
export async function regexSearchFiles(
cwd: string,
directoryPath: string,
regex: string,
filePattern?: string
): Promise<string> {
const vscodeAppRoot = vscode.env.appRoot
const rgPath = await getBinPath(vscodeAppRoot)
if (!rgPath) {
throw new Error("Could not find ripgrep binary")
}
const args = ["--json", "-e", regex, "--glob", filePattern || "*", "--context", "1", directoryPath]
let output: string
try {
output = await execRipgrep(rgPath, args)
} catch {
return "No results found"
}
const results: SearchResult[] = []
let currentResult: Partial<SearchResult> | 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)
}
currentResult = {
file: parsed.data.path.text,
line: parsed.data.line_number,
column: parsed.data.submatches[0].start,
match: parsed.data.lines.text,
beforeContext: [],
afterContext: [],
}
} else if (parsed.type === "context" && currentResult) {
if (parsed.data.line_number < currentResult.line!) {
currentResult.beforeContext!.push(parsed.data.lines.text)
} else {
currentResult.afterContext!.push(parsed.data.lines.text)
}
}
} catch (error) {
console.error("Error parsing ripgrep output:", error)
}
}
})
if (currentResult) {
results.push(currentResult as SearchResult)
}
return formatResults(results, cwd)
}
function formatResults(results: SearchResult[], cwd: string): string {
const groupedResults: { [key: string]: SearchResult[] } = {}
let output = ""
if (results.length >= 300) {
output += `Showing first 300 of ${results.length.toLocaleString()} results, use a more specific search if necessary...\n\n`
} else {
output += `Found ${results.length.toLocaleString()} results...\n\n`
}
// Group results by file name
results.slice(0, 300).forEach((result) => {
const relativeFilePath = path.relative(cwd, result.file)
if (!groupedResults[relativeFilePath]) {
groupedResults[relativeFilePath] = []
}
groupedResults[relativeFilePath].push(result)
})
for (const [filePath, fileResults] of Object.entries(groupedResults)) {
output += `${filePath}\n│----\n`
fileResults.forEach((result, index) => {
const allLines = [...result.beforeContext, result.match, ...result.afterContext]
allLines.forEach((line) => {
output += `${line?.trimEnd() ?? ""}\n`
})
if (index < fileResults.length - 1) {
output += "│----\n"
}
})
output += "│----\n\n"
}
return output.trim()
}