f9f3148b8c9747aa6edd4634e011d5f04906dbbb
[idea/community.git] / platform / core-api / src / com / intellij / lang / folding / CustomFoldingBuilder.java
1 package com.intellij.lang.folding;
2
3 import com.intellij.lang.ASTNode;
4 import com.intellij.openapi.editor.Document;
5 import com.intellij.openapi.project.DumbAware;
6 import com.intellij.openapi.util.TextRange;
7 import com.intellij.psi.PsiComment;
8 import com.intellij.psi.PsiElement;
9 import com.intellij.util.containers.Stack;
10 import org.jetbrains.annotations.NotNull;
11 import org.jetbrains.annotations.Nullable;
12
13 import java.util.ArrayList;
14 import java.util.List;
15
16 /**
17  * Builds custom folding regions. If custom folding is supported for a language, its FoldingBuilder must be inherited from this class.
18  * 
19  * @author Rustam Vishnyakov
20  */
21 public abstract class CustomFoldingBuilder extends FoldingBuilderEx implements DumbAware {
22
23   private CustomFoldingProvider myDefaultProvider;
24   private static final int MAX_LOOKUP_DEPTH = 10;
25
26   @NotNull
27   @Override
28   public final FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
29     List<FoldingDescriptor> descriptors = new ArrayList<FoldingDescriptor>();
30     if (CustomFoldingProvider.getAllProviders().length > 0) {
31       myDefaultProvider = null;
32       ASTNode rootNode = root.getNode();
33       addCustomFoldingRegionsRecursively(new FoldingStack(rootNode), rootNode, descriptors, 0);
34     }
35     buildLanguageFoldRegions(descriptors, root, document, quick);
36     return descriptors.toArray(new FoldingDescriptor[descriptors.size()]);
37   }
38
39   @NotNull
40   @Override
41   public final FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
42     return buildFoldRegions(node.getPsi(), document, false);
43   }
44
45   /**
46    * Implement this method to build language folding regions besides custom folding regions.
47    *
48    * @param descriptors The list of folding descriptors to store results to.
49    * @param root        The root node for which the folding is requested.
50    * @param document    The document for which folding is built.
51    * @param quick       whether the result should be provided as soon as possible without reference resolving
52    *                    and complex checks.
53    */
54   protected abstract void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors,
55                                                    @NotNull PsiElement root,
56                                                    @NotNull Document document,
57                                                    boolean quick);
58
59   private void addCustomFoldingRegionsRecursively(@NotNull FoldingStack foldingStack,
60                                                   @NotNull ASTNode node,
61                                                   @NotNull List<FoldingDescriptor> descriptors,
62                                                   int currDepth) {
63     FoldingStack localFoldingStack = isCustomFoldingRoot(node) ? new FoldingStack(node) : foldingStack;
64     for (ASTNode child = node.getFirstChildNode(); child != null; child = child.getTreeNext()) {
65       if (isCustomRegionStart(child)) {
66         localFoldingStack.push(child);
67       }
68       else if (isCustomRegionEnd(child)) {
69         if (!localFoldingStack.isEmpty()) {
70           ASTNode startNode = localFoldingStack.pop();
71           int startOffset = startNode.getTextRange().getStartOffset();
72           TextRange range = new TextRange(startOffset, child.getTextRange().getEndOffset());
73           descriptors.add(new FoldingDescriptor(startNode, range));
74         }
75       }
76       else {
77         if (currDepth < MAX_LOOKUP_DEPTH) {
78           addCustomFoldingRegionsRecursively(localFoldingStack, child, descriptors, currDepth + 1);
79         }
80       }
81     }
82   }
83
84   @Override
85   public final String getPlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
86     if (isCustomFoldingCandidate(node)) {
87       String elementText = node.getText();
88       CustomFoldingProvider defaultProvider = getDefaultProvider(elementText);
89       if (defaultProvider != null && defaultProvider.isCustomRegionStart(elementText)) {
90         return defaultProvider.getPlaceholderText(elementText);
91       }
92     }
93     return getLanguagePlaceholderText(node, range);
94   }
95   
96   protected abstract String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range);
97
98
99   @Override
100   public final String getPlaceholderText(@NotNull ASTNode node) {
101     return "...";
102   }
103
104
105   @Override
106   public final boolean isCollapsedByDefault(@NotNull ASTNode node) {
107     if (isCustomRegionStart(node)) {
108       String childText = node.getText();
109       CustomFoldingProvider defaultProvider = getDefaultProvider(childText);
110       return defaultProvider != null && defaultProvider.isCollapsedByDefault(childText);
111     }
112     return isRegionCollapsedByDefault(node);
113   }
114
115   /**
116    * Returns the default collapsed state for the folding region related to the specified node.
117    *
118    * @param node the node for which the collapsed state is requested.
119    * @return true if the region is collapsed by default, false otherwise.
120    */
121   protected abstract boolean isRegionCollapsedByDefault(@NotNull ASTNode node);
122
123   /**
124    * Returns true if the node corresponds to custom region start. The node must be a custom folding candidate and match custom folding 
125    * start pattern.
126    *
127    * @param node The node which may contain custom region start.
128    * @return True if the node marks a custom region start.
129    */
130   public final boolean isCustomRegionStart(ASTNode node) {
131     if (isCustomFoldingCandidate(node)) {
132       String nodeText = node.getText();
133       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
134       return defaultProvider != null && defaultProvider.isCustomRegionStart(nodeText);
135     }
136     return false;
137   }
138
139   /**
140    * Returns true if the node corresponds to custom region end. The node must be a custom folding candidate and match custom folding
141    * end pattern.
142    *
143    * @param node The node which may contain custom region end
144    * @return True if the node marks a custom region end.
145    */
146   protected final boolean isCustomRegionEnd(ASTNode node) {
147     if (isCustomFoldingCandidate(node)) {
148       String nodeText = node.getText();
149       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
150       return defaultProvider != null && defaultProvider.isCustomRegionEnd(nodeText);
151     }
152     return false;
153   }
154
155   @Nullable
156   private CustomFoldingProvider getDefaultProvider(String elementText) {
157     if (myDefaultProvider == null) {
158       for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
159         if (provider.isCustomRegionStart(elementText) || provider.isCustomRegionEnd(elementText)) {
160           myDefaultProvider = provider;
161         }
162       }
163     }
164     return myDefaultProvider;
165   }
166
167   /**
168    * Checks if a node may contain custom folding tags. By default returns true for PsiComment but a language folding builder may override
169    * this method to allow only specific subtypes of comments (for example, line comments only).
170    * @param node The node to check.
171    * @return True if the node may contain custom folding tags.
172    */
173   protected boolean isCustomFoldingCandidate(ASTNode node) {
174     return node.getPsi() instanceof PsiComment;
175   }
176
177   /**
178    * 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
179    * even if they are located at different levels of PSI tree. By default the method returns true if the node has any child elements
180    * (only custom folding comments at the same PSI tree level are processed, start/end comments at different levels will be ignored).
181    *
182    * @param node  The node to check.
183    * @return      True if the node is a root for custom foldings.
184    */
185   protected boolean isCustomFoldingRoot(ASTNode node) {
186     return node.getFirstChildNode() != null;
187   }
188
189   private static class FoldingStack extends Stack<ASTNode> {
190     private final ASTNode owner;
191
192     public FoldingStack(@NotNull ASTNode owner) {
193       super(1);
194       this.owner = owner;
195     }
196
197     @NotNull
198     public ASTNode getOwner() {
199       return owner;
200     }
201   }
202 }