diff options
| author | Dax Raad <[email protected]> | 2025-05-30 20:47:56 -0400 |
|---|---|---|
| committer | Dax Raad <[email protected]> | 2025-05-30 20:48:36 -0400 |
| commit | f3da73553c45f17e04b1e77cb13eb0fca714d1bd (patch) | |
| tree | a24317a19e1ab2a89da50db669dc6894f15d00d1 /js/src/tool/grep.ts | |
| parent | 9a26b3058ffc1023e5c7e54b6d571c903d15888e (diff) | |
| download | opencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.tar.gz opencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.zip | |
sync
Diffstat (limited to 'js/src/tool/grep.ts')
| -rw-r--r-- | js/src/tool/grep.ts | 345 |
1 files changed, 0 insertions, 345 deletions
diff --git a/js/src/tool/grep.ts b/js/src/tool/grep.ts deleted file mode 100644 index 8b2375f63..000000000 --- a/js/src/tool/grep.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { z } from "zod"; -import { Tool } from "./tool"; -import { App } from "../app/app"; -import { spawn } from "child_process"; -import { promises as fs } from "fs"; -import path from "path"; - -const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first). - -WHEN TO USE THIS TOOL: -- Use when you need to find files containing specific text or patterns -- Great for searching code bases for function names, variable declarations, or error messages -- Useful for finding all files that use a particular API or pattern - -HOW TO USE: -- Provide a regex pattern to search for within file contents -- Set literal_text=true if you want to search for the exact text with special characters (recommended for non-regex users) -- Optionally specify a starting directory (defaults to current working directory) -- Optionally provide an include pattern to filter which files to search -- Results are sorted with most recently modified files first - -REGEX PATTERN SYNTAX (when literal_text=false): -- Supports standard regular expression syntax -- 'function' searches for the literal text "function" -- 'log\\..*Error' finds text starting with "log." and ending with "Error" -- 'import\\s+.*\\s+from' finds import statements in JavaScript/TypeScript - -COMMON INCLUDE PATTERN EXAMPLES: -- '*.js' - Only search JavaScript files -- '*.{ts,tsx}' - Only search TypeScript files -- '*.go' - Only search Go files - -LIMITATIONS: -- Results are limited to 100 files (newest first) -- Performance depends on the number of files being searched -- Very large binary files may be skipped -- Hidden files (starting with '.') are skipped - -TIPS: -- For faster, more targeted searches, first use Glob to find relevant files, then use Grep -- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead -- Always check if results are truncated and refine your search pattern if needed -- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`; - -interface GrepMatch { - path: string; - modTime: number; - lineNum: number; - lineText: string; -} - -function escapeRegexPattern(pattern: string): string { - const specialChars = [ - "\\", - ".", - "+", - "*", - "?", - "(", - ")", - "[", - "]", - "{", - "}", - "^", - "$", - "|", - ]; - let escaped = pattern; - - for (const char of specialChars) { - escaped = escaped.replaceAll(char, "\\" + char); - } - - return escaped; -} - -function globToRegex(glob: string): string { - let regexPattern = glob.replaceAll(".", "\\."); - regexPattern = regexPattern.replaceAll("*", ".*"); - regexPattern = regexPattern.replaceAll("?", "."); - - // Handle {a,b,c} patterns - regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => { - return "(" + inner.replace(/,/g, "|") + ")"; - }); - - return regexPattern; -} - -async function searchWithRipgrep( - pattern: string, - searchPath: string, - include?: string, -): Promise<GrepMatch[]> { - return new Promise((resolve, reject) => { - const args = ["-n", pattern]; - if (include) { - args.push("--glob", include); - } - args.push(searchPath); - - const rg = spawn("rg", args); - let output = ""; - let errorOutput = ""; - - rg.stdout.on("data", (data) => { - output += data.toString(); - }); - - rg.stderr.on("data", (data) => { - errorOutput += data.toString(); - }); - - rg.on("close", async (code) => { - if (code === 1) { - // No matches found - resolve([]); - return; - } - - if (code !== 0) { - reject(new Error(`ripgrep failed: ${errorOutput}`)); - return; - } - - const lines = output.trim().split("\n"); - const matches: GrepMatch[] = []; - - for (const line of lines) { - if (!line) continue; - - // Parse ripgrep output format: file:line:content - const parts = line.split(":", 3); - if (parts.length < 3) continue; - - const filePath = parts[0]; - const lineNum = parseInt(parts[1], 10); - const lineText = parts[2]; - - try { - const stats = await fs.stat(filePath); - matches.push({ - path: filePath, - modTime: stats.mtime.getTime(), - lineNum, - lineText, - }); - } catch { - // Skip files we can't access - continue; - } - } - - resolve(matches); - }); - - rg.on("error", (err) => { - reject(err); - }); - }); -} - -async function searchFilesWithRegex( - pattern: string, - rootPath: string, - include?: string, -): Promise<GrepMatch[]> { - const matches: GrepMatch[] = []; - const regex = new RegExp(pattern); - - let includePattern: RegExp | undefined; - if (include) { - const regexPattern = globToRegex(include); - includePattern = new RegExp(regexPattern); - } - - async function walkDir(dir: string) { - if (matches.length >= 200) return; - - try { - const entries = await fs.readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - if (matches.length >= 200) break; - - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - // Skip hidden directories - if (entry.name.startsWith(".")) continue; - await walkDir(fullPath); - } else if (entry.isFile()) { - // Skip hidden files - if (entry.name.startsWith(".")) continue; - - if (includePattern && !includePattern.test(fullPath)) { - continue; - } - - try { - const content = await fs.readFile(fullPath, "utf-8"); - const lines = content.split("\n"); - - for (let i = 0; i < lines.length; i++) { - if (regex.test(lines[i])) { - const stats = await fs.stat(fullPath); - matches.push({ - path: fullPath, - modTime: stats.mtime.getTime(), - lineNum: i + 1, - lineText: lines[i], - }); - break; // Only first match per file - } - } - } catch { - // Skip files we can't read - continue; - } - } - } - } catch { - // Skip directories we can't read - return; - } - } - - await walkDir(rootPath); - return matches; -} - -async function searchFiles( - pattern: string, - rootPath: string, - include?: string, - limit: number = 100, -): Promise<{ matches: GrepMatch[]; truncated: boolean }> { - let matches: GrepMatch[]; - - try { - matches = await searchWithRipgrep(pattern, rootPath, include); - } catch { - matches = await searchFilesWithRegex(pattern, rootPath, include); - } - - // Sort by modification time (newest first) - matches.sort((a, b) => b.modTime - a.modTime); - - const truncated = matches.length > limit; - if (truncated) { - matches = matches.slice(0, limit); - } - - return { matches, truncated }; -} - -export const grep = Tool.define({ - name: "opencode.grep", - description: DESCRIPTION, - parameters: z.object({ - pattern: z - .string() - .describe("The regex pattern to search for in file contents"), - path: z - .string() - .describe( - "The directory to search in. Defaults to the current working directory.", - ) - .optional(), - include: z - .string() - .describe( - 'File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")', - ) - .optional(), - literalText: z - .boolean() - .describe( - "If true, the pattern will be treated as literal text with special regex characters escaped. Default is false.", - ) - .optional(), - }), - async execute(params) { - if (!params.pattern) { - throw new Error("pattern is required"); - } - - const app = await App.use(); - const searchPath = params.path || app.root; - - // If literalText is true, escape the pattern - const searchPattern = params.literalText - ? escapeRegexPattern(params.pattern) - : params.pattern; - - const { matches, truncated } = await searchFiles( - searchPattern, - searchPath, - params.include, - 100, - ); - - if (matches.length === 0) { - return { - metadata: { matches: 0, truncated }, - output: "No files found" - }; - } - - const lines = [`Found ${matches.length} matches`]; - - let currentFile = ""; - for (const match of matches) { - if (currentFile !== match.path) { - if (currentFile !== "") { - lines.push(""); - } - currentFile = match.path; - lines.push(`${match.path}:`); - } - if (match.lineNum > 0) { - lines.push(` Line ${match.lineNum}: ${match.lineText}`); - } else { - lines.push(` ${match.path}`); - } - } - - if (truncated) { - lines.push(""); - lines.push( - "(Results are truncated. Consider using a more specific path or pattern.)", - ); - } - - return { - metadata: { - matches: matches.length, - truncated, - }, - output: lines.join("\n"), - }; - }, -}); - |
