From d6457a3bb45d9d08e5f7a190cf67ff716d29a860 Mon Sep 17 00:00:00 2001 From: azerr Date: Sat, 29 Jun 2024 09:46:22 +0200 Subject: [PATCH] fix: Formatting is broken when using simple {} Fixes #1345 Signed-off-by: azerr --- .../QuteHtmlFormattingModelBuilder.java | 232 +++++++++--------- .../qute/lang/psi/QuteLexerForStartTag.java | 2 - .../intellij/qute/lang/psi/QuteParsing.java | 39 +-- .../template/QuarkusIntegrationForQute.java | 3 - .../qute/lang/psi/QuteParsingTestCase.java | 50 ++++ testData/qute/psi/HelloWorld.txt | 22 ++ testData/qute/psi/SectionWithHtml.txt | 43 ++++ testData/qute/psi/SimpleText.txt | 4 + 8 files changed, 256 insertions(+), 139 deletions(-) create mode 100644 src/test/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsingTestCase.java create mode 100644 testData/qute/psi/HelloWorld.txt create mode 100644 testData/qute/psi/SectionWithHtml.txt create mode 100644 testData/qute/psi/SimpleText.txt diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lang/format/QuteHtmlFormattingModelBuilder.java b/src/main/java/com/redhat/devtools/intellij/qute/lang/format/QuteHtmlFormattingModelBuilder.java index 8c34fe087..ca852ceeb 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lang/format/QuteHtmlFormattingModelBuilder.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lang/format/QuteHtmlFormattingModelBuilder.java @@ -26,142 +26,140 @@ /** * Template aware formatter which provides formatting for Qute syntax and delegates formatting * for the templated language to that languages formatter - * + *

* This class is a copy/paste from https://github.com/JetBrains/intellij-plugins/blob/master/Qute/src/com/dmarcotte/Qute/format/HbFormattingModelBuilder.java adapted for Qute. */ public class QuteHtmlFormattingModelBuilder extends TemplateLanguageFormattingModelBuilder { - @Override - public TemplateLanguageBlock createTemplateLanguageBlock(@NotNull ASTNode node, - @Nullable Wrap wrap, - @Nullable Alignment alignment, - @Nullable List foreignChildren, - @NotNull CodeStyleSettings codeStyleSettings) { - final FormattingDocumentModelImpl documentModel = FormattingDocumentModelImpl.createOn(node.getPsi().getContainingFile()); - HtmlPolicy policy = new HtmlPolicy(codeStyleSettings, documentModel); - return new QuteBlock(node, wrap, alignment, this, codeStyleSettings, foreignChildren, policy); - } - - /** - * We have to override {@link 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 a stack-overflowing loop of polite format-delegation. - */ - @Override - public @NotNull FormattingModel createModel(@NotNull FormattingContext formattingContext) { - final PsiFile file = formattingContext.getContainingFile(); - Block rootBlock; - - ASTNode node = formattingContext.getNode(); - - if (node.getElementType() == QuteElementTypes.QUTE_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(formattingContext); - } - else { - rootBlock = getRootBlock(file, file.getViewProvider(), formattingContext.getCodeStyleSettings()); - } - return new DocumentBasedFormattingModel( - rootBlock, formattingContext.getProject(), formattingContext.getCodeStyleSettings(), file.getFileType(), file); - } - - /** - * Do format my model! - * - * @return false all the time to tell the {@link TemplateLanguageFormattingModelBuilder} - * to not-not format our model (i.e. yes please! Format away!) - */ - @Override - public boolean dontFormatMyModel() { - return false; - } - - private static class QuteBlock extends TemplateLanguageBlock { - - @NotNull - protected final HtmlPolicy myHtmlPolicy; - - - QuteBlock(@NotNull ASTNode node, - Wrap wrap, - Alignment alignment, - @NotNull TemplateLanguageBlockFactory blockFactory, - @NotNull CodeStyleSettings settings, - @Nullable List foreignChildren, - @NotNull HtmlPolicy htmlPolicy) { - super(node, wrap, alignment, blockFactory, settings, foreignChildren); - myHtmlPolicy = htmlPolicy; - } - - @Override - public Indent getIndent() { - // ignore whitespace - if (myNode.getText().trim().isEmpty()) { - return Indent.getNoneIndent(); - } - - // 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 QuteElementTypes.QUTE_TEXT; + public TemplateLanguageBlock createTemplateLanguageBlock(@NotNull ASTNode node, + @Nullable Wrap wrap, + @Nullable Alignment alignment, + @Nullable List foreignChildren, + @NotNull CodeStyleSettings codeStyleSettings) { + final FormattingDocumentModelImpl documentModel = FormattingDocumentModelImpl.createOn(node.getPsi().getContainingFile()); + HtmlPolicy policy = new HtmlPolicy(codeStyleSettings, documentModel); + return new QuteBlock(node, wrap, alignment, this, codeStyleSettings, foreignChildren, policy); } + /** + * We have to override {@link 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 a stack-overflowing loop of polite format-delegation. + */ @Override - public boolean isRequiredRange(TextRange range) { - // seems our approach doesn't require us to insert any custom DataLanguageBlockFragmentWrapper blocks - return false; + public @NotNull FormattingModel createModel(@NotNull FormattingContext formattingContext) { + final PsiFile file = formattingContext.getContainingFile(); + Block rootBlock; + + ASTNode node = formattingContext.getNode(); + + if (node.getElementType() == QuteElementTypes.QUTE_OUTER_ELEMENT_TYPE) { + // If we're looking at a QuteElementTypes.QUTE_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(formattingContext); + } else { + rootBlock = getRootBlock(file, file.getViewProvider(), formattingContext.getCodeStyleSettings()); + } + return new DocumentBasedFormattingModel( + rootBlock, formattingContext.getProject(), formattingContext.getCodeStyleSettings(), file.getFileType(), file); } /** - *

