Permalink
Browse files

Enhance mismatched block 'stache errors

Previously we only highlighted the close mustache in the case of a
mismatch, and we did not provide a quick fix (which users probably
expect if they are used to the XML/HTML editing capabilities).

Move the mismatch detection out of the parser and into an Annotation
object (and "Inspection") and add quick fixes for making the names
match.

Also smarten up the check to cover complex ids.  Previously we were only
ensuring that "foo" matched in "{{#foo.bar}}{{/foo.baz}}" and hence
missing errors in cases like these.
  • Loading branch information...
1 parent dd40a10 commit 31b2989296832f18b116879e040536b45b493c5b @dmarcotte committed Apr 6, 2013
Showing with 347 additions and 105 deletions.
  1. +1 −0 META-INF/plugin.xml
  2. +7 −3 resources/messages/HbBundle.properties
  3. +102 −0 src/com/dmarcotte/handlebars/inspections/HbBlockMismatchFix.java
  4. +58 −0 src/com/dmarcotte/handlebars/inspections/HbBlockMismatchInspection.java
  5. +12 −67 src/com/dmarcotte/handlebars/parsing/HbParsing.java
  6. +29 −0 src/com/dmarcotte/handlebars/psi/HbBlockMustache.java
  7. +4 −1 src/com/dmarcotte/handlebars/psi/HbCloseBlockMustache.java
  8. +4 −1 src/com/dmarcotte/handlebars/psi/HbOpenBlockMustache.java
  9. +27 −0 src/com/dmarcotte/handlebars/psi/impl/HbBlockMustacheImpl.java
  10. +13 −1 src/com/dmarcotte/handlebars/psi/impl/HbCloseBlockMustacheImpl.java
  11. +10 −6 src/com/dmarcotte/handlebars/psi/impl/HbOpenBlockMustacheImpl.java
  12. +2 −3 test/data/folding/foldsWithUnclosedBlocks.hbs
  13. +4 −0 test/data/inspections/afterWrongCloseBlock1.hbs
  14. +4 −0 test/data/inspections/afterWrongCloseBlock2.hbs
  15. +4 −0 test/data/inspections/afterWrongOpenBlock1.hbs
  16. +4 −0 test/data/inspections/afterWrongOpenBlock2.hbs
  17. +4 −0 test/data/inspections/beforeWrongCloseBlock1.hbs
  18. +4 −0 test/data/inspections/beforeWrongCloseBlock2.hbs
  19. +4 −0 test/data/inspections/beforeWrongOpenBlock1.hbs
  20. +4 −0 test/data/inspections/beforeWrongOpenBlock2.hbs
  21. +8 −9 test/data/parser/CloseNotFollowingOpen.txt
  22. +14 −12 test/data/parser/PoorlyNestedMustaches.txt
  23. +3 −2 test/src/com/dmarcotte/handlebars/editor/actions/HbEnterHandlerTest.java
  24. +21 −0 test/src/com/dmarcotte/handlebars/inspections/HbBlockMismatchFixTest.java
