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);
+ }
+}