OC-9609 broken based on AST-tree folding at project opening +review CR-OC-1561
[idea/community.git] / platform / core-api / src / com / intellij / lang / folding / CustomFoldingBuilder.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.lang.folding;
17
18 import com.intellij.lang.ASTNode;
19 import com.intellij.openapi.editor.Document;
20 import com.intellij.openapi.project.PossiblyDumbAware;
21 import com.intellij.openapi.util.TextRange;
22 import com.intellij.psi.PsiComment;
23 import com.intellij.psi.PsiElement;
24 import com.intellij.util.containers.Stack;
25 import org.jetbrains.annotations.NotNull;
26 import org.jetbrains.annotations.Nullable;
27
28 import java.util.ArrayList;
29 import java.util.List;
30
31 /**
32  * Builds custom folding regions. If custom folding is supported for a language, its FoldingBuilder must be inherited from this class.
33  * 
34  * @author Rustam Vishnyakov
35  */
36 public abstract class CustomFoldingBuilder extends FoldingBuilderEx implements PossiblyDumbAware {
37
38   private CustomFoldingProvider myDefaultProvider;
39   private static final int MAX_LOOKUP_DEPTH = 10;
40
41   @NotNull
42   @Override
43   public final FoldingDescriptor[] buildFoldRegions(@NotNull PsiElement root, @NotNull Document document, boolean quick) {
44     List<FoldingDescriptor> descriptors = new ArrayList<FoldingDescriptor>();
45     if (CustomFoldingProvider.getAllProviders().length > 0) {
46       myDefaultProvider = null;
47       ASTNode rootNode = root.getNode();
48       addCustomFoldingRegionsRecursively(new FoldingStack(rootNode), rootNode, descriptors, 0);
49     }
50     buildLanguageFoldRegions(descriptors, root, document, quick);
51     return descriptors.toArray(new FoldingDescriptor[descriptors.size()]);
52   }
53
54   @NotNull
55   @Override
56   public final FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
57     return buildFoldRegions(node.getPsi(), document, false);
58   }
59
60   /**
61    * Implement this method to build language folding regions besides custom folding regions.
62    *
63    * @param descriptors The list of folding descriptors to store results to.
64    * @param root        The root node for which the folding is requested.
65    * @param document    The document for which folding is built.
66    * @param quick       whether the result should be provided as soon as possible without reference resolving
67    *                    and complex checks.
68    */
69   protected abstract void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors,
70                                                    @NotNull PsiElement root,
71                                                    @NotNull Document document,
72                                                    boolean quick);
73
74   private void addCustomFoldingRegionsRecursively(@NotNull FoldingStack foldingStack,
75                                                   @NotNull ASTNode node,
76                                                   @NotNull List<FoldingDescriptor> descriptors,
77                                                   int currDepth) {
78     FoldingStack localFoldingStack = isCustomFoldingRoot(node) ? new FoldingStack(node) : foldingStack;
79     for (ASTNode child = node.getFirstChildNode(); child != null; child = child.getTreeNext()) {
80       if (isCustomRegionStart(child)) {
81         localFoldingStack.push(child);
82       }
83       else if (isCustomRegionEnd(child)) {
84         if (!localFoldingStack.isEmpty()) {
85           ASTNode startNode = localFoldingStack.pop();
86           int startOffset = startNode.getTextRange().getStartOffset();
87           TextRange range = new TextRange(startOffset, child.getTextRange().getEndOffset());
88           descriptors.add(new FoldingDescriptor(startNode, range));
89         }
90       }
91       else {
92         if (currDepth < MAX_LOOKUP_DEPTH) {
93           addCustomFoldingRegionsRecursively(localFoldingStack, child, descriptors, currDepth + 1);
94         }
95       }
96     }
97   }
98
99   @Override
100   public final String getPlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
101     if (isCustomFoldingCandidate(node)) {
102       String elementText = node.getText();
103       CustomFoldingProvider defaultProvider = getDefaultProvider(elementText);
104       if (defaultProvider != null && defaultProvider.isCustomRegionStart(elementText)) {
105         return defaultProvider.getPlaceholderText(elementText);
106       }
107     }
108     return getLanguagePlaceholderText(node, range);
109   }
110   
111   protected abstract String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range);
112
113
114   @Override
115   public final String getPlaceholderText(@NotNull ASTNode node) {
116     return "...";
117   }
118
119
120   @Override
121   public final boolean isCollapsedByDefault(@NotNull ASTNode node) {
122     if (isCustomRegionStart(node)) {
123       String childText = node.getText();
124       CustomFoldingProvider defaultProvider = getDefaultProvider(childText);
125       return defaultProvider != null && defaultProvider.isCollapsedByDefault(childText);
126     }
127     return isRegionCollapsedByDefault(node);
128   }
129
130   /**
131    * Returns the default collapsed state for the folding region related to the specified node.
132    *
133    * @param node the node for which the collapsed state is requested.
134    * @return true if the region is collapsed by default, false otherwise.
135    */
136   protected abstract boolean isRegionCollapsedByDefault(@NotNull ASTNode node);
137
138   /**
139    * Returns true if the node corresponds to custom region start. The node must be a custom folding candidate and match custom folding 
140    * start pattern.
141    *
142    * @param node The node which may contain custom region start.
143    * @return True if the node marks a custom region start.
144    */
145   public final boolean isCustomRegionStart(ASTNode node) {
146     if (isCustomFoldingCandidate(node)) {
147       String nodeText = node.getText();
148       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
149       return defaultProvider != null && defaultProvider.isCustomRegionStart(nodeText);
150     }
151     return false;
152   }
153
154   /**
155    * Returns true if the node corresponds to custom region end. The node must be a custom folding candidate and match custom folding
156    * end pattern.
157    *
158    * @param node The node which may contain custom region end
159    * @return True if the node marks a custom region end.
160    */
161   protected final boolean isCustomRegionEnd(ASTNode node) {
162     if (isCustomFoldingCandidate(node)) {
163       String nodeText = node.getText();
164       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
165       return defaultProvider != null && defaultProvider.isCustomRegionEnd(nodeText);
166     }
167     return false;
168   }
169
170   @Nullable
171   private CustomFoldingProvider getDefaultProvider(String elementText) {
172     if (myDefaultProvider == null) {
173       for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
174         if (provider.isCustomRegionStart(elementText) || provider.isCustomRegionEnd(elementText)) {
175           myDefaultProvider = provider;
176         }
177       }
178     }
179     return myDefaultProvider;
180   }
181
182   /**
183    * Checks if a node may contain custom folding tags. By default returns true for PsiComment but a language folding builder may override
184    * this method to allow only specific subtypes of comments (for example, line comments only).
185    * @param node The node to check.
186    * @return True if the node may contain custom folding tags.
187    */
188   protected boolean isCustomFoldingCandidate(ASTNode node) {
189     return node.getPsi() instanceof PsiComment;
190   }
191
192   /**
193    * 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
194    * even if they are located at different levels of PSI tree. By default the method returns true if the node has any child elements
195    * (only custom folding comments at the same PSI tree level are processed, start/end comments at different levels will be ignored).
196    *
197    * @param node  The node to check.
198    * @return      True if the node is a root for custom foldings.
199    */
200   protected boolean isCustomFoldingRoot(ASTNode node) {
201     return node.getFirstChildNode() != null;
202   }
203
204   private static class FoldingStack extends Stack<ASTNode> {
205     private final ASTNode owner;
206
207     public FoldingStack(@NotNull ASTNode owner) {
208       super(1);
209       this.owner = owner;
210     }
211
212     @NotNull
213     public ASTNode getOwner() {
214       return owner;
215     }
216   }
217
218   /**
219    * Checks if the folding ranges could be created in the Dumb Mode. In the most of
220    * language implementations the method returns true, but for strong context-dependant
221    * languages (like ObjC/C++) overridden method returns false.
222    *
223    * @return True if the folding ranges could be created in the Dumb Mode
224    */
225   @Override
226   public boolean isDumbAware() {
227     return true;
228   }
229 }