View
@@ -224,5 +224,6 @@
<codeFoldingOptionsProvider
instance="com.dmarcotte.handlebars.config.HbFoldingOptionsProvider" />
<lang.psiStructureViewFactory language="Handlebars" implementationClass="com.dmarcotte.handlebars.structure.HbStructureViewFactory"/>
+ <annotator language="Handlebars" implementationClass="com.dmarcotte.handlebars.inspections.HbBlockMismatchInspection"/>
</extensions>
</idea-plugin>
@@ -10,8 +10,6 @@ hb.page.colors.descriptor.data.key=Data
hb.page.colors.descriptor.escape.key=Escape Character
hb.parsing.no.open.mustache=No corresponding open mustache
hb.parsing.invalid=Invalid
-hb.parsing.block.not.closed="{0}" block not closed
-hb.parsing.end.tag.bad.match="{0}" does not match "{1}" from block start
hb.parsing.expected.path.or.data=Expected a path or @data
hb.parsing.expected.parameter=Expected a parameter
hb.parsing.expected.hash=Expected a hash
@@ -41,4 +39,10 @@ hb.pages.options.title=Handlebars/Mustache
hb.pages.folding.auto.collapse.blocks=Handlebars/Mustache blocks
hb.page.options.commenter.language=&Language for comments\:
hb.page.options.commenter.language.tooltip=Controls which language's comment syntax to use for "Comment with Block Comment" and "Comment with Line Comment" actions
-hb.parsing.comment.unclosed=Unclosed comment
+hb.parsing.comment.unclosed=Unclosed comment
+hb.block.mismatch.intention.rename.open=Change block start ''{0}'' to ''{1}''
+hb.block.mismatch.intention.rename.close=Change block end ''{0}'' to ''{1}''
+hb.block.mismatch.inspection.open.block=''{0}'' does not match ''{1}'' from block end
+hb.block.mismatch.inspection.close.block=''{1}'' does not match ''{0}'' from block start
+hb.block.mismatch.inspection.missing.end.block=''{0}'' block not closed
+hb.block.mismatch.inspection.missing.start.block=No block start for ''{0}''
@@ -0,0 +1,102 @@
+package com.dmarcotte.handlebars.inspections;
+
+import com.dmarcotte.handlebars.HbBundle;
+import com.dmarcotte.handlebars.psi.HbBlockMustache;
+import com.dmarcotte.handlebars.psi.HbOpenBlockMustache;
+import com.dmarcotte.handlebars.psi.HbPath;
+import com.intellij.codeInsight.CodeInsightUtilBase;
+import com.intellij.codeInsight.intention.IntentionAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+
+class HbBlockMismatchFix implements IntentionAction {
+ private final boolean myUpdateOpenMustache;
+ private final String myCorrectedName;
+ private final String myOriginalName;
+
+ /**
+ * @param correctedName The name this action will update a block element to
+ * @param originalName The original name of the element this action corrects
+ * @param updateOpenMustache Whether or not this updates the open mustache of this block
+ */
+ public HbBlockMismatchFix(String correctedName, String originalName, boolean updateOpenMustache) {
+ myUpdateOpenMustache = updateOpenMustache;
+ myCorrectedName = correctedName;
+ myOriginalName = originalName;
+ }
+
+ @NotNull
+ @Override
+ public String getText() {
+ return getName();
+ }
+
+ @NotNull
+ @Override
+ public String getFamilyName() {
+ return getName();
+ }
+
+ @Override
+ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
+ return true;
+ }
+
+ @Override
+ public void invoke(@NotNull Project project, Editor editor, PsiFile file)
+ throws IncorrectOperationException {
+ final int offset = editor.getCaretModel().getOffset();
+ PsiElement psiElement = file.findElementAt(offset);
+
+ if (psiElement == null || !psiElement.isValid()) return;
+ if (!CodeInsightUtilBase.prepareFileForWrite(psiElement.getContainingFile())) return;
+
+ if (psiElement instanceof PsiWhiteSpace) psiElement = PsiTreeUtil.prevLeaf(psiElement);
+
+ HbBlockMustache blockMustache = (HbBlockMustache) PsiTreeUtil.findFirstParent(psiElement, true, new Condition<PsiElement>() {
+ @Override
+ public boolean value(PsiElement psiElement) {
+ return psiElement instanceof HbBlockMustache;
+ }
+ });
+
+ if (blockMustache == null) {
+ return;
+ }
+
+ HbBlockMustache targetBlockMustache = blockMustache;
+
+ // ensure we update the open or close mustache for this block appropriately
+ if (myUpdateOpenMustache != (targetBlockMustache instanceof HbOpenBlockMustache)) {
+ targetBlockMustache = blockMustache.getPairedElement();
+ }
+
+ HbPath path = PsiTreeUtil.findChildOfType(targetBlockMustache, HbPath.class);
+ final Document document = PsiDocumentManager.getInstance(project).getDocument(file);
+ if (path != null && document != null) {
+ final TextRange textRange = path.getTextRange();
+ document.replaceString(textRange.getStartOffset(), textRange.getEndOffset(), myCorrectedName);
+ }
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return true;
+ }
+
+ private String getName() {
+ return myUpdateOpenMustache
+ ? HbBundle.message("hb.block.mismatch.intention.rename.open", myOriginalName, myCorrectedName)
+ : HbBundle.message("hb.block.mismatch.intention.rename.close", myOriginalName, myCorrectedName);
+ }
+}
@@ -0,0 +1,58 @@
+package com.dmarcotte.handlebars.inspections;
+
+import com.dmarcotte.handlebars.HbBundle;
+import com.dmarcotte.handlebars.psi.HbCloseBlockMustache;
+import com.dmarcotte.handlebars.psi.HbOpenBlockMustache;
+import com.dmarcotte.handlebars.psi.HbPath;
+import com.intellij.lang.annotation.Annotation;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+public class HbBlockMismatchInspection implements Annotator {
+ @Override
+ public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) {
+ if (element instanceof HbOpenBlockMustache) {
+ HbOpenBlockMustache openBlockMustache = (HbOpenBlockMustache) element;
+ HbPath openBlockMainPath = openBlockMustache.getBlockMainPath();
+
+ HbCloseBlockMustache closeBlockMustache = openBlockMustache.getPairedElement();
+ if (closeBlockMustache != null) {
+ HbPath closeBlockMainPath = closeBlockMustache.getBlockMainPath();
+
+ if (openBlockMainPath == null || closeBlockMainPath == null) {
+ return;
+ }
+
+ String openBlockName = openBlockMainPath.getName();
+ String closeBlockName = closeBlockMainPath.getName();
+ if (!openBlockName.equals(closeBlockName)) {
+ Annotation openBlockAnnotation
+ = holder.createErrorAnnotation(openBlockMainPath, HbBundle.message("hb.block.mismatch.inspection.open.block", openBlockName, closeBlockName));
+ openBlockAnnotation.registerFix(new HbBlockMismatchFix(closeBlockName, openBlockName, true));
+ openBlockAnnotation.registerFix(new HbBlockMismatchFix(openBlockName, closeBlockName, false));
+
+ Annotation closeBlockAnnotation
+ = holder.createErrorAnnotation(closeBlockMainPath, HbBundle.message("hb.block.mismatch.inspection.close.block", openBlockName, closeBlockName));
+ closeBlockAnnotation.registerFix(new HbBlockMismatchFix(openBlockName, closeBlockName, false));
+ closeBlockAnnotation.registerFix(new HbBlockMismatchFix(closeBlockName, openBlockName, true));
+ }
+ } else {
+ holder.createErrorAnnotation(openBlockMainPath, HbBundle.message("hb.block.mismatch.inspection.missing.end.block", openBlockMustache.getName()));
+ }
+ }
+
+ if (element instanceof HbCloseBlockMustache) {
+ HbCloseBlockMustache closeBlockMustache = (HbCloseBlockMustache) element;
+ PsiElement openBlockElement = closeBlockMustache.getPairedElement();
+ if (openBlockElement == null) {
+ HbPath closeBlockMainPath = closeBlockMustache.getBlockMainPath();
+ if (closeBlockMainPath == null) {
+ return;
+ }
+ holder.createErrorAnnotation(closeBlockMainPath, HbBundle.message("hb.block.mismatch.inspection.missing.start.block", closeBlockMustache.getName()));
+ }
+ }
+ }
+}
@@ -4,7 +4,6 @@
import com.dmarcotte.handlebars.exception.ShouldNotHappenException;
import com.intellij.lang.PsiBuilder;
import com.intellij.psi.tree.IElementType;
-import com.intellij.util.containers.Stack;
import java.util.HashSet;
import java.util.Set;
@@ -34,12 +33,12 @@
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.PARAM;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.PARTIAL_NAME;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.PARTIAL_STACHE;
+import static com.dmarcotte.handlebars.parsing.HbTokenTypes.PATH;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.SEP;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.SIMPLE_INVERSE;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.STATEMENTS;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.STRING;
import static com.dmarcotte.handlebars.parsing.HbTokenTypes.UNCLOSED_COMMENT;
-import static com.dmarcotte.handlebars.parsing.HbTokenTypes.PATH;
/**
* The parser is based directly on Handlebars.yy
@@ -52,7 +51,6 @@
*/
class HbParsing {
private final PsiBuilder builder;
- private final Stack<String> openTagNamesStack = new Stack<String>();
// the set of tokens which, if we encounter them while in a bad state, we'll try to
// resume parsing from them
@@ -84,9 +82,7 @@ public void parse() {
int problemOffset = builder.getCurrentOffset();
if (tokenType == OPEN_ENDBLOCK) {
- PsiBuilder.Marker badEndBlockMarker = builder.mark();
parseCloseBlock(builder);
- badEndBlockMarker.error(HbBundle.message("hb.parsing.no.open.mustache"));
}
if (builder.getCurrentOffset() == problemOffset) {
@@ -174,9 +170,8 @@ private boolean parseStatement(PsiBuilder builder) {
}
PsiBuilder.Marker blockMarker = builder.mark();
- PsiBuilder.Marker openInverseMarker = builder.mark();
if (parseOpenInverse(builder)) {
- openBlockMarker(builder, openInverseMarker, blockMarker);
+ parseRestOfBlock(builder, blockMarker);
} else {
return false;
}
@@ -186,9 +181,8 @@ private boolean parseStatement(PsiBuilder builder) {
if (tokenType == OPEN_BLOCK) {
PsiBuilder.Marker blockMarker = builder.mark();
- PsiBuilder.Marker openBlockMarker = builder.mark();
if (parseOpenBlock(builder)) {
- openBlockMarker(builder, openBlockMarker, blockMarker);
+ parseRestOfBlock(builder, blockMarker);
} else {
return false;
}
@@ -233,25 +227,13 @@ private boolean parseStatement(PsiBuilder builder) {
}
/**
- * Helper method to take care of the business need after an "open-type mustache" (openBlock or openInverse),
- * including ensuring we've got the right close tag
+ * Helper method to take care of the business needed after an "open-type mustache" (openBlock or openInverse)
*
- * NOTE: will resolve the given openMustacheMarker
+ * NOTE: will resolve the given blockMarker
*/
- private void openBlockMarker(PsiBuilder builder, PsiBuilder.Marker openMustacheMarker, PsiBuilder.Marker blockMarker) {
- PsiBuilder.Marker parseProgramMarker = builder.mark();
+ private void parseRestOfBlock(PsiBuilder builder, PsiBuilder.Marker blockMarker) {
parseProgram(builder);
- if(parseCloseBlock(builder)) {
- openMustacheMarker.drop();
- } else {
- if (!openTagNamesStack.empty()) {
- openMustacheMarker.errorBefore(HbBundle.message("hb.parsing.block.not.closed", openTagNamesStack.pop()), parseProgramMarker);
- } else {
- openMustacheMarker.drop();
- }
- }
- parseProgramMarker.drop();
-
+ parseCloseBlock(builder);
blockMarker.done(HbTokenTypes.BLOCK_WRAPPER);
}
@@ -267,7 +249,7 @@ private boolean parseOpenBlock(PsiBuilder builder) {
return false;
}
- if (parseInMustache(builder, true)) {
+ if (parseInMustache(builder)) {
parseLeafTokenGreedy(builder, CLOSE);
}
@@ -297,7 +279,7 @@ private boolean parseOpenInverse(PsiBuilder builder) {
regularInverseMarker.drop();
}
- if(parseInMustache(builder, true)) {
+ if(parseInMustache(builder)) {
parseLeafTokenGreedy(builder, CLOSE);
}
@@ -318,26 +300,6 @@ private boolean parseCloseBlock(PsiBuilder builder) {
return false;
}
- // HB_CUSTOMIZATION: we store open/close IDs to detect mismatches. Note that if the current token is not
- // an id, we do nothing: the actual parser takes care of detecting the problem
- if (builder.getTokenType() == HbTokenTypes.ID && !openTagNamesStack.empty()) {
- String expectedCloseTag = openTagNamesStack.pop();
- String actualCloseTag = builder.getTokenText();
- if (!expectedCloseTag.equals(actualCloseTag)) {
- // advance all the way to a recovery token or the close stache for this open block 'stache
- while (builder.getTokenType() != CLOSE && !RECOVERY_SET.contains(builder.getTokenType()) && !builder.eof()) {
- builder.advanceLexer();
- }
-
- if (builder.getTokenType() == CLOSE) {
- builder.advanceLexer();
- }
- closeBlockMarker.error(
- HbBundle.message("hb.parsing.end.tag.bad.match", actualCloseTag, expectedCloseTag));
- return true;
- }
- }
-
if(parsePath(builder)) {
parseLeafToken(builder, CLOSE);
}
@@ -362,7 +324,7 @@ private void parseMustache(PsiBuilder builder) {
throw new ShouldNotHappenException();
}
- parseInMustache(builder, false);
+ parseInMustache(builder);
// whether our parseInMustache hit trouble or not, we absolutely must have
// a CLOSE token, so let's find it
parseLeafTokenGreedy(builder, CLOSE);
@@ -446,22 +408,11 @@ private boolean parseSimpleInverse(PsiBuilder builder) {
* | path { $$ = [[$1], null]; }
* | DATA { $$ = [[new yy.DataNode($1)], null]; }
* ;
- *
- * @param hasOpenTag is used to tell this method that the first ID in this 'stache is the open
- * tag of a block (this method stores it so that we can compare to the close tag later)
*/
- private boolean parseInMustache(PsiBuilder builder, boolean hasOpenTag) {
+ private boolean parseInMustache(PsiBuilder builder) {
PsiBuilder.Marker inMustacheMarker = builder.mark();
- // HB_CUSTOMIZATION: we store open/close IDs to detect mismatches. Note that if the current token is not
- // an id, we do nothing: the actual parser takes care of detecting the problem
- if (hasOpenTag && builder.getTokenType() == HbTokenTypes.ID) {
- openTagNamesStack.push(builder.getTokenText());
- }
-
- PsiBuilder.Marker pathMarker = builder.mark();
if (!parsePath(builder)) {
- pathMarker.rollbackTo();
// not a path, try to parse DATA
if (builder.getTokenType() == DATA_PREFIX
&& parseLeafToken(builder, DATA_PREFIX)
@@ -472,8 +423,6 @@ private boolean parseInMustache(PsiBuilder builder, boolean hasOpenTag) {
inMustacheMarker.error(HbBundle.message("hb.parsing.expected.path.or.data"));
return false;
}
- } else {
- pathMarker.drop();
}
// try to extend the 'path' we found to 'path hash'
@@ -550,13 +499,9 @@ private boolean parseParams(PsiBuilder builder) {
private boolean parseParam(PsiBuilder builder) {
PsiBuilder.Marker paramMarker = builder.mark();
- PsiBuilder.Marker pathMarker = builder.mark();
if (parsePath(builder)) {
- pathMarker.drop();
paramMarker.done(PARAM);
return true;
- } else {
- pathMarker.rollbackTo();
}
PsiBuilder.Marker stringMarker = builder.mark();
@@ -675,7 +620,7 @@ private boolean parsePath(PsiBuilder builder) {
pathMarker.done(PATH);
return true;
}
- pathMarker.drop();
+ pathMarker.rollbackTo();
return false;
}
Oops, something went wrong.

0 comments on commit 31b2989

Please sign in to comment.