- * This method handles indent and alignment on Enter. + * Do format my model! + * + * @return false all the time to tell the {@link TemplateLanguageFormattingModelBuilder} + * to not-not format our model (i.e. yes please! Format away!) */ - @NotNull @Override - public ChildAttributes getChildAttributes(int newChildIndex) { - return new ChildAttributes(Indent.getNoneIndent(), null); + public boolean dontFormatMyModel() { + return false; } + private static class QuteBlock extends TemplateLanguageBlock { - /** - * 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; + @NotNull + protected final HtmlPolicy myHtmlPolicy; + + + QuteBlock(@NotNull ASTNode node, + Wrap wrap, + Alignment alignment, + @NotNull TemplateLanguageBlockFactory blockFactory, + @NotNull CodeStyleSettings settings, + @Nullable List foreignChildren, + @NotNull HtmlPolicy htmlPolicy) { + super(node, wrap, alignment, blockFactory, settings, foreignChildren); + myHtmlPolicy = htmlPolicy; } - else if (immediate && parent instanceof QuteBlock) { - break; + + @Override + public Indent getIndent() { + // ignore whitespace + if (myNode.getText().trim().isEmpty()) { + return Indent.getNoneIndent(); + } + + // 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 QuteElementTypes.QUTE_TEXT; } - parent = parent.getParent(); - } - return foreignBlockParent; + @Override + public boolean isRequiredRange(TextRange range) { + // seems our approach doesn't require us to insert any custom DataLanguageBlockFragmentWrapper blocks + return false; + } + + /** + *

+ * This method handles indent and alignment on Enter. + */ + @NotNull + @Override + public ChildAttributes getChildAttributes(int newChildIndex) { + 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 QuteBlock) { + break; + } + parent = parent.getParent(); + } + + return foreignBlockParent; + } } - } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteLexerForStartTag.java b/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteLexerForStartTag.java index ccd4fe597..19187d3d3 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteLexerForStartTag.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteLexerForStartTag.java @@ -36,8 +36,6 @@ public class QuteLexerForStartTag extends AbstractQuteSubLexer { private int myLastState; private IElementType myLastTokenType; private int myLastTokenEnd; - private ExpressionScanner expressionScanner; - private AbstractQuteSubLexer currentSubLexer; public QuteLexerForStartTag(String text, TemplateScanner templateScanner, int startTagOpenOffset) { diff --git a/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsing.java b/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsing.java index a8fbaa6a2..5cba409e2 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsing.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsing.java @@ -39,24 +39,28 @@ public void parseTemplate() { } while (!eof()) { - final IElementType tt = token(); - if (tt == QUTE_START_EXPRESSION) { - parseExpression(); - } else if (tt == QUTE_START_TAG_OPEN) { - parseStartSection(); - } else if (isCommentToken(tt)) { - parseComment(); - } else if (tt == QUTE_TEXT) { - parseText(); - } else { - advance(); - } + parseContent(); } template.done(QUTE_CONTENT); } - private void parseStartSection() { + private void parseContent() { + final IElementType tt = token(); + if (tt == QUTE_START_EXPRESSION) { + parseExpression(); + } else if (tt == QUTE_START_TAG_OPEN) { + parseSection(); + } else if (isCommentToken(tt)) { + parseComment(); + } else if (tt == QUTE_TEXT) { + parseText(); + } else { + advance(); + } + } + + private void parseSection() { final PsiBuilder.Marker startSection = mark(); advance(); @@ -91,17 +95,18 @@ private void parseStartSection() { advance(); propertyPart.done(QUTE_EXPRESSION_PROPERTY_PART); continue; - } else { - //final PsiBuilder.Marker error = mark(); + } else if (tt == QUTE_END_TAG_CLOSE || tt == QUTE_END_TAG_SELF_CLOSE) { advance(); - // error.error("BAD comments!"); - continue; + break; + } else { + parseContent(); } break; } startSection.done(QUTE_START_SECTION); } + private void parseText() { final PsiBuilder.Marker text = mark(); advance(); diff --git a/src/main/java/com/redhat/devtools/intellij/qute/psi/internal/template/QuarkusIntegrationForQute.java b/src/main/java/com/redhat/devtools/intellij/qute/psi/internal/template/QuarkusIntegrationForQute.java index 8fb5ebd24..378bd0a82 100644 --- a/src/main/java/com/redhat/devtools/intellij/qute/psi/internal/template/QuarkusIntegrationForQute.java +++ b/src/main/java/com/redhat/devtools/intellij/qute/psi/internal/template/QuarkusIntegrationForQute.java @@ -126,9 +126,6 @@ private static String convertStreamToString(InputStream is) { } } - private static final String JDT_SCHEME = "jdt"; - private static final String CONTENTS_AUTHORITY = "jarentry"; - // see // https://github.com/microsoft/vscode-java-dependency/blob/27c306b770c23b1eba1f9a7c3e70d2793baced68/jdtls.ext/com.microsoft.jdtls.ext.core/src/com/microsoft/jdtls/ext/core/ExtUtils.java#L39 diff --git a/src/test/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsingTestCase.java b/src/test/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsingTestCase.java new file mode 100644 index 000000000..2462eeaf5 --- /dev/null +++ b/src/test/java/com/redhat/devtools/intellij/qute/lang/psi/QuteParsingTestCase.java @@ -0,0 +1,50 @@ +package com.redhat.devtools.intellij.qute.lang.psi; + +import com.intellij.lang.ParserDefinition; +import com.intellij.mock.MockSmartPointerManager; +import com.intellij.openapi.application.ex.PathManagerEx; +import com.intellij.psi.SmartPointerManager; +import com.intellij.testFramework.ParsingTestCase; +import com.redhat.devtools.intellij.qute.lang.QuteParserDefinition; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; + +public class QuteParsingTestCase extends ParsingTestCase { + + public QuteParsingTestCase() { + super("psi", "qute", new QuteParserDefinition()); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + project.registerService(SmartPointerManager.class, new MockSmartPointerManager()); + } + + @Override + protected String getTestDataPath() { + return new File("testData/qute").getPath(); + } + + public void testHelloWorld() throws IOException { doCodeTest("

Hello {http:param('name', 'Quarkus')}!

"); } + + public void testSimpleText() throws IOException { doCodeTest("foo"); } + + public void testSectionWithHtml() throws IOException { + doCodeTest(""" +
+
+ {#if} + {#if} + {inject:flash.get("key")} +
+
+ {/if} + {/if} +
+
+ """); + } +} diff --git a/testData/qute/psi/HelloWorld.txt b/testData/qute/psi/HelloWorld.txt new file mode 100644 index 000000000..93ce8fe72 --- /dev/null +++ b/testData/qute/psi/HelloWorld.txt @@ -0,0 +1,22 @@ +QUTE_FILE + ASTWrapperPsiElement(QuteElementType.QUTE_CONTENT) + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('

Hello ') + ASTWrapperPsiElement(QuteElementType.QUTE_EXPRESSION) + PsiElement(QuteElementType.QUTE_START_EXPRESSION)('{') + ASTWrapperPsiElement(QuteElementType.QUTE_EXPRESSION_NAMESPACE_PART) + PsiElement(QuteElementType.QUTE_EXPRESSION_NAMESPACE_PART)('http') + PsiElement(QuteElementType.QUTE_EXPRESSION_COLON_SPACE)(':') + PsiElement(QuteElementType.QUTE_EXPRESSION_METHOD_PART)('param') + PsiElement(QuteElementType.QUTE_EXPRESSION_OPEN_BRACKET)('(') + PsiElement(QuteElementType.QUTE_EXPRESSION_START_STRING)(''') + PsiElement(QuteElementType.QUTE_EXPRESSION_STRING)('name') + PsiElement(QuteElementType.QUTE_EXPRESSION_END_STRING)(''') + PsiElement(QuteElementType.QUTE_EXPRESSION_WHITESPACE)(', ') + PsiElement(QuteElementType.QUTE_EXPRESSION_START_STRING)(''') + PsiElement(QuteElementType.QUTE_EXPRESSION_STRING)('Quarkus') + PsiElement(QuteElementType.QUTE_EXPRESSION_END_STRING)(''') + PsiElement(QuteElementType.QUTE_EXPRESSION_CLOSE_BRACKET)(')') + PsiElement(QuteElementType.QUTE_END_EXPRESSION)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('!

') \ No newline at end of file diff --git a/testData/qute/psi/SectionWithHtml.txt b/testData/qute/psi/SectionWithHtml.txt new file mode 100644 index 000000000..8cc34e93d --- /dev/null +++ b/testData/qute/psi/SectionWithHtml.txt @@ -0,0 +1,43 @@ +QUTE_FILE + ASTWrapperPsiElement(QuteElementType.QUTE_CONTENT) + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('
\n
\n ') + ASTWrapperPsiElement(QuteElementType.QUTE_START_SECTION) + PsiElement(QuteElementType.QUTE_START_TAG_OPEN)('{#') + PsiElement(QuteElementType.QUTE_START_TAG)('if') + PsiElement(QuteElementType.QUTE_START_TAG_CLOSE)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('\n ') + ASTWrapperPsiElement(QuteElementType.QUTE_START_SECTION) + PsiElement(QuteElementType.QUTE_START_TAG_OPEN)('{#') + PsiElement(QuteElementType.QUTE_START_TAG)('if') + PsiElement(QuteElementType.QUTE_START_TAG_CLOSE)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('\n ') + ASTWrapperPsiElement(QuteElementType.QUTE_EXPRESSION) + PsiElement(QuteElementType.QUTE_START_EXPRESSION)('{') + ASTWrapperPsiElement(QuteElementType.QUTE_EXPRESSION_NAMESPACE_PART) + PsiElement(QuteElementType.QUTE_EXPRESSION_NAMESPACE_PART)('inject') + PsiElement(QuteElementType.QUTE_EXPRESSION_COLON_SPACE)(':') + ASTWrapperPsiElement(QuteElementType.QUTE_EXPRESSION_OBJECT_PART) + PsiElement(QuteElementType.QUTE_EXPRESSION_OBJECT_PART)('flash') + PsiElement(QuteElementType.QUTE_EXPRESSION_DOT)('.') + PsiElement(QuteElementType.QUTE_EXPRESSION_METHOD_PART)('get') + PsiElement(QuteElementType.QUTE_EXPRESSION_OPEN_BRACKET)('(') + PsiElement(QuteElementType.QUTE_EXPRESSION_START_STRING)('"') + PsiElement(QuteElementType.QUTE_EXPRESSION_STRING)('key') + PsiElement(QuteElementType.QUTE_EXPRESSION_END_STRING)('"') + PsiElement(QuteElementType.QUTE_EXPRESSION_CLOSE_BRACKET)(')') + PsiElement(QuteElementType.QUTE_END_EXPRESSION)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('\n
\n
\n ') + PsiElement(QuteElementType.QUTE_END_TAG_OPEN)('{/') + PsiElement(QuteElementType.QUTE_END_TAG)('if') + PsiElement(QuteElementType.QUTE_END_TAG_CLOSE)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('\n ') + PsiElement(QuteElementType.QUTE_END_TAG_OPEN)('{/') + PsiElement(QuteElementType.QUTE_END_TAG)('if') + PsiElement(QuteElementType.QUTE_END_TAG_CLOSE)('}') + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('\n
\n
\n') \ No newline at end of file diff --git a/testData/qute/psi/SimpleText.txt b/testData/qute/psi/SimpleText.txt new file mode 100644 index 000000000..181478586 --- /dev/null +++ b/testData/qute/psi/SimpleText.txt @@ -0,0 +1,4 @@ +QUTE_FILE + ASTWrapperPsiElement(QuteElementType.QUTE_CONTENT) + ASTWrapperPsiElement(QUTE_TEXT) + PsiElement(QUTE_TEXT)('foo') \ No newline at end of file