fix(tools): preserve each normalized replacement’s Unicode

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
(cherry picked from commit 8e18884e6b4638c8a91ba4fd678821e6812f6130)
This commit is contained in:
Rudy Celekli
2026-09-29 23:45:08 -04:00
committed by kshitij
parent 447b9ee7d6
commit 13f25e5fd9
2 changed files with 39 additions and 5 deletions

View File

@@ -0,0 +1,29 @@
"""Replacing every normalized match preserves each region's untouched Unicode."""
import json
from tools import file_tools, terminal_tool
from tools.registry import registry
def test_replace_all_preserves_each_matches_unicode(tmp_path, monkeypatch):
home = tmp_path / "home"
home.mkdir()
monkeypatch.setenv("HERMES_HOME", str(home))
path = tmp_path / "notes.txt"
original = "Price ‘old’—ready\nPrice 'old'—ready\n"
path.write_text(original, encoding="utf-8")
task = "unicode-replace-all"
terminal_tool.register_task_env_overrides(task, {"env_type": "local", "cwd": str(tmp_path)})
try:
read = json.loads(registry.dispatch("read_file", {"path": str(path)}, task_id=task))
assert "error" not in read
result = json.loads(registry.dispatch("patch", {
"mode": "replace", "path": str(path), "old_string": "Price 'old'--ready",
"new_string": "Price 'new'--ready", "replace_all": True,
}, task_id=task))
assert "error" not in result, result
assert path.read_text(encoding="utf-8") == original.replace("old", "new")
finally:
file_tools.clear_file_ops_cache(task)
terminal_tool.clear_task_env_overrides(task)

View File

@@ -389,11 +389,10 @@ def fuzzy_find_and_replace(content: str, old_string: str, new_string: str,
return content, 0, None, drift_err
effective_new = _maybe_unescape_new_string(new_string, content, matches)
if strategy_name == "unicode_normalized":
effective_new = _preserve_unicode_in_replacement(content, matches, old_string, effective_new)
new_content = _apply_replacements(
content, matches, effective_new,
old_string=old_string if strategy_name != "exact" else None)
old_string=old_string if strategy_name != "exact" else None,
preserve_unicode=strategy_name == "unicode_normalized")
_note_edit_match(strategy_name)
return new_content, len(matches), strategy_name, None
@@ -575,14 +574,20 @@ def _preserve_unicode_in_replacement(content: str, matches: list[Span],
def _apply_replacements(content: str, matches: list[Span],
new_string: str, old_string: Optional[str] = None) -> str:
new_string: str, old_string: Optional[str] = None,
preserve_unicode: bool = False) -> str:
"""Splice ``new_string`` over each span (end-to-start so offsets stay valid);
``old_string`` non-None (non-exact match) re-indents it per region."""
result = content
for start, end in sorted(matches, key=lambda x: x[0], reverse=True):
adjusted = new_string
if old_string is not None:
adjusted = _reindent_replacement(content[start:end], old_string, new_string)
if preserve_unicode:
# Each occurrence may use different typographic characters even
# though all normalize to the same old_string.
adjusted = _preserve_unicode_in_replacement(
content, [(start, end)], old_string, adjusted)
adjusted = _reindent_replacement(content[start:end], old_string, adjusted)
result = result[:start] + adjusted + result[end:]
return result