Created
November 8, 2012 16:21
-
-
Save dmarcotte/4039840 to your computer and use it in GitHub Desktop.
Example of a standalone IDEA formatter for a templating language
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.dmarcotte.handlebars.format; | |
import com.dmarcotte.handlebars.HbLanguage; | |
import com.dmarcotte.handlebars.parsing.HbTokenTypes; | |
import com.intellij.formatting.Alignment; | |
import com.intellij.formatting.Block; | |
import com.intellij.formatting.ChildAttributes; | |
import com.intellij.formatting.FormattingModel; | |
import com.intellij.formatting.FormattingModelBuilder; | |
import com.intellij.formatting.FormattingModelProvider; | |
import com.intellij.formatting.Indent; | |
import com.intellij.formatting.Spacing; | |
import com.intellij.formatting.Wrap; | |
import com.intellij.lang.ASTNode; | |
import com.intellij.openapi.util.TextRange; | |
import com.intellij.psi.PsiElement; | |
import com.intellij.psi.PsiFile; | |
import com.intellij.psi.codeStyle.CodeStyleSettings; | |
import com.intellij.psi.formatter.common.AbstractBlock; | |
import org.jetbrains.annotations.NotNull; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.List; | |
/** | |
* A formatter for Handlebars/Mustache files which ignores the templated content, only formatting the 'stache-y parts. | |
* This is what gets evolved into a full template-aware formatter | |
*/ | |
public class HbStandaloneFormattingBuilder implements FormattingModelBuilder { | |
@NotNull | |
@Override | |
public FormattingModel createModel(PsiElement element, CodeStyleSettings settings) { | |
PsiFile containingFile = element.getContainingFile().getViewProvider().getPsi(HbLanguage.INSTANCE); | |
return FormattingModelProvider | |
.createFormattingModelForPsiFile( | |
containingFile, | |
new HandlebarsBlock(element.getNode(), settings), | |
settings); | |
} | |
@Override | |
public TextRange getRangeAffectingIndent(PsiFile file, int offset, ASTNode elementAtOffset) { | |
return null; // todo this doens't seem to be needed... must take care of a special case. Look into it. | |
} | |
private static class HandlebarsBlock extends AbstractBlock { | |
ASTNode myNode; | |
CodeStyleSettings myStyleSettings; // todo consult these settings for the user's preferred formatting | |
private HandlebarsBlock(ASTNode node, CodeStyleSettings styleSettings) { | |
super(node, null, null); | |
this.myNode = node; | |
this.myStyleSettings = styleSettings; | |
} | |
/** | |
* Instantiates blocks for each of this node's children. | |
* | |
* Seems it's fairly common to do indent and alignment calculation in this method and pass those values | |
* to the Block constructor (which makes sense, since your indent or alignment could very well be determined by | |
* your siblings as much as your parent). | |
* | |
* For this simple formatter, where we can fully determine indent for a given node, we simple iterate over | |
* the children creating our blocks without any other computations (note that we skip whitespace; the | |
* formatter behaves REALLY weird if you don't...) | |
*/ | |
@Override | |
protected List<Block> buildChildren() { | |
if (isLeaf()) { | |
return Collections.emptyList(); | |
} | |
ArrayList<Block> childBlocks = new ArrayList<Block>(); | |
for (ASTNode childNode : myNode.getChildren(null)) { | |
if (childNode.getText().trim().length() == 0) { | |
continue; | |
} | |
childBlocks.add(new HandlebarsBlock(childNode, myStyleSettings)); | |
} | |
return childBlocks; | |
} | |
/** | |
* This method is supposed to return the range covered by this Formatting Block. Since our blocks | |
* map so well to our PSI tree, there's no need to do anything special here. | |
*/ | |
@NotNull | |
@Override | |
public TextRange getTextRange() { | |
return myNode.getTextRange(); | |
} | |
@Override | |
public Wrap getWrap() { | |
return null; // todo implement some wrappin' | |
} | |
/** | |
* We indented the code in the following manner: | |
* * block expressions: | |
* {{#foo}} | |
* INDENTED_CONTENET | |
* {{/foo}} | |
* * inverse block expressions: | |
* {{^bar}} | |
* INDENTED_CONTENT | |
* {{/bar}} | |
* * conditional expressions use the "else" syntax: | |
* {{#if test}} | |
* INDENTED_CONTENT | |
* {{else}} | |
* INDENTED_CONTENT | |
* {{/if}} | |
* * conditional expressions use the "^" syntax: | |
* | |
* {{#if test}} | |
* INDENTED_CONTENT | |
* {{^}} | |
* INDENTED_CONTENT | |
* {{/if}} | |
* | |
* 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. | |
*/ | |
@Override | |
public Indent getIndent() { | |
if (myNode.getText().trim().length() == 0) { | |
return null; | |
} | |
Indent indent; | |
// we indent if this element corresponds to a non-root "statements" expression | |
if (myNode.getElementType() == HbTokenTypes.STATEMENTS | |
&& myNode.getTreeParent().getElementType() != HbTokenTypes.FILE) { | |
indent = Indent.getNormalIndent(); // todo should we pass true for relativeToDirectParent? | |
} else { | |
indent = Indent.getNoneIndent(); | |
} | |
return indent; | |
} | |
@Override | |
public Alignment getAlignment() { | |
return null; // todo implement some alignin' | |
} | |
@Override | |
public Spacing getSpacing(Block child1, Block child2) { | |
return null; // todo implement some spacin' | |
} | |
/** | |
* This method handles indent and alignment on Enter keypresses. | |
* | |
* This method is called on the parent block of the new block which is being created on | |
* keypress, and 'newChildIndex' seems to be the index where the new block will be inserted in | |
* the list of that parent's sub blocks | |
* | |
* For this naive implementation, it is sufficient to simply return a ChildAttributes which contains | |
* the parent block's indent. | |
* TODO if/when we implement alignment, update this method to do alignment properly | |
*/ | |
@NotNull | |
@Override | |
public ChildAttributes getChildAttributes(int newChildIndex) { | |
return new ChildAttributes(getIndent(), null); | |
} | |
@Override | |
public boolean isIncomplete() { | |
return false; // todo dunno what this means... | |
} | |
@Override | |
public boolean isLeaf() { | |
return myNode.getFirstChildNode() == null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment