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