From 00f73eefad4d7e8de5730a906eac7bb2a2bea291 Mon Sep 17 00:00:00 2001 From: sakarpaisen Date: Thu, 22 Oct 2026 16:04:27 +0300 Subject: [PATCH] Count unchanged lines in the text comparison compare_text now reports unchanged_lines, the size of the equal opcodes in the alignment it already builds, so the diff string and the added/removed counts are unchanged. Tests cover the documented replacement example, identical and empty input, entirely different input, line-ending normalization, and repeated lines where the alignment is ambiguous. --- docs/tool-guide.md | 6 ++++-- src/mcp_tools_lab/tools/diff.py | 11 ++++++++--- tests/test_diff.py | 27 +++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/docs/tool-guide.md b/docs/tool-guide.md index ed54a58..7e737cc 100644 --- a/docs/tool-guide.md +++ b/docs/tool-guide.md @@ -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. diff --git a/src/mcp_tools_lab/tools/diff.py b/src/mcp_tools_lab/tools/diff.py index 4a677c4..f87b81b 100644 --- a/src/mcp_tools_lab/tools/diff.py +++ b/src/mcp_tools_lab/tools/diff.py @@ -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} diff --git a/tests/test_diff.py b/tests/test_diff.py index c3f2a0a..7f4923e 100644 --- a/tests/test_diff.py +++ b/tests/test_diff.py @@ -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