blob: ab03627d7d6a03b7573d88c8dc27635e665d5993 [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.lang.folding;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.PossiblyDumbAware;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.util.containers.Stack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* Builds custom folding regions. If custom folding is supported for a language, its FoldingBuilder must be inherited from this class.
*
* @author Rustam Vishnyakov
*/
public abstract class CustomFoldingBuilder extends FoldingBuilderEx implements PossiblyDumbAware {
private CustomFoldingProvider myDefaultProvider;
private static final int MAX_LOOKUP_DEPTH = 10;
@NotNull
@Override
public final FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
List<FoldingDescriptor> descriptors = new ArrayList<FoldingDescriptor>();
if (CustomFoldingProvider.getAllProviders().length > 0) {
myDefaultProvider = null;
ASTNode rootNode = root.getNode();
if (rootNode != null) {
addCustomFoldingRegionsRecursively(new FoldingStack(rootNode), rootNode, descriptors, 0);
}
}
buildLanguageFoldRegions(descriptors, root, document, quick);
return descriptors.toArray(new FoldingDescriptor[descriptors.size()]);
}
@NotNull
@Override
public final FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
return buildFoldRegions(node.getPsi(), document, false);
}
/**
* Implement this method to build language folding regions besides custom folding regions.
*
* @param descriptors The list of folding descriptors to store results to.
* @param root The root node for which the folding is requested.
* @param document The document for which folding is built.
* @param quick whether the result should be provided as soon as possible without reference resolving
* and complex checks.
*/
protected abstract void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors,
@NotNull PsiElement root,
@NotNull Document document,
boolean quick);
private void addCustomFoldingRegionsRecursively(@NotNull FoldingStack foldingStack,
@NotNull ASTNode node,
@NotNull List<FoldingDescriptor> descriptors,
int currDepth) {
FoldingStack localFoldingStack = isCustomFoldingRoot(node) ? new FoldingStack(node) : foldingStack;
for (ASTNode child = node.getFirstChildNode(); child != null; child = child.getTreeNext()) {
if (isCustomRegionStart(child)) {
localFoldingStack.push(child);
}
else if (isCustomRegionEnd(child)) {
if (!localFoldingStack.isEmpty()) {
ASTNode startNode = localFoldingStack.pop();
int startOffset = startNode.getTextRange().getStartOffset();
TextRange range = new TextRange(startOffset, child.getTextRange().getEndOffset());
descriptors.add(new FoldingDescriptor(startNode, range));
}
}
else {
if (currDepth < MAX_LOOKUP_DEPTH) {
addCustomFoldingRegionsRecursively(localFoldingStack, child, descriptors, currDepth + 1);
}
}
}
}
@Override
public final String getPlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
if (isCustomFoldingCandidate(node)) {
String elementText = node.getText();
CustomFoldingProvider defaultProvider = getDefaultProvider(elementText);
if (defaultProvider != null && defaultProvider.isCustomRegionStart(elementText)) {
return defaultProvider.getPlaceholderText(elementText);
}
}
return getLanguagePlaceholderText(node, range);
}
protected abstract String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range);
@Override
public final String getPlaceholderText(@NotNull ASTNode node) {
return "...";
}
@Override
public final boolean isCollapsedByDefault(@NotNull ASTNode node) {
if (isCustomRegionStart(node)) {
String childText = node.getText();
CustomFoldingProvider defaultProvider = getDefaultProvider(childText);
return defaultProvider != null && defaultProvider.isCollapsedByDefault(childText);
}
return isRegionCollapsedByDefault(node);
}
/**
* Returns the default collapsed state for the folding region related to the specified node.
*
* @param node the node for which the collapsed state is requested.
* @return true if the region is collapsed by default, false otherwise.
*/
protected abstract boolean isRegionCollapsedByDefault(@NotNull ASTNode node);
/**
* Returns true if the node corresponds to custom region start. The node must be a custom folding candidate and match custom folding
* start pattern.
*
* @param node The node which may contain custom region start.
* @return True if the node marks a custom region start.
*/
public final boolean isCustomRegionStart(ASTNode node) {
if (isCustomFoldingCandidate(node)) {
String nodeText = node.getText();
CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
return defaultProvider != null && defaultProvider.isCustomRegionStart(nodeText);
}
return false;
}
/**
* Returns true if the node corresponds to custom region end. The node must be a custom folding candidate and match custom folding
* end pattern.
*
* @param node The node which may contain custom region end
* @return True if the node marks a custom region end.
*/
protected final boolean isCustomRegionEnd(ASTNode node) {
if (isCustomFoldingCandidate(node)) {
String nodeText = node.getText();
CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
return defaultProvider != null && defaultProvider.isCustomRegionEnd(nodeText);
}
return false;
}
@Nullable
private CustomFoldingProvider getDefaultProvider(String elementText) {
if (myDefaultProvider == null) {
for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
if (provider.isCustomRegionStart(elementText) || provider.isCustomRegionEnd(elementText)) {
myDefaultProvider = provider;
}
}
}
return myDefaultProvider;
}
/**
* Checks if a node may contain custom folding tags. By default returns true for PsiComment but a language folding builder may override
* this method to allow only specific subtypes of comments (for example, line comments only).
* @param node The node to check.
* @return True if the node may contain custom folding tags.
*/
protected boolean isCustomFoldingCandidate(ASTNode node) {
return node.getPsi() instanceof PsiComment;
}
/**
* Checks if the node is used as custom folding root. Any custom folding elements inside the root are considered to be at the same level
* even if they are located at different levels of PSI tree. By default the method returns true if the node has any child elements
* (only custom folding comments at the same PSI tree level are processed, start/end comments at different levels will be ignored).
*
* @param node The node to check.
* @return True if the node is a root for custom foldings.
*/
protected boolean isCustomFoldingRoot(ASTNode node) {
return node.getFirstChildNode() != null;
}
private static class FoldingStack extends Stack<ASTNode> {
private final ASTNode owner;
public FoldingStack(@NotNull ASTNode owner) {
super(1);
this.owner = owner;
}
@NotNull
public ASTNode getOwner() {
return owner;
}
}
/**
* Checks if the folding ranges can be created in the Dumb Mode. In the most of
* language implementations the method returns true, but for strong context-dependent
* languages (like ObjC/C++) overridden method returns false.
*
* @return True if the folding ranges can be created in the Dumb Mode
*/
@Override
public boolean isDumbAware() {
return true;
}
}