package com.dmarcotte.handlebars.format;
import com.dmarcotte.handlebars.config.HbConfig;
import com.dmarcotte.handlebars.parsing.HbTokenTypes;
import com.dmarcotte.handlebars.psi.HbPsiUtil;
import com.intellij.formatting.*;
import com.intellij.formatting.templateLanguages.*;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiErrorElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.formatter.DocumentBasedFormattingModel;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import com.intellij.psi.formatter.common.AbstractBlock;
import com.intellij.psi.formatter.xml.HtmlPolicy;
import com.intellij.psi.formatter.xml.SyntheticBlock;
import com.intellij.psi.templateLanguages.SimpleTemplateLanguageFormattingModelBuilder;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import static com.intellij.psi.formatter.WrappingUtil.getWrapType;
/**
* Template aware formatter which provides formatting for Handlebars/Mustache syntax and delegates formatting
* for the templated language to that languages formatter
*/
public class HbFormattingModelBuilder extends TemplateLanguageFormattingModelBuilder {
@Override
public TemplateLanguageBlock createTemplateLanguageBlock(@NotNull ASTNode node,
@Nullable Wrap wrap,
@Nullable Alignment alignment,
@Nullable List<DataLanguageBlockWrapper> foreignChildren,
@NotNull CodeStyleSettings codeStyleSettings) {
final FormattingDocumentModelImpl documentModel = FormattingDocumentModelImpl.createOn(node.getPsi().getContainingFile());
HtmlPolicy policy = new HtmlPolicy(codeStyleSettings, documentModel);
return HbTokenTypes.TAGS.contains(node.getElementType()) ?
new HandlebarsTagBlock(node, wrap, alignment, this, codeStyleSettings, foreignChildren, policy) :
new HandlebarsBlock(node, wrap, alignment, this, codeStyleSettings, foreignChildren, policy);
}
/**
* We have to override {@link com.intellij.formatting.templateLanguages.TemplateLanguageFormattingModelBuilder#createModel}
* since after we delegate to some templated languages, those languages (xml/html for sure, potentially others)
* delegate right back to us to format the HbTokenTypes.OUTER_ELEMENT_TYPE token we tell them to ignore,
* causing an stack-overflowing loop of polite format-delegation.
*/
@NotNull
public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) {
if (!HbConfig.isFormattingEnabled()) {
// formatting is disabled, return the no-op formatter (note that this still delegates formatting
// to the templated language, which lets the users manage that separately)
return new SimpleTemplateLanguageFormattingModelBuilder().createModel(element, settings);
}
final PsiFile file = element.getContainingFile();
Block rootBlock;
ASTNode node = element.getNode();
if (node.getElementType() == HbTokenTypes.OUTER_ELEMENT_TYPE) {
// If we're looking at a HbTokenTypes.OUTER_ELEMENT_TYPE element, then we've been invoked by our templated
// language. Make a dummy block to allow that formatter to continue
return new SimpleTemplateLanguageFormattingModelBuilder().createModel(element, settings);
}
else {
rootBlock = getRootBlock(file, file.getViewProvider(), settings);
}
return new DocumentBasedFormattingModel(rootBlock, element.getProject(), settings, file.getFileType(), file);
}
/**
* Do format my model!
*
* @return false all the time to tell the {@link com.intellij.formatting.templateLanguages.TemplateLanguageFormattingModelBuilder}
* to not-not format our model (i.e. yes please! Format away!)
*/
@Override
public boolean dontFormatMyModel() {
return false;
}
private static class HandlebarsTagBlock extends HandlebarsBlock {
@NotNull
private final Alignment myChildAttributeAlignment;
HandlebarsTagBlock(@NotNull ASTNode node,
Wrap wrap,
Alignment alignment,
@NotNull TemplateLanguageBlockFactory blockFactory,
@NotNull CodeStyleSettings settings,
@Nullable List<DataLanguageBlockWrapper> foreignChildren,
HtmlPolicy htmlPolicy) {
super(node, wrap, alignment, blockFactory, settings, foreignChildren, htmlPolicy);
myChildAttributeAlignment = Alignment.createAlignment();
}
@NotNull
@Override
public ChildAttributes getChildAttributes(int newChildIndex) {
if (newChildIndex > 0) {
List<Block> blocks = getSubBlocks();
if (blocks.size() > newChildIndex - 1) {
Block prevBlock = blocks.get(newChildIndex - 1);
if (prevBlock instanceof AbstractBlock) {
ASTNode node = ((AbstractBlock)prevBlock).getNode();
if (isAttribute(node) ||
node.getElementType() == HbTokenTypes.MUSTACHE_NAME) {
return new ChildAttributes(null, prevBlock.getAlignment());
}
}
}
}
return super.getChildAttributes(newChildIndex);
}
@Override
protected Alignment createChildAlignment(ASTNode child) {
if (isAttribute(child)) {
return myChildAttributeAlignment;
}
return super.createChildAlignment(child);
}
@Override
protected Wrap createChildWrap(ASTNode child) {
if (isAttribute(child)) {
return Wrap.createWrap(getWrapType(myHtmlPolicy.getAttributesWrap()), false);
}
return null;
}
}
private static boolean isAttribute(ASTNode child) {
IElementType type = child.getElementType();
return type == HbTokenTypes.PARAM || type == HbTokenTypes.HASH;
}
private static class HandlebarsBlock extends TemplateLanguageBlock {
@NotNull
protected final HtmlPolicy myHtmlPolicy;
HandlebarsBlock(@NotNull ASTNode node,
Wrap wrap,
Alignment alignment,
@NotNull TemplateLanguageBlockFactory blockFactory,
@NotNull CodeStyleSettings settings,
@Nullable List<DataLanguageBlockWrapper> foreignChildren,
@NotNull HtmlPolicy htmlPolicy) {
super(node, wrap, alignment, blockFactory, settings, foreignChildren);
myHtmlPolicy = htmlPolicy;
}
/**
* We indented the code in the following manner, playing nice with the formatting from the language
* we're templating:
* <pre>
* * Block expressions:
* {{#foo}}
* INDENTED_CONTENT
* {{/foo}}
* * Inverse block expressions:
* {{^bar}}
* INDENTED_CONTENT
* {{/bar}}
* * Conditional expressions using the "else" syntax:
* {{#if test}}
* INDENTED_CONTENT
* {{else}}
* INDENTED_CONTENT
* {{/if}}
* * Conditional expressions using the "^" syntax:
* {{#if test}}
* INDENTED_CONTENT
* {{^}}
* INDENTED_CONTENT
* {{/if}}
* </pre>
* <p/>
* This naturally maps to any "statements" expression in the grammar which is not a child of the
* root "program" element. See {@link com.dmarcotte.handlebars.parsing.HbParsing#parseProgram} and
* {@link com.dmarcotte.handlebars.parsing.HbParsing#parseStatement(com.intellij.lang.PsiBuilder)} for the
* relevant parts of the parser.
* <p/>
* To understand the approach in this method, consider the following:
* <pre>
* {{#foo}}
* BEGIN_STATEMENTS
* TEMPLATE_STUFF
* END_STATEMENTS
* {{/foo}}
* </pre>
* <p/>
* then formatting looks easy. Simply apply an indent (represented here by "[hb_indent]") to the STATEMENTS and call it a day:
* <pre>
* {{#foo}}
* [hb_indent]BEGIN_STATEMENTS
* [hb_indent]TEMPLATE_STUFF
* [hb_indent]END_STATEMENTS
* {{/foo}}
* </pre>
* <p/>
* However, if we're contained in templated language block, it's going to provide some indents of its own
* (call them "[tl_indent]") which quickly leads to undesirable double-indenting:
* <p/>
* <pre>
* <div>
* [tl_indent]{{#foo}}
* [hb_indent]BEGIN_STATEMENTS
* [tl_indent][hb_indent]TEMPLATE_STUFF
* [hb_indent]END_STATEMENTS
* [tl_indent]{{/foo}}
* </div>
* </pre>
* So to behave correctly in both situations, we indent STATEMENTS from the "outside" anytime we're not wrapped
* in a templated language block, and we indent STATEMENTS from the "inside" (i.e. apply an indent to each non-template
* language STATEMENT inside the STATEMENTS) to interleave nicely with templated-language provided indents.
*/
@Override
public Indent getIndent() {
// ignore whitespace
if (myNode.getText().trim().length() == 0) {
return Indent.getNoneIndent();
}
if (isAttribute(myNode)) {
return null;
}
if (HbPsiUtil.isNonRootStatementsElement(myNode.getPsi())) {
// we're computing the indent for a non-root STATEMENTS:
// if it's not contained in a foreign block, indent!
DataLanguageBlockWrapper foreignBlockParent = getForeignBlockParent(false);
if (foreignBlockParent == null) {
return Indent.getNormalIndent();
}
// otherwise, only indent if our foreign parent isn't indenting us
if (foreignBlockParent.getNode() instanceof XmlTag) {
XmlTag xmlTag = (XmlTag)foreignBlockParent.getNode();
if (!myHtmlPolicy.indentChildrenOf(xmlTag)) {
// no indent from xml parent, add our own
return Indent.getNormalIndent();
}
}
return Indent.getNoneIndent();
}
if (myNode.getTreeParent() != null
&& HbPsiUtil.isNonRootStatementsElement(myNode.getTreeParent().getPsi())) {
// we're computing the indent for a direct descendant of a non-root STATEMENTS:
// if its Block parent (i.e. not HB AST Tree parent) is a Handlebars block
// which has NOT been indented, then have the element provide the indent itself
if (getParent() instanceof HandlebarsBlock
&& ((HandlebarsBlock)getParent()).getIndent() == Indent.getNoneIndent()) {
return Indent.getNormalIndent();
}
}
// any element that is the direct descendant of a foreign block gets an indent
// (unless that foreign element has been configured to not indent its children)
DataLanguageBlockWrapper foreignParent = getForeignBlockParent(true);
if (foreignParent != null) {
if (foreignParent.getNode() instanceof XmlTag
&& !myHtmlPolicy.indentChildrenOf((XmlTag)foreignParent.getNode())) {
return Indent.getNoneIndent();
}
return Indent.getNormalIndent();
}
return Indent.getNoneIndent();
}
@Override
protected IElementType getTemplateTextElementType() {
// we ignore CONTENT tokens since they get formatted by the templated language
return HbTokenTypes.CONTENT;
}
@Override
public boolean isRequiredRange(TextRange range) {
// seems our approach doesn't require us to insert any custom DataLanguageBlockFragmentWrapper blocks
return false;
}
/**
* <p/>
* This method handles indent and alignment on Enter.
*/
@NotNull
@Override
public ChildAttributes getChildAttributes(int newChildIndex) {
/*
* We indent if we're in a BLOCK_WRAPPER (note that this works nicely since Enter can only be invoked
* INSIDE a block (i.e. after the open block 'stache).
*
* Also indent if we are wrapped in a block created by the templated language
*/
if (myNode.getElementType() == HbTokenTypes.BLOCK_WRAPPER
|| (getParent() instanceof DataLanguageBlockWrapper
// hack alert: the following check opportunistically fixes com.dmarcotte.handlebars.format.HbFormatOnEnterTest#testSimpleBlockInDiv8
// and com.dmarcotte.handlebars.format.HbFormatOnEnterTest#testSimpleBlockInDiv8
// but isn't really based on solid logic (why do these checks work?), so when there's inevitably a
// format-on-enter bug, this is the first bit of code to be suspicious of
&&
(myNode.getElementType() != HbTokenTypes.STATEMENTS || myNode.getTreeNext() instanceof PsiErrorElement))) {
return new ChildAttributes(Indent.getNormalIndent(), null);
}
return new ChildAttributes(Indent.getNoneIndent(), null);
}
/**
* Returns this block's first "real" foreign block parent if it exists, and null otherwise. (By "real" here, we mean that this method
* skips SyntheticBlock blocks inserted by the template formatter)
*
* @param immediate Pass true to only check for an immediate foreign parent, false to look up the hierarchy.
*/
private DataLanguageBlockWrapper getForeignBlockParent(boolean immediate) {
DataLanguageBlockWrapper foreignBlockParent = null;
BlockWithParent parent = getParent();
while (parent != null) {
if (parent instanceof DataLanguageBlockWrapper && !(((DataLanguageBlockWrapper)parent).getOriginal() instanceof SyntheticBlock)) {
foreignBlockParent = (DataLanguageBlockWrapper)parent;
break;
}
else if (immediate && parent instanceof HandlebarsBlock) {
break;
}
parent = parent.getParent();
}
return foreignBlockParent;
}
}
}