Add DeleteLine{,s} tools

This commit is contained in:
Amar Sood (tekacs) 2025-04-12 08:26:41 -04:00
parent 17964f476c
commit ee94452ffe
5 changed files with 289 additions and 3 deletions

View file

@ -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')

View file

@ -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

View file

@ -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

100
aider/tools/delete_line.py Normal file
View file

@ -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)}"

111
aider/tools/delete_lines.py Normal file
View file

@ -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)}"