diff --git a/gen/org/elixir_lang/heex/lexer/Flex.java b/gen/org/elixir_lang/heex/lexer/Flex.java
new file mode 100644
index 000000000..12502d6bf
--- /dev/null
+++ b/gen/org/elixir_lang/heex/lexer/Flex.java
@@ -0,0 +1,692 @@
+// Generated by JFlex 1.9.2 http://jflex.de/ (tweaked for IntelliJ platform)
+// source: HEEx.flex
+
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import kotlinx.html.SCRIPT;import org.elixir_lang.heex.psi.Types;
+
+
+public class Flex implements com.intellij.lexer.FlexLexer {
+
+ /** This character denotes the end of file */
+ public static final int YYEOF = -1;
+
+ /** initial size of the lookahead buffer */
+ private static final int ZZ_BUFFERSIZE = 16384;
+
+ /** lexical states */
+ public static final int YYINITIAL = 0;
+ public static final int WHITESPACE_MAYBE = 2;
+ public static final int COMMENT = 4;
+ public static final int ELIXIR = 6;
+ public static final int MARKER_MAYBE = 8;
+ public static final int BEGIN_MATCHED_BRACES = 10;
+ public static final int MATCHED_BRACES = 12;
+ public static final int STYLE_TAG = 14;
+ public static final int SCRIPT_TAG = 16;
+
+ /**
+ * ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
+ * ZZ_LEXSTATE[l+1] is the state in the DFA for the lexical state l
+ * at the beginning of a line
+ * l is of the form l = 2*k, k a non negative integer
+ */
+ private static final int ZZ_LEXSTATE[] = {
+ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7,
+ 8, 8
+ };
+
+ /**
+ * Top-level table for translating characters to character classes
+ */
+ private static final int [] ZZ_CMAP_TOP = zzUnpackcmap_top();
+
+ private static final String ZZ_CMAP_TOP_PACKED_0 =
+ "\1\0\1\u0100\24\u0200\1\u0300\11\u0200\1\u0400\17\u0200\1\u0500"+
+ "\u10cf\u0200";
+
+ private static int [] zzUnpackcmap_top() {
+ int [] result = new int[4352];
+ int offset = 0;
+ offset = zzUnpackcmap_top(ZZ_CMAP_TOP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackcmap_top(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Second-level tables for translating characters to character classes
+ */
+ private static final int [] ZZ_CMAP_BLOCKS = zzUnpackcmap_blocks();
+
+ private static final String ZZ_CMAP_BLOCKS_PACKED_0 =
+ "\11\0\2\1\1\2\2\1\22\0\1\3\2\0\1\4"+
+ "\1\0\1\5\11\0\1\6\14\0\1\7\1\10\1\11"+
+ "\4\0\1\12\1\0\1\13\3\0\1\14\2\0\1\15"+
+ "\3\0\1\16\1\0\1\17\1\20\1\21\4\0\1\22"+
+ "\11\0\1\12\1\0\1\13\3\0\1\14\2\0\1\15"+
+ "\3\0\1\16\1\0\1\17\1\20\1\21\4\0\1\22"+
+ "\1\0\1\23\1\24\1\25\7\0\1\2\32\0\1\2"+
+ "\217\0\2\14\115\0\1\20\u0200\0\1\2\177\0\13\2"+
+ "\35\0\2\2\5\0\1\2\57\0\1\2\240\0\1\2"+
+ "\377\0";
+
+ private static int [] zzUnpackcmap_blocks() {
+ int [] result = new int[1536];
+ int offset = 0;
+ offset = zzUnpackcmap_blocks(ZZ_CMAP_BLOCKS_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackcmap_blocks(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /**
+ * Translates DFA states to action switch labels.
+ */
+ private static final int [] ZZ_ACTION = zzUnpackAction();
+
+ private static final String ZZ_ACTION_PACKED_0 =
+ "\11\0\2\1\1\2\2\3\2\4\2\5\1\6\1\7"+
+ "\1\10\1\11\1\12\1\13\1\14\1\15\2\1\1\16"+
+ "\3\0\1\17\2\0\1\20\7\0\1\21\13\0\1\22"+
+ "\2\0\1\23\1\24";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[60];
+ int offset = 0;
+ offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAction(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /**
+ * Translates a state to a row index in the transition table
+ */
+ private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
+
+ private static final String ZZ_ROWMAP_PACKED_0 =
+ "\0\0\0\26\0\54\0\102\0\130\0\156\0\204\0\232"+
+ "\0\260\0\306\0\334\0\306\0\306\0\362\0\306\0\u0108"+
+ "\0\306\0\u0108\0\306\0\306\0\306\0\306\0\306\0\306"+
+ "\0\306\0\306\0\u011e\0\u0134\0\u014a\0\u0160\0\362\0\u0176"+
+ "\0\306\0\u018c\0\u01a2\0\306\0\u01b8\0\u01ce\0\u01e4\0\u01fa"+
+ "\0\u0210\0\u0226\0\u023c\0\306\0\u0252\0\u0268\0\u027e\0\u0294"+
+ "\0\u02aa\0\u02c0\0\u02d6\0\u02ec\0\u0302\0\u0318\0\u032e\0\306"+
+ "\0\u0344\0\u035a\0\306\0\306";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[60];
+ int offset = 0;
+ offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length() - 1;
+ while (i < l) {
+ int high = packed.charAt(i++) << 16;
+ result[j++] = high | packed.charAt(i++);
+ }
+ return j;
+ }
+
+ /**
+ * The transition table of the DFA
+ */
+ private static final int [] ZZ_TRANS = zzUnpacktrans();
+
+ private static final String ZZ_TRANS_PACKED_0 =
+ "\7\12\1\13\13\12\1\14\2\12\1\15\1\16\1\15"+
+ "\1\16\22\15\5\17\1\20\20\17\5\21\1\22\20\21"+
+ "\4\23\1\24\1\23\1\25\1\23\1\26\13\23\1\27"+
+ "\1\23\26\30\23\21\1\31\1\21\1\32\7\12\1\33"+
+ "\25\12\1\34\16\12\33\0\1\35\12\0\1\36\6\0"+
+ "\1\37\1\0\1\37\3\0\1\40\27\0\1\41\21\0"+
+ "\1\35\1\42\24\0\1\35\1\43\24\0\1\44\32\0"+
+ "\1\45\6\0\1\46\11\0\1\47\40\0\1\50\25\0"+
+ "\1\51\24\0\1\52\30\0\1\53\6\0\1\54\43\0"+
+ "\1\55\16\0\1\56\27\0\1\57\26\0\1\60\32\0"+
+ "\1\61\22\0\1\62\24\0\1\63\22\0\1\64\27\0"+
+ "\1\65\24\0\1\66\32\0\1\67\5\0\3\70\5\0"+
+ "\1\70\27\0\1\71\30\0\1\72\10\0\3\73\5\0"+
+ "\1\73\25\0\1\74\35\0\1\71\4\0";
+
+ private static int [] zzUnpacktrans() {
+ int [] result = new int[880];
+ int offset = 0;
+ offset = zzUnpacktrans(ZZ_TRANS_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpacktrans(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ value--;
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+
+ /* error codes */
+ private static final int ZZ_UNKNOWN_ERROR = 0;
+ private static final int ZZ_NO_MATCH = 1;
+ private static final int ZZ_PUSHBACK_2BIG = 2;
+
+ /* error messages for the codes above */
+ private static final String[] ZZ_ERROR_MSG = {
+ "Unknown internal scanner error",
+ "Error: could not match input",
+ "Error: pushback value was too large"
+ };
+
+ /**
+ * ZZ_ATTRIBUTE[aState] contains the attributes of state {@code aState}
+ */
+ private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
+
+ private static final String ZZ_ATTRIBUTE_PACKED_0 =
+ "\11\0\1\11\1\1\2\11\1\1\1\11\1\1\1\11"+
+ "\1\1\10\11\3\1\3\0\1\11\2\0\1\11\7\0"+
+ "\1\11\13\0\1\11\2\0\2\11";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[60];
+ int offset = 0;
+ offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
+ return result;
+ }
+
+ private static int zzUnpackAttribute(String packed, int offset, int [] result) {
+ int i = 0; /* index in packed string */
+ int j = offset; /* index in unpacked array */
+ int l = packed.length();
+ while (i < l) {
+ int count = packed.charAt(i++);
+ int value = packed.charAt(i++);
+ do result[j++] = value; while (--count > 0);
+ }
+ return j;
+ }
+
+ /** the input device */
+ private java.io.Reader zzReader;
+
+ /** the current state of the DFA */
+ private int zzState;
+
+ /** the current lexical state */
+ private int zzLexicalState = YYINITIAL;
+
+ /** this buffer contains the current text to be matched and is
+ the source of the yytext() string */
+ private CharSequence zzBuffer = "";
+
+ /** the textposition at the last accepting state */
+ private int zzMarkedPos;
+
+ /** the current text position in the buffer */
+ private int zzCurrentPos;
+
+ /** startRead marks the beginning of the yytext() string in the buffer */
+ private int zzStartRead;
+
+ /** endRead marks the last character in the buffer, that has been read
+ from input */
+ private int zzEndRead;
+
+ /** zzAtEOF == true <=> the scanner is at the EOF */
+ private boolean zzAtEOF;
+
+ /** Number of newlines encountered up to the start of the matched text. */
+ @SuppressWarnings("unused")
+ private int yyline;
+
+ /** Number of characters from the last newline up to the start of the matched text. */
+ @SuppressWarnings("unused")
+ protected int yycolumn;
+
+ /** Number of characters up to the start of the matched text. */
+ @SuppressWarnings("unused")
+ private long yychar;
+
+ /** Whether the scanner is currently at the beginning of a line. */
+ @SuppressWarnings("unused")
+ private boolean zzAtBOL = true;
+
+ /** Whether the user-EOF-code has already been executed. */
+ private boolean zzEOFDone;
+
+ /* user code: */
+ private int openBraceCount = 0;
+
+ private void handleInState(int nextLexicalState) {
+ yypushback(yylength());
+ yybegin(nextLexicalState);
+ }
+
+
+ /**
+ * Creates a new scanner
+ *
+ * @param in the java.io.Reader to read input from.
+ */
+ public Flex(java.io.Reader in) {
+ this.zzReader = in;
+ }
+
+
+ /** Returns the maximum size of the scanner buffer, which limits the size of tokens. */
+ private int zzMaxBufferLen() {
+ return Integer.MAX_VALUE;
+ }
+
+ /** Whether the scanner buffer can grow to accommodate a larger token. */
+ private boolean zzCanGrow() {
+ return true;
+ }
+
+ /**
+ * Translates raw input code points to DFA table row
+ */
+ private static int zzCMap(int input) {
+ int offset = input & 255;
+ return offset == input ? ZZ_CMAP_BLOCKS[offset] : ZZ_CMAP_BLOCKS[ZZ_CMAP_TOP[input >> 8] | offset];
+ }
+
+ public final int getTokenStart() {
+ return zzStartRead;
+ }
+
+ public final int getTokenEnd() {
+ return getTokenStart() + yylength();
+ }
+
+ public void reset(CharSequence buffer, int start, int end, int initialState) {
+ zzBuffer = buffer;
+ zzCurrentPos = zzMarkedPos = zzStartRead = start;
+ zzAtEOF = false;
+ zzAtBOL = true;
+ zzEndRead = end;
+ yybegin(initialState);
+ }
+
+ /**
+ * Refills the input buffer.
+ *
+ * @return {@code false}, iff there was new input.
+ *
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ private boolean zzRefill() throws java.io.IOException {
+ return true;
+ }
+
+
+ /**
+ * Returns the current lexical state.
+ */
+ public final int yystate() {
+ return zzLexicalState;
+ }
+
+
+ /**
+ * Enters a new lexical state
+ *
+ * @param newState the new lexical state
+ */
+ public final void yybegin(int newState) {
+ zzLexicalState = newState;
+ }
+
+
+ /**
+ * Returns the text matched by the current regular expression.
+ */
+ public final CharSequence yytext() {
+ return zzBuffer.subSequence(zzStartRead, zzMarkedPos);
+ }
+
+
+ /**
+ * Returns the character at position {@code pos} from the
+ * matched text.
+ *
+ * It is equivalent to yytext().charAt(pos), but faster
+ *
+ * @param pos the position of the character to fetch.
+ * A value from 0 to yylength()-1.
+ *
+ * @return the character at position pos
+ */
+ public final char yycharat(int pos) {
+ return zzBuffer.charAt(zzStartRead+pos);
+ }
+
+
+ /**
+ * Returns the length of the matched text region.
+ */
+ public final int yylength() {
+ return zzMarkedPos-zzStartRead;
+ }
+
+
+ /**
+ * Reports an error that occurred while scanning.
+ *
+ * In a wellformed scanner (no or only correct usage of
+ * yypushback(int) and a match-all fallback rule) this method
+ * will only be called with things that "Can't Possibly Happen".
+ * If this method is called, something is seriously wrong
+ * (e.g. a JFlex bug producing a faulty scanner etc.).
+ *
+ * Usual syntax/scanner level error handling should be done
+ * in error fallback rules.
+ *
+ * @param errorCode the code of the errormessage to display
+ */
+ private void zzScanError(int errorCode) {
+ String message;
+ try {
+ message = ZZ_ERROR_MSG[errorCode];
+ }
+ catch (ArrayIndexOutOfBoundsException e) {
+ message = ZZ_ERROR_MSG[ZZ_UNKNOWN_ERROR];
+ }
+
+ throw new Error(message);
+ }
+
+
+ /**
+ * Pushes the specified amount of characters back into the input stream.
+ *
+ * They will be read again by then next call of the scanning method
+ *
+ * @param number the number of characters to be read again.
+ * This number must not be greater than yylength()!
+ */
+ public void yypushback(int number) {
+ if ( number > yylength() )
+ zzScanError(ZZ_PUSHBACK_2BIG);
+
+ zzMarkedPos -= number;
+ }
+
+
+ /**
+ * Contains user EOF-code, which will be executed exactly once,
+ * when the end of file is reached
+ */
+ private void zzDoEOF() {
+ if (!zzEOFDone) {
+ zzEOFDone = true;
+
+ }
+ }
+
+
+ /**
+ * Resumes scanning until the next regular expression is matched,
+ * the end of input is encountered or an I/O-Error occurs.
+ *
+ * @return the next token
+ * @exception java.io.IOException if any I/O-Error occurs
+ */
+ public IElementType advance() throws java.io.IOException
+ {
+ int zzInput;
+ int zzAction;
+
+ // cached fields:
+ int zzCurrentPosL;
+ int zzMarkedPosL;
+ int zzEndReadL = zzEndRead;
+ CharSequence zzBufferL = zzBuffer;
+
+ int [] zzTransL = ZZ_TRANS;
+ int [] zzRowMapL = ZZ_ROWMAP;
+ int [] zzAttrL = ZZ_ATTRIBUTE;
+
+ while (true) {
+ zzMarkedPosL = zzMarkedPos;
+
+ zzAction = -1;
+
+ zzCurrentPosL = zzCurrentPos = zzStartRead = zzMarkedPosL;
+
+ zzState = ZZ_LEXSTATE[zzLexicalState];
+
+ // set up zzAction for empty match case:
+ int zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ }
+
+
+ zzForAction: {
+ while (true) {
+
+ if (zzCurrentPosL < zzEndReadL) {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ else if (zzAtEOF) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ // store back cached positions
+ zzCurrentPos = zzCurrentPosL;
+ zzMarkedPos = zzMarkedPosL;
+ boolean eof = zzRefill();
+ // get translated positions and possibly new buffer
+ zzCurrentPosL = zzCurrentPos;
+ zzMarkedPosL = zzMarkedPos;
+ zzBufferL = zzBuffer;
+ zzEndReadL = zzEndRead;
+ if (eof) {
+ zzInput = YYEOF;
+ break zzForAction;
+ }
+ else {
+ zzInput = Character.codePointAt(zzBufferL, zzCurrentPosL);
+ zzCurrentPosL += Character.charCount(zzInput);
+ }
+ }
+ int zzNext = zzTransL[ zzRowMapL[zzState] + zzCMap(zzInput) ];
+ if (zzNext == -1) break zzForAction;
+ zzState = zzNext;
+
+ zzAttributes = zzAttrL[zzState];
+ if ( (zzAttributes & 1) == 1 ) {
+ zzAction = zzState;
+ zzMarkedPosL = zzCurrentPosL;
+ if ( (zzAttributes & 8) == 8 ) break zzForAction;
+ }
+
+ }
+ }
+
+ // store back cached position
+ zzMarkedPos = zzMarkedPosL;
+
+ if (zzInput == YYEOF && zzStartRead == zzCurrentPos) {
+ zzAtEOF = true;
+ zzDoEOF();
+ return null;
+ }
+ else {
+ switch (zzAction < 0 ? zzAction : ZZ_ACTION[zzAction]) {
+ case 1:
+ { return Types.DATA;
+ }
+ // fall through
+ case 21: break;
+ case 2:
+ { yybegin(BEGIN_MATCHED_BRACES);
+ return Types.BRACE_OPENING;
+ }
+ // fall through
+ case 22: break;
+ case 3:
+ { handleInState(YYINITIAL);
+ }
+ // fall through
+ case 23: break;
+ case 4:
+ { return Types.COMMENT;
+ }
+ // fall through
+ case 24: break;
+ case 5:
+ { return Types.ELIXIR;
+ }
+ // fall through
+ case 25: break;
+ case 6:
+ { handleInState(ELIXIR);
+ return Types.EMPTY_MARKER;
+ }
+ // fall through
+ case 26: break;
+ case 7:
+ { yybegin(COMMENT);
+ return Types.COMMENT_MARKER;
+ }
+ // fall through
+ case 27: break;
+ case 8:
+ { yybegin(ELIXIR);
+ return Types.FORWARD_SLASH_MARKER;
+ }
+ // fall through
+ case 28: break;
+ case 9:
+ { yybegin(ELIXIR);
+ return Types.EQUALS_MARKER;
+ }
+ // fall through
+ case 29: break;
+ case 10:
+ { yybegin(ELIXIR);
+ return Types.PIPE_MARKER;
+ }
+ // fall through
+ case 30: break;
+ case 11:
+ { handleInState(MATCHED_BRACES);
+ return Types.EQUALS_MARKER;
+ }
+ // fall through
+ case 31: break;
+ case 12:
+ { openBraceCount++;
+ return Types.ELIXIR;
+ }
+ // fall through
+ case 32: break;
+ case 13:
+ { if (openBraceCount > 0) {
+ openBraceCount--;
+ return Types.ELIXIR;
+ } else {
+ yybegin(YYINITIAL);
+ return Types.BRACE_CLOSING;
+ }
+ }
+ // fall through
+ case 33: break;
+ case 14:
+ { yybegin(MARKER_MAYBE);
+ return Types.OPENING;
+ }
+ // fall through
+ case 34: break;
+ case 15:
+ { yybegin(WHITESPACE_MAYBE);
+ return Types.CLOSING;
+ }
+ // fall through
+ case 35: break;
+ case 16:
+ { return Types.ESCAPED_OPENING;
+ }
+ // fall through
+ case 36: break;
+ case 17:
+ // lookahead expression with fixed lookahead length
+ zzMarkedPos = Character.offsetByCodePoints
+ (zzBufferL, zzMarkedPos, -3);
+ { yybegin(YYINITIAL);
+ return TokenType.WHITE_SPACE;
+ }
+ // fall through
+ case 37: break;
+ case 18:
+ { yybegin(STYLE_TAG); return Types.DATA;
+ }
+ // fall through
+ case 38: break;
+ case 19:
+ { yybegin(SCRIPT_TAG); return Types.DATA;
+ }
+ // fall through
+ case 39: break;
+ case 20:
+ { yybegin(YYINITIAL); return Types.DATA;
+ }
+ // fall through
+ case 40: break;
+ default:
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 83d3b7839..cee5617a9 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -64,6 +64,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/icons/file/heex.svg b/resources/icons/file/heex.svg
new file mode 100755
index 000000000..ffc9875d6
--- /dev/null
+++ b/resources/icons/file/heex.svg
@@ -0,0 +1,17 @@
+
diff --git a/resources/icons/file/heex_dark.svg b/resources/icons/file/heex_dark.svg
new file mode 100755
index 000000000..003de668a
--- /dev/null
+++ b/resources/icons/file/heex_dark.svg
@@ -0,0 +1,17 @@
+
diff --git a/src/org/elixir_lang/HEEx.bnf b/src/org/elixir_lang/HEEx.bnf
new file mode 100644
index 000000000..5ea147864
--- /dev/null
+++ b/src/org/elixir_lang/HEEx.bnf
@@ -0,0 +1,40 @@
+{
+ // CANNOT be called `Parser` because `
+ parserClass="org.elixir_lang.heex.Parser"
+ parserUtilClass="org.elixir_lang.heex.HEExParserUtil"
+
+ extends="com.intellij.extapi.psi.ASTWrapperPsiElement"
+
+ psiClassPrefix="HEEx"
+ psiImplClassSuffix="Impl"
+ psiPackage="org.elixir_lang.heex.psi"
+ psiImplPackage="org.elixir_lang.heex.psi.impl"
+
+ elementTypeHolderClass="org.elixir_lang.heex.psi.Types"
+ elementTypeClass="org.elixir_lang.heex.psi.ElementType"
+ tokenTypeClass="org.elixir_lang.heex.psi.TokenType"
+
+ tokens = [
+ CLOSING = "%>"
+ COMMENT = "Comment"
+ COMMENT_MARKER = "#"
+ DATA = "Data"
+ EMPTY_MARKER = "Empty Marker"
+ EQUALS_MARKER = "="
+ ELIXIR = "Elixir"
+ ESCAPED_OPENING = "<%%"
+ FORWARD_SLASH_MARKER = "/"
+ OPENING = "<%"
+ PIPE_MARKER = "|"
+ ]
+}
+
+private heexFile ::= (DATA | ESCAPED_OPENING | tag | braces)*
+tag ::= OPENING (commentBody | elixirBody) CLOSING
+ { pin = 1 }
+braces ::= BRACE_OPENING EQUALS_MARKER ELIXIR BRACE_CLOSING
+
+private commentBody ::= COMMENT_MARKER COMMENT?
+ { pin = 1 }
+private elixirBody ::= elixirMarker? ELIXIR?
+private elixirMarker ::= EMPTY_MARKER | EQUALS_MARKER | FORWARD_SLASH_MARKER | PIPE_MARKER
diff --git a/src/org/elixir_lang/HEEx.flex b/src/org/elixir_lang/HEEx.flex
new file mode 100644
index 000000000..b80a24952
--- /dev/null
+++ b/src/org/elixir_lang/HEEx.flex
@@ -0,0 +1,135 @@
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+import kotlinx.html.SCRIPT;import org.elixir_lang.heex.psi.Types;
+
+%%
+
+// public instead of package-local to make testing easier.
+%public
+%class Flex
+%implements com.intellij.lexer.FlexLexer
+%unicode
+%ignorecase
+%function advance
+%type IElementType
+%eof{ return;
+%eof}
+
+%{
+ private int openBraceCount = 0;
+
+ private void handleInState(int nextLexicalState) {
+ yypushback(yylength());
+ yybegin(nextLexicalState);
+ }
+%}
+
+OPENING = "<%"
+CLOSING = "%>"
+
+BRACE_OPENING = "{"
+BRACE_CLOSING = "}"
+
+COMMENT_MARKER = "#"
+EQUALS_MARKER = "="
+// See https://github.com/elixir-lang/elixir/pull/6281
+FORWARD_SLASH_MARKER = "/"
+PIPE_MARKER = "|"
+ESCAPED_OPENING = "<%%"
+PROCEDURAL_OPENING = {OPENING} " "
+
+WHITE_SPACE = [\ \t\f\r\n]+
+ANY = [^]
+
+START_SCRIPT_TAG = "
+START_STYLE_TAG = "
+
+%state WHITESPACE_MAYBE
+%state COMMENT
+%state ELIXIR
+%state MARKER_MAYBE
+%state BEGIN_MATCHED_BRACES, MATCHED_BRACES
+%state STYLE_TAG,SCRIPT_TAG
+
+%%
+
+ {
+ {BRACE_OPENING} { yybegin(BEGIN_MATCHED_BRACES);
+ return Types.BRACE_OPENING; }
+ {START_SCRIPT_TAG} { yybegin(SCRIPT_TAG); return Types.DATA; }
+ {START_STYLE_TAG} { yybegin(STYLE_TAG); return Types.DATA; }
+}
+
+ {
+ {END_SCRIPT_TAG} { yybegin(YYINITIAL); return Types.DATA; }
+}
+
+ {
+ {END_STYLE_TAG} { yybegin(YYINITIAL); return Types.DATA; }
+}
+
+ {
+ {ESCAPED_OPENING} { return Types.ESCAPED_OPENING; }
+ {OPENING} { yybegin(MARKER_MAYBE);
+ return Types.OPENING; }
+ {ANY} { return Types.DATA; }
+}
+
+ {
+ {COMMENT_MARKER} { yybegin(COMMENT);
+ return Types.COMMENT_MARKER; }
+ {EQUALS_MARKER} { yybegin(ELIXIR);
+ return Types.EQUALS_MARKER; }
+ {FORWARD_SLASH_MARKER} { yybegin(ELIXIR);
+ return Types.FORWARD_SLASH_MARKER; }
+ {PIPE_MARKER} { yybegin(ELIXIR);
+ return Types.PIPE_MARKER; }
+ {ANY} { handleInState(ELIXIR);
+ return Types.EMPTY_MARKER; }
+}
+
+ {
+ // We pretend there is an equals marker so it looks like a <%= tag to the Elixir parser
+ {ANY} { handleInState(MATCHED_BRACES);
+ return Types.EQUALS_MARKER; }
+}
+
+ {
+ {BRACE_OPENING} { openBraceCount++;
+ return Types.ELIXIR; }
+ {BRACE_CLOSING} {
+ if (openBraceCount > 0) {
+ openBraceCount--;
+ return Types.ELIXIR;
+ } else {
+ yybegin(YYINITIAL);
+ return Types.BRACE_CLOSING;
+ }
+ }
+ {ANY} { return Types.ELIXIR; }
+}
+
+ {
+ {CLOSING} { yybegin(WHITESPACE_MAYBE);
+ return Types.CLOSING; }
+}
+
+ {
+ {ANY} { return Types.COMMENT; }
+}
+
+ {
+ {ANY} { return Types.ELIXIR; }
+}
+
+ {
+ // Only completely whitespace before a procedural tag counts as whitespace
+ {WHITE_SPACE} / {PROCEDURAL_OPENING} { yybegin(YYINITIAL);
+ return TokenType.WHITE_SPACE; }
+ {ANY} { handleInState(YYINITIAL); }
+}
+
diff --git a/src/org/elixir_lang/HEEx.kt b/src/org/elixir_lang/HEEx.kt
new file mode 100644
index 000000000..7050151fb
--- /dev/null
+++ b/src/org/elixir_lang/HEEx.kt
@@ -0,0 +1,27 @@
+package org.elixir_lang
+
+import com.intellij.psi.ResolveState
+import org.elixir_lang.psi.call.Call
+
+object HEEx {
+ fun isFunctionFrom(call: Call, state: ResolveState): Boolean =
+ call.functionName()?.let { functionName ->
+ when (functionName) {
+ FUNCTION_FROM_FILE_ARITY_RANGE.name ->
+ call.resolvedFinalArity() in FUNCTION_FROM_FILE_ARITY_RANGE.arityRange &&
+ resolvesToHEEx(call, state)
+ FUNCTION_FROM_STRING_ARITY_RANGE.name ->
+ call.resolvedFinalArity() in FUNCTION_FROM_STRING_ARITY_RANGE.arityRange &&
+ resolvesToHEEx(call, state)
+ else -> false
+ }
+ } ?: false
+
+ private fun resolvesToHEEx(call: Call, state: ResolveState): Boolean =
+ resolvesToModularName(call, state, "HEEx")
+
+ // function_from_file(kind, name, file, args \\ [], options \\ [])
+ val FUNCTION_FROM_FILE_ARITY_RANGE = NameArityRange("function_from_file", 3..5)
+ // function_from_string(kind, name, source, args \\ [], options \\ [])
+ val FUNCTION_FROM_STRING_ARITY_RANGE = NameArityRange("function_from_string", 3..5)
+}
diff --git a/src/org/elixir_lang/heex/ElementType.java b/src/org/elixir_lang/heex/ElementType.java
new file mode 100644
index 000000000..2d9fbe274
--- /dev/null
+++ b/src/org/elixir_lang/heex/ElementType.java
@@ -0,0 +1,12 @@
+package org.elixir_lang.heex;
+
+
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.NotNull;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/parsing/HbElementType.java
+public class ElementType extends IElementType {
+ public ElementType(@NotNull String debugName) {
+ super(debugName, HeexLanguage.INSTANCE);
+ }
+}
diff --git a/src/org/elixir_lang/heex/File.java b/src/org/elixir_lang/heex/File.java
new file mode 100644
index 000000000..cc2588029
--- /dev/null
+++ b/src/org/elixir_lang/heex/File.java
@@ -0,0 +1,25 @@
+package org.elixir_lang.heex;
+
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.psi.FileViewProvider;
+import org.elixir_lang.heex.file.Type;
+import org.jetbrains.annotations.NotNull;
+
+public class File extends PsiFileBase {
+ public File(@NotNull FileViewProvider fileViewProvider) {
+ super(fileViewProvider, HeexLanguage.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public FileType getFileType() {
+ return Type.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return "HTML Embedded Elixir File";
+ }
+}
diff --git a/src/org/elixir_lang/heex/HEExParserUtil.java b/src/org/elixir_lang/heex/HEExParserUtil.java
new file mode 100644
index 000000000..136440cf5
--- /dev/null
+++ b/src/org/elixir_lang/heex/HEExParserUtil.java
@@ -0,0 +1,6 @@
+package org.elixir_lang.heex;
+
+import com.intellij.lang.parser.GeneratedParserUtilBase;
+
+public class HEExParserUtil extends GeneratedParserUtilBase {
+}
diff --git a/src/org/elixir_lang/heex/HeexLanguage.java b/src/org/elixir_lang/heex/HeexLanguage.java
new file mode 100644
index 000000000..e5c423730
--- /dev/null
+++ b/src/org/elixir_lang/heex/HeexLanguage.java
@@ -0,0 +1,28 @@
+package org.elixir_lang.heex;
+
+import com.intellij.openapi.fileTypes.FileTypes;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.psi.templateLanguages.TemplateLanguage;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/HbLanguage.java
+public class HeexLanguage extends com.intellij.lang.Language implements TemplateLanguage {
+ public static final HeexLanguage INSTANCE = new HeexLanguage();
+
+ protected HeexLanguage(@Nullable com.intellij.lang.Language baseLanguage,
+ @NotNull String ID,
+ @NotNull String... mimeTypes) {
+ super(baseLanguage, ID, mimeTypes);
+ }
+
+ public HeexLanguage() {
+ super("HEEx");
+ }
+
+ @Contract(pure = true)
+ public static LanguageFileType defaultTemplateLanguageFileType() {
+ return FileTypes.PLAIN_TEXT;
+ }
+}
diff --git a/src/org/elixir_lang/heex/Highlighter.java b/src/org/elixir_lang/heex/Highlighter.java
new file mode 100644
index 000000000..75cccb2ba
--- /dev/null
+++ b/src/org/elixir_lang/heex/Highlighter.java
@@ -0,0 +1,23 @@
+package org.elixir_lang.heex;
+
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.heex.lexer.LookAhead;
+import org.jetbrains.annotations.NotNull;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/HbHighlighter.java
+public class Highlighter extends SyntaxHighlighterBase {
+ @NotNull
+ @Override
+ public Lexer getHighlightingLexer() {
+ return new LookAhead();
+ }
+
+ @NotNull
+ @Override
+ public TextAttributesKey[] getTokenHighlights(IElementType tokenType) {
+ return new TextAttributesKey[0];
+ }
+}
diff --git a/src/org/elixir_lang/heex/Icons.kt b/src/org/elixir_lang/heex/Icons.kt
new file mode 100644
index 000000000..fe8426126
--- /dev/null
+++ b/src/org/elixir_lang/heex/Icons.kt
@@ -0,0 +1,8 @@
+package org.elixir_lang.heex
+
+import com.intellij.openapi.util.IconLoader
+
+object Icons {
+ @JvmField
+ val FILE = IconLoader.getIcon("/icons/file/heex.svg", Icons.javaClass)
+}
diff --git a/src/org/elixir_lang/heex/Parser.java b/src/org/elixir_lang/heex/Parser.java
new file mode 100644
index 000000000..4e7cec877
--- /dev/null
+++ b/src/org/elixir_lang/heex/Parser.java
@@ -0,0 +1,156 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex;
+
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.PsiBuilder.Marker;
+import static org.elixir_lang.heex.psi.Types.*;
+import static org.elixir_lang.heex.HEExParserUtil.*;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.lang.PsiParser;
+import com.intellij.lang.LightPsiParser;
+
+@SuppressWarnings({"SimplifiableIfStatement", "UnusedAssignment"})
+public class Parser implements PsiParser, LightPsiParser {
+
+ public ASTNode parse(IElementType t, PsiBuilder b) {
+ parseLight(t, b);
+ return b.getTreeBuilt();
+ }
+
+ public void parseLight(IElementType t, PsiBuilder b) {
+ boolean r;
+ b = adapt_builder_(t, b, this, null);
+ Marker m = enter_section_(b, 0, _COLLAPSE_, null);
+ r = parse_root_(t, b);
+ exit_section_(b, 0, m, t, r, true, TRUE_CONDITION);
+ }
+
+ protected boolean parse_root_(IElementType t, PsiBuilder b) {
+ return parse_root_(t, b, 0);
+ }
+
+ static boolean parse_root_(IElementType t, PsiBuilder b, int l) {
+ return heexFile(b, l + 1);
+ }
+
+ /* ********************************************************** */
+ // BRACE_OPENING EQUALS_MARKER ELIXIR BRACE_CLOSING
+ public static boolean braces(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "braces")) return false;
+ if (!nextTokenIs(b, BRACE_OPENING)) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = consumeTokens(b, 0, BRACE_OPENING, EQUALS_MARKER, ELIXIR, BRACE_CLOSING);
+ exit_section_(b, m, BRACES, r);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // COMMENT_MARKER COMMENT?
+ static boolean commentBody(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "commentBody")) return false;
+ if (!nextTokenIs(b, COMMENT_MARKER)) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_);
+ r = consumeToken(b, COMMENT_MARKER);
+ p = r; // pin = 1
+ r = r && commentBody_1(b, l + 1);
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ // COMMENT?
+ private static boolean commentBody_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "commentBody_1")) return false;
+ consumeToken(b, COMMENT);
+ return true;
+ }
+
+ /* ********************************************************** */
+ // elixirMarker? ELIXIR?
+ static boolean elixirBody(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "elixirBody")) return false;
+ boolean r;
+ Marker m = enter_section_(b);
+ r = elixirBody_0(b, l + 1);
+ r = r && elixirBody_1(b, l + 1);
+ exit_section_(b, m, null, r);
+ return r;
+ }
+
+ // elixirMarker?
+ private static boolean elixirBody_0(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "elixirBody_0")) return false;
+ elixirMarker(b, l + 1);
+ return true;
+ }
+
+ // ELIXIR?
+ private static boolean elixirBody_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "elixirBody_1")) return false;
+ consumeToken(b, ELIXIR);
+ return true;
+ }
+
+ /* ********************************************************** */
+ // EMPTY_MARKER | EQUALS_MARKER | FORWARD_SLASH_MARKER | PIPE_MARKER
+ static boolean elixirMarker(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "elixirMarker")) return false;
+ boolean r;
+ r = consumeToken(b, EMPTY_MARKER);
+ if (!r) r = consumeToken(b, EQUALS_MARKER);
+ if (!r) r = consumeToken(b, FORWARD_SLASH_MARKER);
+ if (!r) r = consumeToken(b, PIPE_MARKER);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // (DATA | ESCAPED_OPENING | tag | braces)*
+ static boolean heexFile(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "heexFile")) return false;
+ while (true) {
+ int c = current_position_(b);
+ if (!heexFile_0(b, l + 1)) break;
+ if (!empty_element_parsed_guard_(b, "heexFile", c)) break;
+ }
+ return true;
+ }
+
+ // DATA | ESCAPED_OPENING | tag | braces
+ private static boolean heexFile_0(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "heexFile_0")) return false;
+ boolean r;
+ r = consumeToken(b, DATA);
+ if (!r) r = consumeToken(b, ESCAPED_OPENING);
+ if (!r) r = tag(b, l + 1);
+ if (!r) r = braces(b, l + 1);
+ return r;
+ }
+
+ /* ********************************************************** */
+ // OPENING (commentBody | elixirBody) CLOSING
+ public static boolean tag(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "tag")) return false;
+ if (!nextTokenIs(b, OPENING)) return false;
+ boolean r, p;
+ Marker m = enter_section_(b, l, _NONE_, TAG, null);
+ r = consumeToken(b, OPENING);
+ p = r; // pin = 1
+ r = r && report_error_(b, tag_1(b, l + 1));
+ r = p && consumeToken(b, CLOSING) && r;
+ exit_section_(b, l, m, r, p, null);
+ return r || p;
+ }
+
+ // commentBody | elixirBody
+ private static boolean tag_1(PsiBuilder b, int l) {
+ if (!recursion_guard_(b, l, "tag_1")) return false;
+ boolean r;
+ r = commentBody(b, l + 1);
+ if (!r) r = elixirBody(b, l + 1);
+ return r;
+ }
+
+}
diff --git a/src/org/elixir_lang/heex/ParserDefinition.java b/src/org/elixir_lang/heex/ParserDefinition.java
new file mode 100644
index 000000000..ea6a1df04
--- /dev/null
+++ b/src/org/elixir_lang/heex/ParserDefinition.java
@@ -0,0 +1,74 @@
+package org.elixir_lang.heex;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+import org.elixir_lang.heex.File;
+import org.elixir_lang.heex.Parser;
+import org.elixir_lang.heex.file.ElementType;
+import org.elixir_lang.heex.lexer.LookAhead;
+import org.elixir_lang.heex.psi.Types;
+import org.jetbrains.annotations.NotNull;
+
+public class ParserDefinition implements com.intellij.lang.ParserDefinition {
+ private static final TokenSet COMMENT_TOKENS = TokenSet.create(Types.COMMENT);
+ private static final TokenSet STRING_LITERAL_ELEMENTS = TokenSet.EMPTY;
+ private static final TokenSet WHITESPACE_TOKENS = TokenSet.create(TokenType.WHITE_SPACE);
+
+ @NotNull
+ @Override
+ public Lexer createLexer(Project project) {
+ return new LookAhead();
+ }
+
+ @Override
+ public PsiParser createParser(Project project) {
+ return new Parser();
+ }
+
+ @Override
+ public IFileElementType getFileNodeType() {
+ return ElementType.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getWhitespaceTokens() {
+ return WHITESPACE_TOKENS;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getCommentTokens() {
+ return COMMENT_TOKENS;
+ }
+
+ @NotNull
+ @Override
+ public TokenSet getStringLiteralElements() {
+ return STRING_LITERAL_ELEMENTS;
+ }
+
+ @NotNull
+ @Override
+ public PsiElement createElement(ASTNode astNode) {
+ return Types.Factory.createElement(astNode);
+ }
+
+ @Override
+ public PsiFile createFile(FileViewProvider fileViewProvider) {
+ return new File(fileViewProvider);
+ }
+
+ @Override
+ public SpaceRequirements spaceExistenceTypeBetweenTokens(ASTNode astNode, ASTNode astNode1) {
+ return SpaceRequirements.MUST_NOT;
+ }
+}
diff --git a/src/org/elixir_lang/heex/TemplateHighlighter.java b/src/org/elixir_lang/heex/TemplateHighlighter.java
new file mode 100644
index 000000000..4480b33ed
--- /dev/null
+++ b/src/org/elixir_lang/heex/TemplateHighlighter.java
@@ -0,0 +1,68 @@
+package org.elixir_lang.heex;
+
+import com.intellij.ide.highlighter.HtmlFileType;
+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.FileTypes;
+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.elixir_lang.ElixirFileType;
+import org.elixir_lang.heex.html.HeexHTMLFileHighlighter;
+import org.elixir_lang.heex.psi.Types;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import static org.elixir_lang.heex.file.Type.onlyTemplateDataFileType;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/HbTemplateHighlighter.java
+public class TemplateHighlighter extends LayeredLexerEditorHighlighter {
+ public TemplateHighlighter(@Nullable Project project,
+ @Nullable VirtualFile virtualFile,
+ @NotNull EditorColorsScheme editorColorsScheme) {
+ // create main highlighter
+ super(new Highlighter(), editorColorsScheme);
+
+ // highlighter for outer lang
+ FileType type = null;
+
+ if (project == null || virtualFile == null) {
+ type = FileTypes.PLAIN_TEXT;
+ } else {
+ com.intellij.lang.Language language =
+ TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile);
+
+ if (language != null) {
+ type = language.getAssociatedFileType();
+ }
+
+ if (type == null) {
+ type = onlyTemplateDataFileType(virtualFile).orElse(null);
+ }
+
+ if (type == null) {
+ type = HeexLanguage.defaultTemplateLanguageFileType();
+ }
+ }
+
+ SyntaxHighlighter dataHighlighter;
+ if (type == HtmlFileType.INSTANCE) {
+ dataHighlighter = new HeexHTMLFileHighlighter();
+ } else {
+ dataHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(type, project, virtualFile);
+ }
+ registerLayer(Types.DATA, new LayerDescriptor(dataHighlighter, ""));
+
+ SyntaxHighlighter elixirHighligher = SyntaxHighlighterFactory.getSyntaxHighlighter(ElixirFileType.INSTANCE, project, virtualFile);
+ registerLayer(Types.ELIXIR, new LayerDescriptor(elixirHighligher, ""));
+ }
+
+ @Override
+ protected boolean updateLayers() {
+ return true;
+ }
+}
diff --git a/src/org/elixir_lang/heex/element_type/Factory.java b/src/org/elixir_lang/heex/element_type/Factory.java
new file mode 100644
index 000000000..dfa6317fd
--- /dev/null
+++ b/src/org/elixir_lang/heex/element_type/Factory.java
@@ -0,0 +1,12 @@
+package org.elixir_lang.heex.element_type;
+
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.ElementTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+public class Factory {
+ @NotNull
+ public static IElementType factory(@NotNull String name) {
+ return ElementTypeFactory.factory("org.elixir_lang.heex.psi.stub.type", name);
+ }
+}
diff --git a/src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java b/src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java
new file mode 100644
index 000000000..f231cfc70
--- /dev/null
+++ b/src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java
@@ -0,0 +1,39 @@
+package org.elixir_lang.heex.element_type;
+
+import com.intellij.lang.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.*;
+import com.intellij.psi.tree.IFileElementType;
+import org.elixir_lang.ElixirLanguage;
+
+/**
+ * Both Elixir and enough of the HEEx tags and {@link org.elixir_lang.heex.psi.Types#DATA}, so that Elixir parses
+ * correctly, such as separating {@link org.elixir_lang.heex.psi.Types#ELIXIR} inside {@code <%= %>} tags, so that the
+ * Elixir.bnf parses it like it was an interpolated expression separated by an outer string instead of adjacent Elixir
+ * expressions.
+ */
+public class HTMLEmbeddedElixir extends IFileElementType {
+ public HTMLEmbeddedElixir() {
+ super(ElixirLanguage.INSTANCE);
+ }
+
+ @Override
+ public ASTNode parseContents(ASTNode chameleon) {
+ PsiElement psi = chameleon.getPsi();
+
+ assert psi != null : "Bad chameleon: " + chameleon;
+
+ Project project = psi.getProject();
+ Language languageForParser = this.getLanguageForParser(psi);
+ PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(
+ project,
+ chameleon,
+ new org.elixir_lang.heex.lexer.HTMLEmbeddedElixir(project),
+ languageForParser,
+ chameleon.getChars()
+ );
+ PsiParser parser = LanguageParserDefinitions.INSTANCE.forLanguage(languageForParser).createParser(project);
+ ASTNode node = parser.parse(this, builder);
+ return node.getFirstChildNode();
+ }
+}
diff --git a/src/org/elixir_lang/heex/file/ElementType.java b/src/org/elixir_lang/heex/file/ElementType.java
new file mode 100644
index 000000000..7a0d465e2
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/ElementType.java
@@ -0,0 +1,58 @@
+package org.elixir_lang.heex.file;
+
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.StubBuilder;
+import com.intellij.psi.stubs.DefaultStubBuilder;
+import com.intellij.psi.stubs.StubElement;
+import com.intellij.psi.stubs.StubInputStream;
+import com.intellij.psi.stubs.StubOutputStream;
+import com.intellij.psi.tree.IStubFileElementType;
+import org.elixir_lang.heex.File;
+import org.elixir_lang.heex.HeexLanguage;
+import org.elixir_lang.heex.file.psi.Stub;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+
+public class ElementType extends IStubFileElementType {
+ public static final IStubFileElementType INSTANCE = new ElementType();
+
+ public ElementType() {
+ super("HEEX_FILE", HeexLanguage.INSTANCE);
+ }
+
+ @Override
+ public StubBuilder getBuilder() {
+ return new DefaultStubBuilder() {
+ @Override
+ protected StubElement createStubForFile(@NotNull PsiFile psiFile) {
+ StubElement stubElement;
+
+ if (psiFile instanceof File) {
+ stubElement = new Stub((File) psiFile);
+ } else {
+ stubElement = super.createStubForFile(psiFile);
+ }
+
+ return stubElement;
+ }
+ };
+ }
+
+ @NotNull
+ @Override
+ public String getExternalId() {
+ return "elixir.html_embedded.FILE";
+ }
+
+ @Override
+ public void serialize(@NotNull Stub stub, @NotNull StubOutputStream dataStream) {
+ }
+
+ @NotNull
+ @Override
+ public Stub deserialize(@NotNull StubInputStream dataStream, StubElement parentStub) throws IOException {
+ return new Stub(null);
+ }
+
+}
diff --git a/src/org/elixir_lang/heex/file/Type.kt b/src/org/elixir_lang/heex/file/Type.kt
new file mode 100644
index 000000000..248258a53
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/Type.kt
@@ -0,0 +1,71 @@
+package org.elixir_lang.heex.file
+
+import com.intellij.lang.Language
+import com.intellij.openapi.editor.colors.EditorColorsScheme
+import com.intellij.openapi.fileTypes.*
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import org.elixir_lang.heex.HeexLanguage
+import org.elixir_lang.heex.Icons
+import org.elixir_lang.heex.TemplateHighlighter
+import java.util.*
+import java.util.stream.Collectors
+import javax.swing.Icon
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/file/HbFileType.java
+open class Type protected constructor(lang: Language? = HeexLanguage.INSTANCE) :
+ LanguageFileType(lang!!), TemplateLanguageFileType {
+ override fun getName(): String = "HTML Embedded Elixir"
+ override fun getDescription(): String = "HTML Embedded Elixir file"
+ override fun getDefaultExtension(): String = DEFAULT_EXTENSION
+ override fun getIcon(): Icon? = Icons.FILE
+
+ companion object {
+ private const val DEFAULT_EXTENSION = "heex"
+
+ @JvmField
+ val INSTANCE: LanguageFileType = Type()
+ private fun templateDataFileTypeSet(virtualFile: VirtualFile): Set {
+ val path = virtualFile.path
+ val pathLength = path.length
+ val fileTypeManager = FileTypeManager.getInstance()
+ return fileTypeManager
+ .getAssociations(virtualFile.fileType)
+ .stream()
+ .filter { obj: FileNameMatcher? -> ExtensionFileNameMatcher::class.java.isInstance(obj) }
+ .map { obj: FileNameMatcher? -> ExtensionFileNameMatcher::class.java.cast(obj) }
+ .map { obj: ExtensionFileNameMatcher -> obj.extension }
+ .map { extension: String -> ".$extension" }
+ .filter { suffix: String? -> path.endsWith(suffix!!) }
+ .map { dotExtension: String -> path.substring(0, pathLength - dotExtension.length) }
+ .map { fileName: String? -> fileTypeManager.getFileTypeByFileName(fileName!!) }
+ .collect(Collectors.toSet())
+ }
+
+ @JvmStatic
+ fun onlyTemplateDataFileType(virtualFile: VirtualFile): Optional =
+ templateDataFileTypeSet(virtualFile)
+ .singleOrNull()
+ ?.let { type ->
+ if (type === FileTypes.UNKNOWN) {
+ null
+ } else {
+ Optional.of(type)
+ }
+ }
+ ?: Optional.empty()
+ }
+
+ init {
+ FileTypeEditorHighlighterProviders.INSTANCE.addExplicitExtension(
+ this,
+ EditorHighlighterProvider { project: Project?, _: FileType?, virtualFile: VirtualFile?, editorColorsScheme: EditorColorsScheme? ->
+ TemplateHighlighter(
+ project,
+ virtualFile,
+ editorColorsScheme!!
+ )
+ }
+ )
+ }
+}
diff --git a/src/org/elixir_lang/heex/file/ViewProvider.java b/src/org/elixir_lang/heex/file/ViewProvider.java
new file mode 100644
index 000000000..9def4f387
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/ViewProvider.java
@@ -0,0 +1,155 @@
+package org.elixir_lang.heex.file;
+
+import com.intellij.lang.LanguageParserDefinitions;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.LanguageSubstitutors;
+import com.intellij.psi.MultiplePsiFilesPerDocumentFileViewProvider;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.source.PsiFileImpl;
+import com.intellij.psi.templateLanguages.ConfigurableTemplateLanguageFileViewProvider;
+import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings;
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.ElixirLanguage;
+import org.elixir_lang.heex.HeexLanguage;
+import org.elixir_lang.heex.element_type.HTMLEmbeddedElixir;
+import org.elixir_lang.heex.file.psi.TemplateData;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import static org.elixir_lang.heex.file.Type.onlyTemplateDataFileType;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/file/HbFileViewProvider.java
+public class ViewProvider extends MultiplePsiFilesPerDocumentFileViewProvider
+ implements ConfigurableTemplateLanguageFileViewProvider {
+ private static final ConcurrentMap ELEMENT_TYPE_BY_LANGUAGE_ID = new ConcurrentHashMap<>();
+ @NotNull
+ private final com.intellij.lang.Language baseLanguage;
+ @NotNull
+ private final com.intellij.lang.Language templateDataLanguage;
+
+ public ViewProvider(@NotNull PsiManager manager,
+ @NotNull VirtualFile file,
+ boolean physical,
+ @NotNull com.intellij.lang.Language baseLanguage,
+ @NotNull com.intellij.lang.Language templateLanguage) {
+ super(manager, file, physical);
+ this.baseLanguage = baseLanguage;
+ this.templateDataLanguage = templateLanguage;
+ }
+
+ public ViewProvider(@NotNull PsiManager psiManager,
+ @NotNull VirtualFile virtualFile,
+ boolean physical,
+ @NotNull com.intellij.lang.Language baseLanguage) {
+ this(psiManager, virtualFile, physical, baseLanguage, templateDataLanguage(psiManager, virtualFile));
+ }
+
+ private static IElementType elementType(com.intellij.lang.Language language) {
+ return ELEMENT_TYPE_BY_LANGUAGE_ID.computeIfAbsent(
+ language.getID(),
+ languageID -> {
+ if (language == ElixirLanguage.INSTANCE) {
+ return new HTMLEmbeddedElixir();
+ } else {
+ return TemplateData.INSTANCE;
+ }
+ }
+ );
+ }
+
+ private static com.intellij.lang.Language templateDataLanguage(@NotNull PsiManager psiManager,
+ @NotNull VirtualFile virtualFile) {
+ Project project = psiManager.getProject();
+ com.intellij.lang.Language templateDataLanguage =
+ TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile);
+
+ if (templateDataLanguage == null) {
+ templateDataLanguage = onlyTemplateDataFileType(virtualFile)
+ .filter(LanguageFileType.class::isInstance)
+ .map(LanguageFileType.class::cast)
+ .map(LanguageFileType::getLanguage)
+ .orElse(null);
+ }
+
+ if (templateDataLanguage == null) {
+ templateDataLanguage = HeexLanguage.defaultTemplateLanguageFileType().getLanguage();
+ }
+
+ com.intellij.lang.Language substituteLang =
+ LanguageSubstitutors.getInstance().substituteLanguage(templateDataLanguage, virtualFile, project);
+
+ // only use a substituted language if it's templateable
+ if (TemplateDataLanguageMappings.getTemplateableLanguages().contains(substituteLang)) {
+ templateDataLanguage = substituteLang;
+ }
+
+ return templateDataLanguage;
+ }
+
+ @Nullable
+ @Override
+ protected PsiFile createFile(@NotNull com.intellij.lang.Language language) {
+ ParserDefinition parserDefinition;
+ PsiFileImpl psiFileImpl;
+
+ parserDefinition = getDefinition(language);
+
+ if (parserDefinition == null) {
+ psiFileImpl = null;
+ } else if (language.isKindOf(getBaseLanguage())) {
+ psiFileImpl = (PsiFileImpl) parserDefinition.createFile(this);
+ } else {
+ psiFileImpl = (PsiFileImpl) parserDefinition.createFile(this);
+ psiFileImpl.setContentElementType(elementType(language));
+ }
+
+ return psiFileImpl;
+ }
+
+ @Nullable
+ private ParserDefinition getDefinition(@NotNull com.intellij.lang.Language language) {
+ com.intellij.lang.Language baseLanguage = getBaseLanguage();
+
+ if (language.isKindOf(baseLanguage)) {
+ language = baseLanguage;
+ }
+
+ return LanguageParserDefinitions.INSTANCE.forLanguage(language);
+ }
+
+ @NotNull
+ @Override
+ public com.intellij.lang.Language getBaseLanguage() {
+ return baseLanguage;
+ }
+
+ @NotNull
+ @Override
+ public Set getLanguages() {
+ return Set.of(getTemplateDataLanguage(), getBaseLanguage(), ElixirLanguage.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public com.intellij.lang.Language getTemplateDataLanguage() {
+ return templateDataLanguage;
+ }
+
+ @Override
+ protected @NotNull MultiplePsiFilesPerDocumentFileViewProvider cloneInner(@NotNull VirtualFile fileCopy) {
+ return new ViewProvider(getManager(), fileCopy, false, baseLanguage, templateDataLanguage);
+ }
+
+ @Override
+ public boolean supportsIncrementalReparse(@NotNull com.intellij.lang.Language rootLanguage) {
+ return false;
+ }
+}
diff --git a/src/org/elixir_lang/heex/file/psi/Stub.java b/src/org/elixir_lang/heex/file/psi/Stub.java
new file mode 100644
index 000000000..041060ca7
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/psi/Stub.java
@@ -0,0 +1,19 @@
+package org.elixir_lang.heex.file.psi;
+
+import com.intellij.psi.stubs.PsiFileStubImpl;
+import com.intellij.psi.tree.IStubFileElementType;
+import org.elixir_lang.heex.file.ElementType;
+import org.elixir_lang.heex.File;
+import org.jetbrains.annotations.NotNull;
+
+public class Stub extends PsiFileStubImpl {
+ public Stub(File file) {
+ super(file);
+ }
+
+ @NotNull
+ @Override
+ public IStubFileElementType getType() {
+ return ElementType.INSTANCE;
+ }
+}
diff --git a/src/org/elixir_lang/heex/file/psi/TemplateData.java b/src/org/elixir_lang/heex/file/psi/TemplateData.java
new file mode 100644
index 000000000..c49645d89
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/psi/TemplateData.java
@@ -0,0 +1,58 @@
+package org.elixir_lang.heex.file.psi;
+
+import com.intellij.lang.Language;
+import com.intellij.lang.html.HTMLLanguage;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.SingleRootFileViewProvider;
+import com.intellij.psi.impl.source.html.HtmlFileImpl;
+import com.intellij.psi.templateLanguages.TemplateDataElementType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.testFramework.LightVirtualFile;
+import org.elixir_lang.heex.HeexLanguage;
+import org.elixir_lang.heex.html.HeexHTMLFileElementType;
+import org.elixir_lang.heex.html.HeexHTMLLanguage;
+import org.elixir_lang.heex.psi.Types;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class TemplateData extends TemplateDataElementType {
+ public static final TemplateData INSTANCE = new TemplateData(
+ "HEEX_TEMPLATE_DATA",
+ HeexLanguage.INSTANCE,
+ Types.DATA,
+ Types.HEEX_OUTER_ELEMENT
+ );
+
+ protected TemplateData(@NonNls String debugName, Language language, @NotNull IElementType templateElementType, @NotNull IElementType outerElementType) {
+ super(debugName, language, templateElementType, outerElementType);
+ }
+
+ @Override
+ protected boolean isInsertionToken(@Nullable IElementType tokenType, @NotNull CharSequence tokenSequence) {
+ return true;
+ }
+
+ @Override
+ protected PsiFile createPsiFileFromSource(final Language language, CharSequence sourceCode, PsiManager manager) {
+ if (language == HTMLLanguage.INSTANCE) {
+ return createSpoofedPsiFileForHTML(sourceCode, manager);
+ }
+
+ return super.createPsiFileFromSource(language, sourceCode, manager);
+ }
+
+ /** For HTML, we manually create the PSI file so we can force it to use our custom lexer */
+ private PsiFile createSpoofedPsiFileForHTML(CharSequence sourceCode, PsiManager manager) {
+ LightVirtualFile virtualFile = new LightVirtualFile("HEExHTML", this.createTemplateFakeFileType(HeexHTMLLanguage.INSTANCE), sourceCode);
+ FileViewProvider viewProvider = new SingleRootFileViewProvider(manager, virtualFile, false) {
+ public @NotNull Language getBaseLanguage() {
+ return HTMLLanguage.INSTANCE;
+ }
+ };
+
+ return new HtmlFileImpl(viewProvider, HeexHTMLFileElementType.INSTANCE);
+ }
+}
diff --git a/src/org/elixir_lang/heex/file/view_provider/Factory.java b/src/org/elixir_lang/heex/file/view_provider/Factory.java
new file mode 100644
index 000000000..99594a2d4
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/view_provider/Factory.java
@@ -0,0 +1,22 @@
+package org.elixir_lang.heex.file.view_provider;
+
+import com.intellij.lang.Language;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiManager;
+import org.elixir_lang.heex.HeexLanguage;
+import org.elixir_lang.heex.file.ViewProvider;
+import org.jetbrains.annotations.NotNull;
+
+// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/file/HbFileViewProviderFactory.java
+public class Factory implements com.intellij.psi.FileViewProviderFactory {
+ @NotNull
+ @Override
+ public FileViewProvider createFileViewProvider(@NotNull VirtualFile virtualFile,
+ @NotNull Language language,
+ @NotNull PsiManager psiManager,
+ boolean eventSystemEnabled) {
+ assert language.isKindOf(HeexLanguage.INSTANCE);
+ return new ViewProvider(psiManager, virtualFile, eventSystemEnabled, language);
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
new file mode 100644
index 000000000..c20c7550d
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
@@ -0,0 +1,32 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lang.*;
+import com.intellij.lang.html.HTMLLanguage;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.xml.HtmlFileElementType;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLFileElementType extends HtmlFileElementType {
+ public static final HeexHTMLFileElementType INSTANCE = new HeexHTMLFileElementType();
+
+ /** @see com.intellij.psi.tree.ILazyParseableElementType#doParseContents */
+ @Override
+ public ASTNode parseContents(ASTNode chameleon) {
+ PsiElement psi = chameleon.getPsi();
+
+ assert psi != null : "Bad chameleon: " + chameleon;
+
+ Project project = psi.getProject();
+ PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, new HeexHTMLLexer(), HTMLLanguage.INSTANCE, chameleon.getChars());
+ PsiParser parser = (LanguageParserDefinitions.INSTANCE.forLanguage(HTMLLanguage.INSTANCE)).createParser(project);
+ ASTNode node = parser.parse(this, builder);
+
+ return node.getFirstChildNode();
+ }
+
+ @Override
+ public @NotNull Language getLanguage() {
+ return HTMLLanguage.INSTANCE;
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java
new file mode 100644
index 000000000..dcc5bfb7c
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java
@@ -0,0 +1,12 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.ide.highlighter.HtmlFileHighlighter;
+import com.intellij.lexer.Lexer;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLFileHighlighter extends HtmlFileHighlighter {
+ @Override
+ public @NotNull Lexer getHighlightingLexer() {
+ return new HeexHTMLLexer(true);
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java b/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
new file mode 100644
index 000000000..8bdc6258b
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
@@ -0,0 +1,11 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lang.html.HTMLLanguage;
+
+public class HeexHTMLLanguage extends HTMLLanguage {
+ public static final HeexHTMLLanguage INSTANCE = new HeexHTMLLanguage();
+
+ protected HeexHTMLLanguage() {
+ super(HTMLLanguage.INSTANCE, "HEExHTML", "text/html", "text/htmlh");
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLLexer.java b/src/org/elixir_lang/heex/html/HeexHTMLLexer.java
new file mode 100644
index 000000000..3ba79ccdb
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLLexer.java
@@ -0,0 +1,54 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lexer.HtmlLexer;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLLexer extends HtmlLexer {
+ public HeexHTMLLexer() {
+ super();
+ }
+
+ public HeexHTMLLexer(boolean highlightMode) {
+ super(highlightMode);
+ }
+
+ @Override
+ public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
+ CharSequence maskedBuffer = maskRelativeComponentDots(buffer, startOffset, endOffset);
+
+ super.start(maskedBuffer, 0, endOffset - startOffset, initialState);
+ }
+
+ /**
+ * The HTML lexer does not support tag names beginning with `.`. This method masks these dots by replacing with 'C',
+ * allowing the lexer to properly process HEEx relative component tags (e.g. <.button>).
+ */
+ private CharSequence maskRelativeComponentDots(@NotNull CharSequence buffer, int startOffset, int endOffset) {
+ int startIndex = startOffset;
+ StringBuilder stringBuilder = new StringBuilder(endOffset);
+
+ for (int i = startOffset; i < endOffset; i++) {
+ if (buffer.charAt(i) == '<') {
+ if (endOffset > i + 1 && buffer.charAt(i + 1) == '.') {
+ stringBuilder
+ .append(buffer.subSequence(startIndex, i + 1))
+ .append('C');
+
+ startIndex = i + 2;
+ i += 1;
+ } else if (endOffset > i + 2 && buffer.charAt(i + 1) == '/' && buffer.charAt(i + 2) == '.') {
+ stringBuilder
+ .append(buffer.subSequence(startIndex, i + 2))
+ .append('C');
+
+ startIndex = i + 3;
+ i += 2;
+ }
+ }
+ }
+
+ stringBuilder.append(buffer.subSequence(startIndex, endOffset));
+
+ return stringBuilder;
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java
new file mode 100644
index 000000000..f888b7eaf
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java
@@ -0,0 +1,12 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.psi.templateLanguages.TemplateDataElementType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class HeexHTMLOuterLanguageRangePatcher implements TemplateDataElementType.OuterLanguageRangePatcher {
+ @Override
+ public @Nullable String getTextForOuterLanguageInsertionRange(@NotNull TemplateDataElementType templateDataElementType, @NotNull CharSequence charSequence) {
+ return "Injection";
+ }
+}
diff --git a/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
new file mode 100644
index 000000000..40f904901
--- /dev/null
+++ b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
@@ -0,0 +1,44 @@
+package org.elixir_lang.heex.inspections;
+
+import com.intellij.codeInspection.InspectionSuppressor;
+import com.intellij.codeInspection.SuppressQuickFix;
+import com.intellij.codeInspection.htmlInspections.HtmlUnknownTagInspection;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.psi.xml.XmlTag;
+import com.intellij.xml.util.CheckEmptyTagInspection;
+import org.elixir_lang.heex.HeexLanguage;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public class HTMLInspectionSuppressor implements InspectionSuppressor {
+ public static final List SUPPRESSED_INSPECTIONS = List.of(
+ new CheckEmptyTagInspection().getSuppressId(),
+ new HtmlUnknownTagInspection().getSuppressId()
+ );
+
+
+ public boolean isSuppressedFor(@NotNull PsiElement element, @NotNull String toolId) {
+ if (!SUPPRESSED_INSPECTIONS.contains(toolId)) {
+ return false;
+ }
+
+ PsiFile file = element.getContainingFile();
+ if (file != null && file.getViewProvider().hasLanguage(HeexLanguage.INSTANCE)) {
+ XmlTag xmlTag = PsiTreeUtil.getParentOfType(element, XmlTag.class, false);
+
+ // Tag names that contain dots are HEEx components
+ return xmlTag != null && xmlTag.getName().contains(".");
+ }
+
+ return false;
+ }
+
+ @Override
+ public SuppressQuickFix @NotNull [] getSuppressActions(@Nullable PsiElement psiElement, @NotNull String s) {
+ return SuppressQuickFix.EMPTY_ARRAY;
+ }
+}
diff --git a/src/org/elixir_lang/heex/lexer/Adapter.java b/src/org/elixir_lang/heex/lexer/Adapter.java
new file mode 100644
index 000000000..e274d532a
--- /dev/null
+++ b/src/org/elixir_lang/heex/lexer/Adapter.java
@@ -0,0 +1,12 @@
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.lexer.FlexAdapter;
+import org.elixir_lang.heex.lexer.Flex;
+
+import java.io.Reader;
+
+public class Adapter extends FlexAdapter {
+ public Adapter() {
+ super(new Flex((Reader) null));
+ }
+}
diff --git a/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
new file mode 100644
index 000000000..e7c10d396
--- /dev/null
+++ b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
@@ -0,0 +1,219 @@
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.lexer.Lexer;
+import com.intellij.lexer.LexerBase;
+import com.intellij.lexer.LexerPosition;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.tree.IElementType;
+import gnu.trove.THashMap;
+import org.elixir_lang.ElixirLanguage;
+import org.elixir_lang.ElixirLexer;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+import static com.intellij.psi.TokenType.BAD_CHARACTER;
+import static com.intellij.psi.TokenType.WHITE_SPACE;
+import static org.elixir_lang.heex.psi.Types.*;
+import static org.elixir_lang.heex.psi.Types.COMMENT;
+import static org.elixir_lang.psi.ElixirTypes.*;
+
+/**
+ * Like {@link com.intellij.lexer.LookAheadLexer}, but uses 2 base lexers. Since which base lexer is being used, we
+ * can't use LookAheadLexer since it's {@link com.intellij.lexer.LookAheadLexer.LookAheadLexerPosition} only works for a
+ * single lexer.
+ */
+public class HTMLEmbeddedElixir extends LexerBase {
+ @NotNull
+ private static final Map HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE = new THashMap<>();
+ @NotNull
+ final Lexer heexLexer;
+ @NotNull
+ final Lexer elixirLexer;
+
+ static {
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(BAD_CHARACTER, BAD_CHARACTER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(BRACE_CLOSING, EEX_CLOSING);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(BRACE_OPENING, EEX_OPENING);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(CLOSING, EEX_CLOSING);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(COMMENT, EEX_COMMENT);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(COMMENT_MARKER, EEX_COMMENT_MARKER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(DATA, EEX_DATA);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(ESCAPED_OPENING, EEX_ESCAPED_OPENING);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(EMPTY_MARKER, EEX_EMPTY_MARKER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(EQUALS_MARKER, EEX_EQUALS_MARKER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(FORWARD_SLASH_MARKER, EEX_FORWARD_SLASH_MARKER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(OPENING, EEX_OPENING);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(PIPE_MARKER, EEX_PIPE_MARKER);
+ HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(WHITE_SPACE, WHITE_SPACE);
+ }
+
+ public HTMLEmbeddedElixir(Project project) {
+ this.heexLexer = new LookAhead();
+ this.elixirLexer = new ElixirLexer(project);
+ }
+
+ @Contract(pure = true)
+ @Nullable
+ private static T forLanguage(@NotNull Lexer heexLexer, @NotNull T forHEEx, @Nullable T forElixir) {
+ return forLanguage(heexLexer.getTokenType(), forHEEx, forElixir);
+ }
+
+ @Contract(pure = true)
+ @Nullable
+ private static T forLanguage(@Nullable IElementType tokenType, @NotNull T forHEEx, @Nullable T forElixir) {
+ T forLanguage;
+
+ if (tokenType == ELIXIR) {
+ forLanguage = forElixir;
+ } else {
+ forLanguage = forHEEx;
+ }
+
+ return forLanguage;
+ }
+
+ @Contract(pure = true)
+ @NotNull
+ private Lexer lexer() {
+ //noinspection ConstantConditions
+ return forLanguage(heexLexer, heexLexer, elixirLexer);
+ }
+
+ public void advance() {
+ if (heexLexer.getTokenType() == ELIXIR) {
+ elixirLexer.advance();
+
+ if (elixirLexer.getTokenType() == null) {
+ heexLexer.advance();
+ }
+ } else {
+ heexLexer.advance();
+
+ if (heexLexer.getTokenType() == ELIXIR) {
+ // start automatically does equivalent of `advance` since `elixirLexer` is also a look-ahead lexer
+ elixirLexer.start(getBufferSequence(), heexLexer.getTokenStart(), heexLexer.getTokenEnd());
+ }
+ }
+ }
+
+ @NotNull
+ public CharSequence getBufferSequence() {
+ // elixirLexer only has a subsequence that is `heexLexer`'s
+ return heexLexer.getBufferSequence();
+ }
+
+ public int getBufferEnd() {
+ // since {@link #getBufferSequence} uses `heexLexer`, so does this.
+ return heexLexer.getBufferEnd();
+ }
+
+ private int lexerLanguageFlag() {
+ //noinspection ConstantConditions
+ return forLanguage(heexLexer, 0, 1);
+ }
+
+ public int getState() {
+ return lexer().getState() | (lexerLanguageFlag() << 16);
+ }
+
+ public int getTokenEnd() {
+ return lexer().getTokenEnd();
+ }
+
+ public int getTokenStart() {
+ return lexer().getTokenStart();
+ }
+
+ @NotNull
+ public HTMLEmbeddedElixir.Position getCurrentPosition() {
+ return new Position(this);
+ }
+
+ public final void restore(@NotNull final LexerPosition position) {
+ restore((Position) position);
+ }
+
+ private void restore(Position position) {
+ restoreHEExPosition(position.heexPosition);
+ restoreElixirPosition(position.elixirPosition);
+ }
+
+ private void restoreHEExPosition(@NotNull LexerPosition heexPosition) {
+ heexLexer.restore(heexPosition);
+ }
+
+ private void restoreElixirPosition(@Nullable LexerPosition elixirPosition) {
+ if (elixirPosition != null) {
+ elixirLexer.start(getBufferSequence(), heexLexer.getTokenStart(), heexLexer.getTokenEnd());
+ elixirLexer.restore(elixirPosition);
+ }
+ }
+
+ @Nullable
+ public IElementType getTokenType() {
+ IElementType tokenType = lexer().getTokenType();
+
+ if (tokenType != null && tokenType.getLanguage() != ElixirLanguage.INSTANCE) {
+ IElementType elixirTokenType = HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.get(tokenType);
+
+ assert elixirTokenType != null : "HEEx TokenType " + tokenType + " is not mapped to an Elixir TokenType";
+
+ tokenType = elixirTokenType;
+ }
+
+ return tokenType;
+ }
+
+ @Override
+ public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
+ heexLexer.start(buffer, startOffset, endOffset, initialState & 0xFFFF);
+
+ if (heexLexer.getTokenType() == ELIXIR) {
+ elixirLexer.start(buffer, startOffset, endOffset);
+ } else {
+ elixirLexer.start(buffer, startOffset, endOffset);
+ }
+ }
+
+ protected static class Position implements LexerPosition {
+ @NotNull
+ private final LexerPosition heexPosition;
+ @Nullable
+ private final LexerPosition elixirPosition;
+
+ Position(final HTMLEmbeddedElixir lexer) {
+ this.heexPosition = lexer.heexLexer.getCurrentPosition();
+
+ if (lexer.heexLexer.getTokenType() == ELIXIR) {
+ this.elixirPosition = lexer.elixirLexer.getCurrentPosition();
+ } else {
+ this.elixirPosition = null;
+ }
+ }
+
+ @Contract(pure = true)
+ @NotNull
+ private LexerPosition position() {
+ LexerPosition position;
+
+ if (elixirPosition != null) {
+ position = elixirPosition;
+ } else {
+ position = heexPosition;
+ }
+
+ return position;
+ }
+
+ public int getOffset() {
+ return position().getOffset();
+ }
+
+ public int getState() {
+ return position().getState();
+ }
+ }
+}
diff --git a/src/org/elixir_lang/heex/lexer/LookAhead.java b/src/org/elixir_lang/heex/lexer/LookAhead.java
new file mode 100644
index 000000000..98e84e357
--- /dev/null
+++ b/src/org/elixir_lang/heex/lexer/LookAhead.java
@@ -0,0 +1,23 @@
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.lexer.MergingLexerAdapter;
+import com.intellij.psi.tree.TokenSet;
+import org.elixir_lang.heex.lexer.Adapter;
+import org.elixir_lang.heex.psi.Types;
+
+public class LookAhead extends com.intellij.lexer.LookAheadLexer {
+ public static final TokenSet MERGABLE_TOKEN_SET = TokenSet.create(
+ Types.COMMENT,
+ Types.DATA,
+ Types.ELIXIR
+ );
+
+ public LookAhead() {
+ super(
+ new MergingLexerAdapter(
+ new Adapter(),
+ MERGABLE_TOKEN_SET
+ )
+ );
+ }
+}
diff --git a/src/org/elixir_lang/heex/psi/ElementType.java b/src/org/elixir_lang/heex/psi/ElementType.java
new file mode 100644
index 000000000..972cd0410
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/ElementType.java
@@ -0,0 +1,11 @@
+package org.elixir_lang.heex.psi;
+
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.heex.HeexLanguage;
+import org.jetbrains.annotations.NotNull;
+
+public class ElementType extends IElementType {
+ public ElementType(@NotNull String debugName) {
+ super(debugName, HeexLanguage.INSTANCE);
+ }
+}
diff --git a/src/org/elixir_lang/heex/psi/HEExBraces.java b/src/org/elixir_lang/heex/psi/HEExBraces.java
new file mode 100644
index 000000000..01bebebdb
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/HEExBraces.java
@@ -0,0 +1,10 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface HEExBraces extends PsiElement {
+
+}
diff --git a/src/org/elixir_lang/heex/psi/HEExTag.java b/src/org/elixir_lang/heex/psi/HEExTag.java
new file mode 100644
index 000000000..da15d26bf
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/HEExTag.java
@@ -0,0 +1,10 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElement;
+
+public interface HEExTag extends PsiElement {
+
+}
diff --git a/src/org/elixir_lang/heex/psi/HEExVisitor.java b/src/org/elixir_lang/heex/psi/HEExVisitor.java
new file mode 100644
index 000000000..99b0fa12f
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/HEExVisitor.java
@@ -0,0 +1,22 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi;
+
+import org.jetbrains.annotations.*;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiElement;
+
+public class HEExVisitor extends PsiElementVisitor {
+
+ public void visitBraces(@NotNull HEExBraces o) {
+ visitPsiElement(o);
+ }
+
+ public void visitTag(@NotNull HEExTag o) {
+ visitPsiElement(o);
+ }
+
+ public void visitPsiElement(@NotNull PsiElement o) {
+ visitElement(o);
+ }
+
+}
diff --git a/src/org/elixir_lang/heex/psi/TokenType.java b/src/org/elixir_lang/heex/psi/TokenType.java
new file mode 100644
index 000000000..9bcd7a5c0
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/TokenType.java
@@ -0,0 +1,11 @@
+package org.elixir_lang.heex.psi;
+
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.heex.HeexLanguage;
+import org.jetbrains.annotations.NotNull;
+
+public class TokenType extends IElementType {
+ public TokenType(@NotNull String debugName) {
+ super(debugName, HeexLanguage.INSTANCE);
+ }
+}
diff --git a/src/org/elixir_lang/heex/psi/Types.java b/src/org/elixir_lang/heex/psi/Types.java
new file mode 100644
index 000000000..4f22f0cd3
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/Types.java
@@ -0,0 +1,45 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi;
+
+import com.intellij.psi.templateLanguages.TemplateDataElementType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.PsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.tree.OuterLanguageElementType;
+import org.elixir_lang.heex.HeexLanguage;
+import org.elixir_lang.heex.psi.impl.*;
+
+public interface Types {
+
+ IElementType BRACES = new ElementType("BRACES");
+ IElementType TAG = new ElementType("TAG");
+
+ IElementType BRACE_CLOSING = new TokenType("BRACE_CLOSING");
+ IElementType BRACE_OPENING = new TokenType("BRACE_OPENING");
+ IElementType CLOSING = new TokenType("%>");
+ IElementType COMMENT = new TokenType("Comment");
+ IElementType COMMENT_MARKER = new TokenType("#");
+ IElementType DATA = new TokenType("Data");
+ IElementType ELIXIR = new TokenType("Elixir");
+ IElementType EMPTY_MARKER = new TokenType("Empty Marker");
+ IElementType EQUALS_MARKER = new TokenType("=");
+ IElementType ESCAPED_OPENING = new TokenType("<%%");
+ IElementType FORWARD_SLASH_MARKER = new TokenType("/");
+ IElementType OPENING = new TokenType("<%");
+ IElementType PIPE_MARKER = new TokenType("|");
+
+ IElementType HEEX_OUTER_ELEMENT = new OuterLanguageElementType("HEEx", HeexLanguage.INSTANCE);
+
+ class Factory {
+ public static PsiElement createElement(ASTNode node) {
+ IElementType type = node.getElementType();
+ if (type == BRACES) {
+ return new HEExBracesImpl(node);
+ }
+ else if (type == TAG) {
+ return new HEExTagImpl(node);
+ }
+ throw new AssertionError("Unknown element type: " + type);
+ }
+ }
+}
diff --git a/src/org/elixir_lang/heex/psi/impl/HEExBracesImpl.java b/src/org/elixir_lang/heex/psi/impl/HEExBracesImpl.java
new file mode 100644
index 000000000..1fdfdbc40
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/impl/HEExBracesImpl.java
@@ -0,0 +1,30 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static org.elixir_lang.heex.psi.Types.*;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import org.elixir_lang.heex.psi.*;
+
+public class HEExBracesImpl extends ASTWrapperPsiElement implements HEExBraces {
+
+ public HEExBracesImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull HEExVisitor visitor) {
+ visitor.visitBraces(this);
+ }
+
+ @Override
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof HEExVisitor) accept((HEExVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+}
diff --git a/src/org/elixir_lang/heex/psi/impl/HEExTagImpl.java b/src/org/elixir_lang/heex/psi/impl/HEExTagImpl.java
new file mode 100644
index 000000000..bcfff2d6a
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/impl/HEExTagImpl.java
@@ -0,0 +1,30 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi.impl;
+
+import java.util.List;
+import org.jetbrains.annotations.*;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.util.PsiTreeUtil;
+import static org.elixir_lang.heex.psi.Types.*;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import org.elixir_lang.heex.psi.*;
+
+public class HEExTagImpl extends ASTWrapperPsiElement implements HEExTag {
+
+ public HEExTagImpl(@NotNull ASTNode node) {
+ super(node);
+ }
+
+ public void accept(@NotNull HEExVisitor visitor) {
+ visitor.visitTag(this);
+ }
+
+ @Override
+ public void accept(@NotNull PsiElementVisitor visitor) {
+ if (visitor instanceof HEExVisitor) accept((HEExVisitor)visitor);
+ else super.accept(visitor);
+ }
+
+}