| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.jetbrains.plugins.groovy.lang.folding; |
| |
| import com.intellij.codeInsight.folding.JavaCodeFoldingSettings; |
| import com.intellij.codeInsight.folding.impl.JavaFoldingBuilderBase; |
| import com.intellij.lang.ASTNode; |
| import com.intellij.lang.folding.CustomFoldingBuilder; |
| import com.intellij.lang.folding.FoldingDescriptor; |
| import com.intellij.lang.folding.NamedFoldingDescriptor; |
| import com.intellij.openapi.editor.Document; |
| import com.intellij.openapi.editor.FoldingGroup; |
| import com.intellij.openapi.project.DumbAware; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.source.tree.LeafPsiElement; |
| import com.intellij.psi.tree.IElementType; |
| import com.intellij.util.containers.hash.HashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import org.jetbrains.plugins.groovy.lang.groovydoc.parser.GroovyDocElementTypes; |
| import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes; |
| import org.jetbrains.plugins.groovy.lang.lexer.TokenSets; |
| import org.jetbrains.plugins.groovy.lang.parser.GroovyElementTypes; |
| import org.jetbrains.plugins.groovy.lang.psi.GroovyFile; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrString; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition; |
| import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinitionBody; |
| import org.jetbrains.plugins.groovy.lang.psi.api.toplevel.imports.GrImportStatement; |
| import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil; |
| import org.jetbrains.plugins.groovy.lang.psi.util.GrStringUtil; |
| |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * @author ilyas |
| */ |
| public class GroovyFoldingBuilder extends CustomFoldingBuilder implements DumbAware { |
| |
| @Override |
| protected void buildLanguageFoldRegions(@NotNull List<FoldingDescriptor> descriptors, |
| @NotNull PsiElement root, |
| @NotNull Document document, |
| boolean quick) { |
| appendDescriptors(root, descriptors, new HashSet<PsiElement>()); |
| } |
| |
| private void appendDescriptors(PsiElement element, List<FoldingDescriptor> descriptors, Set<PsiElement> usedComments) { |
| ASTNode node = element.getNode(); |
| if (node == null) return; |
| IElementType type = node.getElementType(); |
| |
| if (TokenSets.BLOCK_SET.contains(type) && !isSingleHighLevelClassBody(element) || type == GroovyElementTypes.CLOSABLE_BLOCK) { |
| if (isMultiline(element)) { |
| collapseBlock(descriptors, element); |
| } |
| } |
| // comments |
| if (((type.equals(GroovyTokenTypes.mML_COMMENT) && !isCustomRegionStart(node)) || type.equals(GroovyDocElementTypes.GROOVY_DOC_COMMENT)) && |
| isMultiline(element) && |
| isWellEndedComment(element)) { |
| descriptors.add(new FoldingDescriptor(node, node.getTextRange())); |
| } |
| |
| if (type.equals(GroovyTokenTypes.mSL_COMMENT) && !isCustomRegionStart(node) && !usedComments.contains(element)) { |
| usedComments.add(element); |
| PsiElement end = null; |
| for (PsiElement current = element.getNextSibling(); current != null; current = current.getNextSibling()) { |
| if (PsiImplUtil.isWhiteSpaceOrNls(current)) continue; |
| |
| IElementType elementType = current.getNode().getElementType(); |
| if (elementType == GroovyTokenTypes.mSL_COMMENT) { |
| end = current; |
| usedComments.add(current); |
| continue; |
| } |
| break; |
| } |
| if (end != null) { |
| final TextRange range = new TextRange(element.getTextRange().getStartOffset(), end.getTextRange().getEndOffset()); |
| descriptors.add(new FoldingDescriptor(element, range)); |
| } |
| } |
| |
| //multiline strings |
| addFoldingForStrings(descriptors, node); |
| |
| Set<PsiElement> newUsedComments = new HashSet<PsiElement>(); |
| for (PsiElement child = element.getFirstChild(); child != null; child = child.getNextSibling()) { |
| appendDescriptors(child, descriptors, newUsedComments); |
| } |
| |
| if (element instanceof GroovyFile) { |
| processImports(descriptors, ((GroovyFile)element).getImportStatements()); |
| } |
| } |
| |
| private static void collapseBlock(List<FoldingDescriptor> descriptors, PsiElement psi) { |
| if (psi instanceof GrCodeBlock) { |
| final int lineFeedCount = StringUtil.countChars(psi.getText(), '\n'); |
| if (lineFeedCount <= 2) { |
| final PsiElement lbrace = ((GrCodeBlock)psi).getLBrace(); |
| final PsiElement rbrace = ((GrCodeBlock)psi).getRBrace(); |
| if (lbrace != null && rbrace != null) { |
| final PsiElement next = lbrace.getNextSibling(); |
| final PsiElement prev = rbrace.getPrevSibling(); |
| if (next != null && PsiImplUtil.isWhiteSpaceOrNls(next) && |
| prev != null && PsiImplUtil.isWhiteSpaceOrNls(prev)) { |
| final FoldingGroup group = FoldingGroup.newGroup("block_group"); |
| descriptors.add(new NamedFoldingDescriptor(psi, lbrace.getTextRange().getStartOffset(), next.getTextRange().getEndOffset(), group, "{")); |
| descriptors.add(new NamedFoldingDescriptor(psi, prev.getTextRange().getStartOffset(), rbrace.getTextRange().getEndOffset(), group, "}")); |
| return; |
| } |
| } |
| } |
| } |
| descriptors.add(new FoldingDescriptor(psi, psi.getTextRange())); |
| } |
| |
| private static boolean isSingleHighLevelClassBody(PsiElement element) { |
| if (!(element instanceof GrTypeDefinitionBody)) return false; |
| |
| final PsiElement parent = element.getParent(); |
| if (!(parent instanceof GrTypeDefinition)) return false; |
| |
| final GrTypeDefinition clazz = (GrTypeDefinition)parent; |
| if (clazz.isAnonymous() || clazz.getContainingClass() != null) return false; |
| |
| final PsiFile file = element.getContainingFile(); |
| return file instanceof GroovyFile && ((GroovyFile)file).getClasses().length == 1; |
| } |
| |
| private static void addFoldingForStrings(List<FoldingDescriptor> descriptors, ASTNode node) { |
| if (!isMultiLineStringLiteral(node)) return; |
| |
| if (!node.getElementType().equals(GroovyElementTypes.GSTRING) && !node.getElementType().equals(GroovyElementTypes.REGEX)) { |
| descriptors.add(new FoldingDescriptor(node, node.getTextRange())); |
| return; |
| } |
| |
| final GrString grString = (GrString)node.getPsi(); |
| if (grString == null) return; |
| |
| final GrStringInjection[] injections = grString.getInjections(); |
| if (injections.length == 0) { |
| descriptors.add(new FoldingDescriptor(node, node.getTextRange())); |
| return; |
| } |
| final String start_quote = GrStringUtil.getStartQuote(node.getText()); |
| final String end_quote = GrStringUtil.getEndQuote(node.getText()); |
| final FoldingGroup group = FoldingGroup.newGroup("GString"); |
| final TextRange nodeRange = node.getTextRange(); |
| int startOffset = nodeRange.getStartOffset(); |
| |
| GrStringInjection injection = injections[0]; |
| TextRange injectionRange = injection.getTextRange(); |
| if (startOffset + 1 < injectionRange.getStartOffset()) { |
| descriptors.add(new NamedFoldingDescriptor(node, startOffset, injectionRange.getStartOffset(), group, start_quote)); |
| } |
| |
| final String placeholder = " "; |
| startOffset = injectionRange.getEndOffset(); |
| for (int i = 1; i < injections.length; i++) { |
| injection = injections[i]; |
| injectionRange = injection.getTextRange(); |
| final int endOffset = injectionRange.getStartOffset(); |
| if (endOffset - startOffset >= 2) { |
| descriptors.add(new NamedFoldingDescriptor(injection.getNode().getTreePrev(), startOffset, endOffset, group, placeholder)); |
| } |
| startOffset = injectionRange.getEndOffset(); |
| } |
| if (startOffset + 1 < nodeRange.getEndOffset()) { |
| descriptors.add(new NamedFoldingDescriptor(node.getLastChildNode(), startOffset, nodeRange.getEndOffset(), group, end_quote)); |
| } |
| } |
| |
| private static void processImports(final List<FoldingDescriptor> descriptors, GrImportStatement[] imports) { |
| if (imports.length < 2) return; |
| |
| PsiElement first = imports[0]; |
| while (first != null) { |
| PsiElement marker = first; |
| PsiElement next = first.getNextSibling(); |
| while (next instanceof GrImportStatement || next instanceof LeafPsiElement) { |
| if (next instanceof GrImportStatement) marker = next; |
| next = next.getNextSibling(); |
| } |
| if (marker != first) { |
| int start = first.getTextRange().getStartOffset(); |
| int end = marker.getTextRange().getEndOffset(); |
| int tail = "import ".length(); |
| if (start + tail < end && !JavaFoldingBuilderBase.hasErrorElementsNearby(first.getContainingFile(), start, end)) { |
| descriptors.add(new FoldingDescriptor(first.getNode(), new TextRange(start + tail, end))); |
| } |
| } |
| while (!(next instanceof GrImportStatement) && next != null) next = next.getNextSibling(); |
| first = next; |
| } |
| } |
| |
| private static boolean isWellEndedComment(PsiElement element) { |
| return element.getText().endsWith("*/"); |
| } |
| |
| private static boolean isMultiline(PsiElement element) { |
| String text = element.getText(); |
| return text.contains("\n") || text.contains("\r") || text.contains("\r\n"); |
| } |
| |
| @Nullable |
| @Override |
| protected String getLanguagePlaceholderText(@NotNull ASTNode node, @NotNull TextRange range) { |
| final IElementType elemType = node.getElementType(); |
| if (TokenSets.BLOCK_SET.contains(elemType) || elemType == GroovyElementTypes.CLOSABLE_BLOCK) { |
| return "{...}"; |
| } |
| if (elemType.equals(GroovyTokenTypes.mML_COMMENT)) { |
| return "/*...*/"; |
| } |
| if (elemType.equals(GroovyDocElementTypes.GROOVY_DOC_COMMENT)) { |
| return "/**...*/"; |
| } |
| if (GroovyElementTypes.IMPORT_STATEMENT.equals(elemType)) { |
| return "..."; |
| } |
| if (isMultiLineStringLiteral(node)) { |
| final String start_quote = GrStringUtil.getStartQuote(node.getText()); |
| final String end_quote = GrStringUtil.getEndQuote(node.getText()); |
| return start_quote + "..." + end_quote; |
| } |
| return null; |
| } |
| |
| @Override |
| protected boolean isRegionCollapsedByDefault(@NotNull ASTNode node) { |
| final JavaCodeFoldingSettings settings = JavaCodeFoldingSettings.getInstance(); |
| if ( node.getElementType() == GroovyElementTypes.IMPORT_STATEMENT){ |
| return settings.isCollapseImports(); |
| } |
| |
| if (node.getElementType() == GroovyDocElementTypes.GROOVY_DOC_COMMENT || node.getElementType() == GroovyTokenTypes.mML_COMMENT) { |
| PsiElement element = node.getPsi(); |
| PsiElement parent = element.getParent(); |
| if (parent instanceof GroovyFile) { |
| PsiElement firstChild = parent.getFirstChild(); |
| if (firstChild instanceof PsiWhiteSpace) { |
| firstChild = firstChild.getNextSibling(); |
| } |
| if (element.equals(firstChild)) { |
| return settings.isCollapseFileHeader(); |
| } |
| } |
| if (node.getElementType() == GroovyDocElementTypes.GROOVY_DOC_COMMENT) { |
| return settings.isCollapseJavadocs(); |
| } |
| } |
| |
| if ((node.getElementType() == GroovyElementTypes.OPEN_BLOCK || node.getElementType() == GroovyElementTypes.CONSTRUCTOR_BODY) && node.getTreeParent().getElementType() == |
| GroovyElementTypes.METHOD_DEFINITION) { |
| return settings.isCollapseMethods(); |
| } |
| |
| if (node.getElementType() == GroovyElementTypes.CLOSABLE_BLOCK) { |
| return settings.isCollapseAnonymousClasses(); |
| } |
| |
| if (node.getElementType() == GroovyElementTypes.CLASS_BODY) { |
| final PsiElement parent = node.getPsi().getParent(); |
| if (parent instanceof PsiClass) { |
| if (parent instanceof PsiAnonymousClass) { |
| return settings.isCollapseAnonymousClasses(); |
| } |
| if (((PsiClass)parent).getContainingClass() != null) { |
| return settings.isCollapseInnerClasses(); |
| } |
| } |
| } |
| |
| if (node.getElementType() == GroovyTokenTypes.mSL_COMMENT) { |
| return settings.isCollapseEndOfLineComments(); |
| } |
| |
| return false; |
| } |
| |
| private static boolean isMultiLineStringLiteral(ASTNode node) { |
| return (TokenSets.STRING_LITERAL_SET.contains(node.getElementType()) || |
| node.getElementType().equals(GroovyElementTypes.GSTRING) || |
| node.getElementType().equals(GroovyElementTypes.REGEX)) && |
| isMultiline(node.getPsi()) && |
| GrStringUtil.isWellEndedString(node.getPsi()); |
| } |
| |
| @Override |
| protected boolean isCustomFoldingCandidate(ASTNode node) { |
| return node.getElementType() == GroovyTokenTypes.mSL_COMMENT; |
| } |
| |
| @Override |
| protected boolean isCustomFoldingRoot(ASTNode node) { |
| IElementType nodeType = node.getElementType(); |
| return nodeType == GroovyElementTypes.CLASS_DEFINITION || nodeType == GroovyElementTypes.OPEN_BLOCK; |
| } |
| } |