diff --git a/build.gradle b/build.gradle index 2beefa7..d614b64 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ intellij { pluginName 'jetbrains-plugin-st4' downloadSources true updateSinceUntilBuild false + plugins 'java', 'IntelliLang' } group 'antlr' diff --git a/src/main/antlr/STLexer.g4 b/src/main/antlr/STLexer.g4 index 32c7416..9bc90f0 100644 --- a/src/main/antlr/STLexer.g4 +++ b/src/main/antlr/STLexer.g4 @@ -71,6 +71,7 @@ LBRACE : LBrace { startSubTemplate(); } ; RDELIM : . { isRDelim() }? -> mode(DEFAULT_MODE) ; STRING : DQuoteLiteral ; +EOF_STRING : DQuote ( EscSeq | ~["\r\n\\] )* ; IF : 'if' ; ELSEIF : 'elseif' ; diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandler.java b/src/main/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandler.java new file mode 100644 index 0000000..1a4a071 --- /dev/null +++ b/src/main/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandler.java @@ -0,0 +1,107 @@ +package org.antlr.jetbrains.st4plugin.editor; + +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter; +import com.intellij.openapi.editor.highlighter.EditorHighlighter; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ReflectionUtil; +import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; +import org.antlr.jetbrains.st4plugin.parsing.STGLexer; +import org.antlr.jetbrains.st4plugin.parsing.STLexer; +import org.antlr.jetbrains.st4plugin.psi.STFile; +import org.antlr.jetbrains.st4plugin.psi.STGroupFile; +import org.antlr.jetbrains.st4plugin.psi.STTokenTypes; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +import static org.antlr.jetbrains.st4plugin.psi.STGroupTokenTypes.getTokenElementType; + +/** + * Automatically closes tag delimiters, template delimiters etc. + */ +public class AutoCloseTypedHandler extends TypedHandlerDelegate { + + @Override + public @NotNull Result beforeCharTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file, @NotNull FileType fileType) { + List validBeforeLDelim = Arrays.asList( + STTokenTypes.getTokenElementType(STLexer.TEXT), + STTokenTypes.getTokenElementType(STLexer.RDELIM), + STTokenTypes.getTokenElementType(STLexer.HORZ_WS) + ); + + if (file instanceof STFile) { + int offset = editor.getCaretModel().getOffset(); + + EditorHighlighter highlighter = ((EditorEx) editor).getHighlighter(); + if (highlighter instanceof LexerEditorHighlighter) { + Lexer lexer = ReflectionUtil.getField(LexerEditorHighlighter.class, highlighter, Lexer.class, "myLexer"); + org.antlr.v4.runtime.Lexer antlrLexer = ReflectionUtil.getField(ANTLRLexerAdaptor.class, lexer, org.antlr.v4.runtime.Lexer.class, "lexer"); + + if (antlrLexer instanceof STLexer) { + char ldelim = ((STLexer) antlrLexer).getlDelim(); + char rdelim = ((STLexer) antlrLexer).getrDelim(); + + if (c == ldelim) { + IElementType previousToken = offset == 0 ? null : highlighter.createIterator(offset - 1).getTokenType(); + + if (offset == 0 || validBeforeLDelim.contains(previousToken)) { + EditorModificationUtil.insertStringAtCaret(editor, "" + c + rdelim, false, 1); + return Result.STOP; + } + } + } + } + } + + return super.beforeCharTyped(c, project, editor, file, fileType); + } + + @Override + public @NotNull Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { + if (file instanceof STGroupFile) { + int offset = editor.getCaretModel().getOffset(); + + if ((c == '<' ||c == '%') && offset > 1) { + autoCloseTemplate(c, editor, file, offset); + } + + // TODO we could let IntelliJ handle this automatically if anonymous templates were parsed as `LBRACE ANON_TEMPLATE_CONTENT RBRACE` + // => we'd also have braces matching + if (c == '{') { + PsiElement previousElement = file.findElementAt(offset - 1); + previousElement = previousElement == null ? null : PsiTreeUtil.prevVisibleLeaf(previousElement); + + if (previousElement != null && previousElement.getNode().getElementType() == getTokenElementType(STGLexer.ASSIGN)) { + EditorModificationUtil.insertStringAtCaret(editor, "}", false, 0); + } + } + } + + return super.charTyped(c, project, editor, file); + } + + private void autoCloseTemplate(char c, @NotNull Editor editor, @NotNull PsiFile file, int offset) { + char previous = editor.getDocument().getCharsSequence().charAt(offset - 2); + + if (previous == '<') { + PsiElement previousElement = file.findElementAt(offset - 2); + previousElement = previousElement == null ? null : PsiTreeUtil.prevVisibleLeaf(previousElement); + + if (previousElement != null && previousElement.getNode().getElementType() == getTokenElementType(STGLexer.TMPL_ASSIGN)) { + String closing = c == '<' ? ">>" : "%>"; + EditorModificationUtil.insertStringAtCaret(editor, closing, false, 0); + } + } + } +} diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/editor/STGroupQuoteHandler.java b/src/main/java/org/antlr/jetbrains/st4plugin/editor/STGroupQuoteHandler.java new file mode 100644 index 0000000..f559a03 --- /dev/null +++ b/src/main/java/org/antlr/jetbrains/st4plugin/editor/STGroupQuoteHandler.java @@ -0,0 +1,17 @@ +package org.antlr.jetbrains.st4plugin.editor; + +import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler; +import org.antlr.jetbrains.st4plugin.STGroupLanguage; +import org.antlr.jetbrains.st4plugin.parsing.STGLexer; + +import static org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory.createTokenSet; + +/** + * Automatically closes {@link STGLexer#STRING} quotes. + */ +public class STGroupQuoteHandler extends SimpleTokenSetQuoteHandler { + + public STGroupQuoteHandler() { + super(createTokenSet(STGroupLanguage.INSTANCE, STGLexer.STRING, STGLexer.EOF_STRING)); + } +} diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/editor/STQuoteHandler.java b/src/main/java/org/antlr/jetbrains/st4plugin/editor/STQuoteHandler.java new file mode 100644 index 0000000..00f1e92 --- /dev/null +++ b/src/main/java/org/antlr/jetbrains/st4plugin/editor/STQuoteHandler.java @@ -0,0 +1,17 @@ +package org.antlr.jetbrains.st4plugin.editor; + +import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler; +import org.antlr.jetbrains.st4plugin.STLanguage; +import org.antlr.jetbrains.st4plugin.parsing.STLexer; + +import static org.antlr.intellij.adaptor.lexer.PSIElementTypeFactory.createTokenSet; + +/** + * Automatically closes {@link STLexer#STRING} quotes. + */ +public class STQuoteHandler extends SimpleTokenSetQuoteHandler { + + public STQuoteHandler() { + super(createTokenSet(STLanguage.INSTANCE, STLexer.STRING, STLexer.EOF_STRING)); + } +} diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighter.java b/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighter.java new file mode 100644 index 0000000..4b998d7 --- /dev/null +++ b/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighter.java @@ -0,0 +1,51 @@ +package org.antlr.jetbrains.st4plugin.highlight; + +import com.intellij.lang.Language; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.ex.util.LayerDescriptor; +import com.intellij.openapi.editor.ex.util.LayeredLexerEditorHighlighter; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings; +import org.antlr.intellij.adaptor.lexer.ANTLRLexerAdaptor; +import org.antlr.jetbrains.st4plugin.STGroupFileType; +import org.antlr.jetbrains.st4plugin.parsing.STLexer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.antlr.jetbrains.st4plugin.psi.STTokenTypes.getTokenElementType; + +/** + * Highlights the "outer language", i.e. the target language. + * The target language can be configured in the Template Data Languages settings. + */ +public class STEditorHighlighter extends LayeredLexerEditorHighlighter { + + public STEditorHighlighter(@Nullable VirtualFile virtualFile, + @Nullable Project project, + @NotNull FileType fileType, + @NotNull EditorColorsScheme scheme) { + super(fileType == STGroupFileType.INSTANCE ? new STGroupSyntaxHighlighter() : new STSyntaxHighlighter(), scheme); + + if (project != null && virtualFile != null) { + Language language = TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile); + + if (language != null) { + SyntaxHighlighter outerHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language.getAssociatedFileType(), project, virtualFile); + + if (outerHighlighter != null) { + registerLayer(getTokenElementType(STLexer.TEXT), new LayerDescriptor(outerHighlighter, "")); + } + } + } + } + + @Override + public @NotNull ANTLRLexerAdaptor getLexer() { + return (ANTLRLexerAdaptor) super.getLexer(); + } +} diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighterProvider.java b/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighterProvider.java index 7c2c2f1..d4cf6d1 100644 --- a/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighterProvider.java +++ b/src/main/java/org/antlr/jetbrains/st4plugin/highlight/STEditorHighlighterProvider.java @@ -1,6 +1,7 @@ package org.antlr.jetbrains.st4plugin.highlight; import com.intellij.lang.Language; +import com.intellij.lexer.Lexer; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.ex.util.LayerDescriptor; import com.intellij.openapi.editor.ex.util.LayeredLexerEditorHighlighter; @@ -26,29 +27,3 @@ public EditorHighlighter getEditorHighlighter(@Nullable Project project, @NotNul return new STEditorHighlighter(virtualFile, project, fileType, colors); } } - -/** - * Highlights the "outer language", i.e. the target language. - * The target language can be configured in the Template Data Languages settings. - */ -class STEditorHighlighter extends LayeredLexerEditorHighlighter { - - public STEditorHighlighter(@Nullable VirtualFile virtualFile, - @Nullable Project project, - @NotNull FileType fileType, - @NotNull EditorColorsScheme scheme) { - super(fileType == STGroupFileType.INSTANCE ? new STGroupSyntaxHighlighter() : new STSyntaxHighlighter(), scheme); - - if (project != null && virtualFile != null) { - Language language = TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile); - - if (language != null) { - SyntaxHighlighter outerHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language.getAssociatedFileType(), project, virtualFile); - - if (outerHighlighter != null) { - registerLayer(getTokenElementType(STLexer.TEXT), new LayerDescriptor(outerHighlighter, "")); - } - } - } - } -} diff --git a/src/main/java/org/antlr/jetbrains/st4plugin/parsing/LexerAdaptor.java b/src/main/java/org/antlr/jetbrains/st4plugin/parsing/LexerAdaptor.java index da5d600..305ecba 100644 --- a/src/main/java/org/antlr/jetbrains/st4plugin/parsing/LexerAdaptor.java +++ b/src/main/java/org/antlr/jetbrains/st4plugin/parsing/LexerAdaptor.java @@ -155,6 +155,14 @@ public void setDelimiters(char lDelim, char rDelim) { this.rDelim = rDelim; } + public char getlDelim() { + return lDelim; + } + + public char getrDelim() { + return rDelim; + } + public boolean isLDelim() { return lDelim == _input.LA(-1); } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 351fc9a..c3c22f2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -7,7 +7,7 @@ This plugin is for StringTemplate v4 .stg/.st files. It works with - IntelliJ IDEA 15, 2016.1-2020.1. It should work in other IntelliJ-based IDEs. + IntelliJ IDEA 15, 2016.1-2020.2. It should work in other IntelliJ-based IDEs.

Github source

@@ -27,19 +27,9 @@ - com.intellij.modules.lang org.intellij.intelliLang - - - - - - - - - @@ -61,5 +51,9 @@ + + + + diff --git a/src/test/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandlerTest.java b/src/test/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandlerTest.java new file mode 100644 index 0000000..c828718 --- /dev/null +++ b/src/test/java/org/antlr/jetbrains/st4plugin/editor/AutoCloseTypedHandlerTest.java @@ -0,0 +1,107 @@ +package org.antlr.jetbrains.st4plugin.editor; + +import com.intellij.testFramework.EditorTestUtil; +import com.intellij.testFramework.LightPlatformCodeInsightTestCase; + +public class AutoCloseTypedHandlerTest extends LightPlatformCodeInsightTestCase { + + public void testParensInFormalArgs() { + configureFromFileText("test.stg", "tpl"); + + EditorTestUtil.performTypingAction(getEditor(), '('); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl()", actual); + } + + public void testAnonymousTemplateInArg() { + configureFromFileText("test.stg", "tpl(foo = ) ::= << >>"); + + EditorTestUtil.performTypingAction(getEditor(), '{'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl(foo = {}) ::= << >>", actual); + } + + public void testBigString() { + configureFromFileText("test.stg", "tpl() ::= <"); + + EditorTestUtil.performTypingAction(getEditor(), '<'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl() ::= <<>>", actual); + } + + public void testNotBigString() { + configureFromFileText("test.stg", "tpl() ::= < "); + + EditorTestUtil.performTypingAction(getEditor(), '<'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl() ::= < <", actual); + } + + public void testBigStringNoNl() { + configureFromFileText("test.stg", "tpl() ::= <"); + + EditorTestUtil.performTypingAction(getEditor(), '%'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl() ::= <%%>", actual); + } + + public void testNotBigStringNoNl() { + configureFromFileText("test.stg", "tpl() ::= < "); + + EditorTestUtil.performTypingAction(getEditor(), '%'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl() ::= < %", actual); + } + + public void testStringTemplate() { + configureFromFileText("test.stg", "tpl() ::= "); + + EditorTestUtil.performTypingAction(getEditor(), '"'); + + String actual = getEditor().getDocument().getText(); + assertEquals("tpl() ::= \"\"", actual); + } + + public void testTagCustomDelimiter() { + configureFromFileText("test.stg", "delimiters \"[\", \"]\" tpl() ::= <>>"); + + EditorTestUtil.performTypingAction(getEditor(), '['); + + String actual = getEditor().getDocument().getText() + .substring(3); // trim the custom delimiters that were passed to the sub-lexer + assertEquals("foo []", actual); + } + + public void testAnonymousTemplateInTag() { + configureFromFileText("test.st", "blah blah"); + + EditorTestUtil.performTypingAction(getEditor(), '{'); + + String actual = getEditor().getDocument().getText(); + assertEquals("blah "); + + EditorTestUtil.performTypingAction(getEditor(), '<'); + + String actual = getEditor().getDocument().getText(); + assertEquals("foo <>", actual); + } + + public void testQuotesInTemplate() { + configureFromFileText("test.st", "foo >"); + + EditorTestUtil.performTypingAction(getEditor(), '"'); + + String actual = getEditor().getDocument().getText(); + assertEquals("foo ", actual); + } +}