com.intellij.openapi.editor.impl.FoldRegionsTree Maven / Gradle / Ivy
/*
* Copyright 2000-2015 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.*;
import com.intellij.openapi.util.Key;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* User: cdr
*/
abstract class FoldRegionsTree {
private static final Key VISIBLE = Key.create("visible.fold.region");
@NotNull private volatile CachedData myCachedData = new CachedData();
//sorted using RangeMarker.BY_START_OFFSET comparator
//i.e., first by start offset, then, if start offsets are equal, by end offset
@NotNull
private List myRegions = ContainerUtil.newArrayList();
private static final Comparator BY_END_OFFSET = new Comparator() {
@Override
public int compare(FoldRegion r1, FoldRegion r2) {
int end1 = r1.getEndOffset();
int end2 = r2.getEndOffset();
if (end1 < end2) return -1;
if (end1 > end2) return 1;
return 0;
}
};
private static final Comparator super FoldRegion> BY_END_OFFSET_REVERSE = Collections.reverseOrder(BY_END_OFFSET);
private static final TObjectHashingStrategy OFFSET_BASED_HASHING_STRATEGY = new TObjectHashingStrategy() {
@Override
public int computeHashCode(FoldRegion o) {
return o.getStartOffset() * 31 + o.getEndOffset();
}
@Override
public boolean equals(FoldRegion o1, FoldRegion o2) {
return o1.getStartOffset() == o2.getStartOffset() && o1.getEndOffset() == o2.getEndOffset();
}
};
void clear() {
clearCachedValues();
for (FoldRegion region : myRegions) {
region.dispose();
}
myRegions = new ArrayList();
}
void clearCachedValues() {
myCachedData = new CachedData();
}
protected abstract boolean isFoldingEnabled();
void rebuild() {
List topLevels = new ArrayList(myRegions.size() / 2);
List visible = new ArrayList(myRegions.size());
List allValid = new ArrayList(myRegions.size());
THashMap distinctRegions = new THashMap(myRegions.size(), OFFSET_BASED_HASHING_STRATEGY);
for (FoldRegion region : myRegions) {
if (!region.isValid()) {
continue;
}
if (distinctRegions.contains(region)) {
if (region.getUserData(VISIBLE) == null) {
region.dispose();
continue;
}
else {
FoldRegion identicalRegion = distinctRegions.remove(region);
identicalRegion.dispose();
}
}
distinctRegions.put(region, region);
}
for (FoldRegion region : myRegions) {
if (region.isValid()) {
allValid.add(region);
}
}
if (allValid.size() < myRegions.size()) {
myRegions = allValid;
}
Collections.sort(myRegions, RangeMarker.BY_START_OFFSET); // the order could have changed due to document changes
FoldRegion currentCollapsed = null;
for (FoldRegion region : myRegions) {
if (!region.isExpanded()) {
removeRegionsWithSameStartOffset(visible, region);
removeRegionsWithSameStartOffset(topLevels, region);
}
if (currentCollapsed == null || !contains(currentCollapsed, region)) {
visible.add(region);
region.putUserData(VISIBLE, Boolean.TRUE);
if (!region.isExpanded()) {
currentCollapsed = region;
topLevels.add(region);
}
}
else {
region.putUserData(VISIBLE, null);
}
}
FoldRegion[] topLevelRegions = toFoldArray(topLevels);
FoldRegion[] visibleRegions = toFoldArray(visible);
Arrays.sort(topLevelRegions, BY_END_OFFSET);
Arrays.sort(visibleRegions, BY_END_OFFSET_REVERSE);
updateCachedOffsets(visibleRegions, topLevelRegions);
}
private static void removeRegionsWithSameStartOffset(List 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 topLevels) {
return topLevels.isEmpty() ? FoldRegion.EMPTY_ARRAY : topLevels.toArray(new FoldRegion[topLevels.size()]);
}
void updateCachedOffsets() {
CachedData cachedData = myCachedData;
updateCachedOffsets(cachedData.visibleRegions, cachedData.topLevelRegions);
}
private void updateCachedOffsets(FoldRegion[] visibleRegions, FoldRegion[] topLevelRegions) {
if (!isFoldingEnabled()) {
return;
}
if (visibleRegions == null) {
rebuild();
return;
}
Set distinctRegions = new THashSet(visibleRegions.length, OFFSET_BASED_HASHING_STRATEGY);
for (FoldRegion foldRegion : visibleRegions) {
if (!foldRegion.isValid() || !distinctRegions.add(foldRegion)) {
rebuild();
return;
}
}
int length = topLevelRegions.length;
int[] startOffsets = ArrayUtil.newIntArray(length);
int[] endOffsets = ArrayUtil.newIntArray(length);
int[] foldedLines = ArrayUtil.newIntArray(length);
int sum = 0;
for (int i = 0; i < length; i++) {
FoldRegion region = topLevelRegions[i];
startOffsets[i] = region.getStartOffset();
endOffsets[i] = region.getEndOffset() - 1;
Document document = region.getDocument();
sum += document.getLineNumber(region.getEndOffset()) - document.getLineNumber(region.getStartOffset());
foldedLines[i] = sum;
}
myCachedData = new CachedData(visibleRegions, topLevelRegions, startOffsets, endOffsets, foldedLines);
}
boolean addRegion(@NotNull FoldRegion range) {
int start = range.getStartOffset();
int end = range.getEndOffset();
int insertionIndex = myRegions.size();
for (int i = 0; i < myRegions.size(); i++) {
FoldRegion region = myRegions.get(i);
int rStart = region.getStartOffset();
int rEnd = region.getEndOffset();
if (rStart < start) {
if (region.isValid() && start < rEnd && rEnd < end) {
return false;
}
}
else if (rStart == start) {
if (rEnd == end) {
return false;
}
else if (rEnd > end) {
insertionIndex = Math.min(insertionIndex, i);
}
}
else {
insertionIndex = Math.min(insertionIndex, i);
if (rStart > end) {
break;
}
if (region.isValid() && rStart < end && end < rEnd) {
return false;
}
}
}
myRegions.add(insertionIndex, range);
return true;
}
@Nullable
FoldRegion fetchOutermost(int offset) {
CachedData cachedData = myCachedData;
if (cachedData.isUnavailable()) return null;
final int[] starts = cachedData.startOffsets;
final int[] ends = cachedData.endOffsets;
if (starts == null || ends == null) {
return null;
}
int start = 0;
int end = ends.length - 1;
while (start <= end) {
int i = (start + end) / 2;
if (offset < starts[i]) {
end = i - 1;
} else if (offset > ends[i]) {
start = i + 1;
}
else {
return cachedData.topLevelRegions[i];
}
}
return null;
}
FoldRegion[] fetchVisible() {
CachedData cachedData = myCachedData;
return cachedData.isUnavailable() ? FoldRegion.EMPTY_ARRAY : cachedData.visibleRegions;
}
@Nullable
FoldRegion[] fetchTopLevel() {
CachedData cachedData = myCachedData;
return cachedData.isUnavailable() ? null : cachedData.topLevelRegions;
}
private static boolean contains(FoldRegion outer, FoldRegion inner) {
return outer.getStartOffset() <= inner.getStartOffset() && outer.getEndOffset() >= inner.getEndOffset();
}
static boolean contains(FoldRegion region, int offset) {
return region.getStartOffset() < offset && region.getEndOffset() > offset;
}
public FoldRegion[] fetchCollapsedAt(int offset) {
if (myCachedData.isUnavailable()) return FoldRegion.EMPTY_ARRAY;
ArrayList allCollapsed = new ArrayList();
for (FoldRegion region : myRegions) {
if (!region.isExpanded() && contains(region, offset)) {
allCollapsed.add(region);
}
}
return toFoldArray(allCollapsed);
}
boolean intersectsRegion(int startOffset, int endOffset) {
if (!isFoldingEnabled()) return true;
for (FoldRegion region : myRegions) {
boolean contains1 = contains(region, startOffset);
boolean contains2 = contains(region, endOffset);
if (contains1 != contains2) {
return true;
}
}
return false;
}
FoldRegion[] fetchAllRegions() {
if (myCachedData.isUnavailable()) return FoldRegion.EMPTY_ARRAY;
return toFoldArray(myRegions);
}
void removeRegion(@NotNull FoldRegion range) {
myRegions.remove(range);
}
int getFoldedLinesCountBefore(int offset) {
CachedData snapshot = myCachedData;
int idx = getLastTopLevelIndexBefore(snapshot, offset);
if (idx == -1) return 0;
return snapshot.foldedLines[idx];
}
public int getLastTopLevelIndexBefore(int offset) {
return getLastTopLevelIndexBefore(myCachedData, offset);
}
private static int getLastTopLevelIndexBefore(CachedData snapshot, int offset) {
int[] endOffsets = snapshot.endOffsets;
if (snapshot.isUnavailable() || endOffsets == null) return -1;
offset--; // end offsets are decremented in cache
int start = 0;
int end = endOffsets.length - 1;
while (start <= end) {
int i = (start + end) / 2;
if (offset < endOffsets[i]) {
end = i - 1;
} else if (offset > endOffsets[i]) {
start = i + 1;
}
else {
return i;
}
}
return end;
}
@Nullable
public FoldRegion getRegionAt(int startOffset, int endOffset) {
int index = Collections.binarySearch(myRegions, new DummyFoldRegion(startOffset, endOffset), RangeMarker.BY_START_OFFSET);
return index < 0 ? null : myRegions.get(index);
}
private class CachedData implements Cloneable {
private final FoldRegion[] visibleRegions;
private final FoldRegion[] topLevelRegions;
private final int[] startOffsets;
private final int[] endOffsets;
private final int[] foldedLines;
private CachedData() {
this.visibleRegions = null;
this.topLevelRegions = null;
this.startOffsets = null;
this.endOffsets = null;
this.foldedLines = null;
}
private CachedData(FoldRegion[] visibleRegions, FoldRegion[] topLevelRegions, int[] startOffsets, int[] endOffsets, int[] foldedLines) {
this.visibleRegions = visibleRegions;
this.topLevelRegions = topLevelRegions;
this.startOffsets = startOffsets;
this.endOffsets = endOffsets;
this.foldedLines = foldedLines;
}
private boolean isUnavailable() {
return !isFoldingEnabled() || visibleRegions == null;
}
}
private static class DummyFoldRegion implements FoldRegion {
private final int myStartOffset;
private final int myEndOffset;
private DummyFoldRegion(int startOffset, int endOffset) {
myStartOffset = startOffset;
myEndOffset = endOffset;
}
@Override
public boolean isExpanded() {
throw new UnsupportedOperationException();
}
@Override
public void setExpanded(boolean expanded) {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public String getPlaceholderText() {
throw new UnsupportedOperationException();
}
@Override
public Editor getEditor() {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public FoldingGroup getGroup() {
throw new UnsupportedOperationException();
}
@Override
public boolean shouldNeverExpand() {
throw new UnsupportedOperationException();
}
@NotNull
@Override
public Document getDocument() {
throw new UnsupportedOperationException();
}
@Override
public int getStartOffset() {
return myStartOffset;
}
@Override
public int getEndOffset() {
return myEndOffset;
}
@Override
public boolean isValid() {
throw new UnsupportedOperationException();
}
@Override
public void setGreedyToLeft(boolean greedy) {
throw new UnsupportedOperationException();
}
@Override
public void setGreedyToRight(boolean greedy) {
throw new UnsupportedOperationException();
}
@Override
public boolean isGreedyToRight() {
throw new UnsupportedOperationException();
}
@Override
public boolean isGreedyToLeft() {
throw new UnsupportedOperationException();
}
@Override
public void dispose() {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public T getUserData(@NotNull Key key) {
throw new UnsupportedOperationException();
}
@Override
public void putUserData(@NotNull Key key, @Nullable T value) {
throw new UnsupportedOperationException();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy