/*
* Copyright 2000-2016 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.codeInsight.folding.impl;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.FoldRegion;
import com.intellij.openapi.editor.FoldingGroup;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.FoldingModelEx;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.SmartPointerManager;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static com.intellij.util.containers.ContainerUtil.newArrayList;
import static com.intellij.util.containers.ContainerUtil.newTroveMap;
/**
* @author cdr
*/
class UpdateFoldRegionsOperation implements Runnable {
enum ApplyDefaultStateMode { YES, EXCEPT_CARET_REGION, NO }
private static final Logger LOG = Logger.getInstance("#" + UpdateFoldRegionsOperation.class.getName());
private static final Key<Boolean> CAN_BE_REMOVED_WHEN_COLLAPSED = Key.create("canBeRemovedWhenCollapsed");
private final Project myProject;
private final Editor myEditor;
private final PsiFile myFile;
private final ApplyDefaultStateMode myApplyDefaultState;
private final FoldingUpdate.FoldingMap myElementsToFoldMap;
private final boolean myKeepCollapsedRegions;
private final boolean myForInjected;
UpdateFoldRegionsOperation(@NotNull Project project,
@NotNull Editor editor,
@NotNull PsiFile file,
@NotNull FoldingUpdate.FoldingMap elementsToFoldMap,
ApplyDefaultStateMode applyDefaultState,
boolean keepCollapsedRegions,
boolean forInjected) {
myProject = project;
myEditor = editor;
myFile = file;
myApplyDefaultState = applyDefaultState;
myElementsToFoldMap = elementsToFoldMap;
myKeepCollapsedRegions = keepCollapsedRegions;
myForInjected = forInjected;
}
@Override
public void run() {
EditorFoldingInfo info = EditorFoldingInfo.get(myEditor);
FoldingModelEx foldingModel = (FoldingModelEx)myEditor.getFoldingModel();
Map<TextRange,Boolean> rangeToExpandStatusMap = newTroveMap();
// FoldingUpdate caches instances of our object, so they must be immutable.
FoldingUpdate.FoldingMap elementsToFold = new FoldingUpdate.FoldingMap(myElementsToFoldMap);
removeInvalidRegions(info, foldingModel, elementsToFold, rangeToExpandStatusMap);
Map<FoldRegion, Boolean> shouldExpand = newTroveMap();
Map<FoldingGroup, Boolean> groupExpand = newTroveMap();
List<FoldRegion> newRegions = addNewRegions(info, foldingModel, elementsToFold, rangeToExpandStatusMap, shouldExpand, groupExpand);
applyExpandStatus(newRegions, shouldExpand, groupExpand);
foldingModel.clearDocumentRangesModificationStatus();
}
private static void applyExpandStatus(@NotNull List<FoldRegion> newRegions,
@NotNull Map<FoldRegion, Boolean> shouldExpand,
@NotNull Map<FoldingGroup, Boolean> groupExpand) {
for (final FoldRegion region : newRegions) {
final FoldingGroup group = region.getGroup();
final Boolean expanded = group == null ? shouldExpand.get(region) : groupExpand.get(group);
if (expanded != null) {
region.setExpanded(expanded.booleanValue());
}
}
}
private List<FoldRegion> addNewRegions(@NotNull EditorFoldingInfo info,
@NotNull FoldingModelEx foldingModel,
FoldingUpdate.FoldingMap elementsToFold, @NotNull Map<TextRange, Boolean> rangeToExpandStatusMap,
@NotNull Map<FoldRegion, Boolean> shouldExpand,
@NotNull Map<FoldingGroup, Boolean> groupExpand) {
List<FoldRegion> newRegions = newArrayList();
SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(myProject);
for (PsiElement element : elementsToFold.keySet()) {
ProgressManager.checkCanceled();
final Collection<FoldingDescriptor> descriptors = elementsToFold.get(element);
for (FoldingDescriptor descriptor : descriptors) {
FoldingGroup group = descriptor.getGroup();
TextRange range = descriptor.getRange();
String placeholder = descriptor.getPlaceholderText();
if (range.getEndOffset() > myEditor.getDocument().getTextLength()) {
LOG.error(String.format("Invalid folding descriptor detected (%s). It ends beyond the document range (%d)",
descriptor, myEditor.getDocument().getTextLength()));
continue;
}
FoldRegion region = foldingModel.createFoldRegion(range.getStartOffset(), range.getEndOffset(),
placeholder == null ? "..." : placeholder,
group,
descriptor.isNonExpandable());
if (region == null) continue;
PsiElement psi = descriptor.getElement().getPsi();
if (psi == null || !psi.isValid() || !foldingModel.addFoldRegion(region) || !myFile.isValid()) {
region.dispose();
continue;
}
if (descriptor.canBeRemovedWhenCollapsed()) region.putUserData(CAN_BE_REMOVED_WHEN_COLLAPSED, Boolean.TRUE);
info.addRegion(region, smartPointerManager.createSmartPsiElementPointer(psi));
newRegions.add(region);
boolean expandStatus = !descriptor.isNonExpandable() && shouldExpandNewRegion(element, range, rangeToExpandStatusMap);
if (group == null) {
shouldExpand.put(region, expandStatus);
}
else {
final Boolean alreadyExpanded = groupExpand.get(group);
groupExpand.put(group, alreadyExpanded == null ? expandStatus : alreadyExpanded.booleanValue() || expandStatus);
}
}
}
return newRegions;
}
private boolean shouldExpandNewRegion(PsiElement element, TextRange range, Map<TextRange, Boolean> rangeToExpandStatusMap) {
if (myApplyDefaultState != ApplyDefaultStateMode.NO) {
// Considering that this code is executed only on initial fold regions construction on editor opening.
if (myApplyDefaultState == ApplyDefaultStateMode.EXCEPT_CARET_REGION) {
TextRange lineRange = OpenFileDescriptor.getRangeToUnfoldOnNavigation(myEditor);
if (lineRange.intersects(range)) {
return true;
}
}
return !FoldingPolicy.isCollapseByDefault(element);
}
final Boolean oldStatus = rangeToExpandStatusMap.get(range);
return oldStatus == null || FoldingUtil.caretInsideRange(myEditor, range) || oldStatus.booleanValue();
}
private void removeInvalidRegions(@NotNull EditorFoldingInfo info,
@NotNull FoldingModelEx foldingModel,
FoldingUpdate.FoldingMap elementsToFold, @NotNull Map<TextRange, Boolean> rangeToExpandStatusMap) {
List<FoldRegion> toRemove = newArrayList();
InjectedLanguageManager injectedManager = InjectedLanguageManager.getInstance(myProject);
for (FoldRegion region : foldingModel.getAllFoldRegions()) {
if (myKeepCollapsedRegions && !region.isExpanded() && !regionOrGroupCanBeRemovedWhenCollapsed(region)) continue;
PsiElement element = info.getPsiElement(region);
if (element != null) {
PsiFile containingFile = element.getContainingFile();
boolean isInjected = injectedManager.isInjectedFragment(containingFile);
if (isInjected != myForInjected) continue;
}
final Collection<FoldingDescriptor> descriptors;
if (element != null && !(descriptors = elementsToFold.get(element)).isEmpty()) {
boolean matchingDescriptorFound = false;
FoldingDescriptor[] array = descriptors.toArray(new FoldingDescriptor[descriptors.size()]);
for (FoldingDescriptor descriptor : array) {
TextRange range = descriptor.getRange();
if (TextRange.areSegmentsEqual(region, range)) {
matchingDescriptorFound = true;
if (!region.isValid() ||
region.getGroup() != null ||
descriptor.getGroup() != null ||
!region.getPlaceholderText().equals(descriptor.getPlaceholderText()) ||
range.getLength() < 2
) {
rangeToExpandStatusMap.put(range, region.isExpanded());
toRemove.add(region);
break;
}
else {
elementsToFold.remove(element, descriptor);
}
}
}
if (!matchingDescriptorFound) {
if (Registry.is("editor.durable.folding.state")) {
for (FoldingDescriptor descriptor : descriptors) {
rangeToExpandStatusMap.put(descriptor.getRange(), region.isExpanded());
}
}
toRemove.add(region);
}
}
else if (region.isValid() && info.isLightRegion(region)) {
boolean isExpanded = region.isExpanded();
rangeToExpandStatusMap.put(TextRange.create(region), isExpanded);
}
else {
toRemove.add(region);
}
}
for (final FoldRegion region : toRemove) {
foldingModel.removeFoldRegion(region);
info.removeRegion(region);
}
}
private boolean regionOrGroupCanBeRemovedWhenCollapsed(FoldRegion region) {
FoldingGroup group = region.getGroup();
List<FoldRegion> affectedRegions = group != null && myEditor instanceof EditorEx
? ((EditorEx)myEditor).getFoldingModel().getGroupedRegions(group)
: Collections.singletonList(region);
for (FoldRegion affectedRegion : affectedRegions) {
if (regionCanBeRemovedWhenCollapsed(affectedRegion)) return true;
}
return false;
}
private boolean regionCanBeRemovedWhenCollapsed(FoldRegion region) {
return Boolean.TRUE.equals(region.getUserData(CAN_BE_REMOVED_WHEN_COLLAPSED)) ||
((FoldingModelEx)myEditor.getFoldingModel()).hasDocumentRegionChangedFor(region) ||
!region.isValid() ||
isRegionInCaretLine(region);
}
private boolean isRegionInCaretLine(FoldRegion region) {
int regionStartLine = myEditor.getDocument().getLineNumber(region.getStartOffset());
int regionEndLine = myEditor.getDocument().getLineNumber(region.getEndOffset());
int caretLine = myEditor.getCaretModel().getLogicalPosition().line;
return caretLine >= regionStartLine && caretLine <= regionEndLine;
}
}