diff --git a/aider/coders/navigator_coder.py b/aider/coders/navigator_coder.py index 31e1d446c..4f93f41aa 100644 --- a/aider/coders/navigator_coder.py +++ b/aider/coders/navigator_coder.py @@ -762,6 +762,21 @@ class NavigatorCoder(Coder): else: result_message = "Error: Missing 'command_string' parameter for CommandInteractive" + # Grep tool + elif norm_tool_name == 'grep': + pattern = params.get('pattern') + file_pattern = params.get('file_pattern', '*') # Default to all files + directory = params.get('directory', '.') # Default to current directory + use_regex = params.get('use_regex', False) # Default to literal search + case_insensitive = params.get('case_insensitive', False) # Default to case-sensitive + + if pattern is not None: + # Import the function if not already imported (it should be) + from aider.tools.grep import _execute_grep + result_message = _execute_grep(self, pattern, file_pattern, directory, use_regex, case_insensitive) + else: + result_message = "Error: Missing required 'pattern' parameter for Grep" + # Granular editing tools elif norm_tool_name == 'replacetext': file_path = params.get('file_path') diff --git a/aider/coders/navigator_legacy_prompts.py b/aider/coders/navigator_legacy_prompts.py index d1e92926a..d0b62bf05 100644 --- a/aider/coders/navigator_legacy_prompts.py +++ b/aider/coders/navigator_legacy_prompts.py @@ -52,6 +52,13 @@ Act as an expert software engineer with the ability to autonomously navigate and Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.** Leverages the repo map for accurate symbol lookup. +- **Grep**: `[tool_call(Grep, pattern="my_variable", file_pattern="*.py", directory="src", use_regex=False, case_insensitive=False)]` + Search for lines matching a pattern in files using the best available tool (`rg`, `ag`, or `grep`). Returns matching lines with line numbers. + `file_pattern` (optional, default "*") filters files using glob syntax. + `directory` (optional, default ".") specifies the search directory relative to the repo root. + `use_regex` (optional, default False): If False, performs a literal/fixed string search. If True, uses basic Extended Regular Expression (ERE) syntax. + `case_insensitive` (optional, default False): If False (default), the search is case-sensitive. If True, the search is case-insensitive. + ### Context Management Tools - **View**: `[tool_call(View, file_path="src/main.py")]` Explicitly add a specific file to context as read-only. @@ -318,4 +325,4 @@ Let me explore the codebase more strategically this time: - I'll use tool calls to automatically continue exploration until I have enough information I'll start exploring again with improved search strategies to find exactly what we need. -""" \ No newline at end of file +""" diff --git a/aider/coders/navigator_prompts.py b/aider/coders/navigator_prompts.py index 5ae644d5a..dd70a7218 100644 --- a/aider/coders/navigator_prompts.py +++ b/aider/coders/navigator_prompts.py @@ -52,6 +52,13 @@ Act as an expert software engineer with the ability to autonomously navigate and Find files containing a specific symbol (function, class, variable). **Found files are automatically added to context as read-only.** Leverages the repo map for accurate symbol lookup. +- **Grep**: `[tool_call(Grep, pattern="my_variable", file_pattern="*.py", directory="src", use_regex=False, case_insensitive=False)]` + Search for lines matching a pattern in files using the best available tool (`rg`, `ag`, or `grep`). Returns matching lines with line numbers. + `file_pattern` (optional, default "*") filters files using glob syntax. + `directory` (optional, default ".") specifies the search directory relative to the repo root. + `use_regex` (optional, default False): If False, performs a literal/fixed string search. If True, uses basic Extended Regular Expression (ERE) syntax. + `case_insensitive` (optional, default False): If False (default), the search is case-sensitive. If True, the search is case-insensitive. + ### Context Management Tools - **View**: `[tool_call(View, file_path="src/main.py")]` Explicitly add a specific file to context as read-only. @@ -508,4 +515,4 @@ Let me explore the codebase more strategically this time: - I'll use tool calls to automatically continue exploration until I have enough information I'll start exploring again with improved search strategies to find exactly what we need. -""" \ No newline at end of file +""" diff --git a/aider/tools/grep.py b/aider/tools/grep.py new file mode 100644 index 000000000..79667459e --- /dev/null +++ b/aider/tools/grep.py @@ -0,0 +1,145 @@ +import shlex +import shutil +from pathlib import Path +from aider.run_cmd import run_cmd_subprocess + +def _find_search_tool(): + """Find the best available command-line search tool (rg, ag, grep).""" + if shutil.which('rg'): + return 'rg', shutil.which('rg') + elif shutil.which('ag'): + return 'ag', shutil.which('ag') + elif shutil.which('grep'): + return 'grep', shutil.which('grep') + else: + return None, None + +def _execute_grep(coder, pattern, file_pattern="*", directory=".", use_regex=False, case_insensitive=False): + """ + Search for lines matching a pattern in files within the project repository. + Uses rg (ripgrep), ag (the silver searcher), or grep, whichever is available. + + Args: + coder: The Coder instance. + pattern (str): The pattern to search for. + file_pattern (str, optional): Glob pattern to filter files. Defaults to "*". + directory (str, optional): Directory to search within relative to repo root. Defaults to ".". + use_regex (bool, optional): Whether the pattern is a regular expression. Defaults to False. + + Returns: + str: Formatted result indicating success or failure, including matching lines or error message. + """ + repo = coder.repo + if not repo: + coder.io.tool_error("Not in a git repository.") + return "Error: Not in a git repository." + + tool_name, tool_path = _find_search_tool() + if not tool_path: + coder.io.tool_error("No search tool (rg, ag, grep) found in PATH.") + return "Error: No search tool (rg, ag, grep) found." + + try: + search_dir_path = Path(repo.root) / directory + if not search_dir_path.is_dir(): + coder.io.tool_error(f"Directory not found: {directory}") + return f"Error: Directory not found: {directory}" + + # Build the command arguments based on the available tool + cmd_args = [tool_path] + + # Common options or tool-specific equivalents + if tool_name in ['rg', 'grep']: + cmd_args.append("-n") # Line numbers for rg and grep + # ag includes line numbers by default + + # Case sensitivity + if case_insensitive: + cmd_args.append("-i") # Add case-insensitivity flag for all tools + + # Pattern type (regex vs fixed string) + if use_regex: + if tool_name == 'grep': + cmd_args.append("-E") # Use extended regex for grep + # rg and ag use regex by default, no flag needed for basic ERE + else: + if tool_name == 'rg': + cmd_args.append("-F") # Fixed strings for rg + elif tool_name == 'ag': + cmd_args.append("-Q") # Literal/fixed strings for ag + elif tool_name == 'grep': + cmd_args.append("-F") # Fixed strings for grep + + # File filtering + if file_pattern != "*": # Avoid adding glob if it's the default '*' which might behave differently + if tool_name == 'rg': + cmd_args.extend(["-g", file_pattern]) + elif tool_name == 'ag': + cmd_args.extend(["-G", file_pattern]) + elif tool_name == 'grep': + # grep needs recursive flag when filtering + cmd_args.append("-r") + cmd_args.append(f"--include={file_pattern}") + elif tool_name == 'grep': + # grep needs recursive flag even without include filter + cmd_args.append("-r") + + # Directory exclusion (rg and ag respect .gitignore/.git by default) + if tool_name == 'grep': + cmd_args.append("--exclude-dir=.git") + + # Add pattern and directory path + cmd_args.extend([pattern, str(search_dir_path)]) + + # Convert list to command string for run_cmd_subprocess + command_string = shlex.join(cmd_args) + + coder.io.tool_output(f"⚙️ Executing {tool_name}: {command_string}") + + # Use run_cmd_subprocess for execution + # Note: rg, ag, and grep return 1 if no matches are found, which is not an error for this tool. + exit_status, combined_output = run_cmd_subprocess( + command_string, + verbose=coder.verbose, + cwd=coder.root # Execute in the project root + ) + + # Format the output for the result message + output_content = combined_output or "" + + # Handle exit codes (consistent across rg, ag, grep) + if exit_status == 0: + # Limit output size if necessary + max_output_lines = 50 # Consider making this configurable + output_lines = output_content.splitlines() + if len(output_lines) > max_output_lines: + truncated_output = "\n".join(output_lines[:max_output_lines]) + result_message = f"Found matches (truncated):\n```text\n{truncated_output}\n... ({len(output_lines) - max_output_lines} more lines)\n```" + elif not output_content: + # Should not happen if return code is 0, but handle defensively + coder.io.tool_warning(f"{tool_name} returned 0 but produced no output.") + result_message = "No matches found (unexpected)." + else: + result_message = f"Found matches:\n```text\n{output_content}\n```" + return result_message + + elif exit_status == 1: + # Exit code 1 means no matches found - this is expected behavior, not an error. + return "No matches found." + else: + # Exit code > 1 indicates an actual error + error_message = f"{tool_name.capitalize()} command failed with exit code {exit_status}." + if output_content: + # Truncate error output as well if it's too long + error_limit = 1000 # Example limit for error output + if len(output_content) > error_limit: + output_content = output_content[:error_limit] + "\n... (error output truncated)" + error_message += f" Output:\n{output_content}" + coder.io.tool_error(error_message) + return f"Error: {error_message}" + + except Exception as e: + # Add command_string to the error message if it's defined + cmd_str_info = f"'{command_string}' " if 'command_string' in locals() else "" + coder.io.tool_error(f"Error executing {tool_name} command {cmd_str_info}: {str(e)}") + return f"Error executing {tool_name}: {str(e)}"