mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-31 09:44:59 +00:00
style: Run linter on patch_coder.py
This commit is contained in:
parent
f565f72679
commit
1ae5f23dc8
1 changed files with 134 additions and 115 deletions
|
@ -42,7 +42,7 @@ class PatchAction:
|
||||||
@dataclass
|
@dataclass
|
||||||
class Patch:
|
class Patch:
|
||||||
actions: Dict[str, PatchAction] = field(default_factory=dict)
|
actions: Dict[str, PatchAction] = field(default_factory=dict)
|
||||||
fuzz: int = 0 # Track fuzziness used during parsing
|
fuzz: int = 0 # Track fuzziness used during parsing
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
@ -53,9 +53,7 @@ def _norm(line: str) -> str:
|
||||||
return line.rstrip("\r")
|
return line.rstrip("\r")
|
||||||
|
|
||||||
|
|
||||||
def find_context_core(
|
def find_context_core(lines: List[str], context: List[str], start: int) -> Tuple[int, int]:
|
||||||
lines: List[str], context: List[str], start: int
|
|
||||||
) -> Tuple[int, int]:
|
|
||||||
"""Finds context block, returns start index and fuzz level."""
|
"""Finds context block, returns start index and fuzz level."""
|
||||||
if not context:
|
if not context:
|
||||||
return start, 0
|
return start, 0
|
||||||
|
@ -68,18 +66,16 @@ def find_context_core(
|
||||||
norm_context = [s.rstrip() for s in context]
|
norm_context = [s.rstrip() for s in context]
|
||||||
for i in range(start, len(lines) - len(context) + 1):
|
for i in range(start, len(lines) - len(context) + 1):
|
||||||
if [s.rstrip() for s in lines[i : i + len(context)]] == norm_context:
|
if [s.rstrip() for s in lines[i : i + len(context)]] == norm_context:
|
||||||
return i, 1 # Fuzz level 1
|
return i, 1 # Fuzz level 1
|
||||||
# Strip match
|
# Strip match
|
||||||
norm_context_strip = [s.strip() for s in context]
|
norm_context_strip = [s.strip() for s in context]
|
||||||
for i in range(start, len(lines) - len(context) + 1):
|
for i in range(start, len(lines) - len(context) + 1):
|
||||||
if [s.strip() for s in lines[i : i + len(context)]] == norm_context_strip:
|
if [s.strip() for s in lines[i : i + len(context)]] == norm_context_strip:
|
||||||
return i, 100 # Fuzz level 100
|
return i, 100 # Fuzz level 100
|
||||||
return -1, 0
|
return -1, 0
|
||||||
|
|
||||||
|
|
||||||
def find_context(
|
def find_context(lines: List[str], context: List[str], start: int, eof: bool) -> Tuple[int, int]:
|
||||||
lines: List[str], context: List[str], start: int, eof: bool
|
|
||||||
) -> Tuple[int, int]:
|
|
||||||
"""Finds context, handling EOF marker."""
|
"""Finds context, handling EOF marker."""
|
||||||
if eof:
|
if eof:
|
||||||
# If EOF marker, first try matching at the very end
|
# If EOF marker, first try matching at the very end
|
||||||
|
@ -89,14 +85,12 @@ def find_context(
|
||||||
return new_index, fuzz
|
return new_index, fuzz
|
||||||
# If not found at end, search from `start` as fallback
|
# If not found at end, search from `start` as fallback
|
||||||
new_index, fuzz = find_context_core(lines, context, start)
|
new_index, fuzz = find_context_core(lines, context, start)
|
||||||
return new_index, fuzz + 10_000 # Add large fuzz penalty if EOF wasn't at end
|
return new_index, fuzz + 10_000 # Add large fuzz penalty if EOF wasn't at end
|
||||||
# Normal case: search from `start`
|
# Normal case: search from `start`
|
||||||
return find_context_core(lines, context, start)
|
return find_context_core(lines, context, start)
|
||||||
|
|
||||||
|
|
||||||
def peek_next_section(
|
def peek_next_section(lines: List[str], index: int) -> Tuple[List[str], List[Chunk], int, bool]:
|
||||||
lines: List[str], index: int
|
|
||||||
) -> Tuple[List[str], List[Chunk], int, bool]:
|
|
||||||
"""
|
"""
|
||||||
Parses one section (context, -, + lines) of an Update block.
|
Parses one section (context, -, + lines) of an Update block.
|
||||||
Returns: (context_lines, chunks_in_section, next_index, is_eof)
|
Returns: (context_lines, chunks_in_section, next_index, is_eof)
|
||||||
|
@ -105,7 +99,7 @@ def peek_next_section(
|
||||||
del_lines: List[str] = []
|
del_lines: List[str] = []
|
||||||
ins_lines: List[str] = []
|
ins_lines: List[str] = []
|
||||||
chunks: List[Chunk] = []
|
chunks: List[Chunk] = []
|
||||||
mode = "keep" # Start by expecting context lines
|
mode = "keep" # Start by expecting context lines
|
||||||
start_index = index
|
start_index = index
|
||||||
|
|
||||||
while index < len(lines):
|
while index < len(lines):
|
||||||
|
@ -120,13 +114,13 @@ def peek_next_section(
|
||||||
"*** Update File:",
|
"*** Update File:",
|
||||||
"*** Delete File:",
|
"*** Delete File:",
|
||||||
"*** Add File:",
|
"*** Add File:",
|
||||||
"*** End of File", # Special terminator
|
"*** End of File", # Special terminator
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
if norm_line == "***": # Legacy/alternative terminator? Handle just in case.
|
if norm_line == "***": # Legacy/alternative terminator? Handle just in case.
|
||||||
break
|
break
|
||||||
if norm_line.startswith("***"): # Invalid line
|
if norm_line.startswith("***"): # Invalid line
|
||||||
raise DiffError(f"Invalid patch line found in update section: {line}")
|
raise DiffError(f"Invalid patch line found in update section: {line}")
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
|
@ -142,15 +136,14 @@ def peek_next_section(
|
||||||
elif line.startswith(" "):
|
elif line.startswith(" "):
|
||||||
mode = "keep"
|
mode = "keep"
|
||||||
line_content = line[1:]
|
line_content = line[1:]
|
||||||
elif line.strip() == "": # Treat blank lines in patch as context ' '
|
elif line.strip() == "": # Treat blank lines in patch as context ' '
|
||||||
mode = "keep"
|
mode = "keep"
|
||||||
line_content = "" # Keep it as a blank line
|
line_content = "" # Keep it as a blank line
|
||||||
else:
|
else:
|
||||||
# Assume lines without prefix are context if format is loose,
|
# Assume lines without prefix are context if format is loose,
|
||||||
# but strict format requires ' '. Raise error for strictness.
|
# but strict format requires ' '. Raise error for strictness.
|
||||||
raise DiffError(f"Invalid line prefix in update section: {line}")
|
raise DiffError(f"Invalid line prefix in update section: {line}")
|
||||||
|
|
||||||
|
|
||||||
# If mode changes from add/delete back to keep, finalize the previous chunk
|
# If mode changes from add/delete back to keep, finalize the previous chunk
|
||||||
if mode == "keep" and last_mode != "keep":
|
if mode == "keep" and last_mode != "keep":
|
||||||
if del_lines or ins_lines:
|
if del_lines or ins_lines:
|
||||||
|
@ -167,7 +160,7 @@ def peek_next_section(
|
||||||
# Collect lines based on mode
|
# Collect lines based on mode
|
||||||
if mode == "delete":
|
if mode == "delete":
|
||||||
del_lines.append(line_content)
|
del_lines.append(line_content)
|
||||||
context_lines.append(line_content) # Deleted lines are part of the original context
|
context_lines.append(line_content) # Deleted lines are part of the original context
|
||||||
elif mode == "add":
|
elif mode == "add":
|
||||||
ins_lines.append(line_content)
|
ins_lines.append(line_content)
|
||||||
elif mode == "keep":
|
elif mode == "keep":
|
||||||
|
@ -189,8 +182,8 @@ def peek_next_section(
|
||||||
index += 1
|
index += 1
|
||||||
is_eof = True
|
is_eof = True
|
||||||
|
|
||||||
if index == start_index and not is_eof: # Should not happen if patch is well-formed
|
if index == start_index and not is_eof: # Should not happen if patch is well-formed
|
||||||
raise DiffError("Empty patch section found.")
|
raise DiffError("Empty patch section found.")
|
||||||
|
|
||||||
return context_lines, chunks, index, is_eof
|
return context_lines, chunks, index, is_eof
|
||||||
|
|
||||||
|
@ -207,6 +200,7 @@ def identify_files_needed(text: str) -> List[str]:
|
||||||
paths.add(norm_line[len("*** Delete File: ") :].strip())
|
paths.add(norm_line[len("*** Delete File: ") :].strip())
|
||||||
return list(paths)
|
return list(paths)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
# PatchCoder Class Implementation
|
# PatchCoder Class Implementation
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
|
@ -236,19 +230,24 @@ class PatchCoder(Coder):
|
||||||
# Allow flexible end, might be EOF or just end of stream
|
# Allow flexible end, might be EOF or just end of stream
|
||||||
# or _norm(lines[-1]) != "*** End Patch"
|
# or _norm(lines[-1]) != "*** End Patch"
|
||||||
):
|
):
|
||||||
# Tolerate missing sentinels if content looks like a patch action
|
# Tolerate missing sentinels if content looks like a patch action
|
||||||
is_patch_like = any(_norm(line).startswith(
|
is_patch_like = any(
|
||||||
("@@", "*** Update File:", "*** Add File:", "*** Delete File:")
|
_norm(line).startswith(
|
||||||
) for line in lines)
|
("@@", "*** Update File:", "*** Add File:", "*** Delete File:")
|
||||||
if not is_patch_like:
|
)
|
||||||
# If it doesn't even look like a patch, return empty
|
for line in lines
|
||||||
self.io.tool_warning("Response does not appear to be in patch format.")
|
)
|
||||||
return []
|
if not is_patch_like:
|
||||||
# If it looks like a patch but lacks sentinels, try parsing anyway but warn.
|
# If it doesn't even look like a patch, return empty
|
||||||
self.io.tool_warning("Patch format warning: Missing '*** Begin Patch'/'*** End Patch' sentinels.")
|
self.io.tool_warning("Response does not appear to be in patch format.")
|
||||||
start_index = 0
|
return []
|
||||||
|
# If it looks like a patch but lacks sentinels, try parsing anyway but warn.
|
||||||
|
self.io.tool_warning(
|
||||||
|
"Patch format warning: Missing '*** Begin Patch'/'*** End Patch' sentinels."
|
||||||
|
)
|
||||||
|
start_index = 0
|
||||||
else:
|
else:
|
||||||
start_index = 1 # Skip "*** Begin Patch"
|
start_index = 1 # Skip "*** Begin Patch"
|
||||||
|
|
||||||
# Identify files needed for context lookups during parsing
|
# Identify files needed for context lookups during parsing
|
||||||
needed_paths = identify_files_needed(content)
|
needed_paths = identify_files_needed(content)
|
||||||
|
@ -259,13 +258,14 @@ class PatchCoder(Coder):
|
||||||
# Use io.read_text to handle potential errors/encodings
|
# Use io.read_text to handle potential errors/encodings
|
||||||
file_content = self.io.read_text(abs_path)
|
file_content = self.io.read_text(abs_path)
|
||||||
if file_content is None:
|
if file_content is None:
|
||||||
raise DiffError(f"File referenced in patch not found or could not be read: {rel_path}")
|
raise DiffError(
|
||||||
|
f"File referenced in patch not found or could not be read: {rel_path}"
|
||||||
|
)
|
||||||
current_files[rel_path] = file_content
|
current_files[rel_path] = file_content
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise DiffError(f"File referenced in patch not found: {rel_path}")
|
raise DiffError(f"File referenced in patch not found: {rel_path}")
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
raise DiffError(f"Error reading file {rel_path}: {e}")
|
raise DiffError(f"Error reading file {rel_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Parse the patch text using adapted logic
|
# Parse the patch text using adapted logic
|
||||||
|
@ -279,7 +279,6 @@ class PatchCoder(Coder):
|
||||||
# Catch unexpected errors during parsing
|
# Catch unexpected errors during parsing
|
||||||
raise ValueError(f"Unexpected error parsing patch: {e}")
|
raise ValueError(f"Unexpected error parsing patch: {e}")
|
||||||
|
|
||||||
|
|
||||||
def _parse_patch_text(
|
def _parse_patch_text(
|
||||||
self, lines: List[str], start_index: int, current_files: Dict[str, str]
|
self, lines: List[str], start_index: int, current_files: Dict[str, str]
|
||||||
) -> Patch:
|
) -> Patch:
|
||||||
|
@ -297,25 +296,29 @@ class PatchCoder(Coder):
|
||||||
|
|
||||||
if norm_line == "*** End Patch":
|
if norm_line == "*** End Patch":
|
||||||
index += 1
|
index += 1
|
||||||
break # Successfully reached end
|
break # Successfully reached end
|
||||||
|
|
||||||
# ---------- UPDATE ---------- #
|
# ---------- UPDATE ---------- #
|
||||||
if norm_line.startswith("*** Update File: "):
|
if norm_line.startswith("*** Update File: "):
|
||||||
path = norm_line[len("*** Update File: ") :].strip()
|
path = norm_line[len("*** Update File: ") :].strip()
|
||||||
index += 1
|
index += 1
|
||||||
if not path: raise DiffError("Update File action missing path.")
|
if not path:
|
||||||
if path in patch.actions: raise DiffError(f"Duplicate action for file: {path}")
|
raise DiffError("Update File action missing path.")
|
||||||
if path not in current_files: raise DiffError(f"Update File Error - missing file content for: {path}")
|
if path in patch.actions:
|
||||||
|
raise DiffError(f"Duplicate action for file: {path}")
|
||||||
|
if path not in current_files:
|
||||||
|
raise DiffError(f"Update File Error - missing file content for: {path}")
|
||||||
|
|
||||||
move_to = None
|
move_to = None
|
||||||
if index < len(lines) and _norm(lines[index]).startswith("*** Move to: "):
|
if index < len(lines) and _norm(lines[index]).startswith("*** Move to: "):
|
||||||
move_to = _norm(lines[index])[len("*** Move to: ") :].strip()
|
move_to = _norm(lines[index])[len("*** Move to: ") :].strip()
|
||||||
index += 1
|
index += 1
|
||||||
if not move_to: raise DiffError("Move to action missing path.")
|
if not move_to:
|
||||||
|
raise DiffError("Move to action missing path.")
|
||||||
|
|
||||||
file_content = current_files[path]
|
file_content = current_files[path]
|
||||||
action, index, fuzz = self._parse_update_file_sections(lines, index, file_content)
|
action, index, fuzz = self._parse_update_file_sections(lines, index, file_content)
|
||||||
action.path = path # Ensure path is set
|
action.path = path # Ensure path is set
|
||||||
action.move_path = move_to
|
action.move_path = move_to
|
||||||
patch.actions[path] = action
|
patch.actions[path] = action
|
||||||
fuzz_accumulator += fuzz
|
fuzz_accumulator += fuzz
|
||||||
|
@ -325,9 +328,14 @@ class PatchCoder(Coder):
|
||||||
elif norm_line.startswith("*** Delete File: "):
|
elif norm_line.startswith("*** Delete File: "):
|
||||||
path = norm_line[len("*** Delete File: ") :].strip()
|
path = norm_line[len("*** Delete File: ") :].strip()
|
||||||
index += 1
|
index += 1
|
||||||
if not path: raise DiffError("Delete File action missing path.")
|
if not path:
|
||||||
if path in patch.actions: raise DiffError(f"Duplicate action for file: {path}")
|
raise DiffError("Delete File action missing path.")
|
||||||
if path not in current_files: raise DiffError(f"Delete File Error - file not found: {path}") # Check against known files
|
if path in patch.actions:
|
||||||
|
raise DiffError(f"Duplicate action for file: {path}")
|
||||||
|
if path not in current_files:
|
||||||
|
raise DiffError(
|
||||||
|
f"Delete File Error - file not found: {path}"
|
||||||
|
) # Check against known files
|
||||||
|
|
||||||
patch.actions[path] = PatchAction(type=ActionType.DELETE, path=path)
|
patch.actions[path] = PatchAction(type=ActionType.DELETE, path=path)
|
||||||
continue
|
continue
|
||||||
|
@ -336,14 +344,16 @@ class PatchCoder(Coder):
|
||||||
elif norm_line.startswith("*** Add File: "):
|
elif norm_line.startswith("*** Add File: "):
|
||||||
path = norm_line[len("*** Add File: ") :].strip()
|
path = norm_line[len("*** Add File: ") :].strip()
|
||||||
index += 1
|
index += 1
|
||||||
if not path: raise DiffError("Add File action missing path.")
|
if not path:
|
||||||
if path in patch.actions: raise DiffError(f"Duplicate action for file: {path}")
|
raise DiffError("Add File action missing path.")
|
||||||
|
if path in patch.actions:
|
||||||
|
raise DiffError(f"Duplicate action for file: {path}")
|
||||||
# Check if file exists in the context provided (should not for Add)
|
# Check if file exists in the context provided (should not for Add)
|
||||||
# Note: We don't have *all* files, just needed ones. A full check requires FS access.
|
# Note: We don't have *all* files, just needed ones. A full check requires FS access.
|
||||||
# if path in current_files: raise DiffError(f"Add File Error - file already exists: {path}")
|
# if path in current_files: raise DiffError(f"Add File Error - file already exists: {path}")
|
||||||
|
|
||||||
action, index = self._parse_add_file_content(lines, index)
|
action, index = self._parse_add_file_content(lines, index)
|
||||||
action.path = path # Ensure path is set
|
action.path = path # Ensure path is set
|
||||||
patch.actions[path] = action
|
patch.actions[path] = action
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -363,14 +373,13 @@ class PatchCoder(Coder):
|
||||||
patch.fuzz = fuzz_accumulator
|
patch.fuzz = fuzz_accumulator
|
||||||
return patch
|
return patch
|
||||||
|
|
||||||
|
|
||||||
def _parse_update_file_sections(
|
def _parse_update_file_sections(
|
||||||
self, lines: List[str], index: int, file_content: str
|
self, lines: List[str], index: int, file_content: str
|
||||||
) -> Tuple[PatchAction, int, int]:
|
) -> Tuple[PatchAction, int, int]:
|
||||||
"""Parses all sections (@@, context, -, +) for a single Update File action."""
|
"""Parses all sections (@@, context, -, +) for a single Update File action."""
|
||||||
action = PatchAction(type=ActionType.UPDATE, path="") # Path set by caller
|
action = PatchAction(type=ActionType.UPDATE, path="") # Path set by caller
|
||||||
orig_lines = file_content.splitlines() # Use splitlines for consistency
|
orig_lines = file_content.splitlines() # Use splitlines for consistency
|
||||||
current_file_index = 0 # Track position in original file content
|
current_file_index = 0 # Track position in original file content
|
||||||
total_fuzz = 0
|
total_fuzz = 0
|
||||||
|
|
||||||
while index < len(lines):
|
while index < len(lines):
|
||||||
|
@ -384,13 +393,13 @@ class PatchCoder(Coder):
|
||||||
"*** Add File:",
|
"*** Add File:",
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
break # End of this file's update section
|
break # End of this file's update section
|
||||||
|
|
||||||
# Handle @@ scope lines (optional)
|
# Handle @@ scope lines (optional)
|
||||||
scope_lines = []
|
scope_lines = []
|
||||||
while index < len(lines) and _norm(lines[index]).startswith("@@"):
|
while index < len(lines) and _norm(lines[index]).startswith("@@"):
|
||||||
scope_line_content = lines[index][len("@@") :].strip()
|
scope_line_content = lines[index][len("@@") :].strip()
|
||||||
if scope_line_content: # Ignore empty @@ lines?
|
if scope_line_content: # Ignore empty @@ lines?
|
||||||
scope_lines.append(scope_line_content)
|
scope_lines.append(scope_line_content)
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
|
@ -401,39 +410,44 @@ class PatchCoder(Coder):
|
||||||
found_scope = False
|
found_scope = False
|
||||||
temp_index = current_file_index
|
temp_index = current_file_index
|
||||||
while temp_index < len(orig_lines):
|
while temp_index < len(orig_lines):
|
||||||
# Check if all scope lines match sequentially from temp_index
|
# Check if all scope lines match sequentially from temp_index
|
||||||
match = True
|
match = True
|
||||||
for i, scope in enumerate(scope_lines):
|
for i, scope in enumerate(scope_lines):
|
||||||
if temp_index + i >= len(orig_lines) or _norm(orig_lines[temp_index + i]).strip() != scope:
|
if (
|
||||||
match = False
|
temp_index + i >= len(orig_lines)
|
||||||
break
|
or _norm(orig_lines[temp_index + i]).strip() != scope
|
||||||
if match:
|
):
|
||||||
current_file_index = temp_index + len(scope_lines)
|
match = False
|
||||||
found_scope = True
|
break
|
||||||
break
|
if match:
|
||||||
temp_index += 1
|
current_file_index = temp_index + len(scope_lines)
|
||||||
|
found_scope = True
|
||||||
|
break
|
||||||
|
temp_index += 1
|
||||||
|
|
||||||
if not found_scope:
|
if not found_scope:
|
||||||
# Try fuzzy scope matching (strip whitespace)
|
# Try fuzzy scope matching (strip whitespace)
|
||||||
temp_index = current_file_index
|
temp_index = current_file_index
|
||||||
while temp_index < len(orig_lines):
|
while temp_index < len(orig_lines):
|
||||||
match = True
|
match = True
|
||||||
for i, scope in enumerate(scope_lines):
|
for i, scope in enumerate(scope_lines):
|
||||||
if temp_index + i >= len(orig_lines) or _norm(orig_lines[temp_index + i]).strip() != scope.strip():
|
if (
|
||||||
match = False
|
temp_index + i >= len(orig_lines)
|
||||||
break
|
or _norm(orig_lines[temp_index + i]).strip() != scope.strip()
|
||||||
if match:
|
):
|
||||||
current_file_index = temp_index + len(scope_lines)
|
match = False
|
||||||
found_scope = True
|
break
|
||||||
total_fuzz += 1 # Add fuzz for scope match difference
|
if match:
|
||||||
break
|
current_file_index = temp_index + len(scope_lines)
|
||||||
temp_index += 1
|
found_scope = True
|
||||||
|
total_fuzz += 1 # Add fuzz for scope match difference
|
||||||
|
break
|
||||||
|
temp_index += 1
|
||||||
|
|
||||||
if not found_scope:
|
if not found_scope:
|
||||||
scope_txt = "\n".join(scope_lines)
|
scope_txt = "\n".join(scope_lines)
|
||||||
raise DiffError(f"Could not find scope context:\n{scope_txt}")
|
raise DiffError(f"Could not find scope context:\n{scope_txt}")
|
||||||
|
|
||||||
|
|
||||||
# Peek and parse the next context/change section
|
# Peek and parse the next context/change section
|
||||||
context_block, chunks_in_section, next_index, is_eof = peek_next_section(lines, index)
|
context_block, chunks_in_section, next_index, is_eof = peek_next_section(lines, index)
|
||||||
|
|
||||||
|
@ -463,10 +477,7 @@ class PatchCoder(Coder):
|
||||||
|
|
||||||
return action, index, total_fuzz
|
return action, index, total_fuzz
|
||||||
|
|
||||||
|
def _parse_add_file_content(self, lines: List[str], index: int) -> Tuple[PatchAction, int]:
|
||||||
def _parse_add_file_content(
|
|
||||||
self, lines: List[str], index: int
|
|
||||||
) -> Tuple[PatchAction, int]:
|
|
||||||
"""Parses the content (+) lines for an Add File action."""
|
"""Parses the content (+) lines for an Add File action."""
|
||||||
added_lines: List[str] = []
|
added_lines: List[str] = []
|
||||||
while index < len(lines):
|
while index < len(lines):
|
||||||
|
@ -487,19 +498,18 @@ class PatchCoder(Coder):
|
||||||
if not line.startswith("+"):
|
if not line.startswith("+"):
|
||||||
# Tolerate blank lines? Or require '+'? Reference implies '+' required.
|
# Tolerate blank lines? Or require '+'? Reference implies '+' required.
|
||||||
if norm_line.strip() == "":
|
if norm_line.strip() == "":
|
||||||
# Treat blank line as adding a blank line
|
# Treat blank line as adding a blank line
|
||||||
added_lines.append("")
|
added_lines.append("")
|
||||||
else:
|
else:
|
||||||
raise DiffError(f"Invalid Add File line (missing '+'): {line}")
|
raise DiffError(f"Invalid Add File line (missing '+'): {line}")
|
||||||
else:
|
else:
|
||||||
added_lines.append(line[1:]) # Strip leading '+'
|
added_lines.append(line[1:]) # Strip leading '+'
|
||||||
|
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
action = PatchAction(type=ActionType.ADD, path="", new_content="\n".join(added_lines))
|
action = PatchAction(type=ActionType.ADD, path="", new_content="\n".join(added_lines))
|
||||||
return action, index
|
return action, index
|
||||||
|
|
||||||
|
|
||||||
def apply_edits(self, edits: List[PatchAction]):
|
def apply_edits(self, edits: List[PatchAction]):
|
||||||
"""
|
"""
|
||||||
Applies the parsed PatchActions to the corresponding files.
|
Applies the parsed PatchActions to the corresponding files.
|
||||||
|
@ -527,14 +537,16 @@ class PatchCoder(Coder):
|
||||||
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
path_obj.parent.mkdir(parents=True, exist_ok=True)
|
||||||
# Ensure single trailing newline, matching reference behavior
|
# Ensure single trailing newline, matching reference behavior
|
||||||
content_to_write = action.new_content
|
content_to_write = action.new_content
|
||||||
if not content_to_write.endswith('\n'):
|
if not content_to_write.endswith("\n"):
|
||||||
content_to_write += '\n'
|
content_to_write += "\n"
|
||||||
self.io.write_text(full_path, content_to_write)
|
self.io.write_text(full_path, content_to_write)
|
||||||
|
|
||||||
elif action.type == ActionType.DELETE:
|
elif action.type == ActionType.DELETE:
|
||||||
self.io.tool_output(f"Deleting {action.path}")
|
self.io.tool_output(f"Deleting {action.path}")
|
||||||
if not path_obj.exists():
|
if not path_obj.exists():
|
||||||
self.io.tool_warning(f"DELETE Warning: File not found, skipping: {action.path}")
|
self.io.tool_warning(
|
||||||
|
f"DELETE Warning: File not found, skipping: {action.path}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
path_obj.unlink()
|
path_obj.unlink()
|
||||||
|
|
||||||
|
@ -550,17 +562,23 @@ class PatchCoder(Coder):
|
||||||
# Apply the update logic using the parsed chunks
|
# Apply the update logic using the parsed chunks
|
||||||
new_content = self._apply_update(current_content, action, action.path)
|
new_content = self._apply_update(current_content, action, action.path)
|
||||||
|
|
||||||
target_full_path = self.abs_root_path(action.move_path) if action.move_path else full_path
|
target_full_path = (
|
||||||
|
self.abs_root_path(action.move_path) if action.move_path else full_path
|
||||||
|
)
|
||||||
target_path_obj = pathlib.Path(target_full_path)
|
target_path_obj = pathlib.Path(target_full_path)
|
||||||
|
|
||||||
if action.move_path:
|
if action.move_path:
|
||||||
self.io.tool_output(f"Updating and moving {action.path} to {action.move_path}")
|
self.io.tool_output(
|
||||||
# Check if target exists before overwriting/moving
|
f"Updating and moving {action.path} to {action.move_path}"
|
||||||
if target_path_obj.exists() and full_path != target_full_path:
|
)
|
||||||
self.io.tool_warning(f"UPDATE Warning: Target file for move already exists, overwriting: {action.move_path}")
|
# Check if target exists before overwriting/moving
|
||||||
|
if target_path_obj.exists() and full_path != target_full_path:
|
||||||
|
self.io.tool_warning(
|
||||||
|
"UPDATE Warning: Target file for move already exists, overwriting:"
|
||||||
|
f" {action.move_path}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.io.tool_output(f"Updating {action.path}")
|
self.io.tool_output(f"Updating {action.path}")
|
||||||
|
|
||||||
|
|
||||||
# Ensure parent directory exists for target
|
# Ensure parent directory exists for target
|
||||||
target_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
target_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
@ -578,9 +596,10 @@ class PatchCoder(Coder):
|
||||||
# Raise a ValueError to signal failure, consistent with other coders.
|
# Raise a ValueError to signal failure, consistent with other coders.
|
||||||
raise ValueError(f"Error applying action '{action.type}' to {action.path}: {e}")
|
raise ValueError(f"Error applying action '{action.type}' to {action.path}: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch unexpected errors during application
|
# Catch unexpected errors during application
|
||||||
raise ValueError(f"Unexpected error applying action '{action.type}' to {action.path}: {e}")
|
raise ValueError(
|
||||||
|
f"Unexpected error applying action '{action.type}' to {action.path}: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def _apply_update(self, text: str, action: PatchAction, path: str) -> str:
|
def _apply_update(self, text: str, action: PatchAction, path: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -591,9 +610,9 @@ class PatchCoder(Coder):
|
||||||
# Should not be called otherwise, but check for safety
|
# Should not be called otherwise, but check for safety
|
||||||
raise DiffError("_apply_update called with non-update action")
|
raise DiffError("_apply_update called with non-update action")
|
||||||
|
|
||||||
orig_lines = text.splitlines() # Use splitlines to handle endings consistently
|
orig_lines = text.splitlines() # Use splitlines to handle endings consistently
|
||||||
dest_lines: List[str] = []
|
dest_lines: List[str] = []
|
||||||
current_orig_line_idx = 0 # Tracks index in orig_lines processed so far
|
current_orig_line_idx = 0 # Tracks index in orig_lines processed so far
|
||||||
|
|
||||||
# Sort chunks by their original index to apply them sequentially
|
# Sort chunks by their original index to apply them sequentially
|
||||||
sorted_chunks = sorted(action.chunks, key=lambda c: c.orig_index)
|
sorted_chunks = sorted(action.chunks, key=lambda c: c.orig_index)
|
||||||
|
@ -604,14 +623,14 @@ class PatchCoder(Coder):
|
||||||
chunk_start_index = chunk.orig_index
|
chunk_start_index = chunk.orig_index
|
||||||
|
|
||||||
if chunk_start_index < current_orig_line_idx:
|
if chunk_start_index < current_orig_line_idx:
|
||||||
# This indicates overlapping chunks or incorrect indices from parsing
|
# This indicates overlapping chunks or incorrect indices from parsing
|
||||||
raise DiffError(
|
raise DiffError(
|
||||||
f"{path}: Overlapping or out-of-order chunk detected."
|
f"{path}: Overlapping or out-of-order chunk detected."
|
||||||
f" Current index {current_orig_line_idx}, chunk starts at {chunk_start_index}."
|
f" Current index {current_orig_line_idx}, chunk starts at {chunk_start_index}."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add lines from original file between the last chunk and this one
|
# Add lines from original file between the last chunk and this one
|
||||||
dest_lines.extend(orig_lines[current_orig_line_idx : chunk_start_index])
|
dest_lines.extend(orig_lines[current_orig_line_idx:chunk_start_index])
|
||||||
|
|
||||||
# Verify that the lines to be deleted actually match the original file content
|
# Verify that the lines to be deleted actually match the original file content
|
||||||
# (The parser should have used find_context, but double-check here)
|
# (The parser should have used find_context, but double-check here)
|
||||||
|
@ -644,6 +663,6 @@ class PatchCoder(Coder):
|
||||||
|
|
||||||
# Join lines and ensure a single trailing newline
|
# Join lines and ensure a single trailing newline
|
||||||
result = "\n".join(dest_lines)
|
result = "\n".join(dest_lines)
|
||||||
if result or orig_lines: # Add newline unless result is empty and original was empty
|
if result or orig_lines: # Add newline unless result is empty and original was empty
|
||||||
result += "\n"
|
result += "\n"
|
||||||
return result
|
return result
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue