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
This commit is contained in:
Aleksey Pivovarov
2015-04-14 17:48:37 +03:00
parent 9476fcf548
commit b4badef6fe
5 changed files with 190 additions and 52 deletions
@@ -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
@@ -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);
@@ -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;
}
}
}
@@ -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();
}
@@ -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) {