summaryrefslogtreecommitdiffhomepage
path: root/js/src/tool/grep.ts
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-30 20:47:56 -0400
committerDax Raad <[email protected]>2025-05-30 20:48:36 -0400
commitf3da73553c45f17e04b1e77cb13eb0fca714d1bd (patch)
treea24317a19e1ab2a89da50db669dc6894f15d00d1 /js/src/tool/grep.ts
parent9a26b3058ffc1023e5c7e54b6d571c903d15888e (diff)
downloadopencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.tar.gz
opencode-f3da73553c45f17e04b1e77cb13eb0fca714d1bd.zip
sync
Diffstat (limited to 'js/src/tool/grep.ts')
-rw-r--r--js/src/tool/grep.ts345
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"),
- };
- },
-});
-