mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-14 23:21:04 +00:00
164 lines
4.8 KiB
Python
164 lines
4.8 KiB
Python
"""Diff utilities for edit tool."""
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from difflib import unified_diff
|
|
|
|
|
|
def detect_line_ending(content: str) -> str:
|
|
"""Detect line ending style (CRLF or LF)."""
|
|
crlf_idx = content.find("\r\n")
|
|
lf_idx = content.find("\n")
|
|
if lf_idx == -1:
|
|
return "\n"
|
|
if crlf_idx == -1:
|
|
return "\n"
|
|
return "\r\n" if crlf_idx < lf_idx else "\n"
|
|
|
|
|
|
def normalize_to_lf(text: str) -> str:
|
|
"""Normalize line endings to LF."""
|
|
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
|
|
|
|
def restore_line_endings(text: str, ending: str) -> str:
|
|
"""Restore original line endings."""
|
|
return text.replace("\n", ending) if ending == "\r\n" else text
|
|
|
|
|
|
def normalize_for_fuzzy_match(text: str) -> str:
|
|
"""Normalize text for fuzzy matching: strip trailing whitespace, normalize quotes/dashes."""
|
|
lines = text.split("\n")
|
|
normalized = "\n".join(line.rstrip() for line in lines)
|
|
|
|
# Smart quotes → ASCII
|
|
normalized = re.sub(r"[\u2018\u2019\u201A\u201B]", "'", normalized)
|
|
normalized = re.sub(r"[\u201C\u201D\u201E\u201F]", '"', normalized)
|
|
|
|
# Dashes → hyphen
|
|
normalized = re.sub(r"[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]", "-", normalized)
|
|
|
|
# Special spaces → regular space
|
|
normalized = re.sub(r"[\u00A0\u2002-\u200A\u202F\u205F\u3000]", " ", normalized)
|
|
|
|
return normalized
|
|
|
|
|
|
@dataclass
|
|
class FuzzyMatchResult:
|
|
"""Result of fuzzy text matching."""
|
|
|
|
found: bool
|
|
index: int
|
|
match_length: int
|
|
used_fuzzy_match: bool
|
|
content_for_replacement: str
|
|
|
|
|
|
def fuzzy_find_text(content: str, old_text: str) -> FuzzyMatchResult:
|
|
"""Find old_text in content, trying exact match first, then fuzzy match."""
|
|
# Try exact match
|
|
exact_index = content.find(old_text)
|
|
if exact_index != -1:
|
|
return FuzzyMatchResult(
|
|
found=True,
|
|
index=exact_index,
|
|
match_length=len(old_text),
|
|
used_fuzzy_match=False,
|
|
content_for_replacement=content,
|
|
)
|
|
|
|
# Try fuzzy match
|
|
fuzzy_content = normalize_for_fuzzy_match(content)
|
|
fuzzy_old_text = normalize_for_fuzzy_match(old_text)
|
|
fuzzy_index = fuzzy_content.find(fuzzy_old_text)
|
|
|
|
if fuzzy_index == -1:
|
|
return FuzzyMatchResult(
|
|
found=False,
|
|
index=-1,
|
|
match_length=0,
|
|
used_fuzzy_match=False,
|
|
content_for_replacement=content,
|
|
)
|
|
|
|
return FuzzyMatchResult(
|
|
found=True,
|
|
index=fuzzy_index,
|
|
match_length=len(fuzzy_old_text),
|
|
used_fuzzy_match=True,
|
|
content_for_replacement=fuzzy_content,
|
|
)
|
|
|
|
|
|
def strip_bom(content: str) -> tuple[str, str]:
|
|
"""Strip UTF-8 BOM, return (bom, text_without_bom)."""
|
|
if content.startswith("\ufeff"):
|
|
return "\ufeff", content[1:]
|
|
return "", content
|
|
|
|
|
|
@dataclass
|
|
class DiffResult:
|
|
"""Result of diff generation."""
|
|
|
|
diff: str
|
|
first_changed_line: int | None
|
|
|
|
|
|
def generate_diff_string(old_content: str, new_content: str, context_lines: int = 4) -> DiffResult:
|
|
"""Generate unified diff with line numbers."""
|
|
old_lines = old_content.split("\n")
|
|
new_lines = new_content.split("\n")
|
|
|
|
# Use difflib to get the changes
|
|
diff_lines = list(
|
|
unified_diff(
|
|
old_lines,
|
|
new_lines,
|
|
lineterm="",
|
|
n=context_lines,
|
|
),
|
|
)
|
|
|
|
if not diff_lines:
|
|
return DiffResult(diff="", first_changed_line=None)
|
|
|
|
# Parse and format the diff
|
|
output = []
|
|
first_changed_line = None
|
|
max_line_num = max(len(old_lines), len(new_lines))
|
|
line_num_width = len(str(max_line_num))
|
|
|
|
old_line_num = 1
|
|
new_line_num = 1
|
|
|
|
for line in diff_lines[2:]: # Skip header lines
|
|
if line.startswith("@@"):
|
|
# Parse hunk header
|
|
match = re.match(r"@@ -(\d+),?\d* \+(\d+),?\d* @@", line)
|
|
if match:
|
|
old_line_num = int(match.group(1))
|
|
new_line_num = int(match.group(2))
|
|
continue
|
|
|
|
if line.startswith("+"):
|
|
if first_changed_line is None:
|
|
first_changed_line = new_line_num
|
|
line_num = str(new_line_num).rjust(line_num_width)
|
|
output.append(f"+{line_num} {line[1:]}")
|
|
new_line_num += 1
|
|
elif line.startswith("-"):
|
|
if first_changed_line is None:
|
|
first_changed_line = new_line_num
|
|
line_num = str(old_line_num).rjust(line_num_width)
|
|
output.append(f"-{line_num} {line[1:]}")
|
|
old_line_num += 1
|
|
else:
|
|
# Context line
|
|
line_num = str(old_line_num).rjust(line_num_width)
|
|
output.append(f" {line_num} {line[1:] if line.startswith(' ') else line}")
|
|
old_line_num += 1
|
|
new_line_num += 1
|
|
|
|
return DiffResult(diff="\n".join(output), first_changed_line=first_changed_line)
|