diff: iterating between differences should make both sides visible
authorAleksey Pivovarov <AMPivovarov@gmail.com>
Mon, 13 Apr 2015 16:30:16 +0000 (19:30 +0300)
committerAleksey Pivovarov <AMPivovarov@gmail.com>
Tue, 14 Apr 2015 14:48:37 +0000 (17:48 +0300)
* work around SyncScroll issue with big insertion/deletion at the end of file
* move carets in all editors to change

platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleDiffViewer.java
platform/diff-impl/src/com/intellij/diff/tools/simple/SimpleThreesideDiffViewer.java
platform/diff-impl/src/com/intellij/diff/tools/util/SyncScrollSupport.java
platform/diff-impl/src/com/intellij/diff/tools/util/twoside/TwosideTextDiffViewer.java
platform/diff-impl/src/com/intellij/diff/util/DiffUtil.java

index 761a7ce8467f9c3e3b04e0d629f74521eafe5a61..f81a25c9b2383a976096e3e9b2f856b4126752df 100644 (file)
@@ -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
index 64e54c98258bf1e0ceb7161d9da1c513b49fa9af..1471ad397d33fd82191efbee19c63249dfeea6b7 100644 (file)
@@ -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);
index 68788e3b90309955847c0ec035412e37bb2c212b..3ae8591b103e8fcdc64643ed31b7fbf939f27d69 100644 (file)
  */
 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);
+
+        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);
 
-      int correction = (masterVerticalScrollOffset + middleY) % myMaster.getLineHeight();
-      Point point = mySlave.logicalPositionToXY(new LogicalPosition(scrollToLine, masterPos.column));
-      int offset = point.y - middleY + correction;
+        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;
+    }
+  }
 }
index cc81f9d397426b16d094b6eb4a3cbea0fcb07304..011b4459bc02ecc1abb78c4801bbec58b45506de 100644 (file)
@@ -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();
       }
index b2af70e18c9293bc7d79fa2e4c278ce190f96063..d1df97e088b316e55e2e9d6d51df4e82169dbb57 100644 (file)
@@ -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) {