From b4badef6fe83600c75fcaae08fce499db36d70be Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 13 Apr 2015 19:30:16 +0300 Subject: [PATCH] diff: iterating between differences should make both sides visible * work around SyncScroll issue with big insertion/deletion at the end of file * move carets in all editors to change --- .../diff/tools/simple/SimpleDiffViewer.java | 15 +- .../simple/SimpleThreesideDiffViewer.java | 1 + .../diff/tools/util/SyncScrollSupport.java | 194 ++++++++++++++---- .../util/twoside/TwosideTextDiffViewer.java | 18 +- .../src/com/intellij/diff/util/DiffUtil.java | 14 +- 5 files changed, 190 insertions(+), 52 deletions(-) diff --git a/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleDiffViewer.java b/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleDiffViewer.java index 761a7ce8467f..f81a25c9b238 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleDiffViewer.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleDiffViewer.java @@ -411,12 +411,19 @@ public class SimpleDiffViewer extends TwosideTextDiffViewer { return true; } - private void doScrollToChange(@NotNull SimpleDiffChange change, boolean animated) { + private void doScrollToChange(@NotNull SimpleDiffChange change, final boolean animated) { if (myEditor1 == null || myEditor2 == null) return; + assert mySyncScrollSupport != null; - EditorEx editor = getCurrentEditor(); - int line = change.getStartLine(getCurrentSide()); - DiffUtil.scrollEditor(editor, line, animated); + final int line1 = change.getStartLine(Side.LEFT); + final int line2 = change.getStartLine(Side.RIGHT); + final int endLine1 = change.getEndLine(Side.LEFT); + final int endLine2 = change.getEndLine(Side.RIGHT); + + DiffUtil.moveCaret(myEditor1, line1); + DiffUtil.moveCaret(myEditor2, line2); + + mySyncScrollSupport.makeVisible(getCurrentSide(), line1, endLine1, line2, endLine2, animated); } @Override diff --git a/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleThreesideDiffViewer.java b/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleThreesideDiffViewer.java index 64e54c98258b..1471ad397d33 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleThreesideDiffViewer.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleThreesideDiffViewer.java @@ -346,6 +346,7 @@ public class SimpleThreesideDiffViewer extends ThreesideTextDiffViewer { } private void doScrollToChange(@NotNull SimpleThreesideDiffChange change, boolean animated) { + // TODO: use anchors to fix scrolling issue at the start/end of file EditorEx editor = getCurrentEditor(); int line = change.getStartLine(getCurrentSide()); DiffUtil.scrollEditor(editor, line, animated); diff --git a/platform/diff-impl/src/com/intellij/diff/tools/util/SyncScrollSupport.java b/platform/diff-impl/src/com/intellij/diff/tools/util/SyncScrollSupport.java index 68788e3b9030..3ae8591b103e 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/util/SyncScrollSupport.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/util/SyncScrollSupport.java @@ -15,15 +15,18 @@ */ package com.intellij.diff.tools.util; +import com.intellij.diff.util.IntPair; import com.intellij.diff.util.Side; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.ScrollingModel; import com.intellij.openapi.editor.event.VisibleAreaEvent; import com.intellij.openapi.editor.event.VisibleAreaListener; +import com.intellij.openapi.editor.ex.EditorEx; import gnu.trove.TIntFunction; import org.jetbrains.annotations.CalledInAwt; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; @@ -82,6 +85,51 @@ public class SyncScrollSupport { public boolean isDuringSyncScroll() { return myDuringSyncScroll; } + + public void makeVisible(@NotNull Side masterSide, + int startLine1, int endLine1, int startLine2, int endLine2, + final boolean animate) { + Side slaveSide = masterSide.other(); + + final IntPair offsets = getTargetOffsets(myEditor1, myEditor2, startLine1, endLine1, startLine2, endLine2); + + final Editor masterEditor = masterSide.select(myEditor1, myEditor2); + final Editor slaveEditor = slaveSide.select(myEditor1, myEditor2); + + final int masterOffset = masterSide.select(offsets.val1, offsets.val2); + final int slaveOffset = slaveSide.select(offsets.val1, offsets.val2); + + int startOffset1 = myEditor1.getScrollingModel().getVisibleArea().y; + int startOffset2 = myEditor2.getScrollingModel().getVisibleArea().y; + final int masterStartOffset = masterSide.select(startOffset1, startOffset2); + + myHelper1.setAnchor(startOffset1, offsets.val1, startOffset2, offsets.val2); + myHelper2.setAnchor(startOffset2, offsets.val2, startOffset1, offsets.val1); + + doScrollHorizontally(masterEditor, 0, false); // animation will be canceled by "scroll vertically" anyway + doScrollVertically(masterEditor, masterOffset, animate); + + masterEditor.getScrollingModel().runActionOnScrollingFinished(new Runnable() { + @Override + public void run() { + myHelper1.removeAnchor(); + myHelper2.removeAnchor(); + + if (masterOffset == masterStartOffset) { // master editor didn't scrolled + myDuringSyncScroll = true; + + doScrollVertically(slaveEditor, slaveOffset, animate); + + slaveEditor.getScrollingModel().runActionOnScrollingFinished(new Runnable() { + @Override + public void run() { + myDuringSyncScroll = false; + } + }); + } + } + }); + } } public static class ThreesideSyncScrollSupport { @@ -157,78 +205,150 @@ public class SyncScrollSupport { private static class MyScrollHelper implements VisibleAreaListener { @NotNull private final Editor myMaster; @NotNull private final Editor mySlave; - @NotNull private final TIntFunction myConvertor; + @Nullable private Anchor myAnchor; + public MyScrollHelper(@NotNull Editor master, @NotNull Editor slave, @NotNull TIntFunction convertor) { myMaster = master; mySlave = slave; myConvertor = convertor; } + public void setAnchor(int masterStartOffset, int masterEndOffset, int slaveStartOffset, int slaveEndOffset) { + myAnchor = new Anchor(masterStartOffset, masterEndOffset, slaveStartOffset, slaveEndOffset); + } + + public void removeAnchor() { + myAnchor = null; + } + @Override public void visibleAreaChanged(VisibleAreaEvent e) { Rectangle newRectangle = e.getNewRectangle(); Rectangle oldRectangle = e.getOldRectangle(); if (oldRectangle == null) return; - syncVerticalScroll(newRectangle, oldRectangle); - syncHorizontalScroll(newRectangle, oldRectangle); + if (newRectangle.x != oldRectangle.x) syncHorizontalScroll(false); + if (newRectangle.y != oldRectangle.y) syncVerticalScroll(false); } - private void syncVerticalScroll(@NotNull Rectangle newRectangle, @NotNull Rectangle oldRectangle) { - if (newRectangle.y == oldRectangle.y) return; - + private void syncVerticalScroll(boolean animated) { if (myMaster.getDocument().getTextLength() == 0) return; - int masterVerticalScrollOffset = myMaster.getScrollingModel().getVerticalScrollOffset(); - Rectangle viewRect = myMaster.getScrollingModel().getVisibleArea(); int middleY = viewRect.height / 3; - LogicalPosition masterPos = myMaster.xyToLogicalPosition(new Point(viewRect.x, masterVerticalScrollOffset + middleY)); - int masterCenterLine = masterPos.line; - int scrollToLine = myConvertor.execute(masterCenterLine); + int offset; + if (myAnchor == null) { + LogicalPosition masterPos = myMaster.xyToLogicalPosition(new Point(viewRect.x, viewRect.y + middleY)); + int masterCenterLine = masterPos.line; + int convertedCenterLine = myConvertor.execute(masterCenterLine); - int correction = (masterVerticalScrollOffset + middleY) % myMaster.getLineHeight(); - Point point = mySlave.logicalPositionToXY(new LogicalPosition(scrollToLine, masterPos.column)); - int offset = point.y - middleY + correction; + Point point = mySlave.logicalPositionToXY(new LogicalPosition(convertedCenterLine, masterPos.column)); + int correction = (viewRect.y + middleY) % myMaster.getLineHeight(); + offset = point.y - middleY + correction; + } + else { + double progress = myAnchor.masterStartOffset == myAnchor.masterEndOffset || viewRect.y == myAnchor.masterEndOffset ? 1 : + ((double)(viewRect.y - myAnchor.masterStartOffset)) / (myAnchor.masterEndOffset - myAnchor.masterStartOffset); + + offset = myAnchor.slaveStartOffset + (int)((myAnchor.slaveEndOffset - myAnchor.slaveStartOffset) * progress); + } int deltaHeaderOffset = getHeaderOffset(mySlave) - getHeaderOffset(myMaster); - doScrollVertically(mySlave.getScrollingModel(), offset + deltaHeaderOffset); + doScrollVertically(mySlave, offset + deltaHeaderOffset, animated); } - private void syncHorizontalScroll(@NotNull Rectangle newRectangle, @NotNull Rectangle oldRectangle) { - if (newRectangle.x == oldRectangle.x) return; - - int offset = newRectangle.x; - - doScrollHorizontally(mySlave.getScrollingModel(), offset); + private void syncHorizontalScroll(boolean animated) { + int offset = myMaster.getScrollingModel().getVisibleArea().x; + doScrollHorizontally(mySlave, offset, animated); } } - private static void doScrollVertically(@NotNull ScrollingModel model, int offset) { - model.disableAnimation(); - try { - model.scrollVertically(offset); - } - finally { - model.enableAnimation(); - } + private static void doScrollVertically(@NotNull Editor editor, int offset, boolean animated) { + ScrollingModel model = editor.getScrollingModel(); + if (!animated) model.disableAnimation(); + model.scrollVertically(offset); + if (!animated) model.enableAnimation(); } - private static void doScrollHorizontally(@NotNull ScrollingModel model, int offset) { - model.disableAnimation(); - try { - model.scrollHorizontally(offset); - } - finally { - model.enableAnimation(); - } + private static void doScrollHorizontally(@NotNull Editor editor, int offset, boolean animated) { + ScrollingModel model = editor.getScrollingModel(); + if (!animated) model.disableAnimation(); + model.scrollHorizontally(offset); + if (!animated) model.enableAnimation(); } private static int getHeaderOffset(@NotNull final Editor editor) { final JComponent header = editor.getHeaderComponent(); return header == null ? 0 : header.getHeight(); } + + @NotNull + private static IntPair getTargetOffsets(@NotNull Editor editor1, @NotNull Editor editor2, + int startLine1, int endLine1, int startLine2, int endLine2) { + int topOffset1 = editor1.logicalPositionToXY(new LogicalPosition(startLine1, 0)).y; + int bottomOffset1 = editor1.logicalPositionToXY(new LogicalPosition(endLine1 + 1, 0)).y; + int topOffset2 = editor2.logicalPositionToXY(new LogicalPosition(startLine2, 0)).y; + int bottomOffset2 = editor2.logicalPositionToXY(new LogicalPosition(endLine2 + 1, 0)).y; + + int rangeHeight1 = bottomOffset1 - topOffset1; + int rangeHeight2 = bottomOffset2 - topOffset2; + + int gapLines1 = 2 * editor1.getLineHeight(); + int gapLines2 = 2 * editor2.getLineHeight(); + + int editorHeight1 = editor1.getScrollingModel().getVisibleArea().height; + int editorHeight2 = editor2.getScrollingModel().getVisibleArea().height; + + int maximumOffset1 = ((EditorEx)editor1).getScrollPane().getVerticalScrollBar().getMaximum() - editorHeight1; + int maximumOffset2 = ((EditorEx)editor1).getScrollPane().getVerticalScrollBar().getMaximum() - editorHeight2; + + // 'shift' here - distance between editor's top and first line of range + + // make whole range visible. If possible, locate it at 'center' (1/3 of height) + // If can't show whole range - show as much as we can + boolean canShow1 = 2 * gapLines1 + rangeHeight1 <= editorHeight1; + boolean canShow2 = 2 * gapLines2 + rangeHeight2 <= editorHeight2; + + int topShift1 = canShow1 ? Math.min(editorHeight1 - gapLines1 - rangeHeight1, editorHeight1 / 3) : gapLines1; + int topShift2 = canShow2 ? Math.min(editorHeight2 - gapLines2 - rangeHeight2, editorHeight2 / 3) : gapLines2; + + int topShift = Math.min(topShift1, topShift2); + + // check if we're at the top of file + topShift = Math.min(topShift, Math.min(topOffset1, topOffset2)); + + int offset1 = topOffset1 - topShift; + int offset2 = topOffset2 - topShift; + if (maximumOffset1 > offset1 && maximumOffset2 > offset2) return new IntPair(offset1, offset2); + + // One of the ranges is at end of file - we can't scroll where we want to. + topShift = Math.min(topOffset1 - maximumOffset1, topOffset2 - maximumOffset2); + + // Try to show as much of range as we can (even if it breaks alignment) + offset1 = topOffset1 - topShift + Math.max(topShift + rangeHeight1 + gapLines1 - editorHeight1, 0); + offset2 = topOffset2 - topShift + Math.max(topShift + rangeHeight2 + gapLines2 - editorHeight2, 0); + + // always show top of the range + offset1 = Math.min(offset1, topOffset1 - gapLines1); + offset2 = Math.min(offset2, topOffset2 - gapLines2); + + return new IntPair(offset1, offset2); + } + + private static class Anchor { + public final int masterStartOffset; + public final int masterEndOffset; + public final int slaveStartOffset; + public final int slaveEndOffset; + + public Anchor(int masterStartOffset, int masterEndOffset, int slaveStartOffset, int slaveEndOffset) { + this.masterStartOffset = masterStartOffset; + this.masterEndOffset = masterEndOffset; + this.slaveStartOffset = slaveStartOffset; + this.slaveEndOffset = slaveEndOffset; + } + } } diff --git a/platform/diff-impl/src/com/intellij/diff/tools/util/twoside/TwosideTextDiffViewer.java b/platform/diff-impl/src/com/intellij/diff/tools/util/twoside/TwosideTextDiffViewer.java index cc81f9d39742..011b4459bc02 100644 --- a/platform/diff-impl/src/com/intellij/diff/tools/util/twoside/TwosideTextDiffViewer.java +++ b/platform/diff-impl/src/com/intellij/diff/tools/util/twoside/TwosideTextDiffViewer.java @@ -82,7 +82,7 @@ public abstract class TwosideTextDiffViewer extends TextDiffViewerBase { @NotNull private final MyScrollToLineHelper myScrollToLineHelper = new MyScrollToLineHelper(); - @Nullable private TwosideSyncScrollSupport mySyncScrollListener; + @Nullable protected TwosideSyncScrollSupport mySyncScrollSupport; @NotNull private Side myCurrentSide; @@ -230,7 +230,7 @@ public abstract class TwosideTextDiffViewer extends TextDiffViewerBase { if (myEditor1 != null && myEditor2 != null) { SyncScrollSupport.SyncScrollable scrollable = getSyncScrollable(); if (scrollable != null) { - mySyncScrollListener = new TwosideSyncScrollSupport(myEditor1, myEditor2, scrollable); + mySyncScrollSupport = new TwosideSyncScrollSupport(myEditor1, myEditor2, scrollable); } } } @@ -248,15 +248,15 @@ public abstract class TwosideTextDiffViewer extends TextDiffViewerBase { myEditor2.getScrollingModel().removeVisibleAreaListener(myVisibleAreaListener); } if (myEditor1 != null && myEditor2 != null) { - if (mySyncScrollListener != null) { - mySyncScrollListener = null; + if (mySyncScrollSupport != null) { + mySyncScrollSupport = null; } } } protected void disableSyncScrollSupport(boolean disable) { - if (mySyncScrollListener != null) { - mySyncScrollListener.myDuringSyncScroll = disable; + if (mySyncScrollSupport != null) { + mySyncScrollSupport.myDuringSyncScroll = disable; } } @@ -325,8 +325,8 @@ public abstract class TwosideTextDiffViewer extends TextDiffViewerBase { @CalledInAwt @NotNull protected LogicalPosition transferPosition(@NotNull Side baseSide, @NotNull LogicalPosition position) { - if (mySyncScrollListener == null) return position; - int line = mySyncScrollListener.getScrollable().transfer(baseSide, position.line); + if (mySyncScrollSupport == null) return position; + int line = mySyncScrollSupport.getScrollable().transfer(baseSide, position.line); return new LogicalPosition(line, position.column); } @@ -479,7 +479,7 @@ public abstract class TwosideTextDiffViewer extends TextDiffViewerBase { private class MyVisibleAreaListener implements VisibleAreaListener { @Override public void visibleAreaChanged(VisibleAreaEvent e) { - if (mySyncScrollListener != null) mySyncScrollListener.visibleAreaChanged(e); + if (mySyncScrollSupport != null) mySyncScrollSupport.visibleAreaChanged(e); if (Registry.is("diff.divider.repainting.fix")) { myContentPanel.repaint(); } diff --git a/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java b/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java index b2af70e18c92..d1df97e088b3 100644 --- a/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java +++ b/platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java @@ -191,6 +191,12 @@ public class DiffUtil { // Scrolling // + public static void moveCaret(@Nullable final Editor editor, int line) { + if (editor == null) return; + editor.getCaretModel().removeSecondaryCarets(); + editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(line, 0)); + } + public static void scrollEditor(@Nullable final Editor editor, int line, boolean animated) { scrollEditor(editor, line, 0, animated); } @@ -203,11 +209,15 @@ public class DiffUtil { } public static void scrollToPoint(@Nullable Editor editor, @NotNull Point point) { + scrollToPoint(editor, point, false); + } + + public static void scrollToPoint(@Nullable Editor editor, @NotNull Point point, boolean animated) { if (editor == null) return; - editor.getScrollingModel().disableAnimation(); + if (!animated) editor.getScrollingModel().disableAnimation(); editor.getScrollingModel().scrollHorizontally(point.x); editor.getScrollingModel().scrollVertically(point.y); - editor.getScrollingModel().enableAnimation(); + if (!animated) editor.getScrollingModel().enableAnimation(); } public static void scrollToCaret(@Nullable Editor editor, boolean animated) {