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 = ] +END_SCRIPT_TAG = "" +START_STYLE_TAG = ] +END_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); + } + +}