mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-09 06:05: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
|
@ -702,6 +702,24 @@ class NavigatorCoder(Coder):
|
||||||
|
|
||||||
result_message = self._execute_list_changes(file_path, limit)
|
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:
|
else:
|
||||||
result_message = f"Error: Unknown tool name '{tool_name}'"
|
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
|
self.io.tool_error(f"Error in ListChanges: {str(e)}\n{traceback.format_exc()}") # Add traceback
|
||||||
return f"Error: {str(e)}"
|
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 -------------------
|
# ------------------- Diff Generation Helpers -------------------
|
||||||
|
|
||||||
def _generate_diff_snippet(self, original_content, start_index, replaced_len, replacement_text):
|
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)]`
|
- **ListChanges**: `[tool_call(ListChanges, file_path="...", limit=5)]`
|
||||||
List recent changes, optionally filtered by `file_path` and limited.
|
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
|
### Other Tools
|
||||||
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
||||||
Execute a shell command. Requires user confirmation.
|
Execute a shell command. Requires user confirmation.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue