diff --git a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java index 0c3c428c743..06d16a0519a 100644 --- a/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java +++ b/team/bundles/org.eclipse.compare/compare/org/eclipse/compare/contentmergeviewer/TextMergeViewer.java @@ -502,6 +502,18 @@ private static RGB interpolate(RGB fg, RGB bg, double scale) { private boolean isConfigured = false; private boolean fRedoDiff = false; + /** + * Combined line count of both sides from which on the first comparison is + * deferred, so that the documents are painted before they are compared. Smaller + * inputs are compared right away: the diff costs next to nothing there, and + * showing the text and jumping to the first change in two steps would only + * flicker. + */ + private static final int DEFER_DIFF_LINE_COUNT = 2000; + + /** Pending first diff of the current input, scheduled after the documents are set. */ + private UIJob fInitialDiffJob; + private double fCurrMagni = 0; private int fCurrentHeight; @@ -2103,6 +2115,11 @@ protected String getDocumentPartitioning() { protected void handleDispose(DisposeEvent event) { OperationHistoryFactory.getOperationHistory().removeOperationHistoryListener(operationHistoryListener); + if (fInitialDiffJob != null) { + fInitialDiffJob.cancel(); + fInitialDiffJob = null; + } + if (fHandlerService != null) { fHandlerService.dispose(); } @@ -3211,41 +3228,73 @@ protected void updateContent(Object ancestor, Object left, Object right) { setSyncScrolling(fPreferenceStore.getBoolean(ComparePreferencePage.SYNCHRONIZE_SCROLLING)); - update(false); - - if (!fHasErrors && !emptyInput && !fComposite.isDisposed()) { - if (isRefreshing()) { - fLeftContributor.updateSelection(fLeft, !fSynchronizedScrolling); - fRightContributor.updateSelection(fRight, !fSynchronizedScrolling); - fAncestorContributor.updateSelection(fAncestor, !fSynchronizedScrolling); - if (fSynchronizedScrolling && fSynchronziedScrollPosition != -1) { - synchronizedScrollVertical(fSynchronziedScrollPosition); - } - } else { - if (isPatchHunk()) { - if (right != null && Adapters.adapt(right, IHunk.class) != null) { - fLeft.getSourceViewer().setTopIndex(getHunkStart()); - } else { - fRight.getSourceViewer().setTopIndex(getHunkStart()); + // A refresh restores the cached selection and scroll position, and that cache is + // dropped as soon as the refresh returns, so a refresh is never deferred. + if (isRefreshing() || fLeftLineCount + fRightLineCount <= DEFER_DIFF_LINE_COUNT) { + update(false); + if (!fHasErrors && !emptyInput && !fComposite.isDisposed()) { + if (isRefreshing()) { + fLeftContributor.updateSelection(fLeft, !fSynchronizedScrolling); + fRightContributor.updateSelection(fRight, !fSynchronizedScrolling); + fAncestorContributor.updateSelection(fAncestor, !fSynchronizedScrolling); + if (fSynchronizedScrolling && fSynchronziedScrollPosition != -1) { + synchronizedScrollVertical(fSynchronziedScrollPosition); } } else { - Diff selectDiff= null; - if (FIX_47640) { - if (leftRange != null) { - selectDiff= fMerger.findDiff(LEFT_CONTRIBUTOR, leftRange); - } else if (rightRange != null) { - selectDiff= fMerger.findDiff(RIGHT_CONTRIBUTOR, rightRange); - } - } - if (selectDiff != null) { - setCurrentDiff(selectDiff, true); - } else { - selectFirstDiff(true); - } + revealInitialDiff(right, leftRange, rightRange); } } + return; } + // The documents are set, so let them be painted before comparing them. The diff + // and everything derived from it follows in a separate UI event. + final boolean isEmptyInput = emptyInput; + final Object rightElement = right; + final Position leftSelectRange = leftRange; + final Position rightSelectRange = rightRange; + if (fInitialDiffJob != null) { + fInitialDiffJob.cancel(); + } + fInitialDiffJob = new UIJob(CompareMessages.DocumentMerger_0) { + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + fInitialDiffJob = null; + if (fComposite == null || fComposite.isDisposed()) { + return Status.OK_STATUS; + } + update(false); + if (!fHasErrors && !isEmptyInput && !fComposite.isDisposed()) { + revealInitialDiff(rightElement, leftSelectRange, rightSelectRange); + } + return Status.OK_STATUS; + } + }; + fInitialDiffJob.schedule(); + } + + private void revealInitialDiff(Object right, Position leftRange, Position rightRange) { + if (isPatchHunk()) { + if (right != null && Adapters.adapt(right, IHunk.class) != null) { + fLeft.getSourceViewer().setTopIndex(getHunkStart()); + } else { + fRight.getSourceViewer().setTopIndex(getHunkStart()); + } + return; + } + Diff selectDiff= null; + if (FIX_47640) { + if (leftRange != null) { + selectDiff= fMerger.findDiff(LEFT_CONTRIBUTOR, leftRange); + } else if (rightRange != null) { + selectDiff= fMerger.findDiff(RIGHT_CONTRIBUTOR, rightRange); + } + } + if (selectDiff != null) { + setCurrentDiff(selectDiff, true); + } else { + selectFirstDiff(true); + } } private void configureSourceViewer(SourceViewer sourceViewer, boolean editable, ContributorInfo contributor) { diff --git a/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/TextMergeViewerTest.java b/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/TextMergeViewerTest.java index 7b9e2f62a98..b7f409a083c 100644 --- a/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/TextMergeViewerTest.java +++ b/team/tests/org.eclipse.compare.tests/src/org/eclipse/compare/tests/TextMergeViewerTest.java @@ -459,6 +459,55 @@ public void testCopyEmptyLeftToRightAndModify() throws Exception { } } + /** + * A large input must be shown before it is compared, so the differences are only + * available once the deferred comparison ran. + */ + @Test + public void testLargeInputIsShownBeforeItIsCompared() throws Exception { + DiffNode parentNode = new DiffNode(new ParentTestElement(), new ParentTestElement()); + DiffNode testNode = new DiffNode(parentNode, Differencer.CHANGE, null, + new EditableTestElement(manyLines("line", 3000).getBytes()), + new EditableTestElement(manyLines("LINE", 3000).getBytes())); + + runInDialog(testNode, () -> { + IMergeViewerTestAdapter ta = viewer.getAdapter(IMergeViewerTestAdapter.class); + assertEquals(0, ta.getChangesCount(), "a large input must not be compared before it is shown"); + waitForChanges(ta); + assertTrue(ta.getChangesCount() > 0, "the deferred comparison did not produce differences"); + }); + } + + private static String manyLines(String prefix, int count) { + StringBuilder content = new StringBuilder(); + for (int i = 0; i < count; i++) { + content.append(prefix).append(' ').append(i).append('\n'); + } + return content.toString(); + } + + private static void waitForChanges(IMergeViewerTestAdapter ta) { + Display display = Display.getCurrent(); + long deadline = System.currentTimeMillis() + 30_000; + // A self-rescheduling timer keeps the loop waking so the deadline is enforced + // even while blocked in Display.sleep(). + Runnable[] wake = new Runnable[1]; + wake[0] = () -> display.timerExec(50, wake[0]); + display.timerExec(50, wake[0]); + try { + while (ta.getChangesCount() == 0) { + if (System.currentTimeMillis() > deadline) { + fail("no differences within 30000ms"); + } + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } finally { + display.timerExec(-1, wake[0]); + } + } + @Test public void testCompareFilter() throws Exception { DiffNode parentNode = new DiffNode(new ParentTestElement(),