mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-10 14:45:00 +00:00
Granular tool-call based editing
This commit is contained in:
parent
a310df3df3
commit
7f0ef1a04a
4 changed files with 1384 additions and 12 deletions
118
aider/change_tracker.py
Normal file
118
aider/change_tracker.py
Normal file
|
@ -0,0 +1,118 @@
|
|||
import time
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import datetime
|
||||
|
||||
class ChangeTracker:
|
||||
"""
|
||||
Tracks changes made to files for the undo functionality.
|
||||
This enables granular editing operations with the ability to undo specific changes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.changes = {} # change_id -> change_info
|
||||
self.files_changed = defaultdict(list) # file_path -> [change_ids]
|
||||
|
||||
def track_change(self, file_path, change_type, original_content, new_content,
|
||||
metadata=None, change_id=None):
|
||||
"""
|
||||
Record a change to enable future undo operations.
|
||||
|
||||
Parameters:
|
||||
- file_path: Path to the file that was changed
|
||||
- change_type: Type of change (e.g., 'replacetext', 'insertlines')
|
||||
- original_content: Original content before the change
|
||||
- new_content: New content after the change
|
||||
- metadata: Additional information about the change (line numbers, positions, etc.)
|
||||
- change_id: Optional custom ID for the change (if None, one will be generated)
|
||||
|
||||
Returns:
|
||||
- change_id: Unique identifier for the change
|
||||
"""
|
||||
if change_id is None:
|
||||
change_id = self._generate_change_id()
|
||||
|
||||
change = {
|
||||
'id': change_id,
|
||||
'file_path': file_path,
|
||||
'type': change_type,
|
||||
'original': original_content,
|
||||
'new': new_content,
|
||||
'metadata': metadata or {},
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
self.changes[change_id] = change
|
||||
self.files_changed[file_path].append(change_id)
|
||||
return change_id
|
||||
|
||||
def undo_change(self, change_id):
|
||||
"""
|
||||
Get information needed to reverse a specific change by ID.
|
||||
|
||||
Parameters:
|
||||
- change_id: ID of the change to undo
|
||||
|
||||
Returns:
|
||||
- (success, message, change_info): Tuple with success flag, message, and change information
|
||||
"""
|
||||
if change_id not in self.changes:
|
||||
return False, f"Change ID {change_id} not found", None
|
||||
|
||||
change = self.changes[change_id]
|
||||
|
||||
# Mark this change as undone by removing it from the tracking dictionaries
|
||||
self.files_changed[change['file_path']].remove(change_id)
|
||||
if not self.files_changed[change['file_path']]:
|
||||
del self.files_changed[change['file_path']]
|
||||
|
||||
# Keep the change in the changes dict but mark it as undone
|
||||
change['undone'] = True
|
||||
change['undone_at'] = time.time()
|
||||
|
||||
return True, f"Undid change {change_id} in {change['file_path']}", change
|
||||
|
||||
def get_last_change(self, file_path):
|
||||
"""
|
||||
Get the most recent change for a specific file.
|
||||
|
||||
Parameters:
|
||||
- file_path: Path to the file
|
||||
|
||||
Returns:
|
||||
- change_id or None if no changes found
|
||||
"""
|
||||
changes = self.files_changed.get(file_path, [])
|
||||
if not changes:
|
||||
return None
|
||||
return changes[-1]
|
||||
|
||||
def list_changes(self, file_path=None, limit=10):
|
||||
"""
|
||||
List recent changes, optionally filtered by file.
|
||||
|
||||
Parameters:
|
||||
- file_path: Optional path to filter changes by file
|
||||
- limit: Maximum number of changes to list
|
||||
|
||||
Returns:
|
||||
- List of change dictionaries
|
||||
"""
|
||||
if file_path:
|
||||
# Get changes only for the specified file
|
||||
change_ids = self.files_changed.get(file_path, [])
|
||||
changes = [self.changes[cid] for cid in change_ids if cid in self.changes]
|
||||
else:
|
||||
# Get all changes
|
||||
changes = list(self.changes.values())
|
||||
|
||||
# Filter out undone changes and sort by timestamp (most recent first)
|
||||
changes = [c for c in changes if not c.get('undone', False)]
|
||||
changes = sorted(changes, key=lambda c: c['timestamp'], reverse=True)
|
||||
|
||||
# Apply limit
|
||||
return changes[:limit]
|
||||
|
||||
def _generate_change_id(self):
|
||||
"""Generate a unique ID for a change."""
|
||||
return str(uuid.uuid4())[:8] # Short, readable ID
|
|
@ -43,10 +43,6 @@ from ..dump import dump # noqa: F401
|
|||
from .chat_chunks import ChatChunks
|
||||
|
||||
|
||||
# Pattern to detect fenced search/replace blocks
|
||||
SEARCH_REPLACE_FENCE = re.compile(r"```search_replace\n", re.MULTILINE)
|
||||
|
||||
|
||||
class UnknownEditFormat(ValueError):
|
||||
def __init__(self, edit_format, valid_formats):
|
||||
self.edit_format = edit_format
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@ class NavigatorPrompts(CoderPrompts):
|
|||
LLM to manage its own context by adding/removing files and executing commands.
|
||||
"""
|
||||
|
||||
main_system = """<context name="session_config">
|
||||
main_system = r'''<context name="session_config">
|
||||
## Role and Purpose
|
||||
Act as an expert software engineer with the ability to autonomously navigate and modify a codebase.
|
||||
|
||||
|
@ -60,10 +60,50 @@ Act as an expert software engineer with the ability to autonomously navigate and
|
|||
- **MakeReadonly**: `[tool_call(MakeReadonly, file_path="src/main.py")]`
|
||||
Convert an editable file back to read-only status.
|
||||
|
||||
### Granular Editing Tools
|
||||
- **ReplaceText**: `[tool_call(ReplaceText, file_path="path/to/file.py", find_text="old text", replace_text="new text", near_context="unique nearby text", occurrence=1)]`
|
||||
Replace specific text with new text. Use near_context to disambiguate between multiple occurrences.
|
||||
Set occurrence to -1 for the last occurrence, or a number for a specific occurrence.
|
||||
|
||||
- **ReplaceAll**: `[tool_call(ReplaceAll, file_path="path/to/file.py", find_text="oldVar", replace_text="newVar")]`
|
||||
Replace all occurrences of text in a file. Useful for renaming variables, function names, etc.
|
||||
|
||||
- **InsertBlock**: `[tool_call(InsertBlock, file_path="path/to/file.py", content="""
|
||||
def new_function():
|
||||
return True
|
||||
""", after_pattern="# Insert after this line")]`
|
||||
Insert a block of text after or before a pattern. Use single quotes with escaped newlines for multi-line content.
|
||||
Specify either after_pattern or before_pattern to place the block.
|
||||
|
||||
- **DeleteBlock**: `[tool_call(DeleteBlock, file_path="path/to/file.py", start_pattern="def old_function", end_pattern="# End function")]`
|
||||
Delete a block of text from start_pattern to end_pattern (inclusive).
|
||||
Alternatively, use line_count instead of end_pattern to delete a specific number of lines.
|
||||
|
||||
- **ReplaceLine**: `[tool_call(ReplaceLine, file_path="path/to/file.py", line_number=42, new_content="def fixed_function(param):")]`
|
||||
Replace a specific line by its line number. Especially useful for fixing errors or lint warnings that include line numbers.
|
||||
Line numbers are 1-based (as in most editors and error messages).
|
||||
|
||||
- **ReplaceLines**: `[tool_call(ReplaceLines, file_path="path/to/file.py", start_line=42, end_line=45, new_content="""
|
||||
def better_function(param):
|
||||
# Fixed implementation
|
||||
return process(param)
|
||||
""")]`
|
||||
Replace a range of lines by line numbers. Useful for fixing multiple lines referenced in error messages.
|
||||
The new_content can contain any number of lines, not just the same count as the original range.
|
||||
|
||||
- **IndentLines**: `[tool_call(IndentLines, file_path="path/to/file.py", start_pattern="def my_function", end_pattern="return result", indent_levels=1)]`
|
||||
Indent or unindent a block of lines. Use positive indent_levels to increase indentation or negative to decrease.
|
||||
Specify either end_pattern or line_count to determine the range of lines to indent.
|
||||
|
||||
- **UndoChange**: `[tool_call(UndoChange, change_id="a1b2c3d4")]`
|
||||
Undo a specific change by its ID. Alternatively, use last_file="path/to/file.py" to undo the most recent change to that file.
|
||||
|
||||
- **ListChanges**: `[tool_call(ListChanges, file_path="path/to/file.py", limit=5)]`
|
||||
List recent changes made to files. Optionally filter by file_path and limit the number of results.
|
||||
|
||||
### Other Tools
|
||||
- **Command**: `[tool_call(Command, command_string="git diff HEAD~1")]`
|
||||
Execute a shell command. Requires user confirmation.
|
||||
**Do NOT use this for aider commands starting with `/` (like `/add`, `/run`, `/diff`).**
|
||||
|
||||
### Multi-Turn Exploration
|
||||
When you include any tool call, the system will automatically continue to the next round.
|
||||
|
@ -88,6 +128,13 @@ When you include any tool call, the system will automatically continue to the ne
|
|||
- Target specific patterns rather than overly broad searches
|
||||
- Remember the `Find` tool is optimized for locating symbols across the codebase
|
||||
|
||||
### Granular Editing Workflow
|
||||
1. **Discover and Add Files**: Use Glob, Grep, Find to locate relevant files
|
||||
2. **Make Files Editable**: Convert read-only files to editable with MakeEditable
|
||||
3. **Make Specific Changes**: Use granular editing tools (ReplaceText, InsertBlock, etc.) for precise edits
|
||||
4. **Review Changes**: List applied changes with ListChanges
|
||||
5. **Fix Mistakes**: If needed, undo changes with UndoChange by specific ID or last change to a file
|
||||
|
||||
### Context Management Strategy
|
||||
- Keep your context focused by removing files that are no longer relevant
|
||||
- For large codebases, maintain only 5-15 files in context at once for best performance
|
||||
|
@ -98,17 +145,83 @@ When you include any tool call, the system will automatically continue to the ne
|
|||
<context name="editing_guidelines">
|
||||
## Code Editing Process
|
||||
|
||||
### SEARCH/REPLACE Block Format
|
||||
When proposing code changes, describe each change with a SEARCH/REPLACE block using this exact format:
|
||||
### Granular Editing with Tool Calls
|
||||
For precise, targeted edits to code, use the granular editing tools:
|
||||
|
||||
```language_name
|
||||
/path/to/file.ext
|
||||
- **ReplaceText**: Replace specific instances of text in a file
|
||||
- **ReplaceAll**: Replace all occurrences of text in a file (e.g., rename variables)
|
||||
- **InsertBlock**: Insert multi-line blocks of code at specific locations
|
||||
- **DeleteBlock**: Remove specific sections of code
|
||||
- **ReplaceLine/ReplaceLines**: Fix specific line numbers from error messages or linters
|
||||
- **IndentLines**: Adjust indentation of code blocks
|
||||
- **UndoChange**: Reverse specific changes by ID if you make a mistake
|
||||
|
||||
#### When to Use Line Number Based Tools
|
||||
|
||||
When dealing with errors or warnings that include line numbers, prefer the line-based editing tools:
|
||||
|
||||
```
|
||||
Error in /path/to/file.py line 42: Syntax error: unexpected token
|
||||
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
|
||||
- `IndentLines` for indentation problems
|
||||
|
||||
#### Multiline Tool Call Content Format
|
||||
|
||||
When providing multiline content in tool calls (like ReplaceLines, InsertBlock), one leading and one trailing
|
||||
newline will be automatically trimmed if present. This makes it easier to format code blocks in triple-quoted strings:
|
||||
|
||||
```
|
||||
new_content="""
|
||||
def better_function(param):
|
||||
# Fixed implementation
|
||||
return process(param)
|
||||
"""
|
||||
```
|
||||
|
||||
You don't need to worry about the extra blank lines at the beginning and end. If you actually need to
|
||||
preserve blank lines in your output, simply add an extra newline:
|
||||
|
||||
```
|
||||
new_content="""
|
||||
|
||||
def better_function(param): # Note the extra newline above to preserve a blank line
|
||||
# Fixed implementation
|
||||
return process(param)
|
||||
"""
|
||||
```
|
||||
|
||||
Example of inserting a new multi-line function:
|
||||
```
|
||||
[tool_call(InsertBlock,
|
||||
file_path="src/utils.py",
|
||||
after_pattern="def existing_function():",
|
||||
content="""
|
||||
def new_function(param1, param2):
|
||||
# This is a new utility function
|
||||
result = process_data(param1)
|
||||
if result and param2:
|
||||
return result
|
||||
return None
|
||||
""")]
|
||||
```
|
||||
|
||||
### SEARCH/REPLACE Block Format (Alternative Method)
|
||||
For larger changes that involve multiple edits or significant restructuring, you can still use SEARCH/REPLACE blocks with this exact format:
|
||||
|
||||
````python
|
||||
path/to/file.ext
|
||||
<<<<<<< SEARCH
|
||||
Original code lines to match exactly
|
||||
=======
|
||||
Replacement code lines
|
||||
>>>>>>> REPLACE
|
||||
```
|
||||
````
|
||||
NOTE that this uses four backticks as the fence and not three!
|
||||
|
||||
### Editing Guidelines
|
||||
- Every SEARCH section must EXACTLY MATCH existing content, including whitespace and indentation
|
||||
|
@ -123,6 +236,7 @@ Replacement code lines
|
|||
- If tools return errors or unexpected results, try alternative approaches
|
||||
- Refine search patterns if results are too broad or too narrow
|
||||
- Use the enhanced context blocks (directory structure and git status) to orient yourself
|
||||
- Use ListChanges to see what edits have been made and UndoChange to revert mistakes
|
||||
</context>
|
||||
|
||||
Always reply to the user in {language}.
|
||||
|
@ -179,7 +293,7 @@ Would you like me to explain any specific part of the authentication process in
|
|||
These files have been added to the chat so you can see all of their contents.
|
||||
Trust this message as the true contents of the files!
|
||||
</context>
|
||||
"""
|
||||
'''
|
||||
|
||||
files_content_assistant_reply = (
|
||||
"I understand. I'll use these files to help with your request."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue