blob: 4c137f8d2a3787b77e02aa6f4e60772f6ebeeb37 [file] [log] [blame]
/*
* 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.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.extensions.Extensions;
import com.intellij.openapi.fileTypes.ContentBasedFileSubstitutor;
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.*;
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 = new Comparator<PsiElement>() {
@Override
public int compare(PsiElement element, PsiElement element1) {
int startOffsetDiff = element.getTextRange().getStartOffset() - element1.getTextRange().getStartOffset();
return startOffsetDiff == 0 ? element.getTextRange().getEndOffset() - element1.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(project, file, document, quick);
final UpdateFoldRegionsOperation operation = new UpdateFoldRegionsOperation(project, editor, file, elementsToFoldMap, applyDefaultState, false);
Runnable runnable = new Runnable() {
@Override
public void run() {
editor.getFoldingModel().runBatchFoldingOperationDoNotCollapseCaret(operation);
}
};
Set<Object> dependencies = new HashSet<Object>();
dependencies.add(document);
for (FoldingDescriptor descriptor : elementsToFoldMap.values()) {
dependencies.addAll(descriptor.getDependencies());
}
return CachedValueProvider.Result.create(runnable, ArrayUtil.toObjectArray(dependencies));
}
private static boolean isContentSubstituted(PsiFile file, Project project) {
final ContentBasedFileSubstitutor[] processors = Extensions.getExtensions(ContentBasedFileSubstitutor.EP_NAME);
for (ContentBasedFileSubstitutor processor : processors) {
if (processor.isApplicable(project, file.getVirtualFile())) {
return true;
}
}
return false;
}
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 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() {
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);
UpdateFoldRegionsOperation op = new UpdateFoldRegionsOperation(project, injectedEditor, injectedFile, map, applyDefaultState, true);
injectedEditor.getFoldingModel().runBatchFoldingOperationDoNotCollapseCaret(op);
}
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 Project project, @NotNull PsiFile file, @NotNull Document document, boolean quick) {
FoldingMap foldingMap = new FoldingMap();
if (!isContentSubstituted(file, project)) {
getFoldingsFor(file instanceof PsiCompiledFile ? ((PsiCompiledFile)file).getDecompiledPsiFile() : 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)) {
TextRange range = descriptor.getRange();
if (!docRange.contains(range)) {
LOG.error("Folding descriptor " + descriptor +
" made by " + foldingBuilder +
" for " +language +
" and called on file " + psi +
" is outside document range: " + docRange,
ApplicationManager.getApplication().isInternal()
? new Attachment[] {AttachmentFactory.createAttachment(document), new Attachment("psiTree.txt", DebugUtil.psiToString(psi, false, true))}
: new Attachment[0]);
}
elementsToFoldMap.putValue(descriptor.getElement().getPsi(), descriptor);
}
}
}
}
public static class FoldingMap extends MultiMap<PsiElement, FoldingDescriptor>{
@NotNull
@Override
protected Map<PsiElement, Collection<FoldingDescriptor>> createMap() {
return new TreeMap<PsiElement, Collection<FoldingDescriptor>>(COMPARE_BY_OFFSET);
}
@NotNull
@Override
protected Collection<FoldingDescriptor> createCollection() {
return new ArrayList<FoldingDescriptor>(1);
}
}
}