diff --git a/aider/coders/navigator_coder.py b/aider/coders/navigator_coder.py index b7763a691..b2381dce9 100644 --- a/aider/coders/navigator_coder.py +++ b/aider/coders/navigator_coder.py @@ -51,6 +51,8 @@ from aider.tools.delete_block import _execute_delete_block from aider.tools.replace_line import _execute_replace_line from aider.tools.replace_lines import _execute_replace_lines from aider.tools.indent_lines import _execute_indent_lines +from aider.tools.delete_line import _execute_delete_line # New +from aider.tools.delete_lines import _execute_delete_lines # New from aider.tools.undo_change import _execute_undo_change from aider.tools.list_changes import _execute_list_changes from aider.tools.extract_lines import _execute_extract_lines @@ -851,7 +853,34 @@ class NavigatorCoder(Coder): ) else: result_message = "Error: Missing required parameters for IndentLines (file_path, start_pattern)" - + + elif norm_tool_name == 'deleteline': + file_path = params.get('file_path') + line_number = params.get('line_number') + change_id = params.get('change_id') + dry_run = params.get('dry_run', False) + + if file_path is not None and line_number is not None: + result_message = _execute_delete_line( + self, file_path, line_number, change_id, dry_run + ) + else: + result_message = "Error: Missing required parameters for DeleteLine (file_path, line_number)" + + elif norm_tool_name == 'deletelines': + file_path = params.get('file_path') + start_line = params.get('start_line') + end_line = params.get('end_line') + change_id = params.get('change_id') + dry_run = params.get('dry_run', False) + + if file_path is not None and start_line is not None and end_line is not None: + result_message = _execute_delete_lines( + self, file_path, start_line, end_line, change_id, dry_run + ) + else: + result_message = "Error: Missing required parameters for DeleteLines (file_path, start_line, end_line)" + elif norm_tool_name == 'undochange': change_id = params.get('change_id') file_path = params.get('file_path') diff --git a/aider/coders/navigator_prompts.py b/aider/coders/navigator_prompts.py index dce1ceffd..4460292aa 100644 --- a/aider/coders/navigator_prompts.py +++ b/aider/coders/navigator_prompts.py @@ -94,10 +94,18 @@ Act as an expert software engineer with the ability to autonomously navigate and - **IndentLines**: `[tool_call(IndentLines, file_path="...", start_pattern="...", end_pattern="...", indent_levels=1, near_context="...", occurrence=1, dry_run=False)]` Indent (`indent_levels` > 0) or unindent (`indent_levels` < 0) a block. Use `end_pattern` or `line_count` for range. Use `near_context` and `occurrence` (optional, default 1, -1 for last) for `start_pattern`. `dry_run=True` simulates. *Useful for fixing indentation errors reported by linters or reformatting code blocks. Also helpful for adjusting indentation after moving code with `ExtractLines`.* - + +- **DeleteLine**: `[tool_call(DeleteLine, file_path="...", line_number=42, dry_run=False)]` + Delete a specific line number (1-based). `dry_run=True` simulates. + *Useful for removing single erroneous lines identified by linters or exact line number.* + +- **DeleteLines**: `[tool_call(DeleteLines, file_path="...", start_line=42, end_line=45, dry_run=False)]` + Delete a range of lines (1-based, inclusive). `dry_run=True` simulates. + *Useful for removing multi-line blocks when exact line numbers are known.* + - **UndoChange**: `[tool_call(UndoChange, change_id="a1b2c3d4")]` or `[tool_call(UndoChange, file_path="...")]` Undo a specific change by ID, or the last change made to the specified `file_path`. - + - **ListChanges**: `[tool_call(ListChanges, file_path="...", limit=5)]` List recent changes, optionally filtered by `file_path` and limited. @@ -222,6 +230,7 @@ SEARCH/REPLACE blocks can appear anywhere in your response if needed. - `InsertBlock`: For adding code blocks. - `DeleteBlock`: For removing code sections. - `ReplaceLine`/`ReplaceLines`: For line-specific fixes (requires strict `ShowNumberedContext` verification). +- `DeleteLine`/`DeleteLines`: For removing lines by number (requires strict `ShowNumberedContext` verification). - `IndentLines`: For adjusting indentation. - `ExtractLines`: For moving code between files. - `UndoChange`: For reverting specific edits. @@ -239,6 +248,8 @@ Warning in /path/to/file.py lines 105-107: This block should be indented For these cases, use: - `ReplaceLine` for single line fixes (e.g., syntax errors) - `ReplaceLines` for multi-line issues +- `DeleteLine` for removing single erroneous lines +- `DeleteLines` for removing multi-line blocks by number - `IndentLines` for indentation problems #### Multiline Tool Call Content Format diff --git a/aider/tools/__init__.py b/aider/tools/__init__.py index e69de29bb..f173e68cc 100644 --- a/aider/tools/__init__.py +++ b/aider/tools/__init__.py @@ -0,0 +1,35 @@ +# flake8: noqa: F401 +# Import tool functions into the aider.tools namespace + +# Discovery +from .ls import execute_ls +from .view_files_at_glob import execute_view_files_at_glob +from .view_files_matching import execute_view_files_matching +from .view_files_with_symbol import _execute_view_files_with_symbol + +# Context Management +from .view import execute_view +from .remove import _execute_remove +from .make_editable import _execute_make_editable +from .make_readonly import _execute_make_readonly +from .show_numbered_context import execute_show_numbered_context + +# Granular Editing +from .replace_text import _execute_replace_text +from .replace_all import _execute_replace_all +from .insert_block import _execute_insert_block +from .delete_block import _execute_delete_block +from .replace_line import _execute_replace_line +from .replace_lines import _execute_replace_lines +from .indent_lines import _execute_indent_lines +from .extract_lines import _execute_extract_lines +from .delete_line import _execute_delete_line +from .delete_lines import _execute_delete_lines + +# Change Tracking +from .undo_change import _execute_undo_change +from .list_changes import _execute_list_changes + +# Other +from .command import _execute_command +from .command_interactive import _execute_command_interactive diff --git a/aider/tools/delete_line.py b/aider/tools/delete_line.py new file mode 100644 index 000000000..e3b470ed2 --- /dev/null +++ b/aider/tools/delete_line.py @@ -0,0 +1,100 @@ +import os +import traceback + +def _execute_delete_line(coder, file_path, line_number, change_id=None, dry_run=False): + """ + Delete a specific line number (1-based). + + Parameters: + - coder: The Coder instance + - file_path: Path to the file to modify + - line_number: The 1-based line number to delete + - change_id: Optional ID for tracking the change + - dry_run: If True, simulate the change without modifying the file + + Returns a result message. + """ + try: + # Get absolute file path + abs_path = coder.abs_root_path(file_path) + rel_path = coder.get_rel_fname(abs_path) + + # Check if file exists + if not os.path.isfile(abs_path): + coder.io.tool_error(f"File '{file_path}' not found") + return f"Error: File not found" + + # Check if file is in editable context + if abs_path not in coder.abs_fnames: + if abs_path in coder.abs_read_only_fnames: + coder.io.tool_error(f"File '{file_path}' is read-only. Use MakeEditable first.") + return f"Error: File is read-only. Use MakeEditable first." + else: + coder.io.tool_error(f"File '{file_path}' not in context") + return f"Error: File not in context" + + # Reread file content immediately before modification + file_content = coder.io.read_text(abs_path) + if file_content is None: + coder.io.tool_error(f"Could not read file '{file_path}' before DeleteLine operation.") + return f"Error: Could not read file '{file_path}'" + + lines = file_content.splitlines() + original_content = file_content + + # Validate line number + try: + line_num_int = int(line_number) + if line_num_int < 1 or line_num_int > len(lines): + raise ValueError(f"Line number {line_num_int} is out of range (1-{len(lines)})") + line_idx = line_num_int - 1 # Convert to 0-based index + except ValueError as e: + coder.io.tool_error(f"Invalid line_number: {e}") + return f"Error: Invalid line_number '{line_number}'" + + # Prepare the deletion + deleted_line = lines[line_idx] + new_lines = lines[:line_idx] + lines[line_idx+1:] + new_content = '\n'.join(new_lines) + + if original_content == new_content: + coder.io.tool_warning(f"No changes made: deleting line {line_num_int} would not change file") + return f"Warning: No changes made (deleting line {line_num_int} would not change file)" + + # Generate diff snippet (using the existing delete block helper for simplicity) + diff_snippet = coder._generate_diff_snippet_delete(original_content, line_idx, line_idx) + + # Handle dry run + if dry_run: + coder.io.tool_output(f"Dry run: Would delete line {line_num_int} in {file_path}") + return f"Dry run: Would delete line {line_num_int}. Diff snippet:\n{diff_snippet}" + + # --- Apply Change (Not dry run) --- + coder.io.write_text(abs_path, new_content) + + # Track the change + try: + metadata = { + 'line_number': line_num_int, + 'deleted_content': deleted_line + } + change_id = coder.change_tracker.track_change( + file_path=rel_path, + change_type='deleteline', + original_content=original_content, + new_content=new_content, + metadata=metadata, + change_id=change_id + ) + except Exception as track_e: + coder.io.tool_error(f"Error tracking change for DeleteLine: {track_e}") + change_id = "TRACKING_FAILED" + + coder.aider_edited_files.add(rel_path) + + coder.io.tool_output(f"✅ Deleted line {line_num_int} in {file_path} (change_id: {change_id})") + return f"Successfully deleted line {line_num_int} (change_id: {change_id}). Diff snippet:\n{diff_snippet}" + + except Exception as e: + coder.io.tool_error(f"Error in DeleteLine: {str(e)}\n{traceback.format_exc()}") + return f"Error: {str(e)}" diff --git a/aider/tools/delete_lines.py b/aider/tools/delete_lines.py new file mode 100644 index 000000000..c24e123ad --- /dev/null +++ b/aider/tools/delete_lines.py @@ -0,0 +1,111 @@ +import os +import traceback + +def _execute_delete_lines(coder, file_path, start_line, end_line, change_id=None, dry_run=False): + """ + Delete a range of lines (1-based, inclusive). + + Parameters: + - coder: The Coder instance + - file_path: Path to the file to modify + - start_line: The 1-based starting line number to delete + - end_line: The 1-based ending line number to delete + - change_id: Optional ID for tracking the change + - dry_run: If True, simulate the change without modifying the file + + Returns a result message. + """ + try: + # Get absolute file path + abs_path = coder.abs_root_path(file_path) + rel_path = coder.get_rel_fname(abs_path) + + # Check if file exists + if not os.path.isfile(abs_path): + coder.io.tool_error(f"File '{file_path}' not found") + return f"Error: File not found" + + # Check if file is in editable context + if abs_path not in coder.abs_fnames: + if abs_path in coder.abs_read_only_fnames: + coder.io.tool_error(f"File '{file_path}' is read-only. Use MakeEditable first.") + return f"Error: File is read-only. Use MakeEditable first." + else: + coder.io.tool_error(f"File '{file_path}' not in context") + return f"Error: File not in context" + + # Reread file content immediately before modification + file_content = coder.io.read_text(abs_path) + if file_content is None: + coder.io.tool_error(f"Could not read file '{file_path}' before DeleteLines operation.") + return f"Error: Could not read file '{file_path}'" + + lines = file_content.splitlines() + original_content = file_content + + # Validate line numbers + try: + start_line_int = int(start_line) + end_line_int = int(end_line) + + if start_line_int < 1 or start_line_int > len(lines): + raise ValueError(f"Start line {start_line_int} is out of range (1-{len(lines)})") + if end_line_int < 1 or end_line_int > len(lines): + raise ValueError(f"End line {end_line_int} is out of range (1-{len(lines)})") + if start_line_int > end_line_int: + raise ValueError(f"Start line {start_line_int} cannot be after end line {end_line_int}") + + start_idx = start_line_int - 1 # Convert to 0-based index + end_idx = end_line_int - 1 # Convert to 0-based index + except ValueError as e: + coder.io.tool_error(f"Invalid line numbers: {e}") + return f"Error: Invalid line numbers '{start_line}', '{end_line}'" + + # Prepare the deletion + deleted_lines = lines[start_idx:end_idx+1] + new_lines = lines[:start_idx] + lines[end_idx+1:] + new_content = '\n'.join(new_lines) + + if original_content == new_content: + coder.io.tool_warning(f"No changes made: deleting lines {start_line_int}-{end_line_int} would not change file") + return f"Warning: No changes made (deleting lines {start_line_int}-{end_line_int} would not change file)" + + # Generate diff snippet + diff_snippet = coder._generate_diff_snippet_delete(original_content, start_idx, end_idx) + + # Handle dry run + if dry_run: + coder.io.tool_output(f"Dry run: Would delete lines {start_line_int}-{end_line_int} in {file_path}") + return f"Dry run: Would delete lines {start_line_int}-{end_line_int}. Diff snippet:\n{diff_snippet}" + + # --- Apply Change (Not dry run) --- + coder.io.write_text(abs_path, new_content) + + # Track the change + try: + metadata = { + 'start_line': start_line_int, + 'end_line': end_line_int, + 'deleted_content': '\n'.join(deleted_lines) + } + change_id = coder.change_tracker.track_change( + file_path=rel_path, + change_type='deletelines', + original_content=original_content, + new_content=new_content, + metadata=metadata, + change_id=change_id + ) + except Exception as track_e: + coder.io.tool_error(f"Error tracking change for DeleteLines: {track_e}") + change_id = "TRACKING_FAILED" + + coder.aider_edited_files.add(rel_path) + + num_deleted = end_idx - start_idx + 1 + coder.io.tool_output(f"✅ Deleted {num_deleted} lines ({start_line_int}-{end_line_int}) in {file_path} (change_id: {change_id})") + return f"Successfully deleted {num_deleted} lines ({start_line_int}-{end_line_int}) (change_id: {change_id}). Diff snippet:\n{diff_snippet}" + + except Exception as e: + coder.io.tool_error(f"Error in DeleteLines: {str(e)}\n{traceback.format_exc()}") + return f"Error: {str(e)}"