/*
* 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.codeInsight.folding.impl;
import com.intellij.diagnostic.AttachmentFactory;
import com.intellij.injected.editor.DocumentWindow;
import com.intellij.injected.editor.EditorWindow;
import com.intellij.lang.Language;
import com.intellij.lang.folding.FoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.lang.folding.LanguageFolding;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Attachment;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.FoldingModel;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.util.ParameterizedCachedValue;
import com.intellij.psi.util.ParameterizedCachedValueProvider;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.codeInsight.folding.impl.UpdateFoldRegionsOperation.ApplyDefaultStateMode.EXCEPT_CARET_REGION;
import static com.intellij.codeInsight.folding.impl.UpdateFoldRegionsOperation.ApplyDefaultStateMode.NO;
public class FoldingUpdate {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.folding.impl.FoldingUpdate");
private static final Key<ParameterizedCachedValue<Runnable, Couple<Boolean>>> CODE_FOLDING_KEY = Key.create("code folding");
private static final Key<String> CODE_FOLDING_FILE_EXTENSION_KEY = Key.create("code folding file extension");
private static final Comparator<PsiElement> COMPARE_BY_OFFSET_REVERSED = new Comparator<PsiElement>() {
@Override
public int compare(PsiElement element, PsiElement element1) {
int startOffsetDiff = element1.getTextRange().getStartOffset() - element.getTextRange().getStartOffset();
return startOffsetDiff == 0 ? element1.getTextRange().getEndOffset() - element.getTextRange().getEndOffset() : startOffsetDiff;
}
};
private FoldingUpdate() {
}
@Nullable
static Runnable updateFoldRegions(@NotNull final Editor editor, @NotNull PsiFile file, final boolean applyDefaultState, final boolean quick) {
ApplicationManager.getApplication().assertReadAccessAllowed();
final Project project = file.getProject();
final Document document = editor.getDocument();
LOG.assertTrue(!PsiDocumentManager.getInstance(project).isUncommited(document));
String currentFileExtension = null;
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
currentFileExtension = virtualFile.getExtension();
}
ParameterizedCachedValue<Runnable, Couple<Boolean>> value = editor.getUserData(CODE_FOLDING_KEY);
if (value != null) {
// There was a problem that old fold regions have been cached on file extension change (e.g. *.java -> *.groovy).
// We want to drop them in such circumstances.
final String oldExtension = editor.getUserData(CODE_FOLDING_FILE_EXTENSION_KEY);
if (oldExtension == null ? currentFileExtension != null : !oldExtension.equals(currentFileExtension)) {
value = null;
editor.putUserData(CODE_FOLDING_KEY, null);
}
}
editor.putUserData(CODE_FOLDING_FILE_EXTENSION_KEY, currentFileExtension);
if (value != null && value.hasUpToDateValue() && !applyDefaultState) return null;
if (quick) return getUpdateResult(file, document, quick, project, editor, applyDefaultState).getValue();
return CachedValuesManager.getManager(project).getParameterizedCachedValue(
editor, CODE_FOLDING_KEY, new ParameterizedCachedValueProvider<Runnable, Couple<Boolean>>() {
@Override
public CachedValueProvider.Result<Runnable> compute(Couple<Boolean> param) {
Document document = editor.getDocument();
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(document);
return getUpdateResult(file, document, param.first, project, editor, param.second);
}
}, false, Couple.of(quick, applyDefaultState));
}
private static CachedValueProvider.Result<Runnable> getUpdateResult(PsiFile file,
@NotNull Document document,
boolean quick,
final Project project,
final Editor editor,
final boolean applyDefaultState) {
final FoldingMap elementsToFoldMap = getFoldingsFor(file, document, quick);
final UpdateFoldRegionsOperation operation = new UpdateFoldRegionsOperation(project, editor, file, elementsToFoldMap,
applyDefaultState ? EXCEPT_CARET_REGION : NO,
!applyDefaultState, false);
Runnable runnable = new Runnable() {
@Override
public void run() {
editor.getFoldingModel().runBatchFoldingOperationDoNotCollapseCaret(operation);
}
};
Set<Object> dependencies = new HashSet<Object>();
dependencies.add(document);
dependencies.add(editor.getFoldingModel());
for (FoldingDescriptor descriptor : elementsToFoldMap.values()) {
dependencies.addAll(descriptor.getDependencies());
}
return CachedValueProvider.Result.create(runnable, ArrayUtil.toObjectArray(dependencies));
}
private static final Key<Object> LAST_UPDATE_INJECTED_STAMP_KEY = Key.create("LAST_UPDATE_INJECTED_STAMP_KEY");
@Nullable
public static Runnable updateInjectedFoldRegions(@NotNull final Editor editor, @NotNull final PsiFile file, final boolean applyDefaultState) {
if (file instanceof PsiCompiledElement) return null;
ApplicationManager.getApplication().assertReadAccessAllowed();
final Project project = file.getProject();
Document document = editor.getDocument();
LOG.assertTrue(!PsiDocumentManager.getInstance(project).isUncommited(document));
final FoldingModel foldingModel = editor.getFoldingModel();
final long timeStamp = document.getModificationStamp();
Object lastTimeStamp = editor.getUserData(LAST_UPDATE_INJECTED_STAMP_KEY);
if (lastTimeStamp instanceof Long && ((Long)lastTimeStamp).longValue() == timeStamp) return null;
List<DocumentWindow> injectedDocuments = InjectedLanguageUtil.getCachedInjectedDocuments(file);
if (injectedDocuments.isEmpty()) return null;
final List<EditorWindow> injectedEditors = new ArrayList<EditorWindow>();
final List<PsiFile> injectedFiles = new ArrayList<PsiFile>();
final List<FoldingMap> maps = new ArrayList<FoldingMap>();
for (final DocumentWindow injectedDocument : injectedDocuments) {
if (!injectedDocument.isValid()) {
continue;
}
InjectedLanguageUtil.enumerate(injectedDocument, file, new PsiLanguageInjectionHost.InjectedPsiVisitor() {
@Override
public void visit(@NotNull PsiFile injectedFile, @NotNull List<PsiLanguageInjectionHost.Shred> places) {
if (!injectedFile.isValid()) return;
Editor injectedEditor = InjectedLanguageUtil.getInjectedEditorForInjectedFile(editor, injectedFile);
if (!(injectedEditor instanceof EditorWindow)) return;
injectedEditors.add((EditorWindow)injectedEditor);
injectedFiles.add(injectedFile);
final FoldingMap map = new FoldingMap();
maps.add(map);
getFoldingsFor(injectedFile, injectedEditor.getDocument(), map, false);
}
});
}
return new Runnable() {
@Override
public void run() {
final ArrayList<Runnable> updateOperations = new ArrayList<Runnable>(injectedEditors.size());
for (int i = 0; i < injectedEditors.size(); i++) {
EditorWindow injectedEditor = injectedEditors.get(i);
PsiFile injectedFile = injectedFiles.get(i);
if (!injectedEditor.getDocument().isValid()) continue;
FoldingMap map = maps.get(i);
updateOperations.add(new UpdateFoldRegionsOperation(project, injectedEditor, injectedFile, map,
applyDefaultState ? EXCEPT_CARET_REGION : NO, !applyDefaultState, true));
}
foldingModel.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
for (Runnable operation : updateOperations) {
operation.run();
}
}
});
editor.putUserData(LAST_UPDATE_INJECTED_STAMP_KEY, timeStamp);
}
};
}
/**
* Checks the ability to initialize folding in the Dumb Mode. Due to language injections it may depend on
* edited file and active injections (not yet implemented).
*
* @param editor the editor that holds file view
* @return true if folding initialization available in the Dumb Mode
*/
public static boolean supportsDumbModeFolding(@NotNull Editor editor) {
Project project = editor.getProject();
if (project != null) {
PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
if (file != null) {
return supportsDumbModeFolding(file);
}
}
return true;
}
/**
* Checks the ability to initialize folding in the Dumb Mode for file.
*
* @param file the file to test
* @return true if folding initialization available in the Dumb Mode
*/
public static boolean supportsDumbModeFolding(@NotNull PsiFile file) {
final FileViewProvider viewProvider = file.getViewProvider();
for (final Language language : viewProvider.getLanguages()) {
final FoldingBuilder foldingBuilder = LanguageFolding.INSTANCE.forLanguage(language);
if(foldingBuilder != null && !DumbService.isDumbAware(foldingBuilder))
return false;
}
return true;
}
static FoldingMap getFoldingsFor(@NotNull PsiFile file, @NotNull Document document, boolean quick) {
FoldingMap foldingMap = new FoldingMap();
if (file instanceof PsiCompiledFile) {
file = ((PsiCompiledFile)file).getDecompiledPsiFile();
}
getFoldingsFor(file, document, foldingMap, quick);
return foldingMap;
}
private static void getFoldingsFor(@NotNull PsiFile file,
@NotNull Document document,
@NotNull FoldingMap elementsToFoldMap,
boolean quick) {
final FileViewProvider viewProvider = file.getViewProvider();
TextRange docRange = TextRange.from(0, document.getTextLength());
for (final Language language : viewProvider.getLanguages()) {
final PsiFile psi = viewProvider.getPsi(language);
final FoldingBuilder foldingBuilder = LanguageFolding.INSTANCE.forLanguage(language);
if (psi != null && foldingBuilder != null) {
for (FoldingDescriptor descriptor : LanguageFolding.buildFoldingDescriptors(foldingBuilder, psi, document, quick)) {
PsiElement psiElement = descriptor.getElement().getPsi();
if (psiElement == null) {
LOG.error("No PSI for folding descriptor " + descriptor);
continue;
}
if (!docRange.contains(descriptor.getRange())) {
diagnoseIncorrectRange(psi, document, language, foldingBuilder, descriptor, psiElement);
continue;
}
elementsToFoldMap.putValue(psiElement, descriptor);
}
}
}
}
private static void diagnoseIncorrectRange(@NotNull PsiFile file,
@NotNull Document document,
Language language,
FoldingBuilder foldingBuilder, FoldingDescriptor descriptor, PsiElement psiElement) {
String message = "Folding descriptor " + descriptor +
" made by " + foldingBuilder +
" for " + language +
" is outside document range" +
", PSI element: " + psiElement +
", PSI element range: " + psiElement.getTextRange() + "; " + DebugUtil.diagnosePsiDocumentInconsistency(psiElement, document);
LOG.error(message, ApplicationManager.getApplication().isInternal()
? new Attachment[]{AttachmentFactory.createAttachment(document), new Attachment("psiTree.txt", DebugUtil.psiToString(file, false, true))}
: new Attachment[0]);
}
public static class FoldingMap extends MultiMap<PsiElement, FoldingDescriptor>{
public FoldingMap() {
}
public FoldingMap(FoldingMap map) {
super(map);
}
@NotNull
@Override
protected Map<PsiElement, Collection<FoldingDescriptor>> createMap() {
return new TreeMap<PsiElement, Collection<FoldingDescriptor>>(COMPARE_BY_OFFSET_REVERSED);
}
@NotNull
@Override
protected Collection<FoldingDescriptor> createCollection() {
return new ArrayList<FoldingDescriptor>(1);
}
}
}