Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/tool-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ Unknown algorithms raise `ValueError`.

## compare_text(before, after)

Returns `added_lines`, `removed_lines`, and a unified `diff` string. A replacement
counts as both removed and added lines. Identical inputs return an empty diff.
Returns `added_lines`, `removed_lines`, `unchanged_lines`, and a unified `diff`
string. A replacement counts as both removed and added lines, and `unchanged_lines`
counts the matching lines in the same alignment, so `added_lines + unchanged_lines`
equals the number of lines in `after`. Identical inputs return an empty diff.
Comparison ignores CRLF versus LF and the presence of a final newline;
other whitespace is significant. Empty input is supported.

Expand Down
11 changes: 8 additions & 3 deletions src/mcp_tools_lab/tools/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@


def compare_text(before: str, after: str) -> dict:
"""Return a unified diff and counts; ignore line-ending style and final newline."""
"""Return a unified diff and counts; ignore line-ending style and final newline.

unchanged_lines counts the lines the alignment marks as equal.
"""
old_lines, new_lines = before.splitlines(), after.splitlines()
added = removed = 0
added = removed = unchanged = 0
matcher = difflib.SequenceMatcher(None, old_lines, new_lines, autojunk=False)
for tag, old_start, old_end, new_start, new_end in matcher.get_opcodes():
if tag in {"replace", "delete"}:
removed += old_end - old_start
if tag in {"replace", "insert"}:
added += new_end - new_start
if tag == "equal":
unchanged += old_end - old_start
diff = "\n".join(difflib.unified_diff(old_lines, new_lines, fromfile="before", tofile="after", lineterm=""))
return {"added_lines": added, "removed_lines": removed, "diff": diff}
return {"added_lines": added, "removed_lines": removed, "unchanged_lines": unchanged, "diff": diff}
27 changes: 27 additions & 0 deletions tests/test_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,30 @@ def test_equal_empty_and_newlines():
assert compare_text("same\r\n", "same")["diff"] == ""
assert compare_text("", "new")["added_lines"] == 1
assert compare_text("old", "")["removed_lines"] == 1


def test_unchanged_lines_for_replacement():
result = compare_text("hello\nold", "hello\nnew")
assert result["unchanged_lines"] == 1
assert result["added_lines"] == 1
assert result["removed_lines"] == 1


def test_unchanged_lines_identical_and_empty():
assert compare_text("one\ntwo", "one\ntwo")["unchanged_lines"] == 2
assert compare_text("", "")["unchanged_lines"] == 0


def test_unchanged_lines_entirely_different():
assert compare_text("old", "new")["unchanged_lines"] == 0


def test_unchanged_lines_ignores_line_endings_and_final_newline():
assert compare_text("same\r\n", "same")["unchanged_lines"] == 1


def test_unchanged_lines_with_repeated_lines():
result = compare_text("a\nb\nb\nc", "a\nb\nc\nc")
assert result["unchanged_lines"] + result["added_lines"] == 4
assert result["unchanged_lines"] + result["removed_lines"] == 4
assert result["unchanged_lines"] == 3
Loading