Custom folding classes moved to platform, other refactoring
[idea/community.git] / platform / lang-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.psi.PsiFile;
10 import com.intellij.util.containers.Stack;
11 import org.jetbrains.annotations.NotNull;
12 import org.jetbrains.annotations.Nullable;
13
14 import java.util.ArrayList;
15 import java.util.List;
16
17 /**
18  * Builds custom folding regions. If custom folding is supported for a language, its FoldingBuilder must be inherited from this class.
19  * 
20  * @author Rustam Vishnyakov
21  */
22 public abstract class CustomFoldingBuilder extends FoldingBuilderEx implements DumbAware {
23
24   private CustomFoldingProvider myDefaultProvider;
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       addCustomFoldingRegionsRecursively(root.getNode(), descriptors);
33     }
34     buildLanguageFoldRegions(descriptors, root, document, quick);
35     return descriptors.toArray(new FoldingDescriptor[descriptors.size()]);
36   }
37
38   @NotNull
39   @Override
40   public final FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) {
41     return buildFoldRegions(node.getPsi(), document, false);
42   }
43
44   /**
45    * Implement this method to build language folding regions besides custom folding regions.
46    *
47    * @param descriptors The list of folding descriptors to store results to.
48    * @param root        The root node for which the folding is requested.
49    * @param document    The document for which folding is built.
50    * @param quick       whether the result should be provided as soon as possible without reference resolving
51    *                    and complex checks.
52    */
53   protected abstract void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors,
54                                                    @NotNull PsiElement root,
55                                                    @NotNull Document document,
56                                                    boolean quick);
57   
58   private void addCustomFoldingRegionsRecursively(@NotNull ASTNode node, List<FoldingDescriptor> descriptors) {
59     Stack<ASTNode> customFoldingNodesStack = new Stack<ASTNode>(1);
60     for (ASTNode child = node.getFirstChildNode(); child != null; child = child.getTreeNext()) {
61       if (isCustomRegionStart(child)) {
62         customFoldingNodesStack.push(child);
63       }
64       else if (isCustomRegionEnd(child)) {
65         if (!customFoldingNodesStack.isEmpty()) {
66           ASTNode startNode = customFoldingNodesStack.pop();
67           int startOffset = startNode.getTextRange().getStartOffset();
68           TextRange range = new TextRange(startOffset, child.getTextRange().getEndOffset());
69           descriptors.add(new FoldingDescriptor(node, range));
70         }
71       }
72       else {
73         addCustomFoldingRegionsRecursively(child, descriptors);
74       }
75     }
76   }
77
78   @Override
79   public final String getPlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) {
80     if (mayContainCustomFoldings(node)) {
81       PsiFile file = node.getPsi().getContainingFile();
82       PsiElement contextElement = file.findElementAt(range.getStartOffset());
83       if (contextElement != null && isCustomFoldingCandidate(contextElement.getNode())) {
84         String elementText = contextElement.getText();
85         CustomFoldingProvider defaultProvider = getDefaultProvider(elementText);
86         if (defaultProvider != null && defaultProvider.isCustomRegionStart(elementText)) {
87           return defaultProvider.getPlaceholderText(elementText);
88         }
89       }
90     }
91     return getLanguagePlaceholderText(node, range);
92   }
93   
94   protected abstract String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range);
95
96
97   @Override
98   public final String getPlaceholderText(@NotNull ASTNode node) {
99     return "...";
100   }
101
102
103   @Override
104   public final boolean isCollapsedByDefault(@NotNull ASTNode node) {
105     // TODO<rv>: Modify Folding API and pass here folding range.
106     if (mayContainCustomFoldings(node)) {
107       for (ASTNode child = node.getFirstChildNode(); child != null; child = child.getTreeNext()) {
108         if (isCustomRegionStart(child)) {
109           String childText = child.getText();
110           CustomFoldingProvider defaultProvider = getDefaultProvider(childText);
111           return defaultProvider != null && defaultProvider.isCollapsedByDefault(childText);
112         }
113       }
114     }
115     return isRegionCollapsedByDefault(node);
116   }
117
118   /**
119    * Returns the default collapsed state for the folding region related to the specified node.
120    *
121    * @param node the node for which the collapsed state is requested.
122    * @return true if the region is collapsed by default, false otherwise.
123    */
124   protected abstract boolean isRegionCollapsedByDefault(@NotNull ASTNode node);
125
126   /**
127    * Returns true if the node corresponds to custom region start. The node must be a custom folding candidate and match custom folding 
128    * start pattern.
129    *
130    * @param node The node which may contain custom region start.
131    * @return True if the node marks a custom region start.
132    */
133   protected final boolean isCustomRegionStart(ASTNode node) {
134     if (isCustomFoldingCandidate(node)) {
135       String nodeText = node.getText();
136       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
137       return defaultProvider != null && defaultProvider.isCustomRegionStart(nodeText);
138     }
139     return false;
140   }
141
142   /**
143    * Returns true if the node corresponds to custom region end. The node must be a custom folding candidate and match custom folding
144    * end pattern.
145    *
146    * @param node The node which may contain custom region end
147    * @return True if the node marks a custom region end.
148    */
149   protected final boolean isCustomRegionEnd(ASTNode node) {
150     if (isCustomFoldingCandidate(node)) {
151       String nodeText = node.getText();
152       CustomFoldingProvider defaultProvider = getDefaultProvider(nodeText);
153       return defaultProvider != null && defaultProvider.isCustomRegionEnd(nodeText);
154     }
155     return false;
156   }
157
158   @Nullable
159   private CustomFoldingProvider getDefaultProvider(String elementText) {
160     if (myDefaultProvider == null) {
161       for (CustomFoldingProvider provider : CustomFoldingProvider.getAllProviders()) {
162         if (provider.isCustomRegionStart(elementText) || provider.isCustomRegionEnd(elementText)) {
163           myDefaultProvider = provider;
164         }
165       }
166     }
167     return myDefaultProvider;
168   }
169
170   /**
171    * Checks if a node may contain custom folding tags. By default returns true for PsiComment but a language folding builder may override
172    * this method to allow only specific subtypes of comments (for example, line comments only).
173    * @param node The node to check.
174    * @return True if the node may contain custom folding tags.
175    */
176   protected boolean isCustomFoldingCandidate(ASTNode node) {
177     return node.getPsi() instanceof PsiComment;
178   }
179
180   /**
181    * Returns true if the node may contain custom foldings in its immediate child nodes. By default any node will be checked for custom
182    * foldings but for performance reasons it makes sense to override this method to check only the nodes which actually may contain
183    * custom folding nodes (for example, group statements).
184    *
185    * @param node  The node to check.
186    * @return      True if the node may contain custom folding nodes (true by default).
187    */
188   protected boolean mayContainCustomFoldings(ASTNode node) {
189     return true;
190   }
191 }