Repository: All-Hands-AI/openhands-aci

Issue:
[Bug]: Editing Error "No replacement was performed" is not informative enough
Cross post from https://github.com/All-Hands-AI/OpenHands/issues/5365

Discussion:
ryanhoangt: Looks good, the resolve rate is the same as baseline.

```
14:46:18 - openhands:INFO: eval_infer.py:418 - # resolved: 6 / 10. (60.00%)
14:46:18 - openhands:INFO: eval_infer.py:418 - # failed_apply_patch: 0 / 10. (0.00%)
14:46:18 - openhands:INFO: eval_infer.py:418 - # error_eval: 0 / 10. (0.00%)
14:46:18 - openhands:INFO: eval_infer.py:418 - # empty_generation: 0 / 10. (0.00%)
```
Should we cut a release now and raise a PR to fix it in the OH repo, or wait for the navigation commands PR to be merged as well? We may also need to handle the <oh_aci_output> tag added to `main` previously.

Patch:
diff --git a/openhands_aci/editor/editor.py b/openhands_aci/editor/editor.py
index 0cbb0a1..e98354b 100644
--- a/openhands_aci/editor/editor.py
+++ b/openhands_aci/editor/editor.py
@@ -110,12 +110,17 @@ class OHEditor:
                 f'No replacement was performed, old_str `{old_str}` did not appear verbatim in {path}.'
             )
         if occurrences > 1:
-            file_content_lines = file_content.split('\n')
-            line_numbers = [
-                idx + 1
-                for idx, line in enumerate(file_content_lines)
-                if old_str in line
-            ]
+            # Find starting line numbers for each occurrence
+            line_numbers = []
+            start_idx = 0
+            while True:
+                idx = file_content.find(old_str, start_idx)
+                if idx == -1:
+                    break
+                # Count newlines before this occurrence to get the line number
+                line_num = file_content.count('\n', 0, idx) + 1
+                line_numbers.append(line_num)
+                start_idx = idx + 1
             raise ToolError(
                 f'No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {line_numbers}. Please ensure it is unique.'
             )


Test patch:
diff --git a/tests/integration/test_oh_editor.py b/tests/integration/test_oh_editor.py
index 39b493e..6cdb6d7 100644
--- a/tests/integration/test_oh_editor.py
+++ b/tests/integration/test_oh_editor.py
@@ -149,6 +149,28 @@ def test_str_replace_error_multiple_occurrences(editor):
             command='str_replace', path=str(test_file), old_str='test', new_str='sample'
         )
     assert 'Multiple occurrences of old_str `test`' in str(exc_info.value.message)
+    assert '[1, 2]' in str(exc_info.value.message)  # Should show both line numbers
+
+
+def test_str_replace_error_multiple_multiline_occurrences(editor):
+    editor, test_file = editor
+    # Create a file with two identical multi-line blocks
+    multi_block = """def example():
+    print("Hello")
+    return True"""
+    content = f"{multi_block}\n\nprint('separator')\n\n{multi_block}"
+    test_file.write_text(content)
+
+    with pytest.raises(ToolError) as exc_info:
+        editor(
+            command='str_replace',
+            path=str(test_file),
+            old_str=multi_block,
+            new_str='def new():\n    print("World")',
+        )
+    error_msg = str(exc_info.value.message)
+    assert 'Multiple occurrences of old_str' in error_msg
+    assert '[1, 7]' in error_msg  # Should show correct starting line numbers
 
 
 def test_str_replace_nonexistent_string(editor):