/* * Copyright 2000-2017 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.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.util.ArrayUtil; import com.intellij.util.containers.MultiMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; 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"); static final Key<ParameterizedCachedValue<Runnable, 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 = (element, 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, 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 value.getValue(null); // param shouldn't matter, as the value is up-to-date if (quick) return getUpdateResult(file, document, true, project, editor, applyDefaultState).getValue(); return CachedValuesManager.getManager(project).getParameterizedCachedValue( editor, CODE_FOLDING_KEY, param -> { Document document1 = editor.getDocument(); PsiFile file1 = PsiDocumentManager.getInstance(project).getPsiFile(document1); return getUpdateResult(file1, document1, false, project, editor, param); }, false, 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); AtomicBoolean alreadyExecuted = new AtomicBoolean(); Runnable runnable = () -> { if (alreadyExecuted.compareAndSet(false, true)) { editor.getFoldingModel().runBatchFoldingOperationDoNotCollapseCaret(operation); } }; Set<Object> dependencies = new HashSet<>(); dependencies.add(document); dependencies.add(editor.getFoldingModel()); for (RegionInfo info : elementsToFoldMap.values()) { dependencies.addAll(info.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<>(); final List<PsiFile> injectedFiles = new ArrayList<>(); final List<FoldingMap> maps = new ArrayList<>(); for (final DocumentWindow injectedDocument : injectedDocuments) { if (!injectedDocument.isValid()) { continue; } InjectedLanguageUtil.enumerate(injectedDocument, file, (injectedFile, 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 () -> { final ArrayList<Runnable> updateOperations = new ArrayList<>(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(() -> { 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 */ 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; } RegionInfo regionInfo = new RegionInfo(descriptor, psiElement); elementsToFoldMap.putValue(psiElement, regionInfo); } } } } 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))} : Attachment.EMPTY_ARRAY); } static class FoldingMap extends MultiMap<PsiElement, RegionInfo>{ FoldingMap() { } FoldingMap(FoldingMap map) { super(map); } @NotNull @Override protected Map<PsiElement, Collection<RegionInfo>> createMap() { return new TreeMap<>(COMPARE_BY_OFFSET_REVERSED); } @NotNull @Override protected Collection<RegionInfo> createCollection() { return new ArrayList<>(1); } } static class RegionInfo { @NotNull public final FoldingDescriptor descriptor; public final boolean collapsedByDefault; private RegionInfo(@NotNull FoldingDescriptor descriptor, @NotNull PsiElement psiElement) { this.descriptor = descriptor; this.collapsedByDefault = FoldingPolicy.isCollapseByDefault(psiElement); } @Override public String toString() { return descriptor + ", collapsedByDefault=" + collapsedByDefault; } } }