mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-09 06:05:00 +00:00
Try factoring out some common functionality. Incremental commit.
This commit is contained in:
parent
551b357559
commit
c55d789c25
5 changed files with 422 additions and 455 deletions
|
@ -1,121 +1,39 @@
|
||||||
import os
|
|
||||||
import traceback
|
import traceback
|
||||||
|
from .tool_utils import (
|
||||||
|
ToolError,
|
||||||
|
validate_file_for_edit,
|
||||||
|
find_pattern_indices,
|
||||||
|
select_occurrence_index,
|
||||||
|
determine_line_range,
|
||||||
|
apply_change,
|
||||||
|
handle_tool_error,
|
||||||
|
format_tool_result,
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_delete_block(coder, file_path, start_pattern, end_pattern=None, line_count=None, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
def _execute_delete_block(coder, file_path, start_pattern, end_pattern=None, line_count=None, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
||||||
"""
|
"""
|
||||||
Delete a block of text between start_pattern and end_pattern (inclusive).
|
Delete a block of text between start_pattern and end_pattern (inclusive).
|
||||||
|
Uses utility functions for validation, finding lines, and applying changes.
|
||||||
Parameters:
|
|
||||||
- coder: The Coder instance
|
|
||||||
- file_path: Path to the file to modify
|
|
||||||
- start_pattern: Pattern marking the start of the block to delete (line containing this pattern)
|
|
||||||
- end_pattern: Optional pattern marking the end of the block (line containing this pattern)
|
|
||||||
- line_count: Optional number of lines to delete (alternative to end_pattern)
|
|
||||||
- near_context: Optional text nearby to help locate the correct instance of the start_pattern
|
|
||||||
- occurrence: Which occurrence of the start_pattern to use (1-based index, or -1 for last)
|
|
||||||
- change_id: Optional ID for tracking the change
|
|
||||||
- dry_run: If True, simulate the change without modifying the file
|
|
||||||
|
|
||||||
Returns a result message.
|
|
||||||
"""
|
"""
|
||||||
|
tool_name = "DeleteBlock"
|
||||||
try:
|
try:
|
||||||
# Get absolute file path
|
# 1. Validate file and get content
|
||||||
abs_path = coder.abs_root_path(file_path)
|
abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
||||||
rel_path = coder.get_rel_fname(abs_path)
|
lines = original_content.splitlines()
|
||||||
|
|
||||||
# Check if file exists
|
# 2. Find the start line
|
||||||
if not os.path.isfile(abs_path):
|
pattern_desc = f"Start pattern '{start_pattern}'"
|
||||||
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 DeleteBlock operation.")
|
|
||||||
return f"Error: Could not read file '{file_path}'"
|
|
||||||
|
|
||||||
# Validate we have either end_pattern or line_count, but not both
|
|
||||||
if end_pattern and line_count:
|
|
||||||
coder.io.tool_error("Cannot specify both end_pattern and line_count")
|
|
||||||
return "Error: Cannot specify both end_pattern and line_count"
|
|
||||||
|
|
||||||
# Split into lines for easier handling
|
|
||||||
lines = file_content.splitlines()
|
|
||||||
original_content = file_content
|
|
||||||
|
|
||||||
# Find occurrences of the start_pattern
|
|
||||||
start_pattern_line_indices = []
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if start_pattern in line:
|
|
||||||
if near_context:
|
if near_context:
|
||||||
context_window_start = max(0, i - 5)
|
pattern_desc += f" near context '{near_context}'"
|
||||||
context_window_end = min(len(lines), i + 6)
|
start_pattern_indices = find_pattern_indices(lines, start_pattern, near_context)
|
||||||
context_block = "\n".join(lines[context_window_start:context_window_end])
|
start_line_idx = select_occurrence_index(start_pattern_indices, occurrence, pattern_desc)
|
||||||
if near_context in context_block:
|
|
||||||
start_pattern_line_indices.append(i)
|
|
||||||
else:
|
|
||||||
start_pattern_line_indices.append(i)
|
|
||||||
|
|
||||||
if not start_pattern_line_indices:
|
# 3. Determine the end line, passing pattern_desc for better error messages
|
||||||
err_msg = f"Start pattern '{start_pattern}' not found"
|
start_line, end_line = determine_line_range(
|
||||||
if near_context: err_msg += f" near context '{near_context}'"
|
lines, start_line_idx, end_pattern, line_count, pattern_desc=pattern_desc
|
||||||
err_msg += f" in file '{file_path}'."
|
)
|
||||||
coder.io.tool_error(err_msg)
|
|
||||||
return f"Error: {err_msg}"
|
|
||||||
|
|
||||||
# Select the occurrence for the start pattern
|
# 4. Prepare the deletion
|
||||||
num_occurrences = len(start_pattern_line_indices)
|
|
||||||
try:
|
|
||||||
occurrence = int(occurrence)
|
|
||||||
if occurrence == -1:
|
|
||||||
target_idx = num_occurrences - 1
|
|
||||||
elif occurrence > 0 and occurrence <= num_occurrences:
|
|
||||||
target_idx = occurrence - 1
|
|
||||||
else:
|
|
||||||
err_msg = f"Occurrence number {occurrence} is out of range for start pattern '{start_pattern}'. Found {num_occurrences} occurrences"
|
|
||||||
if near_context: err_msg += f" near '{near_context}'"
|
|
||||||
err_msg += f" in '{file_path}'."
|
|
||||||
coder.io.tool_error(err_msg)
|
|
||||||
return f"Error: {err_msg}"
|
|
||||||
except ValueError:
|
|
||||||
coder.io.tool_error(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
|
||||||
return f"Error: Invalid occurrence value '{occurrence}'"
|
|
||||||
|
|
||||||
start_line = start_pattern_line_indices[target_idx]
|
|
||||||
occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
|
|
||||||
|
|
||||||
# Find the end line based on end_pattern or line_count
|
|
||||||
end_line = -1
|
|
||||||
if end_pattern:
|
|
||||||
for i in range(start_line, len(lines)):
|
|
||||||
if end_pattern in lines[i]:
|
|
||||||
end_line = i
|
|
||||||
break
|
|
||||||
if end_line == -1:
|
|
||||||
err_msg = f"End pattern '{end_pattern}' not found after {occurrence_str}start pattern '{start_pattern}' (line {start_line + 1}) in '{file_path}'."
|
|
||||||
coder.io.tool_error(err_msg)
|
|
||||||
return f"Error: {err_msg}"
|
|
||||||
elif line_count:
|
|
||||||
try:
|
|
||||||
line_count = int(line_count)
|
|
||||||
if line_count <= 0: raise ValueError("Line count must be positive")
|
|
||||||
end_line = min(start_line + line_count - 1, len(lines) - 1)
|
|
||||||
except ValueError:
|
|
||||||
coder.io.tool_error(f"Invalid line_count value: '{line_count}'. Must be a positive integer.")
|
|
||||||
return f"Error: Invalid line_count value '{line_count}'"
|
|
||||||
else:
|
|
||||||
end_line = start_line
|
|
||||||
|
|
||||||
# Prepare the deletion
|
|
||||||
deleted_lines = lines[start_line:end_line+1]
|
deleted_lines = lines[start_line:end_line+1]
|
||||||
new_lines = lines[:start_line] + lines[end_line+1:]
|
new_lines = lines[:start_line] + lines[end_line+1:]
|
||||||
new_content = '\n'.join(new_lines)
|
new_content = '\n'.join(new_lines)
|
||||||
|
@ -124,19 +42,18 @@ def _execute_delete_block(coder, file_path, start_pattern, end_pattern=None, lin
|
||||||
coder.io.tool_warning(f"No changes made: deletion would not change file")
|
coder.io.tool_warning(f"No changes made: deletion would not change file")
|
||||||
return f"Warning: No changes made (deletion would not change file)"
|
return f"Warning: No changes made (deletion would not change file)"
|
||||||
|
|
||||||
# Generate diff for feedback (assuming _generate_diff_snippet_delete is available on coder)
|
# 5. Generate diff for feedback
|
||||||
diff_snippet = coder._generate_diff_snippet_delete(original_content, start_line, end_line)
|
diff_snippet = coder._generate_diff_snippet_delete(original_content, start_line, end_line)
|
||||||
|
num_deleted = end_line - start_line + 1
|
||||||
|
num_occurrences = len(start_pattern_indices)
|
||||||
|
occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
|
||||||
|
|
||||||
# Handle dry run
|
# 6. Handle dry run
|
||||||
if dry_run:
|
if dry_run:
|
||||||
coder.io.tool_output(f"Dry run: Would delete lines {start_line+1}-{end_line+1} (based on {occurrence_str}start pattern '{start_pattern}') in {file_path}")
|
dry_run_message = f"Dry run: Would delete {num_deleted} lines ({start_line+1}-{end_line+1}) based on {occurrence_str}start pattern '{start_pattern}' in {file_path}."
|
||||||
return f"Dry run: Would delete block. Diff snippet:\n{diff_snippet}"
|
return format_tool_result(coder, tool_name, "", dry_run=True, dry_run_message=dry_run_message, diff_snippet=diff_snippet)
|
||||||
|
|
||||||
# --- Apply Change (Not dry run) ---
|
# 7. Apply Change (Not dry run)
|
||||||
coder.io.write_text(abs_path, new_content)
|
|
||||||
|
|
||||||
# Track the change
|
|
||||||
try:
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'start_line': start_line + 1,
|
'start_line': start_line + 1,
|
||||||
'end_line': end_line + 1,
|
'end_line': end_line + 1,
|
||||||
|
@ -147,25 +64,19 @@ def _execute_delete_block(coder, file_path, start_pattern, end_pattern=None, lin
|
||||||
'occurrence': occurrence,
|
'occurrence': occurrence,
|
||||||
'deleted_content': '\n'.join(deleted_lines)
|
'deleted_content': '\n'.join(deleted_lines)
|
||||||
}
|
}
|
||||||
change_id = coder.change_tracker.track_change(
|
final_change_id = apply_change(
|
||||||
file_path=rel_path,
|
coder, abs_path, rel_path, original_content, new_content, 'deleteblock', metadata, change_id
|
||||||
change_type='deleteblock',
|
|
||||||
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 DeleteBlock: {track_e}")
|
|
||||||
change_id = "TRACKING_FAILED"
|
|
||||||
|
|
||||||
coder.aider_edited_files.add(rel_path)
|
# 8. Format and return result, adding line range to success message
|
||||||
|
success_message = f"Deleted {num_deleted} lines ({start_line+1}-{end_line+1}) (from {occurrence_str}start pattern) in {file_path}"
|
||||||
# Improve feedback
|
return format_tool_result(
|
||||||
num_deleted = end_line - start_line + 1
|
coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
|
||||||
coder.io.tool_output(f"✅ Deleted {num_deleted} lines (from {occurrence_str}start pattern) in {file_path} (change_id: {change_id})")
|
)
|
||||||
return f"Successfully deleted {num_deleted} lines (change_id: {change_id}). Diff snippet:\n{diff_snippet}"
|
|
||||||
|
|
||||||
|
except ToolError as e:
|
||||||
|
# Handle errors raised by utility functions (expected errors)
|
||||||
|
return handle_tool_error(coder, tool_name, e, add_traceback=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
coder.io.tool_error(f"Error in DeleteBlock: {str(e)}\n{traceback.format_exc()}")
|
# Handle unexpected errors
|
||||||
return f"Error: {str(e)}"
|
return handle_tool_error(coder, tool_name, e)
|
||||||
|
|
|
@ -1,133 +1,66 @@
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
from .tool_utils import (
|
||||||
|
ToolError,
|
||||||
|
validate_file_for_edit,
|
||||||
|
find_pattern_indices,
|
||||||
|
select_occurrence_index,
|
||||||
|
apply_change,
|
||||||
|
handle_tool_error,
|
||||||
|
format_tool_result,
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_insert_block(coder, file_path, content, after_pattern=None, before_pattern=None, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
def _execute_insert_block(coder, file_path, content, after_pattern=None, before_pattern=None, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
||||||
"""
|
"""
|
||||||
Insert a block of text after or before a specified pattern.
|
Insert a block of text after or before a specified pattern using utility functions.
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- coder: The Coder instance
|
|
||||||
- file_path: Path to the file to modify
|
|
||||||
- content: Text block to insert
|
|
||||||
- after_pattern: Pattern after which to insert the block (line containing this pattern) - specify one of after/before
|
|
||||||
- before_pattern: Pattern before which to insert the block (line containing this pattern) - specify one of after/before
|
|
||||||
- near_context: Optional text nearby to help locate the correct instance of the pattern
|
|
||||||
- occurrence: Which occurrence of the pattern to use (1-based index, or -1 for last)
|
|
||||||
- change_id: Optional ID for tracking the change
|
|
||||||
- dry_run: If True, simulate the change without modifying the file
|
|
||||||
|
|
||||||
Returns a result message.
|
|
||||||
"""
|
"""
|
||||||
|
tool_name = "InsertBlock"
|
||||||
try:
|
try:
|
||||||
# Get absolute file path
|
# 1. Validate parameters
|
||||||
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 (Fixes Point 3: Stale Reads)
|
|
||||||
file_content = coder.io.read_text(abs_path)
|
|
||||||
if file_content is None:
|
|
||||||
# Provide more specific error (Improves Point 4)
|
|
||||||
coder.io.tool_error(f"Could not read file '{file_path}' before InsertBlock operation.")
|
|
||||||
return f"Error: Could not read file '{file_path}'"
|
|
||||||
|
|
||||||
# Validate we have either after_pattern or before_pattern, but not both
|
|
||||||
if after_pattern and before_pattern:
|
if after_pattern and before_pattern:
|
||||||
coder.io.tool_error("Cannot specify both after_pattern and before_pattern")
|
raise ToolError("Cannot specify both after_pattern and before_pattern")
|
||||||
return "Error: Cannot specify both after_pattern and before_pattern"
|
|
||||||
if not after_pattern and not before_pattern:
|
if not after_pattern and not before_pattern:
|
||||||
coder.io.tool_error("Must specify either after_pattern or before_pattern")
|
raise ToolError("Must specify either after_pattern or before_pattern")
|
||||||
return "Error: Must specify either after_pattern or before_pattern"
|
|
||||||
|
|
||||||
# Split into lines for easier handling
|
# 2. Validate file and get content
|
||||||
lines = file_content.splitlines()
|
abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
||||||
original_content = file_content
|
lines = original_content.splitlines()
|
||||||
|
|
||||||
# Find occurrences of the pattern (either after_pattern or before_pattern)
|
# 3. Find the target line index
|
||||||
pattern = after_pattern if after_pattern else before_pattern
|
pattern = after_pattern if after_pattern else before_pattern
|
||||||
pattern_type = "after" if after_pattern else "before"
|
pattern_type = "after" if after_pattern else "before"
|
||||||
|
pattern_desc = f"Pattern '{pattern}'"
|
||||||
# Find line indices containing the pattern
|
|
||||||
pattern_line_indices = []
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if pattern in line:
|
|
||||||
# If near_context is provided, check if it's nearby
|
|
||||||
if near_context:
|
if near_context:
|
||||||
context_window_start = max(0, i - 5) # Check 5 lines before/after
|
pattern_desc += f" near context '{near_context}'"
|
||||||
context_window_end = min(len(lines), i + 6)
|
|
||||||
context_block = "\n".join(lines[context_window_start:context_window_end])
|
|
||||||
if near_context in context_block:
|
|
||||||
pattern_line_indices.append(i)
|
|
||||||
else:
|
|
||||||
pattern_line_indices.append(i)
|
|
||||||
|
|
||||||
if not pattern_line_indices:
|
pattern_line_indices = find_pattern_indices(lines, pattern, near_context)
|
||||||
err_msg = f"Pattern '{pattern}' not found"
|
target_line_idx = select_occurrence_index(pattern_line_indices, occurrence, pattern_desc)
|
||||||
if near_context: err_msg += f" near context '{near_context}'"
|
|
||||||
err_msg += f" in file '{file_path}'."
|
|
||||||
coder.io.tool_error(err_msg)
|
|
||||||
return f"Error: {err_msg}" # Improve Point 4
|
|
||||||
|
|
||||||
# Select the occurrence (Implements Point 5)
|
|
||||||
num_occurrences = len(pattern_line_indices)
|
|
||||||
try:
|
|
||||||
occurrence = int(occurrence) # Ensure occurrence is an integer
|
|
||||||
if occurrence == -1: # Last occurrence
|
|
||||||
target_idx = num_occurrences - 1
|
|
||||||
elif occurrence > 0 and occurrence <= num_occurrences:
|
|
||||||
target_idx = occurrence - 1 # Convert 1-based to 0-based
|
|
||||||
else:
|
|
||||||
err_msg = f"Occurrence number {occurrence} is out of range for pattern '{pattern}'. Found {num_occurrences} occurrences"
|
|
||||||
if near_context: err_msg += f" near '{near_context}'"
|
|
||||||
err_msg += f" in '{file_path}'."
|
|
||||||
coder.io.tool_error(err_msg)
|
|
||||||
return f"Error: {err_msg}" # Improve Point 4
|
|
||||||
except ValueError:
|
|
||||||
coder.io.tool_error(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
|
||||||
return f"Error: Invalid occurrence value '{occurrence}'"
|
|
||||||
|
|
||||||
# Determine the final insertion line index
|
# Determine the final insertion line index
|
||||||
insertion_line_idx = pattern_line_indices[target_idx]
|
insertion_line_idx = target_line_idx
|
||||||
if pattern_type == "after":
|
if pattern_type == "after":
|
||||||
insertion_line_idx += 1 # Insert on the line *after* the matched line
|
insertion_line_idx += 1 # Insert on the line *after* the matched line
|
||||||
# Prepare the content to insert
|
|
||||||
content_lines = content.splitlines()
|
|
||||||
|
|
||||||
# Create the new lines array
|
# 4. Prepare the insertion
|
||||||
|
content_lines = content.splitlines()
|
||||||
new_lines = lines[:insertion_line_idx] + content_lines + lines[insertion_line_idx:]
|
new_lines = lines[:insertion_line_idx] + content_lines + lines[insertion_line_idx:]
|
||||||
new_content = '\n'.join(new_lines) # Use '\n' to match io.write_text behavior
|
new_content = '\n'.join(new_lines)
|
||||||
|
|
||||||
if original_content == new_content:
|
if original_content == new_content:
|
||||||
coder.io.tool_warning(f"No changes made: insertion would not change file")
|
coder.io.tool_warning(f"No changes made: insertion would not change file")
|
||||||
return f"Warning: No changes made (insertion would not change file)"
|
return f"Warning: No changes made (insertion would not change file)"
|
||||||
|
|
||||||
# Generate diff for feedback
|
# 5. Generate diff for feedback
|
||||||
diff_snippet = coder._generate_diff_snippet_insert(original_content, insertion_line_idx, content_lines)
|
diff_snippet = coder._generate_diff_snippet_insert(original_content, insertion_line_idx, content_lines)
|
||||||
|
num_occurrences = len(pattern_line_indices)
|
||||||
# Handle dry run (Implements Point 6)
|
|
||||||
if dry_run:
|
|
||||||
occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
|
occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
|
||||||
coder.io.tool_output(f"Dry run: Would insert block {pattern_type} {occurrence_str}pattern '{pattern}' in {file_path}")
|
|
||||||
return f"Dry run: Would insert block. Diff snippet:\n{diff_snippet}"
|
|
||||||
|
|
||||||
# --- Apply Change (Not dry run) ---
|
# 6. Handle dry run
|
||||||
coder.io.write_text(abs_path, new_content)
|
if dry_run:
|
||||||
|
dry_run_message = f"Dry run: Would insert block {pattern_type} {occurrence_str}pattern '{pattern}' in {file_path} at line {insertion_line_idx + 1}."
|
||||||
|
return format_tool_result(coder, tool_name, "", dry_run=True, dry_run_message=dry_run_message, diff_snippet=diff_snippet)
|
||||||
|
|
||||||
# Track the change
|
# 7. Apply Change (Not dry run)
|
||||||
try:
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'insertion_line_idx': insertion_line_idx,
|
'insertion_line_idx': insertion_line_idx,
|
||||||
'after_pattern': after_pattern,
|
'after_pattern': after_pattern,
|
||||||
|
@ -136,24 +69,19 @@ def _execute_insert_block(coder, file_path, content, after_pattern=None, before_
|
||||||
'occurrence': occurrence,
|
'occurrence': occurrence,
|
||||||
'content': content
|
'content': content
|
||||||
}
|
}
|
||||||
change_id = coder.change_tracker.track_change(
|
final_change_id = apply_change(
|
||||||
file_path=rel_path,
|
coder, abs_path, rel_path, original_content, new_content, 'insertblock', metadata, change_id
|
||||||
change_type='insertblock',
|
|
||||||
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 InsertBlock: {track_e}")
|
|
||||||
change_id = "TRACKING_FAILED"
|
|
||||||
|
|
||||||
coder.aider_edited_files.add(rel_path)
|
# 8. Format and return result
|
||||||
|
success_message = f"Inserted block {pattern_type} {occurrence_str}pattern in {file_path} at line {insertion_line_idx + 1}"
|
||||||
|
return format_tool_result(
|
||||||
|
coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
|
||||||
|
)
|
||||||
|
|
||||||
# Improve feedback (Point 5 & 6)
|
except ToolError as e:
|
||||||
occurrence_str = f"occurrence {occurrence} of " if num_occurrences > 1 else ""
|
# Handle errors raised by utility functions (expected errors)
|
||||||
coder.io.tool_output(f"✅ Inserted block {pattern_type} {occurrence_str}pattern in {file_path} (change_id: {change_id})")
|
return handle_tool_error(coder, tool_name, e, add_traceback=False)
|
||||||
return f"Successfully inserted block (change_id: {change_id}). Diff snippet:\n{diff_snippet}"
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
coder.io.tool_error(f"Error in InsertBlock: {str(e)}\n{traceback.format_exc()}") # Add traceback
|
coder.io.tool_error(f"Error in InsertBlock: {str(e)}\n{traceback.format_exc()}") # Add traceback
|
||||||
|
|
|
@ -1,95 +1,65 @@
|
||||||
import os
|
|
||||||
import traceback
|
import traceback
|
||||||
|
from .tool_utils import (
|
||||||
|
ToolError,
|
||||||
|
validate_file_for_edit,
|
||||||
|
apply_change,
|
||||||
|
handle_tool_error,
|
||||||
|
format_tool_result,
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_replace_all(coder, file_path, find_text, replace_text, change_id=None, dry_run=False):
|
def _execute_replace_all(coder, file_path, find_text, replace_text, change_id=None, dry_run=False):
|
||||||
"""
|
"""
|
||||||
Replace all occurrences of text in a file.
|
Replace all occurrences of text in a file using utility functions.
|
||||||
|
|
||||||
Parameters:
|
|
||||||
- coder: The Coder instance
|
|
||||||
- file_path: Path to the file to modify
|
|
||||||
- find_text: Text to find and replace
|
|
||||||
- replace_text: Text to replace it with
|
|
||||||
- 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
|
# Get absolute file path
|
||||||
abs_path = coder.abs_root_path(file_path)
|
abs_path = coder.abs_root_path(file_path)
|
||||||
rel_path = coder.get_rel_fname(abs_path)
|
rel_path = coder.get_rel_fname(abs_path)
|
||||||
|
tool_name = "ReplaceAll"
|
||||||
|
try:
|
||||||
|
# 1. Validate file and get content
|
||||||
|
abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
||||||
|
|
||||||
# Check if file exists
|
# 2. Count occurrences
|
||||||
if not os.path.isfile(abs_path):
|
count = original_content.count(find_text)
|
||||||
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
|
|
||||||
content = coder.io.read_text(abs_path)
|
|
||||||
if content is None:
|
|
||||||
coder.io.tool_error(f"Could not read file '{file_path}' before ReplaceAll operation.")
|
|
||||||
return f"Error: Could not read file '{file_path}'"
|
|
||||||
|
|
||||||
# Count occurrences
|
|
||||||
count = content.count(find_text)
|
|
||||||
if count == 0:
|
if count == 0:
|
||||||
coder.io.tool_warning(f"Text '{find_text}' not found in file")
|
coder.io.tool_warning(f"Text '{find_text}' not found in file '{file_path}'")
|
||||||
return f"Warning: Text not found in file"
|
return f"Warning: Text not found in file"
|
||||||
|
|
||||||
# Perform the replacement
|
# 3. Perform the replacement
|
||||||
original_content = content
|
new_content = original_content.replace(find_text, replace_text)
|
||||||
new_content = content.replace(find_text, replace_text)
|
|
||||||
|
|
||||||
if original_content == new_content:
|
if original_content == new_content:
|
||||||
coder.io.tool_warning(f"No changes made: replacement text is identical to original")
|
coder.io.tool_warning(f"No changes made: replacement text is identical to original")
|
||||||
return f"Warning: No changes made (replacement identical to original)"
|
return f"Warning: No changes made (replacement identical to original)"
|
||||||
|
|
||||||
# Generate diff for feedback (more comprehensive for ReplaceAll)
|
# 4. Generate diff for feedback
|
||||||
|
# Note: _generate_diff_chunks is currently on the Coder class
|
||||||
diff_examples = coder._generate_diff_chunks(original_content, find_text, replace_text)
|
diff_examples = coder._generate_diff_chunks(original_content, find_text, replace_text)
|
||||||
|
|
||||||
# Handle dry run
|
# 5. Handle dry run
|
||||||
if dry_run:
|
if dry_run:
|
||||||
coder.io.tool_output(f"Dry run: Would replace {count} occurrences of '{find_text}' in {file_path}")
|
dry_run_message = f"Dry run: Would replace {count} occurrences of '{find_text}' in {file_path}."
|
||||||
return f"Dry run: Would replace {count} occurrences. Diff examples:\n{diff_examples}"
|
return format_tool_result(coder, tool_name, "", dry_run=True, dry_run_message=dry_run_message, diff_snippet=diff_examples)
|
||||||
|
|
||||||
# --- Apply Change (Not dry run) ---
|
# 6. Apply Change (Not dry run)
|
||||||
coder.io.write_text(abs_path, new_content)
|
|
||||||
|
|
||||||
# Track the change
|
|
||||||
try:
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'find_text': find_text,
|
'find_text': find_text,
|
||||||
'replace_text': replace_text,
|
'replace_text': replace_text,
|
||||||
'occurrences': count
|
'occurrences': count
|
||||||
}
|
}
|
||||||
change_id = coder.change_tracker.track_change(
|
final_change_id = apply_change(
|
||||||
file_path=rel_path,
|
coder, abs_path, rel_path, original_content, new_content, 'replaceall', metadata, change_id
|
||||||
change_type='replaceall',
|
|
||||||
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 ReplaceAll: {track_e}")
|
|
||||||
change_id = "TRACKING_FAILED"
|
|
||||||
|
|
||||||
coder.aider_edited_files.add(rel_path)
|
# 7. Format and return result
|
||||||
|
success_message = f"Replaced {count} occurrences in {file_path}"
|
||||||
# Improve feedback
|
return format_tool_result(
|
||||||
coder.io.tool_output(f"✅ Replaced {count} occurrences in {file_path} (change_id: {change_id})")
|
coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_examples
|
||||||
return f"Successfully replaced {count} occurrences (change_id: {change_id}). Diff examples:\n{diff_examples}"
|
)
|
||||||
|
|
||||||
|
except ToolError as e:
|
||||||
|
# Handle errors raised by utility functions
|
||||||
|
return handle_tool_error(coder, tool_name, e, add_traceback=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
coder.io.tool_error(f"Error in ReplaceAll: {str(e)}\n{traceback.format_exc()}")
|
# Handle unexpected errors
|
||||||
return f"Error: {str(e)}"
|
return handle_tool_error(coder, tool_name, e)
|
||||||
|
|
|
@ -1,99 +1,71 @@
|
||||||
import os
|
|
||||||
import traceback
|
import traceback
|
||||||
|
from .tool_utils import (
|
||||||
|
ToolError,
|
||||||
|
validate_file_for_edit,
|
||||||
|
apply_change,
|
||||||
|
handle_tool_error,
|
||||||
|
format_tool_result,
|
||||||
|
)
|
||||||
|
|
||||||
def _execute_replace_text(coder, file_path, find_text, replace_text, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
def _execute_replace_text(coder, file_path, find_text, replace_text, near_context=None, occurrence=1, change_id=None, dry_run=False):
|
||||||
"""
|
"""
|
||||||
Replace specific text with new text, optionally using nearby context for disambiguation.
|
Replace specific text with new text, optionally using nearby context for disambiguation.
|
||||||
|
Uses utility functions for validation, finding occurrences, and applying changes.
|
||||||
Parameters:
|
|
||||||
- coder: The Coder instance
|
|
||||||
- file_path: Path to the file to modify
|
|
||||||
- find_text: Text to find and replace
|
|
||||||
- replace_text: Text to replace it with
|
|
||||||
- near_context: Optional text nearby to help locate the correct instance
|
|
||||||
- occurrence: Which occurrence to replace (1-based index, or -1 for last)
|
|
||||||
- change_id: Optional ID for tracking the change
|
|
||||||
- dry_run: If True, simulate the change without modifying the file
|
|
||||||
|
|
||||||
Returns a result message.
|
|
||||||
"""
|
"""
|
||||||
|
tool_name = "ReplaceText"
|
||||||
try:
|
try:
|
||||||
# Get absolute file path
|
# 1. Validate file and get content
|
||||||
abs_path = coder.abs_root_path(file_path)
|
abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
||||||
rel_path = coder.get_rel_fname(abs_path)
|
|
||||||
|
|
||||||
# Check if file exists
|
# 2. Find occurrences using helper function
|
||||||
if not os.path.isfile(abs_path):
|
# Note: _find_occurrences is currently on the Coder class, not in tool_utils
|
||||||
coder.io.tool_error(f"File '{file_path}' not found")
|
occurrences = coder._find_occurrences(original_content, find_text, near_context)
|
||||||
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
|
|
||||||
content = coder.io.read_text(abs_path)
|
|
||||||
if content is None:
|
|
||||||
coder.io.tool_error(f"Could not read file '{file_path}' before ReplaceText operation.")
|
|
||||||
return f"Error: Could not read file '{file_path}'"
|
|
||||||
|
|
||||||
# Find occurrences using helper function (assuming _find_occurrences is available on coder)
|
|
||||||
occurrences = coder._find_occurrences(content, find_text, near_context)
|
|
||||||
|
|
||||||
if not occurrences:
|
if not occurrences:
|
||||||
err_msg = f"Text '{find_text}' not found"
|
err_msg = f"Text '{find_text}' not found"
|
||||||
if near_context:
|
if near_context:
|
||||||
err_msg += f" near context '{near_context}'"
|
err_msg += f" near context '{near_context}'"
|
||||||
err_msg += f" in file '{file_path}'."
|
err_msg += f" in file '{file_path}'."
|
||||||
coder.io.tool_error(err_msg)
|
raise ToolError(err_msg)
|
||||||
return f"Error: {err_msg}"
|
|
||||||
|
|
||||||
# Select the occurrence
|
# 3. Select the occurrence index
|
||||||
num_occurrences = len(occurrences)
|
num_occurrences = len(occurrences)
|
||||||
try:
|
try:
|
||||||
occurrence = int(occurrence)
|
occurrence = int(occurrence)
|
||||||
if occurrence == -1:
|
if occurrence == -1:
|
||||||
|
if num_occurrences == 0:
|
||||||
|
raise ToolError(f"Text '{find_text}' not found, cannot select last occurrence.")
|
||||||
target_idx = num_occurrences - 1
|
target_idx = num_occurrences - 1
|
||||||
elif occurrence > 0 and occurrence <= num_occurrences:
|
elif 1 <= occurrence <= num_occurrences:
|
||||||
target_idx = occurrence - 1
|
target_idx = occurrence - 1 # Convert 1-based to 0-based
|
||||||
else:
|
else:
|
||||||
err_msg = f"Occurrence number {occurrence} is out of range. Found {num_occurrences} occurrences of '{find_text}'"
|
err_msg = f"Occurrence number {occurrence} is out of range. Found {num_occurrences} occurrences of '{find_text}'"
|
||||||
if near_context: err_msg += f" near '{near_context}'"
|
if near_context: err_msg += f" near '{near_context}'"
|
||||||
err_msg += f" in '{file_path}'."
|
err_msg += f" in '{file_path}'."
|
||||||
coder.io.tool_error(err_msg)
|
raise ToolError(err_msg)
|
||||||
return f"Error: {err_msg}"
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
coder.io.tool_error(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
||||||
return f"Error: Invalid occurrence value '{occurrence}'"
|
|
||||||
|
|
||||||
start_index = occurrences[target_idx]
|
start_index = occurrences[target_idx]
|
||||||
|
|
||||||
# Perform the replacement
|
# 4. Perform the replacement
|
||||||
original_content = content
|
new_content = original_content[:start_index] + replace_text + original_content[start_index + len(find_text):]
|
||||||
new_content = content[:start_index] + replace_text + content[start_index + len(find_text):]
|
|
||||||
|
|
||||||
if original_content == new_content:
|
if original_content == new_content:
|
||||||
coder.io.tool_warning(f"No changes made: replacement text is identical to original")
|
coder.io.tool_warning(f"No changes made: replacement text is identical to original")
|
||||||
return f"Warning: No changes made (replacement identical to original)"
|
return f"Warning: No changes made (replacement identical to original)"
|
||||||
|
|
||||||
# Generate diff for feedback (assuming _generate_diff_snippet is available on coder)
|
# 5. Generate diff for feedback
|
||||||
diff_example = coder._generate_diff_snippet(original_content, start_index, len(find_text), replace_text)
|
# Note: _generate_diff_snippet is currently on the Coder class
|
||||||
|
diff_snippet = coder._generate_diff_snippet(original_content, start_index, len(find_text), replace_text)
|
||||||
|
occurrence_str = f"occurrence {occurrence}" if num_occurrences > 1 else "text"
|
||||||
|
|
||||||
# Handle dry run
|
# 6. Handle dry run
|
||||||
if dry_run:
|
if dry_run:
|
||||||
coder.io.tool_output(f"Dry run: Would replace occurrence {occurrence} of '{find_text}' in {file_path}")
|
dry_run_message = f"Dry run: Would replace {occurrence_str} of '{find_text}' in {file_path}."
|
||||||
return f"Dry run: Would replace text (occurrence {occurrence}). Diff snippet:\n{diff_example}"
|
return format_tool_result(coder, tool_name, "", dry_run=True, dry_run_message=dry_run_message, diff_snippet=diff_snippet)
|
||||||
|
|
||||||
# --- Apply Change (Not dry run) ---
|
# 7. Apply Change (Not dry run)
|
||||||
coder.io.write_text(abs_path, new_content)
|
|
||||||
|
|
||||||
# Track the change
|
|
||||||
try:
|
|
||||||
metadata = {
|
metadata = {
|
||||||
'start_index': start_index,
|
'start_index': start_index,
|
||||||
'find_text': find_text,
|
'find_text': find_text,
|
||||||
|
@ -101,25 +73,19 @@ def _execute_replace_text(coder, file_path, find_text, replace_text, near_contex
|
||||||
'near_context': near_context,
|
'near_context': near_context,
|
||||||
'occurrence': occurrence
|
'occurrence': occurrence
|
||||||
}
|
}
|
||||||
change_id = coder.change_tracker.track_change(
|
final_change_id = apply_change(
|
||||||
file_path=rel_path,
|
coder, abs_path, rel_path, original_content, new_content, 'replacetext', metadata, change_id
|
||||||
change_type='replacetext',
|
|
||||||
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 ReplaceText: {track_e}")
|
|
||||||
change_id = "TRACKING_FAILED"
|
|
||||||
|
|
||||||
coder.aider_edited_files.add(rel_path)
|
# 8. Format and return result
|
||||||
|
success_message = f"Replaced {occurrence_str} in {file_path}"
|
||||||
# Improve feedback
|
return format_tool_result(
|
||||||
occurrence_str = f"occurrence {occurrence}" if num_occurrences > 1 else "text"
|
coder, tool_name, success_message, change_id=final_change_id, diff_snippet=diff_snippet
|
||||||
coder.io.tool_output(f"✅ Replaced {occurrence_str} in {file_path} (change_id: {change_id})")
|
)
|
||||||
return f"Successfully replaced {occurrence_str} (change_id: {change_id}). Diff snippet:\n{diff_example}"
|
|
||||||
|
|
||||||
|
except ToolError as e:
|
||||||
|
# Handle errors raised by utility functions or explicitly raised here
|
||||||
|
return handle_tool_error(coder, tool_name, e, add_traceback=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
coder.io.tool_error(f"Error in ReplaceText: {str(e)}\n{traceback.format_exc()}")
|
# Handle unexpected errors
|
||||||
return f"Error: {str(e)}"
|
return handle_tool_error(coder, tool_name, e)
|
||||||
|
|
192
aider/tools/tool_utils.py
Normal file
192
aider/tools/tool_utils.py
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
class ToolError(Exception):
|
||||||
|
"""Custom exception for tool-specific errors that should be reported to the LLM."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def resolve_paths(coder, file_path):
|
||||||
|
"""Resolves absolute and relative paths for a given file path."""
|
||||||
|
try:
|
||||||
|
abs_path = coder.abs_root_path(file_path)
|
||||||
|
rel_path = coder.get_rel_fname(abs_path)
|
||||||
|
return abs_path, rel_path
|
||||||
|
except Exception as e:
|
||||||
|
# Wrap unexpected errors during path resolution
|
||||||
|
raise ToolError(f"Error resolving path '{file_path}': {e}")
|
||||||
|
|
||||||
|
def validate_file_for_edit(coder, file_path):
|
||||||
|
"""
|
||||||
|
Validates if a file exists, is in context, and is editable.
|
||||||
|
Reads and returns original content if valid.
|
||||||
|
Raises ToolError on failure.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (absolute_path, relative_path, original_content)
|
||||||
|
"""
|
||||||
|
abs_path, rel_path = resolve_paths(coder, file_path)
|
||||||
|
|
||||||
|
if not os.path.isfile(abs_path):
|
||||||
|
raise ToolError(f"File '{file_path}' not found")
|
||||||
|
|
||||||
|
if abs_path not in coder.abs_fnames:
|
||||||
|
if abs_path in coder.abs_read_only_fnames:
|
||||||
|
raise ToolError(f"File '{file_path}' is read-only. Use MakeEditable first.")
|
||||||
|
else:
|
||||||
|
# File exists but is not in context at all
|
||||||
|
raise ToolError(f"File '{file_path}' not in context. Use View or MakeEditable first.")
|
||||||
|
|
||||||
|
# Reread content immediately before potential modification
|
||||||
|
content = coder.io.read_text(abs_path)
|
||||||
|
if content is None:
|
||||||
|
# This indicates an issue reading a file we know exists and is in context
|
||||||
|
coder.io.tool_error(f"Internal error: Could not read file '{file_path}' which should be accessible.")
|
||||||
|
raise ToolError(f"Could not read file '{file_path}'")
|
||||||
|
|
||||||
|
return abs_path, rel_path, content
|
||||||
|
|
||||||
|
def find_pattern_indices(lines, pattern, near_context=None):
|
||||||
|
"""Finds all line indices matching a pattern, optionally filtered by context."""
|
||||||
|
indices = []
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if pattern in line:
|
||||||
|
if near_context:
|
||||||
|
# Check if near_context is within a window around the match
|
||||||
|
context_window_start = max(0, i - 5) # Check 5 lines before/after
|
||||||
|
context_window_end = min(len(lines), i + 6)
|
||||||
|
context_block = "\n".join(lines[context_window_start:context_window_end])
|
||||||
|
if near_context in context_block:
|
||||||
|
indices.append(i)
|
||||||
|
else:
|
||||||
|
indices.append(i)
|
||||||
|
return indices
|
||||||
|
|
||||||
|
def select_occurrence_index(indices, occurrence, pattern_desc="Pattern"):
|
||||||
|
"""
|
||||||
|
Selects the target 0-based index from a list of indices based on the 1-based occurrence parameter.
|
||||||
|
Raises ToolError if the pattern wasn't found or the occurrence is invalid.
|
||||||
|
"""
|
||||||
|
num_occurrences = len(indices)
|
||||||
|
if not indices:
|
||||||
|
raise ToolError(f"{pattern_desc} not found")
|
||||||
|
|
||||||
|
try:
|
||||||
|
occurrence = int(occurrence) # Ensure occurrence is an integer
|
||||||
|
if occurrence == -1: # Last occurrence
|
||||||
|
if num_occurrences == 0:
|
||||||
|
raise ToolError(f"{pattern_desc} not found, cannot select last occurrence.")
|
||||||
|
target_idx = num_occurrences - 1
|
||||||
|
elif 1 <= occurrence <= num_occurrences:
|
||||||
|
target_idx = occurrence - 1 # Convert 1-based to 0-based
|
||||||
|
else:
|
||||||
|
raise ToolError(f"Occurrence number {occurrence} is out of range for {pattern_desc}. Found {num_occurrences} occurrences.")
|
||||||
|
except ValueError:
|
||||||
|
raise ToolError(f"Invalid occurrence value: '{occurrence}'. Must be an integer.")
|
||||||
|
|
||||||
|
return indices[target_idx]
|
||||||
|
|
||||||
|
def determine_line_range(lines, start_pattern_line_index, end_pattern=None, line_count=None, pattern_desc="Block"):
|
||||||
|
"""
|
||||||
|
Determines the end line index based on end_pattern or line_count.
|
||||||
|
Raises ToolError if end_pattern is not found or line_count is invalid.
|
||||||
|
"""
|
||||||
|
start_line = start_pattern_line_index
|
||||||
|
end_line = -1
|
||||||
|
|
||||||
|
if end_pattern and line_count:
|
||||||
|
raise ToolError("Cannot specify both end_pattern and line_count")
|
||||||
|
|
||||||
|
if end_pattern:
|
||||||
|
found_end = False
|
||||||
|
# Search from the start_line onwards for the end_pattern
|
||||||
|
for i in range(start_line, len(lines)):
|
||||||
|
if end_pattern in lines[i]:
|
||||||
|
end_line = i
|
||||||
|
found_end = True
|
||||||
|
break
|
||||||
|
if not found_end:
|
||||||
|
raise ToolError(f"End pattern '{end_pattern}' not found after start pattern on line {start_line + 1}")
|
||||||
|
elif line_count:
|
||||||
|
try:
|
||||||
|
line_count = int(line_count)
|
||||||
|
if line_count <= 0:
|
||||||
|
raise ValueError("Line count must be positive")
|
||||||
|
# Calculate end line index, ensuring it doesn't exceed file bounds
|
||||||
|
end_line = min(start_line + line_count - 1, len(lines) - 1)
|
||||||
|
except ValueError:
|
||||||
|
raise ToolError(f"Invalid line_count value: '{line_count}'. Must be a positive integer.")
|
||||||
|
else:
|
||||||
|
# If neither end_pattern nor line_count is given, the range is just the start line
|
||||||
|
end_line = start_line
|
||||||
|
|
||||||
|
return start_line, end_line
|
||||||
|
|
||||||
|
|
||||||
|
def apply_change(coder, abs_path, rel_path, original_content, new_content, change_type, metadata, change_id=None):
|
||||||
|
"""
|
||||||
|
Writes the new content, tracks the change, and updates coder state.
|
||||||
|
Returns the final change ID. Raises ToolError on tracking failure.
|
||||||
|
"""
|
||||||
|
coder.io.write_text(abs_path, new_content)
|
||||||
|
try:
|
||||||
|
final_change_id = coder.change_tracker.track_change(
|
||||||
|
file_path=rel_path,
|
||||||
|
change_type=change_type,
|
||||||
|
original_content=original_content,
|
||||||
|
new_content=new_content,
|
||||||
|
metadata=metadata,
|
||||||
|
change_id=change_id
|
||||||
|
)
|
||||||
|
except Exception as track_e:
|
||||||
|
# Log the error but also raise ToolError to inform the LLM
|
||||||
|
coder.io.tool_error(f"Error tracking change for {change_type}: {track_e}")
|
||||||
|
raise ToolError(f"Failed to track change: {track_e}")
|
||||||
|
|
||||||
|
coder.aider_edited_files.add(rel_path)
|
||||||
|
return final_change_id
|
||||||
|
|
||||||
|
|
||||||
|
def handle_tool_error(coder, tool_name, e, add_traceback=True):
|
||||||
|
"""Logs tool errors and returns a formatted error message for the LLM."""
|
||||||
|
error_message = f"Error in {tool_name}: {str(e)}"
|
||||||
|
if add_traceback:
|
||||||
|
error_message += f"\n{traceback.format_exc()}"
|
||||||
|
coder.io.tool_error(error_message)
|
||||||
|
# Return only the core error message to the LLM for brevity
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
def format_tool_result(coder, tool_name, success_message, change_id=None, diff_snippet=None, dry_run=False, dry_run_message=None):
|
||||||
|
"""Formats the result message for tool execution."""
|
||||||
|
if dry_run:
|
||||||
|
full_message = dry_run_message or f"Dry run: Would execute {tool_name}."
|
||||||
|
if diff_snippet:
|
||||||
|
full_message += f" Diff snippet:\n{diff_snippet}"
|
||||||
|
coder.io.tool_output(full_message) # Log the dry run action
|
||||||
|
return full_message
|
||||||
|
else:
|
||||||
|
# Use the provided success message, potentially adding change_id and diff
|
||||||
|
full_message = f"✅ {success_message}"
|
||||||
|
if change_id:
|
||||||
|
full_message += f" (change_id: {change_id})"
|
||||||
|
coder.io.tool_output(full_message) # Log the success action
|
||||||
|
|
||||||
|
result_for_llm = f"Successfully executed {tool_name}."
|
||||||
|
if change_id:
|
||||||
|
result_for_llm += f" Change ID: {change_id}."
|
||||||
|
if diff_snippet:
|
||||||
|
result_for_llm += f" Diff snippet:\n{diff_snippet}"
|
||||||
|
return result_for_llm
|
||||||
|
|
||||||
|
# Example usage within a hypothetical tool:
|
||||||
|
# try:
|
||||||
|
# abs_path, rel_path, original_content = validate_file_for_edit(coder, file_path)
|
||||||
|
# # ... tool specific logic to determine new_content and metadata ...
|
||||||
|
# if dry_run:
|
||||||
|
# return format_tool_result(coder, "MyTool", "", dry_run=True, diff_snippet=diff)
|
||||||
|
#
|
||||||
|
# change_id = apply_change(coder, abs_path, rel_path, original_content, new_content, 'mytool', metadata)
|
||||||
|
# return format_tool_result(coder, "MyTool", f"Applied change to {file_path}", change_id=change_id, diff_snippet=diff)
|
||||||
|
# except ToolError as e:
|
||||||
|
# return handle_tool_error(coder, "MyTool", e, add_traceback=False) # Don't need traceback for ToolErrors
|
||||||
|
# except Exception as e:
|
||||||
|
# return handle_tool_error(coder, "MyTool", e)
|
Loading…
Add table
Add a link
Reference in a new issue