package com.dmarcotte.handlebars.editor.folding; import com.dmarcotte.handlebars.config.HbConfig; import com.dmarcotte.handlebars.parsing.HbTokenTypes; import com.dmarcotte.handlebars.psi.HbBlockWrapper; import com.dmarcotte.handlebars.psi.HbCloseBlockMustache; import com.dmarcotte.handlebars.psi.HbOpenBlockMustache; import com.intellij.lang.ASTNode; import com.intellij.lang.folding.FoldingBuilder; 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.psi.PsiElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; public class HbFoldingBuilder implements FoldingBuilder, DumbAware { @NotNull @Override public FoldingDescriptor[] buildFoldRegions(@NotNull ASTNode node, @NotNull Document document) { List<FoldingDescriptor> descriptors = new ArrayList<>(); appendDescriptors(node.getPsi(), descriptors, document); return descriptors.toArray(new FoldingDescriptor[descriptors.size()]); } private void appendDescriptors(PsiElement psi, List<FoldingDescriptor> descriptors, Document document) { if (isSingleLine(psi, document)) { return; } if (HbTokenTypes.COMMENT == psi.getNode().getElementType()) { ASTNode commentNode = psi.getNode(); String commentText = commentNode.getText(); // comment might be unclosed or too short (a one character fold triggers an invalid range assertion error), // so do a bit of sanity checking on its length and whether or not it's got the requisite open/close // tags before we allow folding if (commentText.length() > 6 && commentText.substring(0, 3).equals("{{!") && commentText.substring(commentText.length() - 2, commentText.length()).equals("}}")) { TextRange range = new TextRange(commentNode.getTextRange().getStartOffset() + 3, commentNode.getTextRange().getEndOffset() - 2); descriptors.add(new FoldingDescriptor(commentNode, range)); } } if (psi instanceof HbBlockWrapper) { PsiElement endOpenBlockStache = getOpenBlockCloseStacheElement(psi.getFirstChild()); PsiElement endCloseBlockStache = getCloseBlockCloseStacheElement(psi.getLastChild()); // if we've got a well formed block with the open and close elems we need, define a region to fold if (endOpenBlockStache != null && endCloseBlockStache != null) { int endOfFirstOpenStacheLine = document.getLineEndOffset(document.getLineNumber(psi.getTextRange().getStartOffset())); // we set the start of the text we'll fold to be just before the close braces of the open stache, // or, if the open stache spans multiple lines, to the end of the first line int foldingRangeStartOffset = Math.min(endOpenBlockStache.getTextRange().getStartOffset(), endOfFirstOpenStacheLine); // we set the end of the text we'll fold to be just before the final close braces in this block int foldingRangeEndOffset = endCloseBlockStache.getTextRange().getStartOffset(); TextRange range = new TextRange(foldingRangeStartOffset, foldingRangeEndOffset); descriptors.add(new FoldingDescriptor(psi, range)); } } PsiElement child = psi.getFirstChild(); while (child != null) { appendDescriptors(child, descriptors, document); child = child.getNextSibling(); } } /** * If the given element is a {@link com.dmarcotte.handlebars.psi.HbOpenBlockMustache} returns the close 'stache node ("}}") * <p/> * Otherwise, returns null. */ private PsiElement getOpenBlockCloseStacheElement(PsiElement psiElement) { if (psiElement == null || !(psiElement instanceof HbOpenBlockMustache)) { return null; } PsiElement endOpenStache = psiElement.getLastChild(); if (endOpenStache == null || endOpenStache.getNode().getElementType() != HbTokenTypes.CLOSE) { return null; } return endOpenStache; } /** * If the given element is a {@link com.dmarcotte.handlebars.psi.HbCloseBlockMustache}, returns the close 'stache node ("}}") * <p/> * Otherwise, returns null */ private PsiElement getCloseBlockCloseStacheElement(PsiElement psiElement) { if (psiElement == null || !(psiElement instanceof HbCloseBlockMustache)) { return null; } PsiElement endCloseStache = psiElement.getLastChild(); if (endCloseStache == null || endCloseStache.getNode().getElementType() != HbTokenTypes.CLOSE) { return null; } return endCloseStache; } @Nullable @Override public String getPlaceholderText(@NotNull ASTNode node) { return "..."; } @Override public boolean isCollapsedByDefault(@NotNull ASTNode node) { return HbConfig.isAutoCollapseBlocksEnabled(); } /** * Return true if this psi element does not span more than one line in the given document */ private static boolean isSingleLine(PsiElement element, Document document) { TextRange range = element.getTextRange(); return document.getLineNumber(range.getStartOffset()) == document.getLineNumber(range.getEndOffset()); } }