diff --git a/java/java-impl/src/com/intellij/application/options/editor/JavaCodeFoldingOptionsProvider.java b/java/java-impl/src/com/intellij/application/options/editor/JavaCodeFoldingOptionsProvider.java index 4303b4ef5ce2..8b5e882a5243 100644 --- a/java/java-impl/src/com/intellij/application/options/editor/JavaCodeFoldingOptionsProvider.java +++ b/java/java-impl/src/com/intellij/application/options/editor/JavaCodeFoldingOptionsProvider.java @@ -46,5 +46,8 @@ public class JavaCodeFoldingOptionsProvider extends BeanConfigurable processedComments, + @NotNull Predicate isCustomRegionFunc, + boolean isCollapse) { + if (!processedComments.add(comment)) return null; + + final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(comment.getLanguage()); + if (!(commenter instanceof CodeDocumentationAwareCommenter)) return null; + + final CodeDocumentationAwareCommenter docCommenter = (CodeDocumentationAwareCommenter)commenter; + final IElementType commentType = comment.getTokenType(); + + final TextRange commentRange = getCommentRange(comment, processedComments, isCustomRegionFunc, docCommenter); + if (commentRange == null) return null; + + final String placeholder = getCommentPlaceholder(document, commentType, commentRange); + if (placeholder == null) return null; + + return new NamedFoldingDescriptor(comment.getNode(), commentRange, null, placeholder, isCollapse, Collections.emptySet()); + } + + @Nullable + private static TextRange getCommentRange(@NotNull PsiComment comment, + @NotNull Set processedComments, + @NotNull Predicate isCustomRegionFunc, + @NotNull CodeDocumentationAwareCommenter docCommenter) { + final IElementType commentType = comment.getTokenType(); + if (commentType == docCommenter.getDocumentationCommentTokenType() || commentType == docCommenter.getBlockCommentTokenType()) { + return comment.getTextRange(); + } + + if (commentType != docCommenter.getLineCommentTokenType()) return null; + + return getOneLineCommentRange(comment, processedComments, isCustomRegionFunc, docCommenter); + } + + /** + * We want to allow to fold subsequent single line comments like + *
+   *     // this is comment line 1
+   *     // this is comment line 2
+   * 
+ * + * @param startComment comment to check + * @param processedComments set that contains already processed elements. It is necessary because we process all elements of + * the PSI tree, hence, this method may be called for both comments from the example above. However, + * we want to create fold region during the first comment processing, put second comment to it and + * skip processing when current method is called for the second element + */ + @Nullable + private static TextRange getOneLineCommentRange(@NotNull PsiComment startComment, + @NotNull Set processedComments, + @NotNull Predicate isCustomRegionFunc, + @NotNull CodeDocumentationAwareCommenter docCommenter) { + if (isCustomRegionFunc.test(startComment)) return null; + + PsiElement end = null; + for (PsiElement current = startComment.getNextSibling(); current != null; current = current.getNextSibling()) { + ASTNode node = current.getNode(); + if (node == null) { + break; + } + final IElementType elementType = node.getElementType(); + if (elementType == docCommenter.getLineCommentTokenType() && + !isCustomRegionFunc.test(current) && + !processedComments.contains(current)) { + end = current; + // We don't want to process, say, the second comment in case of three subsequent comments when it's being examined + // during all elements traversal. I.e. we expect to start from the first comment and grab as many subsequent + // comments as possible during the single iteration. + processedComments.add(current); + continue; + } + if (elementType == TokenType.WHITE_SPACE) { + continue; + } + break; + } + + if (end == null) return null; + + return new TextRange(startComment.getTextRange().getStartOffset(), end.getTextRange().getEndOffset()); + } + + /** + * Construct placeholder for comment based on its type. + * + * @param document document with comment + * @param commentType type of comment + * @param commentRange text range of comment + */ + @Nullable + public static String getCommentPlaceholder(@NotNull Document document, + @NotNull IElementType commentType, + @NotNull TextRange commentRange) { + return getCommentPlaceholder(document, commentType, commentRange, "..."); + } + + + /** + * Construct placeholder for comment based on its type. + * + * @param document document with comment + * @param commentType type of comment + * @param commentRange text range of comment + * @param replacement replacement for comment content. included in placeholder + */ + @Nullable + public static String getCommentPlaceholder(@NotNull Document document, + @NotNull IElementType commentType, + @NotNull TextRange commentRange, + @NotNull String replacement) { + final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(commentType.getLanguage()); + if (!(commenter instanceof CodeDocumentationAwareCommenter)) return null; + + final CodeDocumentationAwareCommenter docCommenter = (CodeDocumentationAwareCommenter)commenter; + + final String placeholder; + if (commentType == docCommenter.getLineCommentTokenType()) { + placeholder = getLineCommentPlaceholderText(commenter, replacement); + } + else if (commentType == docCommenter.getBlockCommentTokenType()) { + placeholder = getMultilineCommentPlaceholderText(commenter, replacement); + } + else if (commentType == docCommenter.getDocumentationCommentTokenType()) { + placeholder = getDocCommentPlaceholderText(document, docCommenter, commentRange, replacement); + } + else { + placeholder = null; + } + + return placeholder; + } + + @Nullable + private static String getDocCommentPlaceholderText(@NotNull Document document, + @NotNull CodeDocumentationAwareCommenter commenter, + @NotNull TextRange commentRange, + @NotNull String replacement) { + final String prefix = commenter.getDocumentationCommentPrefix(); + final String suffix = commenter.getDocumentationCommentSuffix(); + final String linePrefix = commenter.getDocumentationCommentLinePrefix(); + + if (prefix == null || suffix == null || linePrefix == null) return null; + + final String header = getCommentHeader(document, suffix, linePrefix, commentRange); + + return getCommentPlaceholder(prefix, suffix, header, replacement); + } + + @Nullable + private static String getMultilineCommentPlaceholderText(@NotNull Commenter commenter, @NotNull String replacement) { + final String prefix = commenter.getBlockCommentPrefix(); + final String suffix = commenter.getBlockCommentSuffix(); + + if (prefix == null || suffix == null) return null; + + return getCommentPlaceholder(prefix, suffix, null, replacement); + } + + @Nullable + private static String getLineCommentPlaceholderText(@NotNull Commenter commenter, @NotNull String replacement) { + final String prefix = commenter.getLineCommentPrefix(); + + if (prefix == null) return null; + + return getCommentPlaceholder(prefix, null, null, replacement); + } + + /** + * Construct comment placeholder based on rule placeholder ::= prefix[text ]replacement[suffix] . + * + * @param text part of comment content to include in placeholder + * @param replacement replacement for the rest of comment content + */ + @NotNull + public static String getCommentPlaceholder(@NotNull String prefix, + @Nullable String suffix, + @Nullable String text, + @NotNull String replacement) { + final StringBuilder sb = new StringBuilder(); + sb.append(prefix); + + if (text != null && text.length() > 0) { + sb.append(text); + sb.append(" "); + } + + sb.append(replacement); + + if (suffix != null) sb.append(suffix); + + return sb.toString(); + } + + /** + * Get second line from comment excluding comment suffix and comment line prefix. + * + * @param document document with comment + * @param commentSuffix doc comment suffix + * @param linePrefix prefix for doc comment line + * @param commentRange comment text range in document + */ + @NotNull + public static String getCommentHeader(@NotNull Document document, + @NotNull String commentSuffix, + @NotNull String linePrefix, + @NotNull TextRange commentRange) { + final int nFirstCommentLine = document.getLineNumber(commentRange.getStartOffset()); + final int nSecondCommentLine = nFirstCommentLine + 1; + + if (nSecondCommentLine >= document.getLineCount()) return ""; + + final int endOffset = document.getLineEndOffset(nSecondCommentLine); + if (endOffset > commentRange.getEndOffset()) return ""; + + final int startOffset = document.getLineStartOffset(nSecondCommentLine); + + String line = document.getText(new TextRange(startOffset, endOffset)); + line = line.trim(); + + line = StringUtil.trimEnd(line, commentSuffix); + line = StringUtil.trimStart(line, linePrefix); + + return line; + } +} diff --git a/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaCodeFoldingSettingsBase.java b/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaCodeFoldingSettingsBase.java index 2a8779e5d475..886fb655f40c 100644 --- a/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaCodeFoldingSettingsBase.java +++ b/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaCodeFoldingSettingsBase.java @@ -29,6 +29,7 @@ public class JavaCodeFoldingSettingsBase extends JavaCodeFoldingSettings { private boolean COLLAPSE_I18N_MESSAGES = true; private boolean COLLAPSE_SUPPRESS_WARNINGS = true; private boolean COLLAPSE_END_OF_LINE_COMMENTS; + private boolean COLLAPSE_MULTILINE_COMMENTS; private boolean REPLACE_VAR_WITH_INFERRED_TYPE = false; @Override @@ -165,6 +166,16 @@ public class JavaCodeFoldingSettingsBase extends JavaCodeFoldingSettings { return COLLAPSE_END_OF_LINE_COMMENTS; } + @Override + public void setCollapseMultilineComments(boolean value) { + COLLAPSE_MULTILINE_COMMENTS = value; + } + + @Override + public boolean isCollapseMultilineComments() { + return COLLAPSE_MULTILINE_COMMENTS; + } + @Override public void setCollapseEndOfLineComments(boolean value) { COLLAPSE_END_OF_LINE_COMMENTS = value; diff --git a/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaFoldingBuilderBase.java b/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaFoldingBuilderBase.java index a157f0822081..4f64d03695ae 100644 --- a/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaFoldingBuilderBase.java +++ b/java/java-psi-impl/src/com/intellij/codeInsight/folding/impl/JavaFoldingBuilderBase.java @@ -197,55 +197,26 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem } } - /** - * We want to allow to fold subsequent single line comments like - *
-   *     // this is comment line 1
-   *     // this is comment line 2
-   * 
- * - * @param list fold descriptors holder to store newly created descriptor (if any) - * @param comment comment to check - * @param processedComments set that contains already processed elements. It is necessary because we process all elements of -* the PSI tree, hence, this method may be called for both comments from the example above. However, -* we want to create fold region during the first comment processing, put second comment to it and -* skip processing when current method is called for the second element - */ - private static void addFoldsForComment(@NotNull List list, - @NotNull PsiComment comment, - @NotNull Set processedComments) { - if (processedComments.contains(comment) || comment.getTokenType() != JavaTokenType.END_OF_LINE_COMMENT - || isCustomRegionElement(comment)) { - return; - } - processedComments.add(comment); + private static void addCommentsToFold(@NotNull List list, + @NotNull PsiElement element, + @NotNull Document document, + @NotNull Set processedComments) { + final PsiComment[] comments = PsiTreeUtil.getChildrenOfType(element, PsiComment.class); + if (comments == null) return; - PsiElement end = null; - for (PsiElement current = comment.getNextSibling(); current != null; current = current.getNextSibling()) { - ASTNode node = current.getNode(); - if (node == null) { - break; - } - IElementType elementType = node.getElementType(); - if (elementType == JavaTokenType.END_OF_LINE_COMMENT && !isCustomRegionElement(current) && !processedComments.contains(current)) { - end = current; - // We don't want to process, say, the second comment in case of three subsequent comments when it's being examined - // during all elements traversal. I.e. we expect to start from the first comment and grab as many subsequent - // comments as possible during the single iteration. - processedComments.add(current); - continue; - } - if (elementType == TokenType.WHITE_SPACE) { - continue; - } - break; + for (PsiComment comment : comments) { + addCommentToFold(list, comment, document, processedComments); } + } - if (end != null) { - list.add(new NamedFoldingDescriptor(comment.getNode(), - new TextRange(comment.getTextRange().getStartOffset(), end.getTextRange().getEndOffset()), null, - "//...", JavaCodeFoldingSettings.getInstance().isCollapseEndOfLineComments(), Collections.emptySet())); - } + private static void addCommentToFold(@NotNull List list, + @NotNull PsiComment comment, + @NotNull Document document, + @NotNull Set processedComments) { + final NamedFoldingDescriptor commentDescriptor = CommentFoldingUtil.getCommentDescriptor(comment, document, processedComments, + CustomFoldingBuilder::isCustomRegionElement, + isCollapseCommentByDefault(comment)); + if (commentDescriptor != null) list.add(commentDescriptor); } private static void addMethodGenericParametersFolding(@NotNull List list, @@ -413,19 +384,20 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem boolean quick) { if (!(root instanceof PsiJavaFile)) return; PsiJavaFile file = (PsiJavaFile) root; + Set processedComments = new HashSet<>(); addFoldsForImports(descriptors, file); PsiJavaModule module = file.getModuleDeclaration(); if (module != null) { - addFoldsForModule(descriptors, module, document); + addFoldsForModule(descriptors, module, document, processedComments); } PsiClass[] classes = file.getClasses(); for (PsiClass aClass : classes) { ProgressManager.checkCanceled(); ProgressIndicatorProvider.checkCanceled(); - addFoldsForClass(descriptors, aClass, document, true, quick); + addFoldsForClass(descriptors, aClass, document, processedComments, quick); } addFoldsForFileHeader(descriptors, file, document); @@ -476,43 +448,37 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem private static void addFoldsForModule(@NotNull List list, @NotNull PsiJavaModule module, - @NotNull Document document) { + @NotNull Document document, + @NotNull Set processedComments) { addToFold(list, module, document, true, getCodeBlockPlaceholder(null), moduleRange(module), false); - addDocCommentToFold(list, document, module); + addCommentsToFold(list, module, document, processedComments); addAnnotationsToFold(list, module.getModifierList(), document); } private void addFoldsForClass(@NotNull List list, @NotNull PsiClass aClass, @NotNull Document document, - boolean foldJavaDocs, + @NotNull Set processedComments, boolean quick) { PsiElement parent = aClass.getParent(); if (!(parent instanceof PsiJavaFile) || ((PsiJavaFile)parent).getClasses().length > 1) { addToFold(list, aClass, document, true, getCodeBlockPlaceholder(null), classRange(aClass), !(parent instanceof PsiFile) && JavaCodeFoldingSettings.getInstance().isCollapseInnerClasses()); } - if (foldJavaDocs) { - addDocCommentToFold(list, document, aClass); - } - addAnnotationsToFold(list, aClass.getModifierList(), document); - Set processedComments = new HashSet<>(); for (PsiElement child = aClass.getFirstChild(); child != null; child = child.getNextSibling()) { ProgressIndicatorProvider.checkCanceled(); if (child instanceof PsiMethod) { PsiMethod method = (PsiMethod)child; - addFoldsForMethod(list, method, document, foldJavaDocs, quick, processedComments); + addFoldsForMethod(list, method, document, quick, processedComments); } else if (child instanceof PsiField) { PsiField field = (PsiField)child; - if (foldJavaDocs) { - addDocCommentToFold(list, document, field); - } + addCommentsToFold(list, field, document, processedComments); addAnnotationsToFold(list, field.getModifierList(), document); @@ -531,10 +497,10 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem addCodeBlockFolds(list, child, processedComments, document, quick); } else if (child instanceof PsiClass) { - addFoldsForClass(list, (PsiClass)child, document, true, quick); + addFoldsForClass(list, (PsiClass)child, document, processedComments, quick); } else if (child instanceof PsiComment) { - addFoldsForComment(list, (PsiComment)child, processedComments); + addCommentToFold(list, (PsiComment)child, document, processedComments); } } } @@ -542,7 +508,6 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem private void addFoldsForMethod(@NotNull List list, @NotNull PsiMethod method, @NotNull Document document, - boolean foldJavaDocs, boolean quick, @NotNull Set processedComments) { boolean oneLiner = addOneLineMethodFolding(list, method); @@ -552,9 +517,7 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem addAnnotationsToFold(list, method.getModifierList(), document); - if (foldJavaDocs) { - addDocCommentToFold(list, document, method); - } + addCommentsToFold(list, method, document, processedComments); for (PsiParameter parameter : method.getParameterList().getParameters()) { addAnnotationsToFold(list, parameter.getModifierList(), document); @@ -566,15 +529,6 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem } } - private static void addDocCommentToFold(@NotNull List list, - @NotNull Document document, - @NotNull PsiJavaDocumentedElement element) { - PsiDocComment docComment = element.getDocComment(); - if (docComment != null) { - addToFold(list, docComment, document, true, "/**...*/", docComment.getTextRange(), isCollapseDocCommentByDefault(docComment)); - } - } - private boolean addOneLineMethodFolding(@NotNull List list, @NotNull PsiMethod method) { boolean collapseOneLineMethods = JavaCodeFoldingSettings.getInstance().isCollapseOneLineMethods(); if (!collapseOneLineMethods) { @@ -645,9 +599,14 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem return false; } - private static boolean isCollapseDocCommentByDefault(@NotNull PsiDocComment element) { - JavaCodeFoldingSettings settings = JavaCodeFoldingSettings.getInstance(); - PsiElement parent = element.getParent(); + /** + * Determines whether comment should be collapsed by default. + * If comment has unknown type then it is not collapsed. + */ + private static boolean isCollapseCommentByDefault(@NotNull PsiComment comment) { + final JavaCodeFoldingSettings settings = JavaCodeFoldingSettings.getInstance(); + + final PsiElement parent = comment.getParent(); if (parent instanceof PsiJavaFile) { if (((PsiJavaFile)parent).getName().equals(PsiPackage.PACKAGE_INFO_FILE)) { return false; @@ -656,11 +615,19 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem if (firstChild instanceof PsiWhiteSpace) { firstChild = firstChild.getNextSibling(); } - if (element.equals(firstChild)) { + if (comment.equals(firstChild)) { return settings.isCollapseFileHeader(); } } - return settings.isCollapseJavadocs(); + + if (comment instanceof PsiDocComment) return settings.isCollapseJavadocs(); + + final IElementType commentType = comment.getTokenType(); + + if (commentType == JavaTokenType.END_OF_LINE_COMMENT) return settings.isCollapseEndOfLineComments(); + if (commentType == JavaTokenType.C_STYLE_COMMENT) return settings.isCollapseMultilineComments(); + + return false; } private static boolean isCollapseMethodByDefault(@NotNull PsiMethod element) { @@ -685,7 +652,7 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem public void visitClass(PsiClass aClass) { if (dumb || !addClosureFolding(aClass, document, list, processedComments, quick)) { addToFold(list, aClass, document, true, getCodeBlockPlaceholder(null), classRange(aClass), JavaCodeFoldingSettings.getInstance().isCollapseInnerClasses()); - addFoldsForClass(list, aClass, document, false, quick); + addFoldsForClass(list, aClass, document, processedComments, quick); } } @@ -736,7 +703,7 @@ public abstract class JavaFoldingBuilderBase extends CustomFoldingBuilder implem @Override public void visitComment(PsiComment comment) { - addFoldsForComment(list, comment, processedComments); + addCommentToFold(list, comment, document, processedComments); super.visitComment(comment); } }); diff --git a/java/java-tests/testData/codeInsight/folding/JavadocComments.java b/java/java-tests/testData/codeInsight/folding/JavadocComments.java new file mode 100644 index 000000000000..13a52738064d --- /dev/null +++ b/java/java-tests/testData/codeInsight/folding/JavadocComments.java @@ -0,0 +1,67 @@ +/** + * outer class javadoc + * javadoc body + */ +class Test { + + /** + * method javadoc + * javadoc body + * + * @param i + */ + void foo(int i) { + /** + * method var javadoc + * javadoc body + */ + int j = i; + } + + /**ill-formed javadoc + */ + void bar(char c) { + } + + /** + */ + void emptyJavadoc() { + + } + + /***/ + void oneLineEmptyJavadoc() { + } + + /** + * dangling javadoc + * javadoc body + */ + + /** + * inner class javadoc + * javadoc body + */ + class Inner { + + /** + * javadoc for method in inner class + * javadoc body + */ + void foo() { + /** + * javadoc for class in method + * javadoc body + */ + class MethodInner { + + /** + * javadoc for method inside class defined in method + * javadoc body + */ + void bar() { + } + } + } + } +} \ No newline at end of file diff --git a/java/java-tests/testData/codeInsight/folding/MultilineComments.java b/java/java-tests/testData/codeInsight/folding/MultilineComments.java new file mode 100644 index 000000000000..75c0fcdc58b3 --- /dev/null +++ b/java/java-tests/testData/codeInsight/folding/MultilineComments.java @@ -0,0 +1,44 @@ +/* +outer class comment + */ +class Test { + /* + dangling multiline commnent + */ + + /* + field comment + */ + int field; + + /* + method comment + */ + void foo() { + + /* + method var comment + */ + final int i = 42; + } + + /* + inner class comment + */ + class Inner { + + /* + inner class method comment + */ + void foo() { + class MethodInner { + /* + method inside class declared in method comment + */ + void foo() { + + } + } + } + } +} \ No newline at end of file diff --git a/java/java-tests/testSrc/com/intellij/openapi/editor/impl/JavaFoldingTest.groovy b/java/java-tests/testSrc/com/intellij/openapi/editor/impl/JavaFoldingTest.groovy index a6be00c37677..ca5e3d9fc825 100644 --- a/java/java-tests/testSrc/com/intellij/openapi/editor/impl/JavaFoldingTest.groovy +++ b/java/java-tests/testSrc/com/intellij/openapi/editor/impl/JavaFoldingTest.groovy @@ -42,6 +42,10 @@ class JavaFoldingTest extends JavaFoldingTestCase { public void testEndOfLineComments() { doTest() } + public void testMultilineComments() { doTest() } + + public void testJavadocComments() { doTest() } + public void testEditingImports() { configure """\ import java.util.List; @@ -494,8 +498,8 @@ class A { // 1 // 2 next empty line is significant -// 3 non-folded -// 4 non-folded +// 3 +// 4 int t = 1; // 5 // 6 @@ -506,12 +510,10 @@ class A { def foldingModel = myFixture.editor.foldingModel as FoldingModelImpl assertFolding "// 0" - assertNoFoldingStartsAt "// 3" - assertNoFoldingCovers "// 3" - assertNoFoldingCovers "// 4" + assertFolding "// 3" assertFolding "// 5" - assertEquals 2, foldRegionsCount + assertEquals 3, foldRegionsCount } public void "test custom folding collapsed by default"() { diff --git a/platform/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index 361ff1636bdb..572a32b37396 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -650,6 +650,7 @@ insert.override.annotation=Insert @&Override annotation auto.import=Auto Import checkbox.collapse.suppress.warnings=@SuppressWarnings checkbox.collapse.end.of.line.comments=End of line comments sequence +checkbox.collapse.multiline.comments=Multiline comments title.other.languages=Other Languages title.tabs.and.indents=Tabs and Indents