From 8086000a6ee1d7940f35b6bb72f8595da981a1b5 Mon Sep 17 00:00:00 2001 From: Dmitry Batrak Date: Mon, 23 Dec 2013 15:55:18 +0400 Subject: [PATCH] IDEA-86907 Fold/unfold for the last node is incorrect --- .../editor/impl/DisplayedFoldingAnchor.java | 34 +++ .../impl/EditorGutterComponentImpl.java | 254 +++++------------- .../openapi/editor/impl/EditorImpl.java | 7 +- .../impl/FoldingAnchorsOverlayStrategy.java | 113 ++++++++ .../FoldingAnchorsOverlayStrategyTest.java | 125 +++++++++ 5 files changed, 343 insertions(+), 190 deletions(-) create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/DisplayedFoldingAnchor.java create mode 100644 platform/platform-impl/src/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategy.java create mode 100644 platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategyTest.java diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/DisplayedFoldingAnchor.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DisplayedFoldingAnchor.java new file mode 100644 index 000000000000..d5f70dc5f62b --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/DisplayedFoldingAnchor.java @@ -0,0 +1,34 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl; + +import com.intellij.openapi.editor.FoldRegion; + +class DisplayedFoldingAnchor { + enum Type {COLLAPSED, EXPANDED_TOP, EXPANDED_BOTTOM} + + public final FoldRegion foldRegion; + public final int visualLine; + public final int foldRegionVisualLines; + public final Type type; + + DisplayedFoldingAnchor(FoldRegion foldRegion, int visualLine, int foldRegionVisualLines, Type type) { + this.foldRegion = foldRegion; + this.visualLine = visualLine; + this.foldRegionVisualLines = foldRegionVisualLines; + this.type = type; + } +} diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java index 46e7a79edf0b..2b3ae68232e9 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorGutterComponentImpl.java @@ -73,10 +73,8 @@ import javax.swing.plaf.ComponentUI; import java.awt.*; import java.awt.event.*; import java.awt.geom.AffineTransform; -import java.util.ArrayList; +import java.util.*; import java.util.List; -import java.util.Map; -import java.util.TreeMap; class EditorGutterComponentImpl extends EditorGutterComponentEx implements MouseListener, MouseMotionListener { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.EditorGutterComponentImpl"); @@ -86,6 +84,7 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse private static final TooltipGroup GUTTER_TOOLTIP_GROUP = new TooltipGroup("GUTTER_TOOLTIP_GROUP", 0); private final EditorImpl myEditor; + private final FoldingAnchorsOverlayStrategy myAnchorsDisplayStrategy; private int myLineMarkerAreaWidth = START_ICON_AREA_WIDTH + FREE_PAINTERS_AREA_WIDTH; private int myIconsAreaWidth = START_ICON_AREA_WIDTH; private int myLineNumberAreaWidth = 0; @@ -118,6 +117,7 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse } }); } + myAnchorsDisplayStrategy = new FoldingAnchorsOverlayStrategy(editor); } @SuppressWarnings({"ConstantConditions"}) @@ -237,7 +237,8 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse try { int firstVisibleOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, clip.y - myEditor.getLineHeight()))); int lastVisibleOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, clip.y + clip.height + myEditor.getLineHeight()))); - paintFoldingBackground(g, clip, firstVisibleOffset, lastVisibleOffset); + paintFoldingBackground(g, clip); + paintFoldingLines((Graphics2D)g, clip); paintLineMarkers(g, clip, firstVisibleOffset, lastVisibleOffset); paintFoldingTree(g, clip, firstVisibleOffset, lastVisibleOffset); paintLineNumbers(g, clip); @@ -339,7 +340,7 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse doPaintFoldingTree((Graphics2D)g, clip, firstVisibleOffset, lastVisibleOffset); } else { - UIUtil.drawVDottedLine((Graphics2D)g, clip.x + clip.width -1, clip.y, clip.y + clip.height, null, getOutlineColor(false)); + UIUtil.drawVDottedLine((Graphics2D)g, clip.x + clip.width - 1, clip.y, clip.y + clip.height, null, getOutlineColor(false)); } } @@ -816,51 +817,18 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse updateSize(); } - private int offsetToVisualLine(int offset) { - offset = Math.min(myEditor.getDocument().getTextLength() - 1, offset); - return myEditor.offsetToVisualLine(offset); - } - private void doPaintFoldingTree(final Graphics2D g, final Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { final int anchorX = getFoldingAreaOffset(); final int width = getFoldingAnchorWidth(); - doForVisibleFoldRegions( - new NullableFunction() { - @Override - public Void fun(FoldRegion foldRegion) { - drawAnchor(foldRegion, width, clip, g, anchorX, false, false); - return null; - } - }, - firstVisibleOffset, - lastVisibleOffset - ); - - if (myActiveFoldRegion != null) { - drawAnchor(myActiveFoldRegion, width, clip, g, anchorX, true, true); - drawAnchor(myActiveFoldRegion, width, clip, g, anchorX, true, false); + Collection anchorsToDisplay = + myAnchorsDisplayStrategy.getAnchorsToDisplay(firstVisibleOffset, lastVisibleOffset, myActiveFoldRegion); + for (DisplayedFoldingAnchor anchor : anchorsToDisplay) { + drawAnchor(width, clip, g, anchorX, anchor.visualLine, anchor.type, anchor.foldRegion == myActiveFoldRegion); } } - private void doForVisibleFoldRegions(@NotNull NullableFunction action, int firstVisibleOffset, int lastVisibleOffset) { - FoldRegion[] visibleFoldRegions = myEditor.getFoldingModel().fetchVisible(); - final Document document = myEditor.getDocument(); - for (FoldRegion visibleFoldRegion : visibleFoldRegions) { - if (!visibleFoldRegion.isValid()) continue; - final int startOffset = visibleFoldRegion.getStartOffset(); - if (startOffset > lastVisibleOffset) continue; - final int endOffset = getEndOffset(visibleFoldRegion); - if (endOffset < firstVisibleOffset) continue; - if (document.getLineNumber(startOffset) >= document.getLineNumber(endOffset)) { - //TODO den remove this check as soon as editor performance on dimension mapping is improved (IDEA-69317) - continue; - } - action.fun(visibleFoldRegion); - } - } - - private void paintFoldingBackground(Graphics g, Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { + private void paintFoldingBackground(Graphics g, Rectangle clip) { int lineX = getWhitespaceSeparatorOffset(); paintBackground(g, clip, getFoldingAreaOffset(), getFoldingAreaWidth()); @@ -868,11 +836,9 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse g.fillRect(lineX, clip.y, getFoldingAreaWidth(), clip.height); paintCaretRowBackground(g, lineX, getFoldingAnchorWidth()); - - doPaintFoldingBoxBackground((Graphics2D)g, clip, firstVisibleOffset, lastVisibleOffset); } - private void doPaintFoldingBoxBackground(final Graphics2D g, final Rectangle clip, int firstVisibleOffset, int lastVisibleOffset) { + private void paintFoldingLines(final Graphics2D g, final Rectangle clip) { if (!isFoldingOutlineShown()) return; UIUtil.drawVDottedLine(g, getWhitespaceSeparatorOffset(), clip.y, clip.y + clip.height, null, getOutlineColor(false)); @@ -880,21 +846,20 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse final int anchorX = getFoldingAreaOffset(); final int width = getFoldingAnchorWidth(); - if (myActiveFoldRegion != null) { - drawFoldingLines(myActiveFoldRegion, clip, width, anchorX, g); - } + if (myActiveFoldRegion != null && myActiveFoldRegion.isExpanded() && myActiveFoldRegion.isValid()) { + int foldStart = myEditor.offsetToVisualLine(myActiveFoldRegion.getStartOffset()); + int foldEnd = myEditor.offsetToVisualLine(getEndOffset(myActiveFoldRegion)); + int startY = myEditor.visibleLineToY(foldStart + 1) - myEditor.getDescent(); + int endY = myEditor.visibleLineToY(foldEnd) + myEditor.getLineHeight() - + myEditor.getDescent(); - doForVisibleFoldRegions( - new NullableFunction() { - @Override - public Void fun(FoldRegion foldRegion) { - drawAnchor(foldRegion, width, clip, g, anchorX, false, true); - return null; - } - }, - firstVisibleOffset, - lastVisibleOffset - ); + if (startY <= clip.y + clip.height && endY + 1 + myEditor.getDescent() >= clip.y) { + int lineX = anchorX + width / 2; + + g.setColor(getOutlineColor(true)); + UIUtil.drawLine(g, lineX, startY, lineX, endY); + } + } } @Override @@ -911,52 +876,35 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse public int getHeadCenterY(FoldRegion foldRange) { int width = getFoldingAnchorWidth(); - int foldStart = offsetToVisualLine(foldRange.getStartOffset()); + int foldStart = myEditor.offsetToVisualLine(foldRange.getStartOffset()); return myEditor.visibleLineToY(foldStart) + myEditor.getLineHeight() - myEditor.getDescent() - width / 2; } - private void drawAnchor(FoldRegion foldRange, int width, Rectangle clip, Graphics2D g, - int anchorX, boolean active, boolean paintBackground) { - if (!foldRange.isValid()) { - return; - } - int startOffset = foldRange.getStartOffset(); + private void drawAnchor(int width, Rectangle clip, Graphics2D g, int anchorX, int visualLine, + DisplayedFoldingAnchor.Type type, boolean active) { - final int endOffset = getEndOffset(foldRange); - if (!isFoldingPossible(startOffset, endOffset)) { - return; - } - - int foldStart = offsetToVisualLine(startOffset); - int y = myEditor.visibleLineToY(foldStart) + myEditor.getLineHeight() - myEditor.getDescent() - - width; int height = width + 2; - - final FoldingGroup group = foldRange.getGroup(); - - final boolean drawTop = group == null || myEditor.getFoldingModel().getFirstRegion(group, foldRange) == foldRange; - if (!foldRange.isExpanded()) { - if (y <= clip.y + clip.height && y + height >= clip.y) { - if (drawTop) { - drawSquareWithPlus(g, anchorX, y, width, active, paintBackground); + int y; + switch (type) { + case COLLAPSED: + y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent() - width; + if (y <= clip.y + clip.height && y + height >= clip.y) { + drawSquareWithPlus(g, anchorX, y, width, active); } - } - } - else { - int foldEnd = offsetToVisualLine(endOffset); - int endY = myEditor.visibleLineToY(foldEnd) + myEditor.getLineHeight() - - myEditor.getDescent(); - - if (y <= clip.y + clip.height && y + height >= clip.y) { - if (drawTop) { - drawDirectedBox(g, anchorX, y, width, height, width - 2, active, paintBackground); + break; + case EXPANDED_TOP: + y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent() - width; + if (y <= clip.y + clip.height && y + height >= clip.y) { + drawDirectedBox(g, anchorX, y, width, height, width - 2, active); } - } - - if (endY - height <= clip.y + clip.height && endY >= clip.y) { - drawDirectedBox(g, anchorX, endY, width, -height, -width + 2, active, paintBackground); - } + break; + case EXPANDED_BOTTOM: + y = myEditor.visibleLineToY(visualLine) + myEditor.getLineHeight() - myEditor.getDescent(); + if (y - height <= clip.y + clip.height && y >= clip.y) { + drawDirectedBox(g, anchorX, y, width, -height, -width + 2, active); + } + break; } } @@ -972,7 +920,7 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse int width, int height, int baseHeight, - boolean active, boolean paintBackground) { + boolean active) { Object antialiasing = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); if (SystemInfo.isMac && SystemInfo.JAVA_VERSION.startsWith("1.4.1") || UIUtil.isRetina()) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); @@ -982,19 +930,15 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse int[] xPoints = {anchorX, anchorX + width, anchorX + width, anchorX + width / 2, anchorX}; int[] yPoints = {y, y, y + baseHeight, y + height, y + baseHeight}; - if (paintBackground) { - g.setColor(myEditor.getBackgroundColor()); + g.setColor(myEditor.getBackgroundColor()); + g.fillPolygon(xPoints, yPoints, 5); - g.fillPolygon(xPoints, yPoints, 5); - } - else { - g.setColor(getOutlineColor(active)); - g.drawPolygon(xPoints, yPoints, 5); + g.setColor(getOutlineColor(active)); + g.drawPolygon(xPoints, yPoints, 5); - //Minus - int minusHeight = y + baseHeight / 2 + (height - baseHeight) / 4; - UIUtil.drawLine(g, anchorX + 2, minusHeight, anchorX + width - 2, minusHeight); - } + //Minus + int minusHeight = y + baseHeight / 2 + (height - baseHeight) / 4; + UIUtil.drawLine(g, anchorX + 2, minusHeight, anchorX + width - 2, minusHeight); } finally { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialiasing); @@ -1005,9 +949,8 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse int anchorX, int y, int width, - boolean active, - boolean paintBackground) { - drawSquareWithMinus(g, anchorX, y, width, active, paintBackground); + boolean active) { + drawSquareWithMinus(g, anchorX, y, width, active); UIUtil.drawLine(g, anchorX + width / 2, y + 2, anchorX + width / 2, y + width - 2); } @@ -1017,37 +960,16 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse int anchorX, int y, int width, - boolean active, - boolean paintBackground) { - if (paintBackground) { - g.setColor(myEditor.getBackgroundColor()); - g.fillRect(anchorX, y, width, width); - } - else { - g.setColor(getOutlineColor(active)); - g.drawRect(anchorX, y, width, width); + boolean active) { + g.setColor(myEditor.getBackgroundColor()); + g.fillRect(anchorX, y, width, width); - // Draw plus - if (!active) g.setColor(getOutlineColor(true)); - UIUtil.drawLine(g, anchorX + 2, y + width / 2, anchorX + width - 2, y + width / 2); - } - } + g.setColor(getOutlineColor(active)); + g.drawRect(anchorX, y, width, width); - private void drawFoldingLines(FoldRegion foldRange, Rectangle clip, int width, int anchorX, Graphics2D g) { - if (foldRange.isExpanded() && foldRange.isValid()) { - int foldStart = offsetToVisualLine(foldRange.getStartOffset()); - int foldEnd = offsetToVisualLine(getEndOffset(foldRange)); - int startY = myEditor.visibleLineToY(foldStart + 1) - myEditor.getDescent(); - int endY = myEditor.visibleLineToY(foldEnd) + myEditor.getLineHeight() - - myEditor.getDescent(); - - if (startY > clip.y + clip.height || endY + 1 + myEditor.getDescent() < clip.y) return; - - int lineX = anchorX + width / 2; - - g.setColor(getOutlineColor(true)); - UIUtil.drawLine(g, lineX, startY, lineX, endY); - } + // Draw plus + if (!active) g.setColor(getOutlineColor(true)); + UIUtil.drawLine(g, anchorX + 2, y + width / 2, anchorX + width - 2, y + width / 2); } private int getFoldingAnchorWidth() { @@ -1135,55 +1057,17 @@ class EditorGutterComponentImpl extends EditorGutterComponentEx implements Mouse int anchorX = getFoldingAreaOffset(); int anchorWidth = getFoldingAnchorWidth(); - FoldRegion[] visibleRanges = myEditor.getFoldingModel().fetchVisible(); - for (FoldRegion foldRange : visibleRanges) { - if (!foldRange.isValid()) continue; - final FoldingGroup group = foldRange.getGroup(); - if (group != null && myEditor.getFoldingModel().getFirstRegion(group, foldRange) != foldRange) { - continue; - } + int neighbourhoodStartOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, y - myEditor.getLineHeight()))); + int neighbourhoodEndOffset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(new Point(0, y + myEditor.getLineHeight()))); - int foldStart = offsetToVisualLine(foldRange.getStartOffset()); - final int endOffset = getEndOffset(foldRange); - int foldEnd = offsetToVisualLine(endOffset); - if (!isFoldingPossible(foldRange.getStartOffset(), endOffset)) { - continue; - } - - if (rectangleByFoldOffset(foldStart, anchorWidth, anchorX).contains(x, y)) return foldRange; - if ((group == null || foldRange.isExpanded()) && rectangleByFoldOffset(foldEnd, anchorWidth, anchorX).contains(x, y)) return foldRange; + Collection displayedAnchors = myAnchorsDisplayStrategy.getAnchorsToDisplay(neighbourhoodStartOffset, neighbourhoodEndOffset, null); + for (DisplayedFoldingAnchor anchor : displayedAnchors) { + if (rectangleByFoldOffset(anchor.visualLine, anchorWidth, anchorX).contains(x, y)) return anchor.foldRegion; } return null; } - /** - * Allows to answer if there may be folding for the given offsets. - *

- * The rule is that we can fold range that occupies multiple logical or visual lines. - * - * @param startOffset start offset of the target region to check - * @param endOffset end offset of the target region to check - * @return - */ - private boolean isFoldingPossible(int startOffset, int endOffset) { - Document document = myEditor.getDocument(); - if (startOffset >= document.getTextLength()) { - return false; - } - - int endOffsetToUse = Math.min(endOffset, document.getTextLength()); - if (endOffsetToUse <= startOffset) { - return false; - } - - if (document.getLineNumber(startOffset) != document.getLineNumber(endOffsetToUse)) { - return true; - } - return myEditor.getSettings().isAllowSingleLogicalLineFolding() - && !myEditor.getSoftWrapModel().getSoftWrapsForRange(startOffset, endOffsetToUse).isEmpty(); - } - @SuppressWarnings("SuspiciousNameCombination") private Rectangle rectangleByFoldOffset(int foldStart, int anchorWidth, int anchorX) { int anchorY = myEditor.visibleLineToY(foldStart) + myEditor.getLineHeight() - diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java index ce78e157e221..38686ca693c9 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorImpl.java @@ -1278,11 +1278,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi public int offsetToVisualLine(int offset) { int textLength = getDocument().getTextLength(); if (offset >= textLength) { - int result = Math.max(0, getVisibleLineCount() - 1); // lines are 0 based - if (textLength > 0 && getDocument().getCharsSequence().charAt(textLength - 1) == '\n') { - result++; - } - return result; + return Math.max(0, getVisibleLineCount() - 1); // lines are 0 based } int line = offsetToLogicalLine(offset); int lineStartOffset = line >= myDocument.getLineCount() ? myDocument.getTextLength() : myDocument.getLineStartOffset(line); @@ -5487,6 +5483,7 @@ public final class EditorImpl extends UserDataHolderBase implements EditorEx, Hi y = myGutterComponent.getHeadCenterY(range); getScrollingModel().scrollVertically(y - scrollShift); myGutterComponent.updateSize(); + validateMousePointer(e); return isNavigation; } } diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategy.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategy.java new file mode 100644 index 000000000000..932a8b05d8d3 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategy.java @@ -0,0 +1,113 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.FoldRegion; +import com.intellij.openapi.editor.FoldingGroup; +import com.intellij.util.containers.hash.HashMap; + +import java.util.Collection; +import java.util.Map; + +class FoldingAnchorsOverlayStrategy { + private final EditorImpl myEditor; + + public FoldingAnchorsOverlayStrategy(EditorImpl editor) { + myEditor = editor; + } + + public Collection getAnchorsToDisplay(int firstVisibleOffset, int lastVisibleOffset, FoldRegion activeFoldRegion) { + Map result = new HashMap(); + FoldRegion[] visibleFoldRegions = myEditor.getFoldingModel().fetchVisible(); + for (FoldRegion region : visibleFoldRegions) { + if (!region.isValid()) continue; + final int startOffset = region.getStartOffset(); + if (startOffset > lastVisibleOffset) continue; + final int endOffset = getEndOffset(region); + if (endOffset < firstVisibleOffset) continue; + if (!isFoldingPossible(startOffset, endOffset)) continue; + + final FoldingGroup group = region.getGroup(); + if (group != null && myEditor.getFoldingModel().getFirstRegion(group, region) != region) continue; + + //offset = Math.min(myEditor.getDocument().getTextLength() - 1, offset); + int foldStart = myEditor.offsetToVisualLine(startOffset); + + if (!region.isExpanded()) { + tryAdding(result, region, foldStart, 0, DisplayedFoldingAnchor.Type.COLLAPSED, activeFoldRegion); + } + else { + //offset = Math.min(myEditor.getDocument().getTextLength() - 1, offset); + int foldEnd = myEditor.offsetToVisualLine(endOffset); + tryAdding(result, region, foldStart, foldEnd - foldStart, DisplayedFoldingAnchor.Type.EXPANDED_TOP, activeFoldRegion); + tryAdding(result, region, foldEnd, foldEnd - foldStart, DisplayedFoldingAnchor.Type.EXPANDED_BOTTOM, activeFoldRegion); + } + } + return result.values(); + } + + private static void tryAdding(Map resultsMap, + FoldRegion region, + int visualLine, + int visualHeight, + DisplayedFoldingAnchor.Type type, + FoldRegion activeRegion) { + DisplayedFoldingAnchor prev = resultsMap.get(visualLine); + if (prev != null) { + if (prev.foldRegion == activeRegion) { + return; + } + if (region != activeRegion && prev.foldRegionVisualLines < visualHeight) { + return; + } + } + resultsMap.put(visualLine, new DisplayedFoldingAnchor(region, visualLine, visualHeight, type)); + } + + private int getEndOffset(FoldRegion foldRange) { + FoldingGroup group = foldRange.getGroup(); + return group == null ? foldRange.getEndOffset() : myEditor.getFoldingModel().getEndOffset(group); + } + + /** + * Allows to answer if there may be folding for the given offsets. + *

+ * The rule is that we can fold range that occupies multiple logical or visual lines. + * + * @param startOffset start offset of the target region to check + * @param endOffset end offset of the target region to check + * @return + */ + private boolean isFoldingPossible(int startOffset, int endOffset) { + Document document = myEditor.getDocument(); + if (startOffset >= document.getTextLength()) { + return false; + } + + int endOffsetToUse = Math.min(endOffset, document.getTextLength()); + if (endOffsetToUse <= startOffset) { + return false; + } + + if (document.getLineNumber(startOffset) != document.getLineNumber(endOffsetToUse)) { + return true; + } + return myEditor.getSettings().isAllowSingleLogicalLineFolding() + && !myEditor.getSoftWrapModel().getSoftWrapsForRange(startOffset, endOffsetToUse).isEmpty(); + } + +} diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategyTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategyTest.java new file mode 100644 index 000000000000..94803191efd6 --- /dev/null +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/FoldingAnchorsOverlayStrategyTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2000-2013 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.openapi.editor.impl; + +import com.intellij.codeInsight.folding.CodeFoldingManager; +import com.intellij.openapi.editor.FoldRegion; +import com.intellij.openapi.editor.FoldingModel; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.fileTypes.StdFileTypes; +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixtureTestCase; + +import java.util.*; + +import static com.intellij.openapi.editor.impl.DisplayedFoldingAnchor.Type; + +public class FoldingAnchorsOverlayStrategyTest extends LightPlatformCodeInsightFixtureTestCase { + public void testExpanded() { + prepareEditor("

\n" + + "
\n" + + "some text\n" + + "some text\n" + + "some text\n" + + "
\n" + + "
"); + verifyAnchors(null, + 0, Type.EXPANDED_TOP, + 1, Type.EXPANDED_BOTTOM, + 5, Type.EXPANDED_TOP, + 6, Type.EXPANDED_BOTTOM); + } + + public void testCollapsed() { + prepareEditor("
\n" + + "
\n" + + "some text\n" + + "some text\n" + + "some text\n" + + "
\n" + + "
"); + collapseFoldingRegion(2); + verifyAnchors(null, + 0, Type.EXPANDED_TOP, + 1, Type.COLLAPSED, + 2, Type.EXPANDED_BOTTOM); + } + + public void testWithActiveRegion() { + prepareEditor("
\n" + + "
\n" + + "some text\n" + + "some text\n" + + "some text\n" + + "
\n" + + "
"); + collapseFoldingRegion(2); + verifyAnchors(myFixture.getEditor().getFoldingModel().getAllFoldRegions()[1], + 0, Type.EXPANDED_TOP, + 1, Type.EXPANDED_BOTTOM, + 2, Type.EXPANDED_BOTTOM); + } + + public void testWithEmptyLastLine() { + myFixture.configureByText(FileTypes.PLAIN_TEXT, "some text\n"); + final FoldingModel foldingModel = myFixture.getEditor().getFoldingModel(); + foldingModel.runBatchFoldingOperation(new Runnable() { + @Override + public void run() { + foldingModel.addFoldRegion(0, 10, "..."); + } + }); + verifyAnchors(null, + 0, Type.EXPANDED_TOP, + 1, Type.EXPANDED_BOTTOM); + } + + private void prepareEditor(String text) { + myFixture.configureByText(StdFileTypes.XML, text); + CodeFoldingManager.getInstance(myFixture.getProject()).updateFoldRegions(myFixture.getEditor()); + } + + private void collapseFoldingRegion(int n) { + FoldingModel foldingModel = myFixture.getEditor().getFoldingModel(); + final FoldRegion foldRegion = foldingModel.getAllFoldRegions()[n]; + foldingModel.runBatchFoldingOperation(new Runnable() { + @Override + public void run() { + foldRegion.setExpanded(false); + } + }); + } + + private void verifyAnchors(FoldRegion activeFoldRegion, Object... expectedAnchorParameters) { + Collection actualAnchors = new FoldingAnchorsOverlayStrategy((EditorImpl)myFixture.getEditor()) + .getAnchorsToDisplay(0, myFixture.getEditor().getDocument().getTextLength(), activeFoldRegion); + List sortedActualAnchors = new ArrayList(actualAnchors); + Collections.sort(sortedActualAnchors, new Comparator() { + @Override + public int compare(DisplayedFoldingAnchor o1, DisplayedFoldingAnchor o2) { + return o1.visualLine - o2.visualLine; + } + }); + + assertEquals("Wrong number of anchors", expectedAnchorParameters.length / 2, sortedActualAnchors.size()); + int i = 0; + for (DisplayedFoldingAnchor anchor : sortedActualAnchors) { + int expectedVisualLine = (Integer) expectedAnchorParameters[i++]; + assertEquals("Folding anchor at wrong line found", expectedVisualLine, anchor.visualLine); + Type expectedType = (Type)expectedAnchorParameters[i++]; + assertEquals("Folding anchor of wrong type found at line " + expectedVisualLine, expectedType, anchor.type); + } + } +}