mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-08 21:55:00 +00:00
Add a tool to extract lines from a file into others
This commit is contained in:
parent
643442019a
commit
dea5bd54f2
2 changed files with 242 additions and 1 deletions
|
@ -701,7 +701,25 @@ class NavigatorCoder(Coder):
|
|||
limit = params.get('limit', 10)
|
||||
|
||||
result_message = self._execute_list_changes(file_path, limit)
|
||||
|
||||
|
||||
elif norm_tool_name == 'extractlines':
|
||||
source_file_path = params.get('source_file_path')
|
||||
target_file_path = params.get('target_file_path')
|
||||
start_pattern = params.get('start_pattern')
|
||||
end_pattern = params.get('end_pattern')
|
||||
line_count = params.get('line_count')
|
||||
near_context = params.get('near_context')
|
||||
occurrence = params.get('occurrence', 1)
|
||||
dry_run = params.get('dry_run', False)
|
||||
|
||||
if source_file_path and target_file_path and start_pattern:
|
||||
result_message = self._execute_extract_lines(
|
||||
source_file_path, target_file_path, start_pattern, end_pattern,
|
||||
line_count, near_context, occurrence, dry_run
|
||||
)
|
||||
else:
|
||||
result_message = "Error: Missing required parameters for ExtractLines (source_file_path, target_file_path, start_pattern)"
|
||||
|
||||
else:
|
||||
result_message = f"Error: Unknown tool name '{tool_name}'"
|
||||
|
||||
|
@ -2809,6 +2827,225 @@ Just reply with fixed versions of the {blocks} above that failed to match.
|
|||
self.io.tool_error(f"Error in ListChanges: {str(e)}\n{traceback.format_exc()}") # Add traceback
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def _execute_extract_lines(self, source_file_path, target_file_path, start_pattern, end_pattern=None, line_count=None, near_context=None, occurrence=1, dry_run=False):
|
||||
"""
|
||||
Extract a range of lines from a source file and move them to a target file.
|
||||
|
||||
Parameters:
|
||||
- source_file_path: Path to the file to extract lines from
|
||||
- target_file_path: Path to the file to append extracted lines to (will be created if needed)
|
||||
- start_pattern: Pattern marking the start of the block to extract
|
||||
- end_pattern: Optional pattern marking the end of the block
|
||||
- line_count: Optional number of lines to extract (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)
|
||||
- dry_run: If True, simulate the change without modifying files
|
||||
|
||||
Returns a result message.
|
||||
"""
|
||||
try:
|
||||
# --- Validate Source File ---
|
||||
abs_source_path = self.abs_root_path(source_file_path)
|
||||
rel_source_path = self.get_rel_fname(abs_source_path)
|
||||
|
||||
if not os.path.isfile(abs_source_path):
|
||||
self.io.tool_error(f"Source file '{source_file_path}' not found")
|
||||
return f"Error: Source file not found"
|
||||
|
||||
if abs_source_path not in self.abs_fnames:
|
||||
if abs_source_path in self.abs_read_only_fnames:
|
||||
self.io.tool_error(f"Source file '{source_file_path}' is read-only. Use MakeEditable first.")
|
||||
return f"Error: Source file is read-only. Use MakeEditable first."
|
||||
else:
|
||||
self.io.tool_error(f"Source file '{source_file_path}' not in context")
|
||||
return f"Error: Source file not in context"
|
||||
|
||||
# --- Validate Target File ---
|
||||
abs_target_path = self.abs_root_path(target_file_path)
|
||||
rel_target_path = self.get_rel_fname(abs_target_path)
|
||||
target_exists = os.path.isfile(abs_target_path)
|
||||
target_is_editable = abs_target_path in self.abs_fnames
|
||||
target_is_readonly = abs_target_path in self.abs_read_only_fnames
|
||||
|
||||
if target_exists and not target_is_editable:
|
||||
if target_is_readonly:
|
||||
self.io.tool_error(f"Target file '{target_file_path}' exists but is read-only. Use MakeEditable first.")
|
||||
return f"Error: Target file exists but is read-only. Use MakeEditable first."
|
||||
else:
|
||||
# This case shouldn't happen if file exists, but handle defensively
|
||||
self.io.tool_error(f"Target file '{target_file_path}' exists but is not in context. Add it first.")
|
||||
return f"Error: Target file exists but is not in context."
|
||||
|
||||
# --- Read Source Content ---
|
||||
source_content = self.io.read_text(abs_source_path)
|
||||
if source_content is None:
|
||||
self.io.tool_error(f"Could not read source file '{source_file_path}' before ExtractLines operation.")
|
||||
return f"Error: Could not read source file '{source_file_path}'"
|
||||
|
||||
# --- Find Extraction Range ---
|
||||
if end_pattern and line_count:
|
||||
self.io.tool_error("Cannot specify both end_pattern and line_count")
|
||||
return "Error: Cannot specify both end_pattern and line_count"
|
||||
|
||||
source_lines = source_content.splitlines()
|
||||
original_source_content = source_content
|
||||
|
||||
start_pattern_line_indices = []
|
||||
for i, line in enumerate(source_lines):
|
||||
if start_pattern in line:
|
||||
if near_context:
|
||||
context_window_start = max(0, i - 5)
|
||||
context_window_end = min(len(source_lines), i + 6)
|
||||
context_block = "\n".join(source_lines[context_window_start:context_window_end])
|
||||
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:
|
||||
err_msg = f"Start pattern '{start_pattern}' not found"
|
||||
if near_context: err_msg += f" near context '{near_context}'"
|
||||
err_msg += f" in source file '{source_file_path}'."
|
||||
self.io.tool_error(err_msg)
|
||||
return f"Error: {err_msg}"
|
||||
|
||||
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 '{source_file_path}'."
|
||||
self.io.tool_error(err_msg)
|
||||
return f"Error: {err_msg}"
|
||||
except ValueError:
|
||||
self.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 ""
|
||||
|
||||
end_line = -1
|
||||
if end_pattern:
|
||||
for i in range(start_line, len(source_lines)):
|
||||
if end_pattern in source_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 '{source_file_path}'."
|
||||
self.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(source_lines) - 1)
|
||||
except ValueError:
|
||||
self.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 # Extract just the start line if no end specified
|
||||
|
||||
# --- Prepare Content Changes ---
|
||||
extracted_lines = source_lines[start_line:end_line+1]
|
||||
new_source_lines = source_lines[:start_line] + source_lines[end_line+1:]
|
||||
new_source_content = '\n'.join(new_source_lines)
|
||||
|
||||
target_content = ""
|
||||
if target_exists:
|
||||
target_content = self.io.read_text(abs_target_path)
|
||||
if target_content is None:
|
||||
self.io.tool_error(f"Could not read existing target file '{target_file_path}'.")
|
||||
return f"Error: Could not read target file '{target_file_path}'"
|
||||
original_target_content = target_content # For tracking
|
||||
|
||||
# Append extracted lines to target content, ensuring a newline if target wasn't empty
|
||||
extracted_block = '\n'.join(extracted_lines)
|
||||
if target_content and not target_content.endswith('\n'):
|
||||
target_content += '\n' # Add newline before appending if needed
|
||||
new_target_content = target_content + extracted_block
|
||||
|
||||
# --- Generate Diffs ---
|
||||
source_diff_snippet = self._generate_diff_snippet_delete(original_source_content, start_line, end_line)
|
||||
target_insertion_line = len(target_content.splitlines()) if target_content else 0
|
||||
target_diff_snippet = self._generate_diff_snippet_insert(original_target_content, target_insertion_line, extracted_lines)
|
||||
|
||||
# --- Handle Dry Run ---
|
||||
# --- Handle Dry Run ---
|
||||
if dry_run:
|
||||
num_extracted = end_line - start_line + 1
|
||||
target_action = "append to" if target_exists else "create"
|
||||
self.io.tool_output(f"Dry run: Would extract {num_extracted} lines (from {occurrence_str}start pattern '{start_pattern}') in {source_file_path} and {target_action} {target_file_path}")
|
||||
# Provide more informative dry run response with diffs
|
||||
return (
|
||||
f"Dry run: Would extract {num_extracted} lines from {rel_source_path} and {target_action} {rel_target_path}.\n"
|
||||
f"Source Diff (Deletion):\n{source_diff_snippet}\n"
|
||||
f"Target Diff (Insertion):\n{target_diff_snippet}"
|
||||
)
|
||||
|
||||
# --- Apply Changes (Not Dry Run) ---
|
||||
self.io.write_text(abs_source_path, new_source_content)
|
||||
self.io.write_text(abs_target_path, new_target_content)
|
||||
|
||||
# --- Track Changes ---
|
||||
source_change_id = "TRACKING_FAILED"
|
||||
target_change_id = "TRACKING_FAILED"
|
||||
try:
|
||||
source_metadata = {
|
||||
'start_line': start_line + 1, 'end_line': end_line + 1,
|
||||
'start_pattern': start_pattern, 'end_pattern': end_pattern, 'line_count': line_count,
|
||||
'near_context': near_context, 'occurrence': occurrence,
|
||||
'extracted_content': extracted_block, 'target_file': rel_target_path
|
||||
}
|
||||
source_change_id = self.change_tracker.track_change(
|
||||
file_path=rel_source_path, change_type='extractlines_source',
|
||||
original_content=original_source_content, new_content=new_source_content,
|
||||
metadata=source_metadata
|
||||
)
|
||||
except Exception as track_e:
|
||||
self.io.tool_error(f"Error tracking source change for ExtractLines: {track_e}")
|
||||
|
||||
try:
|
||||
target_metadata = {
|
||||
'insertion_line': target_insertion_line + 1,
|
||||
'inserted_content': extracted_block, 'source_file': rel_source_path
|
||||
}
|
||||
target_change_id = self.change_tracker.track_change(
|
||||
file_path=rel_target_path, change_type='extractlines_target',
|
||||
original_content=original_target_content, new_content=new_target_content,
|
||||
metadata=target_metadata
|
||||
)
|
||||
except Exception as track_e:
|
||||
self.io.tool_error(f"Error tracking target change for ExtractLines: {track_e}")
|
||||
|
||||
# --- Update Context ---
|
||||
self.aider_edited_files.add(rel_source_path)
|
||||
self.aider_edited_files.add(rel_target_path)
|
||||
if not target_exists:
|
||||
# Add the newly created file to editable context
|
||||
self.abs_fnames.add(abs_target_path)
|
||||
self.io.tool_output(f"✨ Created and added '{target_file_path}' to editable context.")
|
||||
|
||||
# --- Return Result ---
|
||||
num_extracted = end_line - start_line + 1
|
||||
target_action = "appended to" if target_exists else "created"
|
||||
self.io.tool_output(f"✅ Extracted {num_extracted} lines from {rel_source_path} (change_id: {source_change_id}) and {target_action} {rel_target_path} (change_id: {target_change_id})")
|
||||
# Provide more informative success response with change IDs and diffs
|
||||
return (
|
||||
f"Successfully extracted {num_extracted} lines from {rel_source_path} and {target_action} {rel_target_path}.\n"
|
||||
f"Source Change ID: {source_change_id}\nSource Diff (Deletion):\n{source_diff_snippet}\n"
|
||||
f"Target Change ID: {target_change_id}\nTarget Diff (Insertion):\n{target_diff_snippet}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.io.tool_error(f"Error in ExtractLines: {str(e)}\n{traceback.format_exc()}")
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
|
||||
# ------------------- Diff Generation Helpers -------------------
|
||||
|
||||
def _generate_diff_snippet(self, original_content, start_index, replaced_len, replacement_text):
|
||||
|
|
|
@ -88,6 +88,10 @@ Act as an expert software engineer with the ability to autonomously navigate and
|
|||
- **ListChanges**: `[tool_call(ListChanges, file_path="...", limit=5)]`
|
||||
List recent changes, optionally filtered by `file_path` and limited.
|
||||
|
||||
- **ExtractLines**: `[tool_call(ExtractLines, source_file_path="...", target_file_path="...", start_pattern="...", end_pattern="...", near_context="...", occurrence=1, dry_run=False)]`
|
||||
Extract lines from `start_pattern` to `end_pattern` (or use `line_count`) in `source_file_path` and move them to `target_file_path`. Creates `target_file_path` if it doesn't exist. Use `near_context` and `occurrence` (optional, default 1, -1 for last) for `start_pattern`. `dry_run=True` simulates.
|
||||
*Useful for refactoring by moving functions, classes, or blocks of code into separate files.*
|
||||
|
||||
### Other Tools
|
||||
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
||||
Execute a shell command. Requires user confirmation.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue