folding model implementation cleanup

This commit is contained in:
Dmitry Batrak
2014-06-05 12:14:53 +04:00
parent c7a1fbb90c
commit 2de765823e
4 changed files with 285 additions and 207 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* Copyright 2000-2014 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.
@@ -33,11 +33,19 @@ public interface FoldingModel {
* @param startOffset the start offset of the region to fold.
* @param endOffset the end offset of the region to fold.
* @param placeholderText the text to display instead of the region contents when the region is folded.
* @return the fold region, or null if folding is currently disabled.
* @return the fold region, or <code>null</code> if folding is currently disabled or corresponding region cannot be added (e.g. if it
* intersects with another existing region)
*/
@Nullable
FoldRegion addFoldRegion(int startOffset, int endOffset, @NotNull String placeholderText);
/**
* Tries to add given region to the folding model. This method must be called
* from the <code>Runnable</code> passed to {@link #runBatchFoldingOperation(Runnable)}.
*
* @return <code>true</code>, if region was added successfully, <code>false</code> if the region cannot be added, e.g. if it
* intersects with another existing region
*/
boolean addFoldRegion(@NotNull FoldRegion region);
/**
@@ -57,13 +65,22 @@ public interface FoldingModel {
FoldRegion[] getAllFoldRegions();
/**
* Checks if the specified offset in the document belongs to a folded region.
* Checks if the specified offset in the document belongs to a folded region. The region must contain given offset or be located right
* after given offset, i.e. the following condition must hold: foldStartOffset <= offset < foldEndOffset.
*
* @param offset the offset to check.
* @return true if the offset belongs to a folded region, false otherwise.
*
* @see #getCollapsedRegionAtOffset(int)
*/
boolean isOffsetCollapsed(int offset);
/**
* Returns collapsed folded region at a given offset or <code>null</code> if there's no such region. Returned region will satisfy the
* following condition: region.getStartOffset() <= offset < region.getEndOffset()
*
* @see #isOffsetCollapsed(int)
*/
@Nullable
FoldRegion getCollapsedRegionAtOffset(int offset);
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
* Copyright 2000-2014 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.
@@ -40,7 +40,11 @@ abstract class FoldRegionsTree {
private int[] myCachedStartOffsets;
private int[] myCachedFoldedLines;
int myCachedLastIndex = -1;
private ArrayList<FoldRegion> myRegions = ContainerUtil.newArrayList(); //sorted in tree left-to-right topdown traversal order
//sorted using RangeMarker.BY_START_OFFSET comparator
//i.e., first by start offset, then, if start offsets are equal, by end offset
private ArrayList<FoldRegion> myRegions = ContainerUtil.newArrayList();
private static final Comparator<FoldRegion> BY_END_OFFSET = new Comparator<FoldRegion>() {
@Override
public int compare(FoldRegion r1, FoldRegion r2) {
@@ -89,13 +93,15 @@ abstract class FoldRegionsTree {
}
allValid.add(region);
if (!region.isExpanded()) {
removeRegionsWithSameStartOffset(visible, region);
removeRegionsWithSameStartOffset(topLevels, region);
}
if (currentCollapsed == null || !contains(currentCollapsed, region)) {
visible.add(region);
}
if (!region.isExpanded()) {
if (currentCollapsed == null || currentCollapsed.getEndOffset() < region.getStartOffset()) {
if (!region.isExpanded()) {
currentCollapsed = region;
topLevels.add(region);
}
@@ -115,6 +121,17 @@ abstract class FoldRegionsTree {
updateCachedOffsets();
}
private static void removeRegionsWithSameStartOffset(List<FoldRegion> regions, FoldRegion region) {
for (int i = regions.size() - 1; i >= 0 ; i--) {
if (regions.get(i).getStartOffset() == region.getStartOffset()) {
regions.remove(i);
}
else {
break;
}
}
}
@NotNull
private static FoldRegion[] toFoldArray(@NotNull List<FoldRegion> topLevels) {
return topLevels.isEmpty() ? FoldRegion.EMPTY_ARRAY : topLevels.toArray(new FoldRegion[topLevels.size()]);
@@ -164,31 +181,25 @@ abstract class FoldRegionsTree {
boolean addRegion(FoldRegion range) {
// During batchProcessing elements are inserted in ascending order,
// binary search find acceptable insertion place first time
final boolean canUseCachedValue;
if (myCachedLastIndex >= myRegions.size()) {
// todo this happens after removeRegion()... myCachedListIndex must die!
canUseCachedValue = false;
boolean canUseCachedValue = false;
if (isBatchFoldingProcessing() && myCachedLastIndex >= 0 && myCachedLastIndex < myRegions.size()) {
FoldRegion lastRegion = myRegions.get(myCachedLastIndex);
if (RangeMarker.BY_START_OFFSET.compare(lastRegion, range) < 0) {
canUseCachedValue = myCachedLastIndex == (myRegions.size() - 1)
|| RangeMarker.BY_START_OFFSET.compare(range, myRegions.get(myCachedLastIndex + 1)) <= 0;
}
}
else {
canUseCachedValue =
myCachedLastIndex != -1 && isBatchFoldingProcessing() && myRegions.get(myCachedLastIndex).getStartOffset() <= range.getStartOffset();
}
int fastIndex = canUseCachedValue ? myCachedLastIndex + 1 : Collections.binarySearch(myRegions, range, RangeMarker.BY_START_OFFSET);
if (fastIndex < 0) fastIndex = -fastIndex - 1;
// There is a possible case that given range is the first at the current batch iteration. It's also possible that it
// range with the same bounds is already registered (e.g. particular range is registered during 'build initial fold regions' phase
// and given range has the same offsets but different 'expanded' status.
// We explicitly check for such situation, remove existing region and add the given one instead.
if (fastIndex < myRegions.size()) {
FoldRegion foldRegion = myRegions.get(fastIndex);
int index = canUseCachedValue ? myCachedLastIndex + 1 : Collections.binarySearch(myRegions, range, RangeMarker.BY_START_OFFSET);
if (index < 0) index = -index - 1;
if (index < myRegions.size()) {
FoldRegion foldRegion = myRegions.get(index);
if (TextRange.areSegmentsEqual(foldRegion, range)) {
removeRegion(foldRegion);
return addRegion(range);
}
return false;
}
}
for (int i = fastIndex - 1; i >=0; --i) {
for (int i = index - 1; i >=0; --i) {
final FoldRegion region = myRegions.get(i);
if (region.getEndOffset() < range.getStartOffset()) break;
if (region.isValid() && intersects(region, range)) {
@@ -196,28 +207,15 @@ abstract class FoldRegionsTree {
}
}
for (int i = fastIndex; i < myRegions.size(); i++) {
for (int i = index; i < myRegions.size(); i++) {
final FoldRegion region = myRegions.get(i);
if (range.getStartOffset() < region.getStartOffset() ||
range.getStartOffset() == region.getStartOffset() && range.getEndOffset() > region.getEndOffset()) {
for (int j = i + 1; j < myRegions.size(); j++) {
final FoldRegion next = myRegions.get(j);
if (next.getEndOffset() >= range.getEndOffset() && next.isValid()) {
if (next.getStartOffset() < range.getStartOffset()) {
return false;
}
else {
break;
}
}
}
myRegions.add(myCachedLastIndex = i, range);
return true;
if (region.getStartOffset() > range.getEndOffset()) break;
if (region.isValid() && intersects(region, range)) {
return false;
}
}
myRegions.add(myCachedLastIndex = myRegions.size(),range);
myRegions.add(myCachedLastIndex = index,range);
return true;
}
@@ -274,7 +272,7 @@ abstract class FoldRegionsTree {
}
private static boolean contains(FoldRegion outer, FoldRegion inner) {
return outer.getStartOffset() < inner.getStartOffset() && outer.getEndOffset() > inner.getStartOffset();
return outer.getStartOffset() <= inner.getStartOffset() && outer.getEndOffset() >= inner.getEndOffset();
}
private static boolean intersects(FoldRegion r1, FoldRegion r2) {
@@ -282,7 +280,7 @@ abstract class FoldRegionsTree {
final int s2 = r2.getStartOffset();
final int e1 = r1.getEndOffset();
final int e2 = r2.getEndOffset();
return s1 == s2 && e1 == e2 || s1 < s2 && s2 < e1 && e1 < e2 || s2 < s1 && s1 < e2 && e2 < e1;
return s1 < s2 && s2 < e1 && e1 < e2 || s2 < s1 && s1 < e2 && e2 < e1;
}
static boolean contains(FoldRegion region, int offset) {
@@ -0,0 +1,92 @@
/*
* Copyright 2000-2014 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;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.PlatformTestCase;
public class FoldingStressTest extends LightPlatformTestCase {
static {
PlatformTestCase.autodetectPlatformPrefix();
}
public void testStressFoldingFromZeroOffset() throws Exception {
for (int len = 2; len < 25; len++) {
stress(len);
}
}
public void testStress8() throws Exception {
DocumentImpl doc = new DocumentImpl("0123456789\n123456789\n23456789");
Editor editor = EditorFactory.getInstance().createEditor(doc);
try {
final FoldingModel model = editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
addAndCollapseFoldRegion(model, 0, 8, "/*...*/");
addAndCollapseFoldRegion(model, 10, 12, "/*...*/");
}
});
assertEquals(10, editor.logicalPositionToOffset(new LogicalPosition(0, 10)));
for (int line = 0; line <= 3; line++) {
for (int column = 0; column <= 100; column++) {
LogicalPosition log = new LogicalPosition(line, column);
editor.logicalToVisualPosition(log);
}
}
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
}
private static void stress(final int len) {
DocumentImpl doc = new DocumentImpl("0123456789\n123456789\n23456789");
Editor editor = EditorFactory.getInstance().createEditor(doc);
try {
final FoldingModel model = editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
addAndCollapseFoldRegion(model, 0, len, "/*...*/");
addAndCollapseFoldRegion(model, len + 2, len + 4, "/*...*/");
}
});
for (int line = 0; line <= 3; line++) {
for (int column = 0; column <= 100; column++) {
LogicalPosition log = new LogicalPosition(line, column);
editor.logicalToVisualPosition(log);
}
}
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
}
private static void addAndCollapseFoldRegion(FoldingModel model, int startOffset, int endOffset, String placeHolder) {
FoldRegion foldRegion = model.addFoldRegion(startOffset, endOffset, placeHolder);
assertNotNull(foldRegion);
foldRegion.setExpanded(false);
}
}
@@ -1,182 +1,153 @@
/*
* Copyright 2000-2014 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;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.ex.FoldingModelEx;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.util.Ref;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.openapi.editor.impl.AbstractEditorTest;
import com.intellij.testFramework.PlatformTestCase;
import org.jetbrains.annotations.NonNls;
import com.intellij.testFramework.TestFileType;
import static org.junit.Assert.assertArrayEquals;
/**
* @author max
*/
public class FoldingTest extends LightPlatformTestCase {
public class FoldingTest extends AbstractEditorTest {
static {
PlatformTestCase.autodetectPlatformPrefix();
}
public void testStressFoldingFromZeroOffset() throws Exception {
for (int len = 2; len < 25; len++) {
stress(len);
}
}
private FoldingModelEx myModel;
public void testStress8() throws Exception {
DocumentImpl doc = new DocumentImpl("0123456789\n123456789\n23456789");
Editor editor = EditorFactory.getInstance().createEditor(doc);
try {
final FoldingModel model = editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
model.addFoldRegion(0, 8, "/*...*/").setExpanded(false);
model.addFoldRegion(10, 12, "/*...*/").setExpanded(false);
}
});
assertEquals(10, editor.logicalPositionToOffset(new LogicalPosition(0, 10)));
for (int line = 0; line <= 3; line++) {
for (int column = 0; column <= 100; column++) {
LogicalPosition log = new LogicalPosition(line, column);
editor.logicalToVisualPosition(log);
}
}
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
}
private static void stress(final int len) {
DocumentImpl doc = new DocumentImpl("0123456789\n123456789\n23456789");
Editor editor = EditorFactory.getInstance().createEditor(doc);
try {
final FoldingModel model = editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
model.addFoldRegion(0, len, "/*...*/").setExpanded(false);
model.addFoldRegion(len + 2, len + 4, "/*...*/").setExpanded(false);
}
});
for (int line = 0; line <= 3; line++) {
for (int column = 0; column <= 100; column++) {
LogicalPosition log = new LogicalPosition(line, column);
editor.logicalToVisualPosition(log);
}
}
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
@Override
public void setUp() throws Exception {
super.setUp();
init("I don't know what you mean by `glory,'\" Alice said" +
"Humpty Dumpty smiled contemptuously. \"Of course you don't -- till I tell you. I meant `there's a nice knock-down argument for you!'" +
"But glory doesn't mean `a nice knock-down argument,'\" Alice objected." +
"When I use a word,\" Humpty Dumpty said, in a rather scornful tone, \"it means just what I choose it to mean -- neither more nor less." +
"The question is,\" said Alice, \"whether you can make words mean so many different things." +
"The question is,\" said Humpty Dumpty, \"which is to be master -- that's all.",
TestFileType.TEXT);
myModel = (FoldingModelEx)myEditor.getFoldingModel();
}
public void testCleanupInvalidRegions() {
final DocumentImpl doc = new DocumentImpl("foo1\nfoo2\nfoo3\nfoo4");
Editor editor = EditorFactory.getInstance().createEditor(doc);
final FoldingModel model = editor.getFoldingModel();
try {
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
model.addFoldRegion(0, 4, "/*...*/");
model.addFoldRegion(5, 9, "/*...*/");
}
});
assertSize(2, model.getAllFoldRegions());
WriteCommandAction.runWriteCommandAction(getProject(), new Runnable() {
@Override
public void run() {
doc.deleteString(0, 5);
}
});
assertSize(1, model.getAllFoldRegions());
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
myModel.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
myModel.addFoldRegion(0, 4, "/*...*/");
myModel.addFoldRegion(5, 9, "/*...*/");
}
});
assertSize(2, myModel.getAllFoldRegions());
WriteCommandAction.runWriteCommandAction(getProject(), new Runnable() {
@Override
public void run() {
myEditor.getDocument().deleteString(0, 5);
}
});
assertSize(1, myModel.getAllFoldRegions());
}
public void testIntersects () throws Exception {
@NonNls DocumentImpl doc = new DocumentImpl("I don't know what you mean by `glory,'\" Alice said" +
"Humpty Dumpty smiled contemptuously. \"Of course you don't -- till I tell you. I meant `there's a nice knock-down argument for you!'" +
"But glory doesn't mean `a nice knock-down argument,'\" Alice objected." +
"When I use a word,\" Humpty Dumpty said, in a rather scornful tone, \"it means just what I choose it to mean -- neither more nor less." +
"The question is,\" said Alice, \"whether you can make words mean so many different things." +
"The question is,\" said Humpty Dumpty, \"which is to be master -- that's all." );
Editor editor = EditorFactory.getInstance().createEditor(doc);
try {
final FoldingModel model = editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
FoldRegion region = model.addFoldRegion(5, 10, ".");
assertNotNull(region);
region = model.addFoldRegion(7, 11, ".");
assertNull(region);
region = model.addFoldRegion(20, 30, ".");
assertNotNull(region);
region = model.addFoldRegion(9, 12, ".");
assertNull(region);
region = model.addFoldRegion(7, 10, ".");
assertNotNull(region);
region = model.addFoldRegion(7, 10, ".");
assertNull(region);
region = model.addFoldRegion(5, 30, ".");
assertNotNull(region);
}
});
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
}
myModel.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
FoldRegion region = myModel.addFoldRegion(5, 10, ".");
assertNotNull(region);
region = myModel.addFoldRegion(7, 11, ".");
assertNull(region);
region = myModel.addFoldRegion(20, 30, ".");
assertNotNull(region);
region = myModel.addFoldRegion(9, 12, ".");
assertNull(region);
region = myModel.addFoldRegion(7, 10, ".");
assertNotNull(region);
region = myModel.addFoldRegion(7, 10, ".");
assertNull(region);
region = myModel.addFoldRegion(5, 30, ".");
assertNotNull(region);
}
});
}
public void testDuplicateRegions() {
StringBuilder text = new StringBuilder();
for (int i = 0; i < 450; i++) {
text.append('a');
}
Editor editor = EditorFactory.getInstance().createEditor(new DocumentImpl(text));
public void testAddEmptyRegion() {
FoldRegion region = null;
try {
final Ref<Boolean> expandedStatus = new Ref<Boolean>();
final int startOffset = 6;
final int endOffset = 16;
final FoldingModelEx model = (FoldingModelEx)editor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
model.addFoldRegion(2, 20, "..");
model.addFoldRegion(4, 18, "..");
FoldRegion oldRegion = model.addFoldRegion(startOffset, endOffset, "..");
assertNotNull(oldRegion);
expandedStatus.set(!oldRegion.isExpanded());
}
});
assertEquals(3, model.getAllFoldRegions().length);
final Ref<FoldRegion> newRegion = new Ref<FoldRegion>();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
newRegion.set(model.createFoldRegion(startOffset, endOffset, "..", null, false));
assertNotNull(newRegion.get());
newRegion.get().setExpanded(expandedStatus.get());
boolean additionFlag = model.addFoldRegion(newRegion.get());
assertTrue(additionFlag);
}
});
FoldRegion fetched = model.fetchOutermost(startOffset);
assertSame(newRegion.get(), fetched);
assertEquals(3, model.getAllFoldRegions().length);
region = myModel.addFoldRegion(5, 5, "...");
}
finally {
EditorFactory.getInstance().releaseEditor(editor);
catch (AssertionError ignored) {
}
assertNull(region);
}
public void testCollapsedRegionQueries() {
addCollapsedFoldRegion(5, 7, "...");
FoldRegion[] regions = myModel.getAllFoldRegions();
assertEquals(1, regions.length);
FoldRegion region = regions[0];
assertNotNull(region);
assertFalse(myModel.isOffsetCollapsed(4));
assertTrue(myModel.isOffsetCollapsed(5));
assertTrue(myModel.isOffsetCollapsed(6));
assertFalse(myModel.isOffsetCollapsed(7));
assertFalse(myModel.isOffsetCollapsed(8));
assertNull(myModel.getCollapsedRegionAtOffset(4));
assertSame(region, myModel.getCollapsedRegionAtOffset(5));
assertSame(region, myModel.getCollapsedRegionAtOffset(6));
assertNull(myModel.getCollapsedRegionAtOffset(7));
assertNull(myModel.getCollapsedRegionAtOffset(8));
}
public void testAdjacentRegions() {
addCollapsedFoldRegion(5, 7, "AA");
addCollapsedFoldRegion(7, 10, "BB");
FoldRegion[] regions = myModel.getAllFoldRegions();
assertEquals(2, regions.length);
FoldRegion region1 = regions[0];
assertNotNull(region1);
FoldRegion region2 = regions[1];
assertNotNull(region2);
assertFalse(myModel.isOffsetCollapsed(4));
assertTrue(myModel.isOffsetCollapsed(5));
assertTrue(myModel.isOffsetCollapsed(6));
assertTrue(myModel.isOffsetCollapsed(7));
assertTrue(myModel.isOffsetCollapsed(8));
assertFalse(myModel.isOffsetCollapsed(10));
assertFalse(myModel.isOffsetCollapsed(11));
assertNull(myModel.getCollapsedRegionAtOffset(4));
assertSame(region1, myModel.getCollapsedRegionAtOffset(5));
assertSame(region1, myModel.getCollapsedRegionAtOffset(6));
assertSame(region2, myModel.getCollapsedRegionAtOffset(7));
assertSame(region2, myModel.getCollapsedRegionAtOffset(8));
assertNull(myModel.getCollapsedRegionAtOffset(10));
assertNull(myModel.getCollapsedRegionAtOffset(11));
}
public void testTopLevel() {
FoldRegion region = addCollapsedFoldRegion(5, 15, "...");
addCollapsedFoldRegion(10, 12, "???");
FoldRegion[] topLevelRegions = myModel.fetchTopLevel();
assertArrayEquals(new FoldRegion[]{region}, topLevelRegions);
}
}