IDEA-127997 lst: fix rendering for folded regions

* Mark lines with folded deletions/insertions as modified.
* Scroll to the first block in collapsed area on click, if there are multiple options.
* Mark lines with folded ignored and non-ignored changes as non-ignored

First, convert ranges into affected visual lines.
Next, merge interleaving ranges together.
This commit is contained in:
Aleksey Pivovarov
2018-07-04 16:18:36 +03:00
parent 4b845d433f
commit 2aec7b74c8
5 changed files with 307 additions and 108 deletions
@@ -70,13 +70,19 @@ public abstract class LineStatusMarkerPopupRenderer extends LineStatusMarkerRend
}
@Override
protected boolean canDoAction(@NotNull Editor editor, @NotNull Range range, MouseEvent e) {
protected boolean canDoAction(@NotNull Editor editor, @NotNull List<? extends Range> ranges, MouseEvent e) {
return isInsideMarkerArea(e);
}
@Override
protected void doAction(@NotNull Editor editor, @NotNull Range range, @NotNull MouseEvent e) {
showHint(editor, range, e);
protected void doAction(@NotNull Editor editor, @NotNull List<? extends Range> ranges, @NotNull MouseEvent e) {
Range range = ranges.get(0);
if (ranges.size() > 1) {
scrollAndShow(editor, range);
}
else {
showHint(editor, range, e);
}
}
@@ -17,20 +17,24 @@ package com.intellij.openapi.vcs.ex;
import com.intellij.diff.util.DiffDrawUtil;
import com.intellij.diff.util.DiffUtil;
import com.intellij.diff.util.IntPair;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.DiffColors;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.VisualPosition;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
import com.intellij.openapi.editor.impl.DocumentMarkupModel;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.TextRange;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
@@ -42,10 +46,13 @@ import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import static com.intellij.diff.util.DiffDrawUtil.lineToY;
import static com.intellij.diff.util.DiffDrawUtil.yToLine;
import static com.intellij.diff.util.DiffUtil.getLineCount;
import static com.intellij.openapi.diagnostic.Logger.getInstance;
import static com.intellij.util.ui.update.MergingUpdateQueue.ANY_COMPONENT;
@@ -136,36 +143,46 @@ public abstract class LineStatusMarkerRenderer {
}
private boolean canDoAction(@NotNull Editor editor, MouseEvent e) {
Range range = getSelectedRange(editor, e);
return range != null && canDoAction(editor, range, e);
List<? extends Range> ranges = getSelectedRanges(editor, e.getY());
return !ranges.isEmpty() && canDoAction(editor, ranges, e);
}
private void doAction(@NotNull Editor editor, MouseEvent e) {
Range range = getSelectedRange(editor, e);
if (range != null) {
doAction(editor, range, e);
List<? extends Range> ranges = getSelectedRanges(editor, e.getY());
if (!ranges.isEmpty()) {
doAction(editor, ranges, e);
}
}
@Nullable
private Range getSelectedRange(@NotNull Editor editor, MouseEvent e) {
int line = yToLine(editor, e.getY());
Range range = myTracker.getRangeForLine(line);
if (range != null) return range;
@NotNull
protected List<? extends Range> getSelectedRanges(@NotNull Editor editor, int y) {
int visualLine = editor.xyToVisualPosition(new Point(0, y)).line;
int line1 = editor.visualToLogicalPosition(new VisualPosition(visualLine, 0)).line;
int line2 = editor.visualToLogicalPosition(new VisualPosition(visualLine + 1, 0)).line;
BitSet lines = new BitSet();
lines.set(line1, line2);
List<? extends Range> ranges = myTracker.getRangesForLines(lines);
if (!ContainerUtil.isEmpty(ranges)) return ranges;
// special handling for deletion at the end of file
int lineCount = DiffUtil.getLineCount(editor.getDocument());
if (line == lineCount - 1) {
return myTracker.getRangeForLine(lineCount);
int lineCount = getLineCount(editor.getDocument());
if (line2 == lineCount) {
Range range = myTracker.getRangeForLine(lineCount);
if (range != null) return Collections.singletonList(range);
}
return null;
return Collections.emptyList();
}
protected boolean canDoAction(@NotNull Editor editor, @NotNull Range range, MouseEvent e) {
protected boolean canDoAction(@NotNull Editor editor, @NotNull List<? extends Range> ranges, MouseEvent e) {
return false;
}
protected void doAction(@NotNull Editor editor, @NotNull Range range, MouseEvent e) {
protected void doAction(@NotNull Editor editor, @NotNull List<? extends Range> ranges, MouseEvent e) {
}
@NotNull
protected VisibleRangeMerger createMerger(@NotNull Editor editor) {
return new VisibleRangeMerger(editor);
}
@Nullable
@@ -212,34 +229,30 @@ public abstract class LineStatusMarkerRenderer {
List<? extends Range> ranges = myTracker.getRanges();
if (ranges == null) return;
Rectangle area = editor.getScrollingModel().getVisibleArea();
int line1 = yToLine(editor, area.y);
int line2 = yToLine(editor, area.y + area.height);
int framingBorder = getFramingBorderSize();
for (Range range : ranges) {
if (range.getLine2() < line1) continue;
if (range.getLine1() > line2) return;
paint(editor, range, g);
List<List<ChangedLines>> blocks = createMerger(editor).run(ranges);
for (List<ChangedLines> block: blocks) {
paintChangedLines(g, editor, block, framingBorder);
}
}
protected void paint(@NotNull Editor editor, @NotNull Range range, @NotNull Graphics g) {
paintRange(g, editor, range, getFramingBorderSize());
}
private static void paintChangedLines(@NotNull Graphics g, @NotNull Editor editor, @NotNull List<ChangedLines> block, int framingBorder) {
EditorImpl editorImpl = (EditorImpl)editor;
public static void paintRange(@NotNull Graphics g,
@NotNull Editor editor,
@NotNull Range range,
int framingBorder) {
Color gutterColor = getGutterColor(range, editor);
Color borderColor = getGutterBorderColor(editor);
Color gutterBackgroundColor = ((EditorEx)editor).getGutterComponentEx().getBackground();
Rectangle area = getMarkerArea(editor, range.getLine1(), range.getLine2());
final int x = area.x;
final int endX = area.x + area.width;
final int y = area.y;
final int endY = area.y + area.height;
int line1 = block.get(0).line1;
int line2 = block.get(block.size() - 1).line2;
IntPair area = getGutterArea(editor);
final int x = area.val1;
final int endX = area.val2;
final int y = editorImpl.visibleLineToY(line1);
final int endY = editorImpl.visibleLineToY(line2);
if (framingBorder > 0) {
if (y != endY) {
@@ -249,55 +262,56 @@ public abstract class LineStatusMarkerRenderer {
}
}
if (y == endY) {
paintTriangle(g, gutterColor, borderColor, x, endX, y);
for (ChangedLines change: block) {
if (change.line1 != change.line2 &&
!change.isIgnored) {
int start = editorImpl.visibleLineToY(change.line1);
int end = editorImpl.visibleLineToY(change.line2);
Color gutterColor = getGutterColor(change.type, editor);
paintRect(g, gutterColor, null, x, start, endX, end);
}
}
if (borderColor == null) {
for (ChangedLines change: block) {
if (change.line1 != change.line2 &&
change.isIgnored) {
int start = editorImpl.visibleLineToY(change.line1);
int end = editorImpl.visibleLineToY(change.line2);
Color ignoredBorderColor = getIgnoredGutterBorderColor(change.type, editor);
paintRect(g, null, ignoredBorderColor, x, start, endX, end);
}
}
}
else {
if (range.getInnerRanges() == null) { // Mode.DEFAULT
paintRect(g, gutterColor, borderColor, x, y, endX, endY);
}
else { // Mode.SMART
List<Range.InnerRange> innerRanges = range.getInnerRanges();
for (Range.InnerRange innerRange : innerRanges) {
if (innerRange.getType() == Range.DELETED) continue;
paintRect(g, null, borderColor, x, y, endX, endY);
}
int start = lineToY(editor, range.getLine1() + innerRange.getLine1());
int end = lineToY(editor, range.getLine1() + innerRange.getLine2());
for (ChangedLines change: block) {
if (change.line1 == change.line2) {
int start = editorImpl.visibleLineToY(change.line1);
paintRect(g, getGutterColor(innerRange, editor), null, x, start, endX, end);
if (!change.isIgnored) {
Color gutterColor = getGutterColor(change.type, editor);
paintTriangle(g, gutterColor, borderColor, x, endX, start);
}
paintRect(g, null, borderColor, x, y, endX, endY);
for (Range.InnerRange innerRange : innerRanges) {
if (innerRange.getType() != Range.DELETED) continue;
int start = lineToY(editor, range.getLine1() + innerRange.getLine1());
paintTriangle(g, getGutterColor(innerRange, editor), borderColor, x, endX, start);
else {
Color ignoredBorderColor = getIgnoredGutterBorderColor(change.type, editor);
paintTriangle(g, null, ignoredBorderColor, x, endX, start);
}
}
}
}
public static void paintIgnoredRange(@NotNull Graphics g,
@NotNull Editor editor,
@NotNull Range range) {
Color borderColor = getGutterBorderColor(editor);
Color ignoredBorderColor = getIgnoredGutterBorderColor(range, editor);
Color color = borderColor != null ? borderColor : ignoredBorderColor;
Rectangle area = getMarkerArea(editor, range.getLine1(), range.getLine2());
final int x = area.x;
final int endX = area.x + area.width;
final int y = area.y;
final int endY = area.y + area.height;
if (area.height == 0) {
paintTriangle(g, null, color, x, endX, y);
}
else {
paintRect(g, null, color, x, y, endX, endY);
public static void paintRange(@NotNull Graphics g,
@NotNull Editor editor,
@NotNull Range range,
int framingBorder) {
List<List<ChangedLines>> blocks = new VisibleRangeMerger(editor).run(Collections.singletonList(range));
for (List<ChangedLines> block: blocks) {
paintChangedLines(g, editor, block, framingBorder);
}
}
@@ -313,10 +327,18 @@ public abstract class LineStatusMarkerRenderer {
}
@NotNull
public static Rectangle getMarkerArea(@NotNull Editor editor, int line1, int line2) {
private static IntPair getGutterArea(@NotNull Editor editor) {
EditorGutterComponentEx gutter = ((EditorEx)editor).getGutterComponentEx();
int x = gutter.getLineMarkerFreePaintersAreaOffset() + 1; // leave 1px for brace highlighters
int endX = gutter.getWhitespaceSeparatorOffset();
return new IntPair(x, endX);
}
@NotNull
public static Rectangle getMarkerArea(@NotNull Editor editor, int line1, int line2) {
IntPair horizontalArea = getGutterArea(editor);
int x = horizontalArea.val1;
int endX = horizontalArea.val2;
int y = lineToY(editor, line1);
int endY = lineToY(editor, line2);
return new Rectangle(x, y, endX - x, endY - y);
@@ -359,9 +381,9 @@ public abstract class LineStatusMarkerRenderer {
}
@Nullable
private static Color getGutterColor(@NotNull Range.InnerRange range, @Nullable Editor editor) {
private static Color getGutterColor(byte type, @Nullable Editor editor) {
final EditorColorsScheme scheme = getColorScheme(editor);
switch (range.getType()) {
switch (type) {
case Range.INSERTED:
return scheme.getColor(EditorColors.ADDED_LINES_COLOR);
case Range.DELETED:
@@ -393,30 +415,18 @@ public abstract class LineStatusMarkerRenderer {
}
@Nullable
private static Color getGutterColor(@NotNull Range range, @Nullable Editor editor) {
final EditorColorsScheme scheme = getColorScheme(editor);
switch (range.getType()) {
case Range.INSERTED:
return scheme.getColor(EditorColors.ADDED_LINES_COLOR);
case Range.DELETED:
return scheme.getColor(EditorColors.DELETED_LINES_COLOR);
case Range.MODIFIED:
return scheme.getColor(EditorColors.MODIFIED_LINES_COLOR);
default:
assert false;
return null;
}
}
private static Color getIgnoredGutterBorderColor(byte type, @Nullable Editor editor) {
Color borderColor = getGutterBorderColor(editor);
if (borderColor != null) return borderColor;
@Nullable
private static Color getIgnoredGutterBorderColor(@NotNull Range range, @Nullable Editor editor) {
final EditorColorsScheme scheme = getColorScheme(editor);
switch (range.getType()) {
switch (type) {
case Range.INSERTED:
return scheme.getColor(EditorColors.IGNORED_ADDED_LINES_BORDER_COLOR);
case Range.DELETED:
return scheme.getColor(EditorColors.IGNORED_DELETED_LINES_BORDER_COLOR);
case Range.MODIFIED:
case Range.EQUAL:
return scheme.getColor(EditorColors.IGNORED_MODIFIED_LINES_BORDER_COLOR);
default:
assert false;
@@ -435,6 +445,191 @@ public abstract class LineStatusMarkerRenderer {
}
protected static class VisibleRangeMerger {
@NotNull private final Editor myEditor;
@NotNull private final List<ChangedLines> myBlock = new ArrayList<>();
@NotNull private final List<List<ChangedLines>> myResult = new ArrayList<>();
public VisibleRangeMerger(@NotNull Editor editor) {
myEditor = editor;
}
protected boolean isIgnored(@NotNull Range range) {
return false;
}
@NotNull
public List<List<ChangedLines>> run(@NotNull List<? extends Range> ranges) {
Rectangle area = myEditor.getScrollingModel().getVisibleArea();
int visibleLineStart = yToLine(myEditor, area.y);
int visibleLineEnd = yToLine(myEditor, area.y + area.height);
for (Range range: ranges) {
int line1 = range.getLine1();
int line2 = range.getLine2();
if (line2 < visibleLineStart) continue;
if (line1 > visibleLineEnd) break;
boolean isIgnored = isIgnored(range);
List<Range.InnerRange> innerRanges = range.getInnerRanges();
if (innerRanges == null || isIgnored) {
processLine(line1, line2, range.getType(), isIgnored);
}
else {
for (Range.InnerRange innerRange: innerRanges) {
int innerLine1 = line1 + innerRange.getLine1();
int innerLine2 = line1 + innerRange.getLine2();
byte innerType = innerRange.getType();
processLine(innerLine1, innerLine2, innerType, isIgnored);
}
}
}
finishBlock();
return myResult;
}
private void processLine(int start, int end, byte type, boolean isIgnored) {
EditorImpl editorImpl = (EditorImpl)myEditor;
Document document = myEditor.getDocument();
int lineCount = getLineCount(document);
int visualStart;
boolean startHasFolding;
if (start < lineCount) {
int startOffset = document.getLineStartOffset(start);
visualStart = editorImpl.offsetToVisualLine(startOffset);
startHasFolding = startOffset > 0 && myEditor.getFoldingModel().isOffsetCollapsed(startOffset - 1);
}
else {
LOG.assertTrue(start == lineCount);
int lastVisualLine = editorImpl.offsetToVisualLine(document.getTextLength());
visualStart = lastVisualLine + start - lineCount + 1;
startHasFolding = false;
}
if (start == end) {
if (startHasFolding) {
appendChange(new ChangedLines(visualStart, visualStart + 1, Range.MODIFIED, isIgnored));
}
else {
appendChange(new ChangedLines(visualStart, visualStart, type, isIgnored));
}
}
else {
int visualEnd;
boolean endHasFolding;
if (end < lineCount) {
int endOffset = document.getLineEndOffset(end - 1);
visualEnd = editorImpl.offsetToVisualLine(endOffset) + 1;
endHasFolding = myEditor.getFoldingModel().isOffsetCollapsed(endOffset);
}
else {
LOG.assertTrue(end == lineCount);
int lastVisualLine = editorImpl.offsetToVisualLine(document.getTextLength());
visualEnd = lastVisualLine + end - lineCount + 1;
endHasFolding = false;
}
if (type == Range.EQUAL || type == Range.MODIFIED) {
appendChange(new ChangedLines(visualStart, visualEnd, type, isIgnored));
}
else {
if (startHasFolding && visualEnd - visualStart > 1) {
appendChange(new ChangedLines(visualStart, visualStart + 1, Range.MODIFIED, isIgnored));
startHasFolding = false;
visualStart++;
}
if (endHasFolding && visualEnd - visualStart > 1) {
appendChange(new ChangedLines(visualStart, visualEnd - 1, type, isIgnored));
appendChange(new ChangedLines(visualEnd - 1, visualEnd, Range.MODIFIED, isIgnored));
}
else {
appendChange(new ChangedLines(visualStart, visualEnd, startHasFolding || endHasFolding ? Range.MODIFIED : type, isIgnored));
}
}
}
}
private void appendChange(@NotNull ChangedLines newChange) {
ChangedLines lastItem = ContainerUtil.getLastItem(myBlock);
if (lastItem != null && lastItem.line2 < newChange.line1) {
finishBlock();
}
if (myBlock.isEmpty()) {
myBlock.add(newChange);
return;
}
ChangedLines lastChange = myBlock.remove(myBlock.size() - 1);
if (lastChange.line1 == lastChange.line2 &&
newChange.line1 == newChange.line2) {
assert lastChange.line1 == newChange.line1;
byte type = lastChange.type == newChange.type ? lastChange.type : Range.MODIFIED;
boolean isIgnored = lastChange.isIgnored && newChange.isIgnored;
myBlock.add(new ChangedLines(lastChange.line1, lastChange.line2, type, isIgnored));
}
else if (lastChange.line1 == lastChange.line2 && newChange.type == Range.EQUAL ||
newChange.line1 == newChange.line2 && lastChange.type == Range.EQUAL) {
myBlock.add(lastChange);
myBlock.add(newChange);
}
else if (lastChange.type == newChange.type &&
lastChange.isIgnored == newChange.isIgnored) {
int union1 = Math.min(lastChange.line1, newChange.line1);
int union2 = Math.max(lastChange.line2, newChange.line2);
myBlock.add(new ChangedLines(union1, union2, lastChange.type, lastChange.isIgnored));
}
else {
int intersection1 = Math.max(lastChange.line1, newChange.line1);
int intersection2 = Math.min(lastChange.line2, newChange.line2);
if (lastChange.line1 != intersection1) {
myBlock.add(new ChangedLines(lastChange.line1, intersection1, lastChange.type, lastChange.isIgnored));
}
if (intersection1 != intersection2) {
byte type = lastChange.type == newChange.type ? lastChange.type : Range.MODIFIED;
boolean isIgnored = lastChange.isIgnored && newChange.isIgnored;
myBlock.add(new ChangedLines(intersection1, intersection2, type, isIgnored));
}
if (newChange.line2 != intersection2) {
myBlock.add(new ChangedLines(intersection2, newChange.line2, newChange.type, newChange.isIgnored));
}
}
}
private void finishBlock() {
if (myBlock.isEmpty()) return;
myResult.add(new ArrayList<>(myBlock));
myBlock.clear();
}
}
private static class ChangedLines {
// VisualPosition.line
public final int line1;
public final int line2;
public final byte type;
private final boolean isIgnored;
public ChangedLines(int line1, int line2, byte type, boolean isIgnored) {
this.line1 = line1;
this.line2 = line2;
this.type = type;
this.isIgnored = isIgnored;
}
}
private class MyActiveGutterRenderer implements ActiveGutterRenderer {
@Override
public void paint(Editor editor, Graphics g, Rectangle r) {
@@ -1361,7 +1361,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi
}
// optimization: do not do column calculations here since we are interested in line number only
int offsetToVisualLine(int offset) {
public int offsetToVisualLine(int offset) {
return myView.offsetToVisualLine(offset, false);
}
@@ -101,9 +101,9 @@ abstract class LineStatusTracker<R : Range> constructor(override val project: Pr
: LineStatusMarkerPopupRenderer(tracker) {
override fun getEditorFilter(): MarkupEditorFilter? = MarkupEditorFilterFactory.createIsNotDiffFilter()
override fun canDoAction(editor: Editor, range: Range, e: MouseEvent?): Boolean {
override fun canDoAction(editor: Editor, ranges: List<Range>, e: MouseEvent?): Boolean {
if (tracker.mode == Mode.SILENT) return false
return super.canDoAction(editor, range, e)
return super.canDoAction(editor, ranges, e)
}
override fun paint(editor: Editor, g: Graphics) {
@@ -53,7 +53,6 @@ import com.intellij.util.ui.JBUI
import com.intellij.vcsUtil.VcsUtil
import org.jetbrains.annotations.CalledInAwt
import java.awt.BorderLayout
import java.awt.Graphics
import java.awt.Point
import java.lang.ref.WeakReference
import java.util.*
@@ -506,12 +505,11 @@ class PartialLocalLineStatusTracker(project: Project,
protected class MyLineStatusMarkerRenderer(override val tracker: PartialLocalLineStatusTracker) :
LineStatusTracker.LocalLineStatusMarkerRenderer(tracker) {
override fun paint(editor: Editor, range: Range, g: Graphics) {
if (range !is LocalRange ||
range.changelistId == tracker.defaultMarker.changelistId) {
super.paint(editor, range, g)
} else {
paintIgnoredRange(g, editor, range)
override fun createMerger(editor: Editor): VisibleRangeMerger {
return object : VisibleRangeMerger(editor) {
override fun isIgnored(range: Range): Boolean {
return range is LocalRange && range.changelistId != tracker.defaultMarker.changelistId
}
}
}