package com.jetbrains.lang.dart.folding;
import com.intellij.codeInsight.folding.CodeFoldingSettings;
import com.intellij.lang.ASTNode;
import com.intellij.lang.folding.CustomFoldingBuilder;
import com.intellij.lang.folding.FoldingDescriptor;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.UnfairTextRange;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.lang.dart.DartComponentType;
import com.jetbrains.lang.dart.DartTokenTypes;
import com.jetbrains.lang.dart.DartTokenTypesSets;
import com.jetbrains.lang.dart.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class DartFoldingBuilder extends CustomFoldingBuilder implements DumbAware {
private static final String SMILEY = "<~>";
protected void buildLanguageFoldRegions(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final PsiElement root,
@NotNull final Document document,
final boolean quick) {
if (!(root instanceof DartFile)) return;
final DartFile dartFile = (DartFile)root;
final TextRange fileHeaderRange = foldFileHeader(descriptors, dartFile, document); // 1. File header
foldConsequentStatements(descriptors, dartFile, DartImportOrExportStatement.class);// 2. Import and export statements
foldConsequentStatements(descriptors, dartFile, DartPartStatement.class); // 3. Part statements
final Collection<PsiElement> psiElements = PsiTreeUtil.collectElementsOfType(
root,
new Class[]{
DartComponent.class,
DartTypeArguments.class,
PsiComment.class,
DartStringLiteralExpression.class,
DartMapLiteralExpression.class});
foldComments(descriptors, psiElements, fileHeaderRange); // 4. Comments and comment sequences
foldClassBodies(descriptors, dartFile); // 5. Class body
foldFunctionBodies(descriptors, psiElements); // 6. Function body
foldTypeArguments(descriptors, psiElements); // 7. Type arguments
foldMultilineStrings(descriptors, psiElements); // 8. Multi-line strings
foldMapLiterals(descriptors, psiElements); // 9. Map literals
}
protected String getLanguagePlaceholderText(@NotNull final ASTNode node, @NotNull final TextRange range) {
final IElementType elementType = node.getElementType();
final PsiElement psiElement = node.getPsi();
if (psiElement instanceof DartFile) return "/.../"; // 1. File header
if (psiElement instanceof DartImportOrExportStatement) return "..."; // 2. Import and export statements
if (psiElement instanceof DartPartStatement) return "..."; // 3. Part statements
if (elementType == DartTokenTypesSets.MULTI_LINE_DOC_COMMENT) return "/**...*/"; // 4.1. Multiline doc comments
if (elementType == DartTokenTypesSets.MULTI_LINE_COMMENT) return "/*...*/"; // 4.2. Multiline comments
if (elementType == DartTokenTypesSets.SINGLE_LINE_DOC_COMMENT) return "///..."; // 4.3. Consequent single line doc comments
if (elementType == DartTokenTypesSets.SINGLE_LINE_COMMENT) return "//..."; // 4.4. Consequent single line comments
if (psiElement instanceof DartClassBody || psiElement instanceof DartEnumDefinition) {
return "{...}"; // 5. Class body
}
if (psiElement instanceof DartFunctionBody) return "{...}"; // 6. Function body
if (psiElement instanceof DartTypeArguments) return SMILEY; // 7. Type arguments
if (psiElement instanceof DartStringLiteralExpression) {
return multilineStringPlaceholder(node); // 8. Multi-line strings
}
if (psiElement instanceof DartMapLiteralExpression) return "{...}"; // 9. Map literals
return "...";
}
protected boolean isRegionCollapsedByDefault(@NotNull final ASTNode node) {
final IElementType elementType = node.getElementType();
final PsiElement psiElement = node.getPsi();
final CodeFoldingSettings settings = CodeFoldingSettings.getInstance();
final DartCodeFoldingSettings dartSettings = DartCodeFoldingSettings.getInstance();
if (psiElement instanceof DartFile) return settings.COLLAPSE_FILE_HEADER; // 1. File header
if (psiElement instanceof DartImportOrExportStatement) return settings.COLLAPSE_IMPORTS; // 2. Import and export statements
if (psiElement instanceof DartPartStatement) return dartSettings.isCollapseParts(); // 3. Part statements
if (elementType == DartTokenTypesSets.MULTI_LINE_DOC_COMMENT || // 4.1. Multiline doc comments
elementType ==
DartTokenTypesSets.SINGLE_LINE_DOC_COMMENT) { // 4.3. Consequent single line doc comments
return settings.COLLAPSE_DOC_COMMENTS; // 4.2 and 4.4 never collapsed by default
}
// 5. Class body never collapsed by default
if (psiElement instanceof DartFunctionBody) return settings.COLLAPSE_METHODS; // 6. Function body
if (psiElement instanceof DartTypeArguments) return dartSettings.isCollapseGenericParameters(); // 7. Type arguments
return false;
}
@Nullable
private static TextRange foldFileHeader(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final DartFile dartFile,
@NotNull final Document document) {
PsiElement firstComment = dartFile.getFirstChild();
if (firstComment instanceof PsiWhiteSpace) firstComment = firstComment.getNextSibling();
if (!(firstComment instanceof PsiComment)) return null;
boolean containsCustomRegionMarker = false;
PsiElement nextAfterComments = firstComment;
while (nextAfterComments instanceof PsiComment || nextAfterComments instanceof PsiWhiteSpace) {
containsCustomRegionMarker |= isCustomRegionElement(nextAfterComments);
nextAfterComments = nextAfterComments.getNextSibling();
}
if (nextAfterComments == null) return null;
if (nextAfterComments instanceof DartLibraryStatement ||
nextAfterComments instanceof DartPartStatement ||
nextAfterComments instanceof DartPartOfStatement ||
nextAfterComments instanceof DartImportOrExportStatement) {
if (nextAfterComments.getPrevSibling() instanceof PsiWhiteSpace) nextAfterComments = nextAfterComments.getPrevSibling();
if (nextAfterComments.equals(firstComment)) return null;
final TextRange fileHeaderCommentsRange =
new UnfairTextRange(firstComment.getTextRange().getStartOffset(), nextAfterComments.getTextRange().getStartOffset());
if (fileHeaderCommentsRange.getLength() > 1 &&
document.getLineNumber(fileHeaderCommentsRange.getEndOffset()) >
document.getLineNumber(fileHeaderCommentsRange.getStartOffset())) {
if (!containsCustomRegionMarker) {
descriptors.add(new FoldingDescriptor(dartFile, fileHeaderCommentsRange));
}
return fileHeaderCommentsRange;
}
}
return null;
}
private static <T extends PsiElement> void foldConsequentStatements(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final DartFile dartFile,
@NotNull final Class<T> aClass) {
final T firstStatement = PsiTreeUtil.getChildOfType(dartFile, aClass);
if (firstStatement == null) return;
PsiElement lastStatement = firstStatement;
PsiElement nextElement = firstStatement;
while (aClass.isInstance(nextElement) ||
nextElement instanceof PsiComment ||
nextElement instanceof PsiWhiteSpace) {
if (aClass.isInstance(nextElement)) {
lastStatement = nextElement;
}
nextElement = nextElement.getNextSibling();
}
if (lastStatement != firstStatement) {
// after "import " or "export " or "part "
final int startOffset = firstStatement.getTextRange().getStartOffset() + firstStatement.getFirstChild().getTextLength() + 1;
final int endOffset = lastStatement.getTextRange().getEndOffset();
final FoldingDescriptor descriptor = new FoldingDescriptor(firstStatement, TextRange.create(startOffset, endOffset));
if (aClass == DartImportOrExportStatement.class) {
// imports are often added/removed automatically, so we enable autoupdate of folded region for foldings even if it's collapsed
descriptor.setCanBeRemovedWhenCollapsed(true);
}
descriptors.add(descriptor);
}
}
private static void foldComments(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final Collection<PsiElement> psiElements,
@Nullable final TextRange fileHeaderRange) {
PsiElement psiElement;
for (Iterator<PsiElement> iter = psiElements.iterator(); iter.hasNext(); ) {
psiElement = iter.next();
if (!(psiElement instanceof PsiComment)) {
continue;
}
if (fileHeaderRange != null && fileHeaderRange.intersects(psiElement.getTextRange())) {
continue;
}
final IElementType elementType = psiElement.getNode().getElementType();
if ((elementType == DartTokenTypesSets.MULTI_LINE_DOC_COMMENT || elementType == DartTokenTypesSets.MULTI_LINE_COMMENT) &&
!isCustomRegionElement(psiElement)) {
descriptors.add(new FoldingDescriptor(psiElement, psiElement.getTextRange()));
}
else if (elementType == DartTokenTypesSets.SINGLE_LINE_DOC_COMMENT || elementType == DartTokenTypesSets.SINGLE_LINE_COMMENT) {
final PsiElement firstCommentInSequence = psiElement;
PsiElement lastCommentInSequence = firstCommentInSequence;
PsiElement nextElement = firstCommentInSequence;
boolean containsCustomRegionMarker = isCustomRegionElement(nextElement);
while (iter.hasNext() && (nextElement = nextElement.getNextSibling()) != null &&
(nextElement instanceof PsiWhiteSpace || nextElement.getNode().getElementType() == elementType)) {
if (nextElement.getNode().getElementType() == elementType) {
// advance iterator to skip processed comments sequence
iter.next();
lastCommentInSequence = nextElement;
containsCustomRegionMarker |= isCustomRegionElement(nextElement);
}
}
if (lastCommentInSequence != firstCommentInSequence && !containsCustomRegionMarker) {
final TextRange range =
TextRange.create(firstCommentInSequence.getTextRange().getStartOffset(), lastCommentInSequence.getTextRange().getEndOffset());
descriptors.add(new FoldingDescriptor(firstCommentInSequence, range));
}
}
}
}
private static void foldClassBodies(@NotNull final List<FoldingDescriptor> descriptors, @NotNull final DartFile dartFile) {
for (DartClass dartClass : PsiTreeUtil.getChildrenOfTypeAsList(dartFile, DartClass.class)) {
if (dartClass instanceof DartClassDefinition) {
final DartClassBody body = ((DartClassDefinition)dartClass).getClassBody();
if (body != null && body.getTextLength() > 2) {
descriptors.add(new FoldingDescriptor(body, body.getTextRange()));
}
}
else if (dartClass instanceof DartEnumDefinition) {
final ASTNode lBrace = dartClass.getNode().findChildByType(DartTokenTypes.LBRACE);
final ASTNode rBrace = dartClass.getNode().findChildByType(DartTokenTypes.RBRACE, lBrace);
if (lBrace != null && rBrace != null && (rBrace.getStartOffset() - lBrace.getStartOffset() > 2)) {
descriptors.add(new FoldingDescriptor(dartClass, TextRange.create(lBrace.getStartOffset(), rBrace.getStartOffset() + 1)));
}
}
}
}
private static void foldFunctionBodies(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final Collection<PsiElement> psiElements) {
for (PsiElement dartComponent : psiElements) {
final DartComponentType componentType = DartComponentType.typeOf(dartComponent);
if (componentType == null) continue;
switch (componentType) {
case CONSTRUCTOR:
case FUNCTION:
case METHOD:
case OPERATOR:
foldFunctionBody(descriptors, dartComponent);
break;
default:
break;
}
}
}
private static void foldFunctionBody(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final PsiElement dartComponentOrOperatorDeclaration) {
final DartFunctionBody functionBody = PsiTreeUtil.getChildOfType(dartComponentOrOperatorDeclaration, DartFunctionBody.class);
final IDartBlock block = functionBody == null ? null : functionBody.getBlock();
if (block != null && block.getTextLength() > 2) {
descriptors.add(new FoldingDescriptor(functionBody, block.getTextRange()));
}
}
private static void foldTypeArguments(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final Collection<PsiElement> psiElements) {
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DartTypeArguments) {
DartTypeArguments dartTypeArguments = (DartTypeArguments)psiElement;
if (PsiTreeUtil.getParentOfType(dartTypeArguments, DartNewExpression.class, DartTypeArguments.class) instanceof DartNewExpression) {
descriptors.add(new FoldingDescriptor(dartTypeArguments, TextRange
.create(dartTypeArguments.getTextRange().getStartOffset(), dartTypeArguments.getTextRange().getEndOffset())));
}
}
}
}
private static void foldMultilineStrings(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final Collection<PsiElement> psiElements) {
for (PsiElement element : psiElements) {
if (element instanceof DartStringLiteralExpression) {
DartStringLiteralExpression dartString = (DartStringLiteralExpression)element;
PsiElement child = dartString.getFirstChild();
if (child == null) continue;
IElementType type = child.getNode().getElementType();
if (type == DartTokenTypes.RAW_TRIPLE_QUOTED_STRING || (type == DartTokenTypes.OPEN_QUOTE && child.getTextLength() == 3)) {
descriptors.add(new FoldingDescriptor(dartString, dartString.getTextRange()));
}
}
}
}
private static String multilineStringPlaceholder(@NotNull final ASTNode node) {
ASTNode child = node.getFirstChildNode();
if (child == null) return "...";
if (child.getElementType() == DartTokenTypes.RAW_TRIPLE_QUOTED_STRING) {
String text = child.getText();
String quotes = text.substring(1, 4);
return "r" + quotes + "..." + quotes;
}
if (child.getElementType() == DartTokenTypes.OPEN_QUOTE) {
String text = child.getText();
String quotes = text.substring(0, 3);
return quotes + "..." + quotes;
}
return "...";
}
private static void foldMapLiterals(@NotNull final List<FoldingDescriptor> descriptors,
@NotNull final Collection<PsiElement> psiElements) {
for (PsiElement psiElement : psiElements) {
if (psiElement instanceof DartMapLiteralExpression) {
final ASTNode node = psiElement.getNode();
final ASTNode lBrace = node.findChildByType(DartTokenTypes.LBRACE);
final ASTNode rBrace = lBrace == null ? null : node.findChildByType(DartTokenTypes.RBRACE, lBrace);
if (lBrace != null && rBrace != null) {
final String text =
node.getText().substring(lBrace.getStartOffset() - node.getStartOffset(), rBrace.getStartOffset() - node.getStartOffset());
if (text.contains("\n")) {
descriptors.add(new FoldingDescriptor(psiElement, TextRange.create(lBrace.getStartOffset(),
rBrace.getStartOffset() + rBrace.getTextLength())));
}
}
}
}
}
}