diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java index 268c16d2f09..f86c9762e71 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/unifieddiff/internal/UnifiedDiffManager.java @@ -871,14 +871,21 @@ private static List computeDiffs(IDocument leftDocument, IDocument case RangeDifference.NOCHANGE: break; case RangeDifference.CHANGE: - var diff = new UnifiedDiff(leftDocument, leftStart, leftEnd, leftDiffSource, rightDocument, - rightStart, rightEnd, rightDiffSource, unifiedDiffs, mode); - unifiedDiffs.add(diff); - // line based fine granular diff via DocumentMerger#simpleTokenDiff ITokenComparator l = createTokenComparator(leftDiffSource, tokenComparatorFactory); ITokenComparator r = createTokenComparator(rightDiffSource, tokenComparatorFactory); RangeDifference[] detailedDiffs = RangeDifferencer.findRanges((IRangeComparator) null, l, r); + // The token comparator defines what counts as a real difference. When it + // reports none, the region is treated as equal in every mode: no annotation + // is shown, and REPLACE_MODE leaves the text as it is rather than rewriting + // it to a form the comparator considers identical. + if (!hasDetailedChanges(detailedDiffs)) { + break; + } + var diff = new UnifiedDiff(leftDocument, leftStart, leftEnd, leftDiffSource, rightDocument, + rightStart, rightEnd, rightDiffSource, unifiedDiffs, mode); + unifiedDiffs.add(diff); + for (RangeDifference detailedDiff : detailedDiffs) { if (detailedDiff.kind() == RangeDifference.NOCHANGE) { continue; @@ -918,6 +925,15 @@ private static List computeDiffs(IDocument leftDocument, IDocument return unifiedDiffs; } + private static boolean hasDetailedChanges(RangeDifference[] detailedDiffs) { + for (RangeDifference detailedDiff : detailedDiffs) { + if (detailedDiff.kind() != RangeDifference.NOCHANGE) { + return true; + } + } + return false; + } + public static void error(Exception e) { Platform.getLog(UnifiedDiffManager.class).error(e.getMessage(), e); } diff --git a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java index c2df06a69d3..1fafd7ac1e9 100644 --- a/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java +++ b/team/tests/org.eclipse.team.tests.core/src/org/eclipse/team/tests/ui/UnifiedDiffManagerTest.java @@ -27,6 +27,8 @@ import java.util.Iterator; import java.util.List; +import org.eclipse.compare.contentmergeviewer.ITokenComparator; +import org.eclipse.compare.rangedifferencer.IRangeComparator; import org.eclipse.compare.unifieddiff.UnifiedDiff; import org.eclipse.compare.unifieddiff.UnifiedDiffMode; import org.eclipse.compare.unifieddiff.internal.UnifiedDiffManager; @@ -255,6 +257,41 @@ public void testReplaceModeAnnotatesTheAppliedText() throws BadLocationException "the annotation must cover the text that was inserted into the document"); } + /** + * The token comparator, not the mode, decides what counts as a difference. A + * case-insensitive comparator reports no difference between "line one" and + * "LINE ONE", so REPLACE_MODE must leave the document untouched rather than + * rewrite it to a form the comparator already considers identical. + */ + @Test + public void testReplaceModeKeepsDocumentWhenTokenComparatorTreatsChangeAsEqual() { + setEditorContent("line one\n"); + + assertTrue(UnifiedDiff.create(editor, "LINE ONE\n", UnifiedDiffMode.REPLACE_MODE).ignoreWhiteSpace(false) + .tokenComparatorFactory(CaseInsensitiveTokenComparator::new).open().isOK()); + + assertTrue(UnifiedDiffManager.get(viewer()).isEmpty(), + "a change the token comparator treats as equal must not be applied, even in REPLACE_MODE"); + assertEquals("line one\n", document().get(), + "REPLACE_MODE must leave the document untouched when the comparator sees no real difference"); + } + + /** + * The counterpart to {@link #testReplaceModeKeepsDocumentWhenTokenComparatorTreatsChangeAsEqual()}: + * with the default comparator a case change is a real difference, so REPLACE_MODE + * applies it and the document becomes the compared source. + */ + @Test + public void testReplaceModeAppliesCaseChangeWithDefaultComparator() { + setEditorContent("line one\n"); + + assertTrue(UnifiedDiff.create(editor, "LINE ONE\n", UnifiedDiffMode.REPLACE_MODE).ignoreWhiteSpace(false).open() + .isOK()); + + assertEquals("LINE ONE\n", document().get(), + "with the default comparator a case change is a real difference and must be applied"); + } + // ---------------------------------------------------- non modifying modes @Test @@ -334,6 +371,17 @@ public void testWhitespaceOnlyChangeIsReportedWhenNotIgnored() { "with ignoreWhiteSpace(false) the changed indentation is a diff"); } + @Test + public void testTokenComparatorCanIgnoreALineChange() { + setEditorContent("line one\n"); + + assertTrue(UnifiedDiff.create(editor, "LINE ONE\n", UnifiedDiffMode.OVERLAY_MODE) + .ignoreWhiteSpace(false).tokenComparatorFactory(CaseInsensitiveTokenComparator::new).open().isOK()); + + assertTrue(UnifiedDiffManager.get(viewer()).isEmpty(), + "a change ignored by the token comparator must not create a parent diff"); + } + /** * A diff at the very end of the document is shown as a code mining. Only a line * header mining reserves its height in the text widget, so an emptied file must @@ -433,6 +481,39 @@ private void assertReplaceModeYields(String left, String right) { assertEquals(right, document().get(), "REPLACE_MODE must transform the document into the compared source"); } + private static final class CaseInsensitiveTokenComparator implements ITokenComparator { + private final String text; + + CaseInsensitiveTokenComparator(String text) { + this.text = text; + } + + @Override + public int getRangeCount() { + return 1; + } + + @Override + public int getTokenStart(int index) { + return index == 0 ? 0 : text.length(); + } + + @Override + public int getTokenLength(int index) { + return index == 0 ? text.length() : 0; + } + + @Override + public boolean rangesEqual(int thisIndex, IRangeComparator other, int otherIndex) { + return other instanceof CaseInsensitiveTokenComparator comparator && text.equalsIgnoreCase(comparator.text); + } + + @Override + public boolean skipRangeComparison(int length, int maxLength, IRangeComparator other) { + return false; + } + } + private ITextViewer viewer() { ITextViewer viewer = editor.getAdapter(ITextViewer.class); assertNotNull(viewer, "editor must adapt to ITextViewer");