EditorConfig documentation test
[idea/community.git] / java / java-psi-impl / src / com / intellij / codeInsight / folding / impl / CommentFoldingUtil.java
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.codeInsight.folding.impl;
3
4 import com.intellij.lang.ASTNode;
5 import com.intellij.lang.CodeDocumentationAwareCommenter;
6 import com.intellij.lang.Commenter;
7 import com.intellij.lang.LanguageCommenters;
8 import com.intellij.lang.folding.FoldingDescriptor;
9 import com.intellij.openapi.editor.Document;
10 import com.intellij.openapi.util.TextRange;
11 import com.intellij.openapi.util.text.StringUtil;
12 import com.intellij.psi.PsiComment;
13 import com.intellij.psi.PsiElement;
14 import com.intellij.psi.TokenType;
15 import com.intellij.psi.tree.IElementType;
16 import org.jetbrains.annotations.Contract;
17 import org.jetbrains.annotations.NotNull;
18 import org.jetbrains.annotations.Nullable;
19
20 import java.util.Collections;
21 import java.util.Set;
22 import java.util.function.Predicate;
23
24 public final class CommentFoldingUtil {
25
26   /**
27    * Construct descriptor for comment folding.
28    *
29    * @param comment            comment to fold
30    * @param document           document with comment
31    * @param isCollapse         is comment collapsed by default or not
32    * @param processedComments  already processed comments
33    * @param isCustomRegionFunc determines whether element contains custom region tag
34    */
35   @Nullable
36   public static FoldingDescriptor getCommentDescriptor(@NotNull PsiComment comment,
37                                                        @NotNull Document document,
38                                                        @NotNull Set<? super PsiElement> processedComments,
39                                                        @NotNull Predicate<? super PsiElement> isCustomRegionFunc,
40                                                        boolean isCollapse) {
41     if (!processedComments.add(comment)) return null;
42
43     final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(comment.getLanguage());
44     if (!(commenter instanceof CodeDocumentationAwareCommenter)) return null;
45
46     final CodeDocumentationAwareCommenter docCommenter = (CodeDocumentationAwareCommenter)commenter;
47     final IElementType commentType = comment.getTokenType();
48
49     final TextRange commentRange = getCommentRange(comment, processedComments, isCustomRegionFunc, docCommenter);
50     if (commentRange == null) return null;
51
52     final String placeholder = getCommentPlaceholder(document, commentType, commentRange);
53     if (placeholder == null) return null;
54
55     return new FoldingDescriptor(comment.getNode(), commentRange, null, placeholder, isCollapse, Collections.emptySet());
56   }
57
58   @Nullable
59   private static TextRange getCommentRange(@NotNull PsiComment comment,
60                                            @NotNull Set<? super PsiElement> processedComments,
61                                            @NotNull Predicate<? super PsiElement> isCustomRegionFunc,
62                                            @NotNull CodeDocumentationAwareCommenter docCommenter) {
63     final IElementType commentType = comment.getTokenType();
64     if (commentType == docCommenter.getDocumentationCommentTokenType() || commentType == docCommenter.getBlockCommentTokenType()) {
65       return comment.getTextRange();
66     }
67
68     if (commentType != docCommenter.getLineCommentTokenType()) return null;
69
70     return getOneLineCommentRange(comment, processedComments, isCustomRegionFunc, docCommenter);
71   }
72
73   /**
74    * We want to allow to fold subsequent single line comments like
75    * <pre>
76    *     // this is comment line 1
77    *     // this is comment line 2
78    * </pre>
79    *
80    * @param startComment      comment to check
81    * @param processedComments set that contains already processed elements. It is necessary because we process all elements of
82    *                          the PSI tree, hence, this method may be called for both comments from the example above. However,
83    *                          we want to create fold region during the first comment processing, put second comment to it and
84    *                          skip processing when current method is called for the second element
85    */
86   @Nullable
87   private static TextRange getOneLineCommentRange(@NotNull PsiComment startComment,
88                                                   @NotNull Set<? super PsiElement> processedComments,
89                                                   @NotNull Predicate<? super PsiElement> isCustomRegionFunc,
90                                                   @NotNull CodeDocumentationAwareCommenter docCommenter) {
91     if (isCustomRegionFunc.test(startComment)) return null;
92
93     PsiElement end = null;
94     for (PsiElement current = startComment.getNextSibling(); current != null; current = current.getNextSibling()) {
95       ASTNode node = current.getNode();
96       if (node == null) {
97         break;
98       }
99       final IElementType elementType = node.getElementType();
100       if (elementType == docCommenter.getLineCommentTokenType() &&
101           !isCustomRegionFunc.test(current) &&
102           !processedComments.contains(current)) {
103         end = current;
104         // We don't want to process, say, the second comment in case of three subsequent comments when it's being examined
105         // during all elements traversal. I.e. we expect to start from the first comment and grab as many subsequent
106         // comments as possible during the single iteration.
107         processedComments.add(current);
108         continue;
109       }
110       if (elementType == TokenType.WHITE_SPACE) {
111         continue;
112       }
113       break;
114     }
115
116     if (end == null) return null;
117
118     return new TextRange(startComment.getTextRange().getStartOffset(), end.getTextRange().getEndOffset());
119   }
120
121   /**
122    * Construct placeholder for comment based on its type.
123    *
124    * @param document     document with comment
125    * @param commentType  type of comment
126    * @param commentRange text range of comment
127    */
128   @Nullable
129   public static String getCommentPlaceholder(@NotNull Document document,
130                                              @NotNull IElementType commentType,
131                                              @NotNull TextRange commentRange) {
132     return getCommentPlaceholder(document, commentType, commentRange, "...");
133   }
134
135
136   /**
137    * Construct placeholder for comment based on its type.
138    *
139    * @param document     document with comment
140    * @param commentType  type of comment
141    * @param commentRange text range of comment
142    * @param replacement  replacement for comment content. included in placeholder
143    */
144   @Nullable
145   public static String getCommentPlaceholder(@NotNull Document document,
146                                              @NotNull IElementType commentType,
147                                              @NotNull TextRange commentRange,
148                                              @NotNull String replacement) {
149     final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage());
150     if (!(commenter instanceof CodeDocumentationAwareCommenter)) return null;
151
152     final CodeDocumentationAwareCommenter docCommenter = (CodeDocumentationAwareCommenter)commenter;
153
154     final String placeholder;
155     if (commentType == docCommenter.getLineCommentTokenType()) {
156       placeholder = getLineCommentPlaceholderText(commenter, replacement);
157     }
158     else if (commentType == docCommenter.getBlockCommentTokenType()) {
159       placeholder = getMultilineCommentPlaceholderText(commenter, replacement);
160     }
161     else if (commentType == docCommenter.getDocumentationCommentTokenType()) {
162       placeholder = getDocCommentPlaceholderText(document, docCommenter, commentRange, replacement);
163     }
164     else {
165       placeholder = null;
166     }
167
168     return placeholder;
169   }
170
171   @Nullable
172   private static String getDocCommentPlaceholderText(@NotNull Document document,
173                                                      @NotNull CodeDocumentationAwareCommenter commenter,
174                                                      @NotNull TextRange commentRange,
175                                                      @NotNull String replacement) {
176     final String prefix = commenter.getDocumentationCommentPrefix();
177     final String suffix = commenter.getDocumentationCommentSuffix();
178     final String linePrefix = commenter.getDocumentationCommentLinePrefix();
179
180     if (prefix == null || suffix == null || linePrefix == null) return null;
181
182     final String header = getCommentHeader(document, suffix, prefix, linePrefix, commentRange);
183
184     return getCommentPlaceholder(prefix, suffix, header, replacement);
185   }
186
187   @Nullable
188   private static String getMultilineCommentPlaceholderText(@NotNull Commenter commenter, @NotNull String replacement) {
189     final String prefix = commenter.getBlockCommentPrefix();
190     final String suffix = commenter.getBlockCommentSuffix();
191
192     if (prefix == null || suffix == null) return null;
193
194     return getCommentPlaceholder(prefix, suffix, null, replacement);
195   }
196
197   @Nullable
198   private static String getLineCommentPlaceholderText(@NotNull Commenter commenter, @NotNull String replacement) {
199     final String prefix = commenter.getLineCommentPrefix();
200
201     if (prefix == null) return null;
202
203     return getCommentPlaceholder(prefix, null, null, replacement);
204   }
205
206   /**
207    * Construct comment placeholder based on rule placeholder ::= prefix[text ]replacement[suffix] .
208    *
209    * @param text        part of comment content to include in placeholder
210    * @param replacement replacement for the rest of comment content
211    */
212   @NotNull
213   public static String getCommentPlaceholder(@NotNull String prefix,
214                                              @Nullable String suffix,
215                                              @Nullable String text,
216                                              @NotNull String replacement) {
217     final StringBuilder sb = new StringBuilder();
218     sb.append(prefix);
219
220     if (text != null && text.length() > 0) {
221       sb.append(text);
222       sb.append(" ");
223     }
224
225     sb.append(replacement);
226
227     if (suffix != null) sb.append(suffix);
228
229     return sb.toString();
230   }
231
232   /**
233    * Get first non-blank line from comment.
234    * If line with comment prefix contains text then it will be used as header, otherwise second line will be used.
235    * If both lines are blank or comment contains only one line then empty string is returned.
236    *
237    * @param document      document with comment
238    * @param commentSuffix doc comment suffix
239    * @param linePrefix    prefix for doc comment line
240    * @param commentRange  comment text range in document
241    */
242   @NotNull
243   public static String getCommentHeader(@NotNull Document document,
244                                         @NotNull String commentSuffix,
245                                         @NotNull String commentPrefix,
246                                         @NotNull String linePrefix,
247                                         @NotNull TextRange commentRange) {
248     final int nFirstCommentLine = document.getLineNumber(commentRange.getStartOffset());
249
250     TextRange lineRange = getLineRange(document, nFirstCommentLine);
251     String line = getCommentLine(document, lineRange, commentPrefix, commentSuffix);
252
253     if (line.chars().anyMatch(c -> !StringUtil.isWhiteSpace((char)c))) return line;
254
255     final int nSecondCommentLine = nFirstCommentLine + 1;
256     if (nSecondCommentLine >= document.getLineCount()) return "";
257
258     lineRange = getLineRange(document, nSecondCommentLine);
259     if (lineRange.getEndOffset() > commentRange.getEndOffset()) return "";
260     line = getCommentLine(document, lineRange, linePrefix, commentSuffix);
261
262     if (line.chars().anyMatch(c -> !StringUtil.isWhiteSpace((char)c))) return line;
263
264     return "";
265   }
266
267   @NotNull
268   @Contract("_, _ -> new")
269   private static TextRange getLineRange(@NotNull Document document, int nLine) {
270     int startOffset = document.getLineStartOffset(nLine);
271     int endOffset = document.getLineEndOffset(nLine);
272     return new TextRange(startOffset, endOffset);
273   }
274
275   @NotNull
276   private static String getCommentLine(@NotNull Document document,
277                                        @NotNull TextRange lineRange,
278                                        @NotNull String prefix,
279                                        @NotNull String suffix) {
280     String line = document.getText(lineRange);
281     line = line.trim();
282
283     line = StringUtil.trimEnd(line, suffix);
284     return StringUtil.trimStart(line, prefix);
285   }
286 }