From 6943242526ba2cd8c7b0ff128fec954147c23498 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Fri, 27 Jun 2025 08:32:51 +0100
Subject: [PATCH 01/12] Add basic support for HEEx copied from EEx
---
gen/org/elixir_lang/heex/lexer/Flex.java | 617 ++++++++++++++++++
resources/META-INF/plugin.xml | 26 +
resources/icons/file/heex.svg | 17 +
resources/icons/file/heex_dark.svg | 17 +
src/org/elixir_lang/HEEx.bnf | 39 ++
src/org/elixir_lang/HEEx.flex | 86 +++
src/org/elixir_lang/HEEx.kt | 27 +
src/org/elixir_lang/heex/ElementType.java | 13 +
src/org/elixir_lang/heex/File.java | 26 +
src/org/elixir_lang/heex/HEExParserUtil.java | 6 +
src/org/elixir_lang/heex/Highlighter.java | 23 +
src/org/elixir_lang/heex/Icons.kt | 8 +
src/org/elixir_lang/heex/Language.java | 28 +
src/org/elixir_lang/heex/Parser.java | 143 ++++
.../elixir_lang/heex/ParserDefinition.java | 74 +++
.../elixir_lang/heex/TemplateHighlighter.java | 69 ++
.../heex/element_type/EmbeddedElixir.java | 39 ++
.../heex/element_type/Factory.java | 12 +
.../heex/element_type/TemplateData.java | 36 +
.../elixir_lang/heex/file/ElementType.java | 58 ++
src/org/elixir_lang/heex/file/Type.kt | 70 ++
.../elixir_lang/heex/file/ViewProvider.java | 159 +++++
src/org/elixir_lang/heex/file/psi/Stub.java | 19 +
.../heex/file/view_provider/Factory.java | 21 +
src/org/elixir_lang/heex/lexer/Adapter.java | 12 +
.../heex/lexer/EmbeddedElixir.java | 218 +++++++
src/org/elixir_lang/heex/lexer/LookAhead.java | 23 +
.../elixir_lang/heex/lexer/TemplateData.kt | 41 ++
src/org/elixir_lang/heex/psi/ElementType.java | 11 +
src/org/elixir_lang/heex/psi/HEExTag.java | 10 +
src/org/elixir_lang/heex/psi/HEExVisitor.java | 18 +
src/org/elixir_lang/heex/psi/TokenType.java | 11 +
src/org/elixir_lang/heex/psi/Types.java | 34 +
.../heex/psi/impl/HEExTagImpl.java | 30 +
34 files changed, 2041 insertions(+)
create mode 100644 gen/org/elixir_lang/heex/lexer/Flex.java
create mode 100755 resources/icons/file/heex.svg
create mode 100755 resources/icons/file/heex_dark.svg
create mode 100644 src/org/elixir_lang/HEEx.bnf
create mode 100644 src/org/elixir_lang/HEEx.flex
create mode 100644 src/org/elixir_lang/HEEx.kt
create mode 100644 src/org/elixir_lang/heex/ElementType.java
create mode 100644 src/org/elixir_lang/heex/File.java
create mode 100644 src/org/elixir_lang/heex/HEExParserUtil.java
create mode 100644 src/org/elixir_lang/heex/Highlighter.java
create mode 100644 src/org/elixir_lang/heex/Icons.kt
create mode 100644 src/org/elixir_lang/heex/Language.java
create mode 100644 src/org/elixir_lang/heex/Parser.java
create mode 100644 src/org/elixir_lang/heex/ParserDefinition.java
create mode 100644 src/org/elixir_lang/heex/TemplateHighlighter.java
create mode 100644 src/org/elixir_lang/heex/element_type/EmbeddedElixir.java
create mode 100644 src/org/elixir_lang/heex/element_type/Factory.java
create mode 100644 src/org/elixir_lang/heex/element_type/TemplateData.java
create mode 100644 src/org/elixir_lang/heex/file/ElementType.java
create mode 100644 src/org/elixir_lang/heex/file/Type.kt
create mode 100644 src/org/elixir_lang/heex/file/ViewProvider.java
create mode 100644 src/org/elixir_lang/heex/file/psi/Stub.java
create mode 100644 src/org/elixir_lang/heex/file/view_provider/Factory.java
create mode 100644 src/org/elixir_lang/heex/lexer/Adapter.java
create mode 100644 src/org/elixir_lang/heex/lexer/EmbeddedElixir.java
create mode 100644 src/org/elixir_lang/heex/lexer/LookAhead.java
create mode 100644 src/org/elixir_lang/heex/lexer/TemplateData.kt
create mode 100644 src/org/elixir_lang/heex/psi/ElementType.java
create mode 100644 src/org/elixir_lang/heex/psi/HEExTag.java
create mode 100644 src/org/elixir_lang/heex/psi/HEExVisitor.java
create mode 100644 src/org/elixir_lang/heex/psi/TokenType.java
create mode 100644 src/org/elixir_lang/heex/psi/Types.java
create mode 100644 src/org/elixir_lang/heex/psi/impl/HEExTagImpl.java
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..ae1d52bae
--- /dev/null
+++ b/gen/org/elixir_lang/heex/lexer/Flex.java
@@ -0,0 +1,617 @@
+// 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 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;
+
+ /**
+ * 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
+ };
+
+ /**
+ * 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\u10ff\u0100";
+
+ 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\0\2\1\22\0\1\2\2\0\1\3"+
+ "\1\0\1\4\11\0\1\5\14\0\1\6\1\7\1\10"+
+ "\75\0\1\11\u0183\0";
+
+ private static int [] zzUnpackcmap_blocks() {
+ int [] result = new int[512];
+ 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 =
+ "\5\0\2\1\2\2\2\3\2\4\1\5\1\6\1\7"+
+ "\1\10\1\11\1\12\2\0\1\13\1\14\1\0\1\15";
+
+ private static int [] zzUnpackAction() {
+ int [] result = new int[25];
+ 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\12\0\24\0\36\0\50\0\62\0\74\0\62"+
+ "\0\106\0\62\0\120\0\62\0\120\0\62\0\62\0\62"+
+ "\0\62\0\62\0\132\0\106\0\144\0\62\0\62\0\156"+
+ "\0\62";
+
+ private static int [] zzUnpackRowMap() {
+ int [] result = new int[25];
+ 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 =
+ "\6\6\1\7\3\6\1\10\2\11\7\10\4\12\1\13"+
+ "\5\12\4\14\1\15\5\14\3\16\1\17\1\16\1\20"+
+ "\1\16\1\21\1\16\1\22\16\0\1\23\6\0\2\24"+
+ "\3\0\1\25\13\0\1\26\5\0\1\27\11\0\1\30"+
+ "\7\0\1\31\7\0";
+
+ private static int [] zzUnpacktrans() {
+ int [] result = new int[120];
+ 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 =
+ "\5\0\1\11\1\1\1\11\1\1\1\11\1\1\1\11"+
+ "\1\1\5\11\1\1\2\0\2\11\1\0\1\11";
+
+ private static int [] zzUnpackAttribute() {
+ int [] result = new int[25];
+ 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 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 14: break;
+ case 2:
+ { handleInState(YYINITIAL);
+ }
+ // fall through
+ case 15: break;
+ case 3:
+ { return Types.COMMENT;
+ }
+ // fall through
+ case 16: break;
+ case 4:
+ { return Types.ELIXIR;
+ }
+ // fall through
+ case 17: break;
+ case 5:
+ { handleInState(ELIXIR);
+ return Types.EMPTY_MARKER;
+ }
+ // fall through
+ case 18: break;
+ case 6:
+ { yybegin(COMMENT);
+ return Types.COMMENT_MARKER;
+ }
+ // fall through
+ case 19: break;
+ case 7:
+ { yybegin(ELIXIR);
+ return Types.FORWARD_SLASH_MARKER;
+ }
+ // fall through
+ case 20: break;
+ case 8:
+ { yybegin(ELIXIR);
+ return Types.EQUALS_MARKER;
+ }
+ // fall through
+ case 21: break;
+ case 9:
+ { yybegin(ELIXIR);
+ return Types.PIPE_MARKER;
+ }
+ // fall through
+ case 22: break;
+ case 10:
+ { yybegin(MARKER_MAYBE);
+ return Types.OPENING;
+ }
+ // fall through
+ case 23: break;
+ case 11:
+ { yybegin(WHITESPACE_MAYBE);
+ return Types.CLOSING;
+ }
+ // fall through
+ case 24: break;
+ case 12:
+ { return Types.ESCAPED_OPENING;
+ }
+ // fall through
+ case 25: break;
+ case 13:
+ // lookahead expression with fixed lookahead length
+ zzMarkedPos = Character.offsetByCodePoints
+ (zzBufferL, zzMarkedPos, -3);
+ { yybegin(YYINITIAL);
+ return TokenType.WHITE_SPACE;
+ }
+ // fall through
+ case 26: break;
+ default:
+ zzScanError(ZZ_NO_MATCH);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 83d3b7839..abd22f797 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -10,6 +10,9 @@
on how to target different products -->
com.intellij.modules.lang
org.intellij.plugins.markdown
+
+
+
com.intellij.modules.java
@@ -64,6 +67,29 @@
+
+
+
+
+
+
+
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..20f784dd0
--- /dev/null
+++ b/src/org/elixir_lang/HEEx.bnf
@@ -0,0 +1,39 @@
+{
+ // 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)*
+tag ::= OPENING (commentBody | elixirBody) CLOSING
+ { pin = 1 }
+
+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..925dfacb0
--- /dev/null
+++ b/src/org/elixir_lang/HEEx.flex
@@ -0,0 +1,86 @@
+package org.elixir_lang.heex.lexer;
+
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+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
+%function advance
+%type IElementType
+%eof{ return;
+%eof}
+
+%{
+ private void handleInState(int nextLexicalState) {
+ yypushback(yylength());
+ yybegin(nextLexicalState);
+ }
+%}
+
+OPENING = "<%"
+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 = [^]
+
+%state WHITESPACE_MAYBE
+%state COMMENT
+%state ELIXIR
+%state MARKER_MAYBE
+
+%%
+
+ {
+ {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; }
+}
+
+ {
+ {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..83462e2c6
--- /dev/null
+++ b/src/org/elixir_lang/heex/ElementType.java
@@ -0,0 +1,13 @@
+package org.elixir_lang.heex;
+
+
+import com.intellij.psi.tree.IElementType;
+import org.elixir_lang.heex.Language;
+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, Language.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..18c2ab407
--- /dev/null
+++ b/src/org/elixir_lang/heex/File.java
@@ -0,0 +1,26 @@
+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.Language;
+import org.elixir_lang.heex.file.Type;
+import org.jetbrains.annotations.NotNull;
+
+public class File extends PsiFileBase {
+ public File(@NotNull FileViewProvider fileViewProvider) {
+ super(fileViewProvider, Language.INSTANCE);
+ }
+
+ @NotNull
+ @Override
+ public FileType getFileType() {
+ return Type.INSTANCE;
+ }
+
+ @NotNull
+ @Override
+ public String toString() {
+ return "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/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/Language.java b/src/org/elixir_lang/heex/Language.java
new file mode 100644
index 000000000..31d75358a
--- /dev/null
+++ b/src/org/elixir_lang/heex/Language.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 Language extends com.intellij.lang.Language implements TemplateLanguage {
+ public static final Language INSTANCE = new Language();
+
+ protected Language(@Nullable com.intellij.lang.Language baseLanguage,
+ @NotNull String ID,
+ @NotNull String... mimeTypes) {
+ super(baseLanguage, ID, mimeTypes);
+ }
+
+ public Language() {
+ super("HEEx");
+ }
+
+ @Contract(pure = true)
+ public static LanguageFileType defaultTemplateLanguageFileType() {
+ return FileTypes.PLAIN_TEXT;
+ }
+}
diff --git a/src/org/elixir_lang/heex/Parser.java b/src/org/elixir_lang/heex/Parser.java
new file mode 100644
index 000000000..d3b7a28cf
--- /dev/null
+++ b/src/org/elixir_lang/heex/Parser.java
@@ -0,0 +1,143 @@
+// 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);
+ }
+
+ /* ********************************************************** */
+ // 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)*
+ 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
+ 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);
+ 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..e6ca6152b
--- /dev/null
+++ b/src/org/elixir_lang/heex/TemplateHighlighter.java
@@ -0,0 +1,69 @@
+package org.elixir_lang.heex;
+
+import com.google.common.collect.Iterables;
+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.*;
+import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx;
+import com.intellij.openapi.fileTypes.impl.FileTypeAssocTable;
+import com.intellij.openapi.fileTypes.impl.FileTypeConfigurable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
+import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings;
+import org.elixir_lang.ElixirFileType;
+import org.elixir_lang.ElixirLanguage;
+import org.elixir_lang.heex.Highlighter;
+import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.file.Type;
+import org.elixir_lang.heex.psi.Types;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+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 = Language.defaultTemplateLanguageFileType();
+ }
+ }
+
+ SyntaxHighlighter 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, ""));
+ }
+}
diff --git a/src/org/elixir_lang/heex/element_type/EmbeddedElixir.java b/src/org/elixir_lang/heex/element_type/EmbeddedElixir.java
new file mode 100644
index 000000000..2ac725d1d
--- /dev/null
+++ b/src/org/elixir_lang/heex/element_type/EmbeddedElixir.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 EmbeddedElixir extends IFileElementType {
+ public EmbeddedElixir() {
+ 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.EmbeddedElixir(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/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/TemplateData.java b/src/org/elixir_lang/heex/element_type/TemplateData.java
new file mode 100644
index 000000000..5e61bebe3
--- /dev/null
+++ b/src/org/elixir_lang/heex/element_type/TemplateData.java
@@ -0,0 +1,36 @@
+package org.elixir_lang.heex.element_type;
+
+import com.intellij.lang.Language;
+import com.intellij.lexer.Lexer;
+import com.intellij.psi.templateLanguages.TemplateDataElementType;
+import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
+import org.jetbrains.annotations.NotNull;
+
+import static org.elixir_lang.heex.lexer.TemplateData.HEEX;
+import static org.elixir_lang.heex.psi.Types.DATA;
+
+public class TemplateData extends TemplateDataElementType {
+ private final Language templateFileLanguage;
+
+ public TemplateData(@NotNull Language templateFileLanguage) {
+ super(
+ "HEEX_TEMPLATE_DATA",
+ org.elixir_lang.heex.Language.INSTANCE,
+ DATA,
+ HEEX
+ );
+ this.templateFileLanguage = templateFileLanguage;
+ }
+
+ @NotNull
+ @Override
+ protected Lexer createBaseLexer(@NotNull TemplateLanguageFileViewProvider templateLanguageFileViewProvider) {
+ return new org.elixir_lang.heex.lexer.TemplateData();
+ }
+
+ @NotNull
+ @Override
+ protected Language getTemplateFileLanguage(TemplateLanguageFileViewProvider templateLanguageFileViewProvider) {
+ return templateFileLanguage;
+ }
+}
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..faf6f00c3
--- /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.Language;
+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", Language.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.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..b409ddd84
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/Type.kt
@@ -0,0 +1,70 @@
+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.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? = org.elixir_lang.heex.Language.INSTANCE) :
+ LanguageFileType(lang!!), TemplateLanguageFileType {
+ override fun getName(): String = "Embedded Elixir"
+ override fun getDescription(): String = "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..f335a97eb
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/ViewProvider.java
@@ -0,0 +1,159 @@
+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 gnu.trove.THashSet;
+import org.elixir_lang.ElixirLanguage;
+import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.element_type.EmbeddedElixir;
+import org.elixir_lang.heex.element_type.TemplateData;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+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 -> {
+ IElementType elementType;
+
+ if (language == ElixirLanguage.INSTANCE) {
+ elementType = new EmbeddedElixir();
+ } else {
+ elementType = new TemplateData(language);
+ }
+
+ return elementType;
+ }
+ );
+ }
+
+ 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 = Language.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 = getDefinition(language);
+ PsiFileImpl psiFileImpl;
+
+ 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 new THashSet<>(Arrays.asList(getTemplateDataLanguage(), getBaseLanguage(), ElixirLanguage.INSTANCE));
+ }
+
+ @NotNull
+ @Override
+ public com.intellij.lang.Language getTemplateDataLanguage() {
+ return templateDataLanguage;
+ }
+
+ @Override
+ protected MultiplePsiFilesPerDocumentFileViewProvider cloneInner(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/view_provider/Factory.java b/src/org/elixir_lang/heex/file/view_provider/Factory.java
new file mode 100644
index 000000000..d1907c53a
--- /dev/null
+++ b/src/org/elixir_lang/heex/file/view_provider/Factory.java
@@ -0,0 +1,21 @@
+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.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(org.elixir_lang.heex.Language.INSTANCE);
+ return new ViewProvider(psiManager, virtualFile, eventSystemEnabled, language);
+ }
+}
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/EmbeddedElixir.java b/src/org/elixir_lang/heex/lexer/EmbeddedElixir.java
new file mode 100644
index 000000000..014bd306c
--- /dev/null
+++ b/src/org/elixir_lang/heex/lexer/EmbeddedElixir.java
@@ -0,0 +1,218 @@
+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.elixir_lang.heex.lexer.LookAhead;
+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 EmbeddedElixir 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(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 EmbeddedElixir(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 EmbeddedElixir.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 EmbeddedElixir 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/lexer/TemplateData.kt b/src/org/elixir_lang/heex/lexer/TemplateData.kt
new file mode 100644
index 000000000..5578de139
--- /dev/null
+++ b/src/org/elixir_lang/heex/lexer/TemplateData.kt
@@ -0,0 +1,41 @@
+package org.elixir_lang.heex.lexer
+
+import com.intellij.lexer.Lexer
+import com.intellij.lexer.MergingLexerAdapterBase
+import com.intellij.psi.tree.IElementType
+import com.intellij.psi.tree.OuterLanguageElementType
+import org.elixir_lang.heex.Language
+import org.elixir_lang.heex.lexer.LookAhead
+import org.elixir_lang.heex.psi.Types
+
+/**
+ * Merges together all HEEx opening, body, and closing tokens into a single HEEx type
+ */
+class TemplateData : MergingLexerAdapterBase(LookAhead()) {
+ private val mergeFunction: MergeFunction = MergeFunction()
+
+ override fun getMergeFunction(): com.intellij.lexer.MergeFunction = mergeFunction
+
+ private inner class MergeFunction : com.intellij.lexer.MergeFunction {
+ override fun merge(type: IElementType, originalLexer: Lexer): IElementType = if (type !== Types.DATA) {
+ while (true) {
+ val originalTokenType = originalLexer.tokenType
+
+ if (originalTokenType != null && originalTokenType !== Types.DATA) {
+ originalLexer.advance()
+ } else {
+ break
+ }
+ }
+
+ HEEX
+ } else {
+ type
+ }
+ }
+
+ companion object {
+ @JvmField
+ val HEEX: IElementType = OuterLanguageElementType("HEEx", Language.INSTANCE)
+ }
+}
\ No newline at end of file
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..b6aa36ddd
--- /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.Language;
+import org.jetbrains.annotations.NotNull;
+
+public class ElementType extends IElementType {
+ public ElementType(@NotNull String debugName) {
+ super(debugName, Language.INSTANCE);
+ }
+}
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..298865861
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/HEExVisitor.java
@@ -0,0 +1,18 @@
+// 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 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..d90c274ce
--- /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.Language;
+import org.jetbrains.annotations.NotNull;
+
+public class TokenType extends IElementType {
+ public TokenType(@NotNull String debugName) {
+ super(debugName, Language.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..4bfa97964
--- /dev/null
+++ b/src/org/elixir_lang/heex/psi/Types.java
@@ -0,0 +1,34 @@
+// This is a generated file. Not intended for manual editing.
+package org.elixir_lang.heex.psi;
+
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.PsiElement;
+import com.intellij.lang.ASTNode;
+import org.elixir_lang.heex.psi.impl.*;
+
+public interface Types {
+
+ IElementType TAG = new ElementType("TAG");
+
+ 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("|");
+
+ class Factory {
+ public static PsiElement createElement(ASTNode node) {
+ IElementType type = node.getElementType();
+ if (type == TAG) {
+ return new HEExTagImpl(node);
+ }
+ throw new AssertionError("Unknown element type: " + type);
+ }
+ }
+}
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);
+ }
+
+}
From 25b3e90e497da814c641cfe60132c8dc0722b50e Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Sat, 28 Jun 2025 15:27:07 +0100
Subject: [PATCH 02/12] Add support for HEEx relative components in HTML
---
resources/META-INF/plugin.xml | 19 ++++++--
src/org/elixir_lang/heex/File.java | 2 +-
...dedElixir.java => HTMLEmbeddedElixir.java} | 6 +--
.../heex/element_type/TemplateData.java | 1 +
.../elixir_lang/heex/file/ElementType.java | 2 +-
src/org/elixir_lang/heex/file/Type.kt | 4 +-
.../elixir_lang/heex/file/ViewProvider.java | 19 ++++++--
.../heex/html/HeexHTMLFileElementType.java | 27 +++++++++++
.../heex/html/HeexHTMLFileType.java | 40 ++++++++++++++++
.../heex/html/HeexHTMLLanguage.java | 11 +++++
.../elixir_lang/heex/html/HeexHTMLLexer.java | 46 +++++++++++++++++++
.../heex/html/HeexHTMLParserDefinition.java | 13 ++++++
...dedElixir.java => HTMLEmbeddedElixir.java} | 9 ++--
13 files changed, 179 insertions(+), 20 deletions(-)
rename src/org/elixir_lang/heex/element_type/{EmbeddedElixir.java => HTMLEmbeddedElixir.java} (89%)
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileType.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLLexer.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
rename src/org/elixir_lang/heex/lexer/{EmbeddedElixir.java => HTMLEmbeddedElixir.java} (96%)
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index abd22f797..36c3c6298 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -70,26 +70,35 @@
+ implementationClass="org.elixir_lang.heex.file.view_provider.Factory"/>
+
+
+
diff --git a/src/org/elixir_lang/heex/File.java b/src/org/elixir_lang/heex/File.java
index 18c2ab407..ebba2a77f 100644
--- a/src/org/elixir_lang/heex/File.java
+++ b/src/org/elixir_lang/heex/File.java
@@ -21,6 +21,6 @@ public FileType getFileType() {
@NotNull
@Override
public String toString() {
- return "Embedded Elixir File";
+ return "HTML Embedded Elixir File";
}
}
diff --git a/src/org/elixir_lang/heex/element_type/EmbeddedElixir.java b/src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java
similarity index 89%
rename from src/org/elixir_lang/heex/element_type/EmbeddedElixir.java
rename to src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java
index 2ac725d1d..f231cfc70 100644
--- a/src/org/elixir_lang/heex/element_type/EmbeddedElixir.java
+++ b/src/org/elixir_lang/heex/element_type/HTMLEmbeddedElixir.java
@@ -12,8 +12,8 @@
* Elixir.bnf parses it like it was an interpolated expression separated by an outer string instead of adjacent Elixir
* expressions.
*/
-public class EmbeddedElixir extends IFileElementType {
- public EmbeddedElixir() {
+public class HTMLEmbeddedElixir extends IFileElementType {
+ public HTMLEmbeddedElixir() {
super(ElixirLanguage.INSTANCE);
}
@@ -28,7 +28,7 @@ public ASTNode parseContents(ASTNode chameleon) {
PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(
project,
chameleon,
- new org.elixir_lang.heex.lexer.EmbeddedElixir(project),
+ new org.elixir_lang.heex.lexer.HTMLEmbeddedElixir(project),
languageForParser,
chameleon.getChars()
);
diff --git a/src/org/elixir_lang/heex/element_type/TemplateData.java b/src/org/elixir_lang/heex/element_type/TemplateData.java
index 5e61bebe3..165e8ab1c 100644
--- a/src/org/elixir_lang/heex/element_type/TemplateData.java
+++ b/src/org/elixir_lang/heex/element_type/TemplateData.java
@@ -1,6 +1,7 @@
package org.elixir_lang.heex.element_type;
import com.intellij.lang.Language;
+import com.intellij.lang.html.HTMLLanguage;
import com.intellij.lexer.Lexer;
import com.intellij.psi.templateLanguages.TemplateDataElementType;
import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
diff --git a/src/org/elixir_lang/heex/file/ElementType.java b/src/org/elixir_lang/heex/file/ElementType.java
index faf6f00c3..811925322 100644
--- a/src/org/elixir_lang/heex/file/ElementType.java
+++ b/src/org/elixir_lang/heex/file/ElementType.java
@@ -42,7 +42,7 @@ protected StubElement createStubForFile(@NotNull PsiFile psiFile) {
@NotNull
@Override
public String getExternalId() {
- return "elixir.embedded.FILE";
+ return "elixir.html_embedded.FILE";
}
@Override
diff --git a/src/org/elixir_lang/heex/file/Type.kt b/src/org/elixir_lang/heex/file/Type.kt
index b409ddd84..b226e72e9 100644
--- a/src/org/elixir_lang/heex/file/Type.kt
+++ b/src/org/elixir_lang/heex/file/Type.kt
@@ -14,8 +14,8 @@ 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? = org.elixir_lang.heex.Language.INSTANCE) :
LanguageFileType(lang!!), TemplateLanguageFileType {
- override fun getName(): String = "Embedded Elixir"
- override fun getDescription(): String = "Embedded Elixir file"
+ 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
diff --git a/src/org/elixir_lang/heex/file/ViewProvider.java b/src/org/elixir_lang/heex/file/ViewProvider.java
index f335a97eb..d868dead2 100644
--- a/src/org/elixir_lang/heex/file/ViewProvider.java
+++ b/src/org/elixir_lang/heex/file/ViewProvider.java
@@ -2,6 +2,7 @@
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.html.HTMLLanguage;
import com.intellij.openapi.fileTypes.LanguageFileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
@@ -16,11 +17,15 @@
import gnu.trove.THashSet;
import org.elixir_lang.ElixirLanguage;
import org.elixir_lang.heex.Language;
-import org.elixir_lang.heex.element_type.EmbeddedElixir;
+import org.elixir_lang.heex.element_type.HTMLEmbeddedElixir;
import org.elixir_lang.heex.element_type.TemplateData;
+import org.elixir_lang.heex.html.HeexHTMLFileElementType;
+import org.elixir_lang.heex.html.HeexHTMLFileType;
+import org.elixir_lang.heex.html.HeexHTMLLanguage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import javax.swing.text.html.HTML;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -61,7 +66,9 @@ private static IElementType elementType(com.intellij.lang.Language language) {
IElementType elementType;
if (language == ElixirLanguage.INSTANCE) {
- elementType = new EmbeddedElixir();
+ elementType = new HTMLEmbeddedElixir();
+ } else if (language == HTMLLanguage.INSTANCE) {
+ elementType = new HeexHTMLFileElementType();
} else {
elementType = new TemplateData(language);
}
@@ -103,9 +110,15 @@ private static com.intellij.lang.Language templateDataLanguage(@NotNull PsiManag
@Nullable
@Override
protected PsiFile createFile(@NotNull com.intellij.lang.Language language) {
- ParserDefinition parserDefinition = getDefinition(language);
+ ParserDefinition parserDefinition;
PsiFileImpl psiFileImpl;
+ if (language.isKindOf(HTMLLanguage.INSTANCE)) {
+ parserDefinition = getDefinition(HeexHTMLLanguage.INSTANCE);
+ } else {
+ parserDefinition = getDefinition(language);
+ }
+
if (parserDefinition == null) {
psiFileImpl = null;
} else if (language.isKindOf(getBaseLanguage())) {
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..368f7e786
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
@@ -0,0 +1,27 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lang.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.ParsingDiagnostics;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.xml.HtmlFileElementType;
+
+public class HeexHTMLFileElementType extends HtmlFileElementType {
+ /** @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();
+ Language languageForParser = this.getLanguageForParser(psi);
+ PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, null, HeexHTMLLanguage.INSTANCE, chameleon.getChars());
+ PsiParser parser = (LanguageParserDefinitions.INSTANCE.forLanguage(HeexHTMLLanguage.INSTANCE)).createParser(project);
+ long startTime = System.nanoTime();
+ ASTNode node = parser.parse(this, builder);
+ ParsingDiagnostics.registerParse(builder, languageForParser, System.nanoTime() - startTime);
+
+ return node.getFirstChildNode();
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileType.java b/src/org/elixir_lang/heex/html/HeexHTMLFileType.java
new file mode 100644
index 000000000..e05fc1826
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileType.java
@@ -0,0 +1,40 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by FernFlower decompiler)
+//
+
+package org.elixir_lang.heex.html;
+
+import com.intellij.icons.AllIcons.FileTypes;
+import com.intellij.ide.highlighter.HtmlFileType;
+import com.intellij.ide.highlighter.XmlLikeFileType;
+import com.intellij.lang.Language;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/** @see HtmlFileType */
+public class HeexHTMLFileType extends XmlLikeFileType {
+ public static final HeexHTMLFileType INSTANCE = new HeexHTMLFileType();
+
+ private HeexHTMLFileType() {
+ super(HeexHTMLLanguage.INSTANCE);
+ }
+
+ public @NotNull String getName() {
+ return "HEEx HTML";
+ }
+
+ public @NotNull String getDescription() {
+ return "HTML Embedded in HEEx";
+ }
+
+ public @NotNull String getDefaultExtension() {
+ return "html";
+ }
+
+ public Icon getIcon() {
+ return FileTypes.Html;
+ }
+}
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..afcec1559
--- /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.xml.XMLLanguage;
+
+public class HeexHTMLLanguage extends XMLLanguage {
+ public static final HeexHTMLLanguage INSTANCE = new HeexHTMLLanguage();
+
+ protected HeexHTMLLanguage() {
+ super(XMLLanguage.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..96605ac6f
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLLexer.java
@@ -0,0 +1,46 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lexer.HtmlLexer;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLLexer extends HtmlLexer {
+ @Override
+ public void start(@NotNull CharSequence buffer, int startOffset, int endOffset, int initialState) {
+ CharSequence maskedBuffer = maskRelativeComponentDots(buffer, startOffset, endOffset);
+
+ super.start(maskedBuffer, startOffset, endOffset, 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 = 0;
+ 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/HeexHTMLParserDefinition.java b/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
new file mode 100644
index 000000000..8b1a8f7f1
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
@@ -0,0 +1,13 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lang.html.HTMLParserDefinition;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLParserDefinition extends HTMLParserDefinition {
+ @Override
+ public @NotNull Lexer createLexer(Project project) {
+ return new HeexHTMLLexer();
+ }
+}
diff --git a/src/org/elixir_lang/heex/lexer/EmbeddedElixir.java b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
similarity index 96%
rename from src/org/elixir_lang/heex/lexer/EmbeddedElixir.java
rename to src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
index 014bd306c..8c7b9ae22 100644
--- a/src/org/elixir_lang/heex/lexer/EmbeddedElixir.java
+++ b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
@@ -8,7 +8,6 @@
import gnu.trove.THashMap;
import org.elixir_lang.ElixirLanguage;
import org.elixir_lang.ElixirLexer;
-import org.elixir_lang.heex.lexer.LookAhead;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -26,7 +25,7 @@
* can't use LookAheadLexer since it's {@link com.intellij.lexer.LookAheadLexer.LookAheadLexerPosition} only works for a
* single lexer.
*/
-public class EmbeddedElixir extends LexerBase {
+public class HTMLEmbeddedElixir extends LexerBase {
@NotNull
private static final Map HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE = new THashMap<>();
@NotNull
@@ -49,7 +48,7 @@ public class EmbeddedElixir extends LexerBase {
HEEX_TOKEN_TYPE_TO_ELIXIR_TOKEN_TYPE.put(WHITE_SPACE, WHITE_SPACE);
}
- public EmbeddedElixir(Project project) {
+ public HTMLEmbeddedElixir(Project project) {
this.heexLexer = new LookAhead();
this.elixirLexer = new ElixirLexer(project);
}
@@ -127,7 +126,7 @@ public int getTokenStart() {
}
@NotNull
- public EmbeddedElixir.Position getCurrentPosition() {
+ public HTMLEmbeddedElixir.Position getCurrentPosition() {
return new Position(this);
}
@@ -183,7 +182,7 @@ protected static class Position implements LexerPosition {
@Nullable
private final LexerPosition elixirPosition;
- Position(final EmbeddedElixir lexer) {
+ Position(final HTMLEmbeddedElixir lexer) {
this.heexPosition = lexer.heexLexer.getCurrentPosition();
if (lexer.heexLexer.getTokenType() == ELIXIR) {
From 3edc6e16549dd8737aca4382e5d94901261d1655 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Sun, 29 Jun 2025 10:04:30 +0100
Subject: [PATCH 03/12] Add HTML inspection suppressor for HEEx to ignore
warning for HEEx compnents.
---
resources/META-INF/plugin.xml | 5 +++
.../inspections/HTMLInspectionSuppressor.java | 45 +++++++++++++++++++
2 files changed, 50 insertions(+)
create mode 100644 src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 36c3c6298..4bcb3dd4b 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -99,6 +99,11 @@
fieldName="INSTANCE"
language="HEExHTML"/>
+
+
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..556b5e757
--- /dev/null
+++ b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
@@ -0,0 +1,45 @@
+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.codeInspection.htmlInspections.RequiredAttributesInspection;
+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.Language;
+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(PsiElement element, String toolId) {
+ if (!SUPPRESSED_INSPECTIONS.contains(toolId)) {
+ return false;
+ }
+
+ PsiFile file = element.getContainingFile();
+ if (file != null && file.getViewProvider().hasLanguage(Language.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;
+ }
+}
From f921780c8ccff8df9666daf262cc5d418ec3365f Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Sun, 29 Jun 2025 18:04:53 +0100
Subject: [PATCH 04/12] Add parsing of HEEx braces to HEEx BNF
---
gen/org/elixir_lang/heex/lexer/Flex.java | 121 +++++++++++-------
src/org/elixir_lang/HEEx.bnf | 3 +-
src/org/elixir_lang/HEEx.flex | 29 +++++
src/org/elixir_lang/heex/Parser.java | 17 ++-
.../heex/lexer/HTMLEmbeddedElixir.java | 2 +
src/org/elixir_lang/heex/psi/HEExBraces.java | 10 ++
src/org/elixir_lang/heex/psi/HEExVisitor.java | 4 +
src/org/elixir_lang/heex/psi/Types.java | 8 +-
.../heex/psi/impl/HEExBracesImpl.java | 30 +++++
9 files changed, 177 insertions(+), 47 deletions(-)
create mode 100644 src/org/elixir_lang/heex/psi/HEExBraces.java
create mode 100644 src/org/elixir_lang/heex/psi/impl/HEExBracesImpl.java
diff --git a/gen/org/elixir_lang/heex/lexer/Flex.java b/gen/org/elixir_lang/heex/lexer/Flex.java
index ae1d52bae..1f86ec9ac 100644
--- a/gen/org/elixir_lang/heex/lexer/Flex.java
+++ b/gen/org/elixir_lang/heex/lexer/Flex.java
@@ -22,6 +22,8 @@ public class Flex implements com.intellij.lexer.FlexLexer {
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;
/**
* ZZ_LEXSTATE[l] is the state in the DFA for the lexical state l
@@ -30,7 +32,7 @@ public class Flex implements com.intellij.lexer.FlexLexer {
* 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
+ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6
};
/**
@@ -69,7 +71,7 @@ private static int zzUnpackcmap_top(String packed, int offset, int [] result) {
private static final String ZZ_CMAP_BLOCKS_PACKED_0 =
"\11\0\2\1\1\0\2\1\22\0\1\2\2\0\1\3"+
"\1\0\1\4\11\0\1\5\14\0\1\6\1\7\1\10"+
- "\75\0\1\11\u0183\0";
+ "\74\0\1\11\1\12\1\13\u0182\0";
private static int [] zzUnpackcmap_blocks() {
int [] result = new int[512];
@@ -96,11 +98,12 @@ private static int zzUnpackcmap_blocks(String packed, int offset, int [] result)
private static final int [] ZZ_ACTION = zzUnpackAction();
private static final String ZZ_ACTION_PACKED_0 =
- "\5\0\2\1\2\2\2\3\2\4\1\5\1\6\1\7"+
- "\1\10\1\11\1\12\2\0\1\13\1\14\1\0\1\15";
+ "\7\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\1\16\2\0"+
+ "\1\17\1\20\1\0\1\21";
private static int [] zzUnpackAction() {
- int [] result = new int[25];
+ int [] result = new int[31];
int offset = 0;
offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
return result;
@@ -125,13 +128,13 @@ private static int zzUnpackAction(String packed, int offset, int [] result) {
private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
private static final String ZZ_ROWMAP_PACKED_0 =
- "\0\0\0\12\0\24\0\36\0\50\0\62\0\74\0\62"+
- "\0\106\0\62\0\120\0\62\0\120\0\62\0\62\0\62"+
- "\0\62\0\62\0\132\0\106\0\144\0\62\0\62\0\156"+
- "\0\62";
+ "\0\0\0\14\0\30\0\44\0\60\0\74\0\110\0\124"+
+ "\0\140\0\124\0\124\0\154\0\124\0\170\0\124\0\170"+
+ "\0\124\0\124\0\124\0\124\0\124\0\124\0\124\0\124"+
+ "\0\204\0\154\0\220\0\124\0\124\0\234\0\124";
private static int [] zzUnpackRowMap() {
- int [] result = new int[25];
+ int [] result = new int[31];
int offset = 0;
offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
return result;
@@ -154,14 +157,15 @@ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
private static final int [] ZZ_TRANS = zzUnpacktrans();
private static final String ZZ_TRANS_PACKED_0 =
- "\6\6\1\7\3\6\1\10\2\11\7\10\4\12\1\13"+
- "\5\12\4\14\1\15\5\14\3\16\1\17\1\16\1\20"+
- "\1\16\1\21\1\16\1\22\16\0\1\23\6\0\2\24"+
- "\3\0\1\25\13\0\1\26\5\0\1\27\11\0\1\30"+
- "\7\0\1\31\7\0";
+ "\6\10\1\11\2\10\1\12\2\10\1\13\2\14\11\13"+
+ "\4\15\1\16\7\15\4\17\1\20\7\17\3\21\1\22"+
+ "\1\21\1\23\1\21\1\24\2\21\1\25\1\21\14\26"+
+ "\11\17\1\27\1\17\1\30\20\0\1\31\10\0\2\32"+
+ "\3\0\1\33\15\0\1\34\7\0\1\35\13\0\1\36"+
+ "\11\0\1\37\11\0";
private static int [] zzUnpacktrans() {
- int [] result = new int[120];
+ int [] result = new int[168];
int offset = 0;
offset = zzUnpacktrans(ZZ_TRANS_PACKED_0, offset, result);
return result;
@@ -199,11 +203,11 @@ private static int zzUnpacktrans(String packed, int offset, int [] result) {
private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
private static final String ZZ_ATTRIBUTE_PACKED_0 =
- "\5\0\1\11\1\1\1\11\1\1\1\11\1\1\1\11"+
- "\1\1\5\11\1\1\2\0\2\11\1\0\1\11";
+ "\7\0\1\11\1\1\2\11\1\1\1\11\1\1\1\11"+
+ "\1\1\10\11\1\1\2\0\2\11\1\0\1\11";
private static int [] zzUnpackAttribute() {
- int [] result = new int[25];
+ int [] result = new int[31];
int offset = 0;
offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
return result;
@@ -270,6 +274,8 @@ the source of the yytext() string */
private boolean zzEOFDone;
/* user code: */
+ private int openBraceCount = 0;
+
private void handleInState(int nextLexicalState) {
yypushback(yylength());
yybegin(nextLexicalState);
@@ -534,70 +540,99 @@ else if (zzAtEOF) {
{ return Types.DATA;
}
// fall through
- case 14: break;
+ case 18: break;
case 2:
- { handleInState(YYINITIAL);
+ { yybegin(BEGIN_MATCHED_BRACES);
+ return Types.BRACE_OPENING;
}
// fall through
- case 15: break;
+ case 19: break;
case 3:
- { return Types.COMMENT;
+ { handleInState(YYINITIAL);
}
// fall through
- case 16: break;
+ case 20: break;
case 4:
- { return Types.ELIXIR;
+ { return Types.COMMENT;
}
// fall through
- case 17: break;
+ case 21: break;
case 5:
+ { return Types.ELIXIR;
+ }
+ // fall through
+ case 22: break;
+ case 6:
{ handleInState(ELIXIR);
return Types.EMPTY_MARKER;
}
// fall through
- case 18: break;
- case 6:
+ case 23: break;
+ case 7:
{ yybegin(COMMENT);
return Types.COMMENT_MARKER;
}
// fall through
- case 19: break;
- case 7:
+ case 24: break;
+ case 8:
{ yybegin(ELIXIR);
return Types.FORWARD_SLASH_MARKER;
}
// fall through
- case 20: break;
- case 8:
+ case 25: break;
+ case 9:
{ yybegin(ELIXIR);
return Types.EQUALS_MARKER;
}
// fall through
- case 21: break;
- case 9:
+ case 26: break;
+ case 10:
{ yybegin(ELIXIR);
return Types.PIPE_MARKER;
}
// fall through
- case 22: break;
- case 10:
+ case 27: break;
+ case 11:
+ { handleInState(MATCHED_BRACES);
+ return Types.EQUALS_MARKER;
+ }
+ // fall through
+ case 28: break;
+ case 12:
+ { openBraceCount++;
+ return Types.ELIXIR;
+ }
+ // fall through
+ case 29: break;
+ case 13:
+ { if (openBraceCount > 0) {
+ openBraceCount--;
+ return Types.ELIXIR;
+ } else {
+ yybegin(YYINITIAL);
+ return Types.BRACE_CLOSING;
+ }
+ }
+ // fall through
+ case 30: break;
+ case 14:
{ yybegin(MARKER_MAYBE);
return Types.OPENING;
}
// fall through
- case 23: break;
- case 11:
+ case 31: break;
+ case 15:
{ yybegin(WHITESPACE_MAYBE);
return Types.CLOSING;
}
// fall through
- case 24: break;
- case 12:
+ case 32: break;
+ case 16:
{ return Types.ESCAPED_OPENING;
}
// fall through
- case 25: break;
- case 13:
+ case 33: break;
+ case 17:
// lookahead expression with fixed lookahead length
zzMarkedPos = Character.offsetByCodePoints
(zzBufferL, zzMarkedPos, -3);
@@ -605,7 +640,7 @@ else if (zzAtEOF) {
return TokenType.WHITE_SPACE;
}
// fall through
- case 26: break;
+ case 34: break;
default:
zzScanError(ZZ_NO_MATCH);
}
diff --git a/src/org/elixir_lang/HEEx.bnf b/src/org/elixir_lang/HEEx.bnf
index 20f784dd0..5ea147864 100644
--- a/src/org/elixir_lang/HEEx.bnf
+++ b/src/org/elixir_lang/HEEx.bnf
@@ -29,9 +29,10 @@
]
}
-private heexFile ::= (DATA | ESCAPED_OPENING | tag)*
+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 }
diff --git a/src/org/elixir_lang/HEEx.flex b/src/org/elixir_lang/HEEx.flex
index 925dfacb0..87daa2537 100644
--- a/src/org/elixir_lang/HEEx.flex
+++ b/src/org/elixir_lang/HEEx.flex
@@ -17,6 +17,8 @@ import org.elixir_lang.heex.psi.Types;
%eof}
%{
+ private int openBraceCount = 0;
+
private void handleInState(int nextLexicalState) {
yypushback(yylength());
yybegin(nextLexicalState);
@@ -26,6 +28,9 @@ import org.elixir_lang.heex.psi.Types;
OPENING = "<%"
CLOSING = "%>"
+BRACE_OPENING = "{"
+BRACE_CLOSING = "}"
+
COMMENT_MARKER = "#"
EQUALS_MARKER = "="
// See https://github.com/elixir-lang/elixir/pull/6281
@@ -41,6 +46,7 @@ ANY = [^]
%state COMMENT
%state ELIXIR
%state MARKER_MAYBE
+%state BEGIN_MATCHED_BRACES, MATCHED_BRACES
%%
@@ -48,6 +54,8 @@ ANY = [^]
{ESCAPED_OPENING} { return Types.ESCAPED_OPENING; }
{OPENING} { yybegin(MARKER_MAYBE);
return Types.OPENING; }
+ {BRACE_OPENING} { yybegin(BEGIN_MATCHED_BRACES);
+ return Types.BRACE_OPENING; }
{ANY} { return Types.DATA; }
}
@@ -64,6 +72,27 @@ ANY = [^]
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; }
diff --git a/src/org/elixir_lang/heex/Parser.java b/src/org/elixir_lang/heex/Parser.java
index d3b7a28cf..4e7cec877 100644
--- a/src/org/elixir_lang/heex/Parser.java
+++ b/src/org/elixir_lang/heex/Parser.java
@@ -35,6 +35,18 @@ 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) {
@@ -95,7 +107,7 @@ static boolean elixirMarker(PsiBuilder b, int l) {
}
/* ********************************************************** */
- // (DATA | ESCAPED_OPENING | tag)*
+ // (DATA | ESCAPED_OPENING | tag | braces)*
static boolean heexFile(PsiBuilder b, int l) {
if (!recursion_guard_(b, l, "heexFile")) return false;
while (true) {
@@ -106,13 +118,14 @@ static boolean heexFile(PsiBuilder b, int l) {
return true;
}
- // DATA | ESCAPED_OPENING | tag
+ // 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;
}
diff --git a/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
index 8c7b9ae22..e7c10d396 100644
--- a/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
+++ b/src/org/elixir_lang/heex/lexer/HTMLEmbeddedElixir.java
@@ -35,6 +35,8 @@ public class HTMLEmbeddedElixir extends LexerBase {
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);
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/HEExVisitor.java b/src/org/elixir_lang/heex/psi/HEExVisitor.java
index 298865861..99b0fa12f 100644
--- a/src/org/elixir_lang/heex/psi/HEExVisitor.java
+++ b/src/org/elixir_lang/heex/psi/HEExVisitor.java
@@ -7,6 +7,10 @@
public class HEExVisitor extends PsiElementVisitor {
+ public void visitBraces(@NotNull HEExBraces o) {
+ visitPsiElement(o);
+ }
+
public void visitTag(@NotNull HEExTag o) {
visitPsiElement(o);
}
diff --git a/src/org/elixir_lang/heex/psi/Types.java b/src/org/elixir_lang/heex/psi/Types.java
index 4bfa97964..4ae5a3a9c 100644
--- a/src/org/elixir_lang/heex/psi/Types.java
+++ b/src/org/elixir_lang/heex/psi/Types.java
@@ -8,8 +8,11 @@
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("#");
@@ -25,7 +28,10 @@ public interface Types {
class Factory {
public static PsiElement createElement(ASTNode node) {
IElementType type = node.getElementType();
- if (type == TAG) {
+ 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);
+ }
+
+}
From 23a6b4223cb49ad2960e8bb73ca2be0435cbd3e6 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Mon, 30 Jun 2025 18:41:53 +0100
Subject: [PATCH 05/12] Fix injection of outer elements into HTML sections of
HEEx templates
---
src/org/elixir_lang/heex/ElementType.java | 3 +-
src/org/elixir_lang/heex/File.java | 3 +-
.../heex/{Language.java => HeexLanguage.java} | 12 +++---
.../elixir_lang/heex/TemplateHighlighter.java | 22 ++++------
.../heex/element_type/TemplateData.java | 37 -----------------
.../elixir_lang/heex/file/ElementType.java | 4 +-
src/org/elixir_lang/heex/file/Type.kt | 3 +-
.../elixir_lang/heex/file/ViewProvider.java | 20 +++++----
.../heex/file/view_provider/Factory.java | 3 +-
.../heex/html/HeexHTMLFileElementType.java | 6 +--
.../heex/html/HeexHTMLFileImpl.java | 22 ++++++++++
.../heex/html/HeexHTMLFileType.java | 11 +----
.../heex/html/HeexHTMLLanguage.java | 6 +--
.../elixir_lang/heex/html/HeexHTMLLexer.java | 12 +++++-
.../heex/html/HeexHTMLParserDefinition.java | 7 ++++
.../inspections/HTMLInspectionSuppressor.java | 5 +--
.../elixir_lang/heex/lexer/TemplateData.kt | 41 -------------------
src/org/elixir_lang/heex/psi/ElementType.java | 4 +-
src/org/elixir_lang/heex/psi/TokenType.java | 4 +-
src/org/elixir_lang/heex/psi/Types.java | 11 +++++
20 files changed, 94 insertions(+), 142 deletions(-)
rename src/org/elixir_lang/heex/{Language.java => HeexLanguage.java} (65%)
delete mode 100644 src/org/elixir_lang/heex/element_type/TemplateData.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java
delete mode 100644 src/org/elixir_lang/heex/lexer/TemplateData.kt
diff --git a/src/org/elixir_lang/heex/ElementType.java b/src/org/elixir_lang/heex/ElementType.java
index 83462e2c6..2d9fbe274 100644
--- a/src/org/elixir_lang/heex/ElementType.java
+++ b/src/org/elixir_lang/heex/ElementType.java
@@ -2,12 +2,11 @@
import com.intellij.psi.tree.IElementType;
-import org.elixir_lang.heex.Language;
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, Language.INSTANCE);
+ super(debugName, HeexLanguage.INSTANCE);
}
}
diff --git a/src/org/elixir_lang/heex/File.java b/src/org/elixir_lang/heex/File.java
index ebba2a77f..cc2588029 100644
--- a/src/org/elixir_lang/heex/File.java
+++ b/src/org/elixir_lang/heex/File.java
@@ -3,13 +3,12 @@
import com.intellij.extapi.psi.PsiFileBase;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.psi.FileViewProvider;
-import org.elixir_lang.heex.Language;
import org.elixir_lang.heex.file.Type;
import org.jetbrains.annotations.NotNull;
public class File extends PsiFileBase {
public File(@NotNull FileViewProvider fileViewProvider) {
- super(fileViewProvider, Language.INSTANCE);
+ super(fileViewProvider, HeexLanguage.INSTANCE);
}
@NotNull
diff --git a/src/org/elixir_lang/heex/Language.java b/src/org/elixir_lang/heex/HeexLanguage.java
similarity index 65%
rename from src/org/elixir_lang/heex/Language.java
rename to src/org/elixir_lang/heex/HeexLanguage.java
index 31d75358a..e5c423730 100644
--- a/src/org/elixir_lang/heex/Language.java
+++ b/src/org/elixir_lang/heex/HeexLanguage.java
@@ -8,16 +8,16 @@
import org.jetbrains.annotations.Nullable;
// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/HbLanguage.java
-public class Language extends com.intellij.lang.Language implements TemplateLanguage {
- public static final Language INSTANCE = new Language();
+public class HeexLanguage extends com.intellij.lang.Language implements TemplateLanguage {
+ public static final HeexLanguage INSTANCE = new HeexLanguage();
- protected Language(@Nullable com.intellij.lang.Language baseLanguage,
- @NotNull String ID,
- @NotNull String... mimeTypes) {
+ protected HeexLanguage(@Nullable com.intellij.lang.Language baseLanguage,
+ @NotNull String ID,
+ @NotNull String... mimeTypes) {
super(baseLanguage, ID, mimeTypes);
}
- public Language() {
+ public HeexLanguage() {
super("HEEx");
}
diff --git a/src/org/elixir_lang/heex/TemplateHighlighter.java b/src/org/elixir_lang/heex/TemplateHighlighter.java
index e6ca6152b..a03fd8e44 100644
--- a/src/org/elixir_lang/heex/TemplateHighlighter.java
+++ b/src/org/elixir_lang/heex/TemplateHighlighter.java
@@ -1,31 +1,19 @@
package org.elixir_lang.heex;
-import com.google.common.collect.Iterables;
+import com.intellij.lang.html.HTMLLanguage;
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.*;
-import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx;
-import com.intellij.openapi.fileTypes.impl.FileTypeAssocTable;
-import com.intellij.openapi.fileTypes.impl.FileTypeConfigurable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings;
import org.elixir_lang.ElixirFileType;
-import org.elixir_lang.ElixirLanguage;
-import org.elixir_lang.heex.Highlighter;
-import org.elixir_lang.heex.Language;
-import org.elixir_lang.heex.file.Type;
+import org.elixir_lang.heex.html.HeexHTMLLanguage;
import org.elixir_lang.heex.psi.Types;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
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
@@ -46,6 +34,10 @@ public TemplateHighlighter(@Nullable Project project,
TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile);
if (language != null) {
+ if (language.is(HTMLLanguage.INSTANCE)) {
+ language = HeexHTMLLanguage.INSTANCE;
+ }
+
type = language.getAssociatedFileType();
}
@@ -54,7 +46,7 @@ public TemplateHighlighter(@Nullable Project project,
}
if (type == null) {
- type = Language.defaultTemplateLanguageFileType();
+ type = HeexLanguage.defaultTemplateLanguageFileType();
}
}
diff --git a/src/org/elixir_lang/heex/element_type/TemplateData.java b/src/org/elixir_lang/heex/element_type/TemplateData.java
deleted file mode 100644
index 165e8ab1c..000000000
--- a/src/org/elixir_lang/heex/element_type/TemplateData.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.elixir_lang.heex.element_type;
-
-import com.intellij.lang.Language;
-import com.intellij.lang.html.HTMLLanguage;
-import com.intellij.lexer.Lexer;
-import com.intellij.psi.templateLanguages.TemplateDataElementType;
-import com.intellij.psi.templateLanguages.TemplateLanguageFileViewProvider;
-import org.jetbrains.annotations.NotNull;
-
-import static org.elixir_lang.heex.lexer.TemplateData.HEEX;
-import static org.elixir_lang.heex.psi.Types.DATA;
-
-public class TemplateData extends TemplateDataElementType {
- private final Language templateFileLanguage;
-
- public TemplateData(@NotNull Language templateFileLanguage) {
- super(
- "HEEX_TEMPLATE_DATA",
- org.elixir_lang.heex.Language.INSTANCE,
- DATA,
- HEEX
- );
- this.templateFileLanguage = templateFileLanguage;
- }
-
- @NotNull
- @Override
- protected Lexer createBaseLexer(@NotNull TemplateLanguageFileViewProvider templateLanguageFileViewProvider) {
- return new org.elixir_lang.heex.lexer.TemplateData();
- }
-
- @NotNull
- @Override
- protected Language getTemplateFileLanguage(TemplateLanguageFileViewProvider templateLanguageFileViewProvider) {
- return templateFileLanguage;
- }
-}
diff --git a/src/org/elixir_lang/heex/file/ElementType.java b/src/org/elixir_lang/heex/file/ElementType.java
index 811925322..7a0d465e2 100644
--- a/src/org/elixir_lang/heex/file/ElementType.java
+++ b/src/org/elixir_lang/heex/file/ElementType.java
@@ -8,7 +8,7 @@
import com.intellij.psi.stubs.StubOutputStream;
import com.intellij.psi.tree.IStubFileElementType;
import org.elixir_lang.heex.File;
-import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.HeexLanguage;
import org.elixir_lang.heex.file.psi.Stub;
import org.jetbrains.annotations.NotNull;
@@ -18,7 +18,7 @@ public class ElementType extends IStubFileElementType {
public static final IStubFileElementType INSTANCE = new ElementType();
public ElementType() {
- super("HEEX_FILE", Language.INSTANCE);
+ super("HEEX_FILE", HeexLanguage.INSTANCE);
}
@Override
diff --git a/src/org/elixir_lang/heex/file/Type.kt b/src/org/elixir_lang/heex/file/Type.kt
index b226e72e9..248258a53 100644
--- a/src/org/elixir_lang/heex/file/Type.kt
+++ b/src/org/elixir_lang/heex/file/Type.kt
@@ -5,6 +5,7 @@ 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.*
@@ -12,7 +13,7 @@ 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? = org.elixir_lang.heex.Language.INSTANCE) :
+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"
diff --git a/src/org/elixir_lang/heex/file/ViewProvider.java b/src/org/elixir_lang/heex/file/ViewProvider.java
index d868dead2..8aa1eaea0 100644
--- a/src/org/elixir_lang/heex/file/ViewProvider.java
+++ b/src/org/elixir_lang/heex/file/ViewProvider.java
@@ -12,26 +12,26 @@
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.templateLanguages.ConfigurableTemplateLanguageFileViewProvider;
+import com.intellij.psi.templateLanguages.TemplateDataElementType;
import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings;
import com.intellij.psi.tree.IElementType;
import gnu.trove.THashSet;
import org.elixir_lang.ElixirLanguage;
-import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.HeexLanguage;
import org.elixir_lang.heex.element_type.HTMLEmbeddedElixir;
-import org.elixir_lang.heex.element_type.TemplateData;
-import org.elixir_lang.heex.html.HeexHTMLFileElementType;
-import org.elixir_lang.heex.html.HeexHTMLFileType;
import org.elixir_lang.heex.html.HeexHTMLLanguage;
+import org.elixir_lang.heex.psi.Types;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import javax.swing.text.html.HTML;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static org.elixir_lang.heex.file.Type.onlyTemplateDataFileType;
+import static org.elixir_lang.heex.psi.Types.HEEX_OUTER_ELEMENT;
+import static org.elixir_lang.heex.psi.Types.HEEX_TEMPLATE_ELEMENT;
// See https://github.com/JetBrains/intellij-plugins/blob/500f42337a87f463e0340f43e2411266fcfa9c5f/handlebars/src/com/dmarcotte/handlebars/file/HbFileViewProvider.java
public class ViewProvider extends MultiplePsiFilesPerDocumentFileViewProvider
@@ -67,10 +67,8 @@ private static IElementType elementType(com.intellij.lang.Language language) {
if (language == ElixirLanguage.INSTANCE) {
elementType = new HTMLEmbeddedElixir();
- } else if (language == HTMLLanguage.INSTANCE) {
- elementType = new HeexHTMLFileElementType();
} else {
- elementType = new TemplateData(language);
+ elementType = HEEX_TEMPLATE_ELEMENT;
}
return elementType;
@@ -93,7 +91,7 @@ private static com.intellij.lang.Language templateDataLanguage(@NotNull PsiManag
}
if (templateDataLanguage == null) {
- templateDataLanguage = Language.defaultTemplateLanguageFileType().getLanguage();
+ templateDataLanguage = HeexLanguage.defaultTemplateLanguageFileType().getLanguage();
}
com.intellij.lang.Language substituteLang =
@@ -157,6 +155,10 @@ public Set getLanguages() {
@NotNull
@Override
public com.intellij.lang.Language getTemplateDataLanguage() {
+ if (templateDataLanguage == HTMLLanguage.INSTANCE) {
+ return HeexHTMLLanguage.INSTANCE;
+ }
+
return templateDataLanguage;
}
diff --git a/src/org/elixir_lang/heex/file/view_provider/Factory.java b/src/org/elixir_lang/heex/file/view_provider/Factory.java
index d1907c53a..99594a2d4 100644
--- a/src/org/elixir_lang/heex/file/view_provider/Factory.java
+++ b/src/org/elixir_lang/heex/file/view_provider/Factory.java
@@ -4,6 +4,7 @@
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;
@@ -15,7 +16,7 @@ public FileViewProvider createFileViewProvider(@NotNull VirtualFile virtualFile,
@NotNull Language language,
@NotNull PsiManager psiManager,
boolean eventSystemEnabled) {
- assert language.isKindOf(org.elixir_lang.heex.Language.INSTANCE);
+ 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
index 368f7e786..0479100b2 100644
--- a/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java
@@ -2,11 +2,12 @@
import com.intellij.lang.*;
import com.intellij.openapi.project.Project;
-import com.intellij.psi.ParsingDiagnostics;
import com.intellij.psi.PsiElement;
import com.intellij.psi.xml.HtmlFileElementType;
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) {
@@ -15,12 +16,9 @@ public ASTNode parseContents(ASTNode chameleon) {
assert psi != null : "Bad chameleon: " + chameleon;
Project project = psi.getProject();
- Language languageForParser = this.getLanguageForParser(psi);
PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, null, HeexHTMLLanguage.INSTANCE, chameleon.getChars());
PsiParser parser = (LanguageParserDefinitions.INSTANCE.forLanguage(HeexHTMLLanguage.INSTANCE)).createParser(project);
- long startTime = System.nanoTime();
ASTNode node = parser.parse(this, builder);
- ParsingDiagnostics.registerParse(builder, languageForParser, System.nanoTime() - startTime);
return node.getFirstChildNode();
}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java b/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java
new file mode 100644
index 000000000..91c4326e3
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java
@@ -0,0 +1,22 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.lang.Language;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.impl.source.html.HtmlFileImpl;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLFileImpl extends HtmlFileImpl {
+ public HeexHTMLFileImpl(FileViewProvider provider) {
+ super(provider, HeexHTMLFileElementType.INSTANCE);
+ }
+
+ @Override
+ public @NotNull Language getLanguage() {
+ return HeexHTMLLanguage.INSTANCE;
+ }
+
+ @Override
+ public String toString() {
+ return "HEEx HTML File: "+ this.getName();
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileType.java b/src/org/elixir_lang/heex/html/HeexHTMLFileType.java
index e05fc1826..bea7b22e7 100644
--- a/src/org/elixir_lang/heex/html/HeexHTMLFileType.java
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileType.java
@@ -1,21 +1,12 @@
-//
-// Source code recreated from a .class file by IntelliJ IDEA
-// (powered by FernFlower decompiler)
-//
-
package org.elixir_lang.heex.html;
import com.intellij.icons.AllIcons.FileTypes;
import com.intellij.ide.highlighter.HtmlFileType;
-import com.intellij.ide.highlighter.XmlLikeFileType;
-import com.intellij.lang.Language;
-import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
-/** @see HtmlFileType */
-public class HeexHTMLFileType extends XmlLikeFileType {
+public class HeexHTMLFileType extends HtmlFileType {
public static final HeexHTMLFileType INSTANCE = new HeexHTMLFileType();
private HeexHTMLFileType() {
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java b/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
index afcec1559..8bdc6258b 100644
--- a/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
+++ b/src/org/elixir_lang/heex/html/HeexHTMLLanguage.java
@@ -1,11 +1,11 @@
package org.elixir_lang.heex.html;
-import com.intellij.lang.xml.XMLLanguage;
+import com.intellij.lang.html.HTMLLanguage;
-public class HeexHTMLLanguage extends XMLLanguage {
+public class HeexHTMLLanguage extends HTMLLanguage {
public static final HeexHTMLLanguage INSTANCE = new HeexHTMLLanguage();
protected HeexHTMLLanguage() {
- super(XMLLanguage.INSTANCE, "HEExHTML", "text/html", "text/htmlh");
+ 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
index 96605ac6f..3ba79ccdb 100644
--- a/src/org/elixir_lang/heex/html/HeexHTMLLexer.java
+++ b/src/org/elixir_lang/heex/html/HeexHTMLLexer.java
@@ -4,11 +4,19 @@
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, startOffset, endOffset, initialState);
+ super.start(maskedBuffer, 0, endOffset - startOffset, initialState);
}
/**
@@ -16,7 +24,7 @@ public void start(@NotNull CharSequence buffer, int startOffset, int endOffset,
* 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 = 0;
+ int startIndex = startOffset;
StringBuilder stringBuilder = new StringBuilder(endOffset);
for (int i = startOffset; i < endOffset; i++) {
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java b/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
index 8b1a8f7f1..934005732 100644
--- a/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
+++ b/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java
@@ -3,6 +3,8 @@
import com.intellij.lang.html.HTMLParserDefinition;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NotNull;
public class HeexHTMLParserDefinition extends HTMLParserDefinition {
@@ -10,4 +12,9 @@ public class HeexHTMLParserDefinition extends HTMLParserDefinition {
public @NotNull Lexer createLexer(Project project) {
return new HeexHTMLLexer();
}
+
+ @Override
+ public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider) {
+ return new HeexHTMLFileImpl(viewProvider);
+ }
}
diff --git a/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
index 556b5e757..c1c8ee333 100644
--- a/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
+++ b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java
@@ -3,13 +3,12 @@
import com.intellij.codeInspection.InspectionSuppressor;
import com.intellij.codeInspection.SuppressQuickFix;
import com.intellij.codeInspection.htmlInspections.HtmlUnknownTagInspection;
-import com.intellij.codeInspection.htmlInspections.RequiredAttributesInspection;
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.Language;
+import org.elixir_lang.heex.html.HeexHTMLLanguage;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -28,7 +27,7 @@ public boolean isSuppressedFor(PsiElement element, String toolId) {
}
PsiFile file = element.getContainingFile();
- if (file != null && file.getViewProvider().hasLanguage(Language.INSTANCE)) {
+ if (file != null && file.getViewProvider().hasLanguage(HeexHTMLLanguage.INSTANCE)) {
XmlTag xmlTag = PsiTreeUtil.getParentOfType(element, XmlTag.class, false);
// Tag names that contain dots are HEEx components
diff --git a/src/org/elixir_lang/heex/lexer/TemplateData.kt b/src/org/elixir_lang/heex/lexer/TemplateData.kt
deleted file mode 100644
index 5578de139..000000000
--- a/src/org/elixir_lang/heex/lexer/TemplateData.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.elixir_lang.heex.lexer
-
-import com.intellij.lexer.Lexer
-import com.intellij.lexer.MergingLexerAdapterBase
-import com.intellij.psi.tree.IElementType
-import com.intellij.psi.tree.OuterLanguageElementType
-import org.elixir_lang.heex.Language
-import org.elixir_lang.heex.lexer.LookAhead
-import org.elixir_lang.heex.psi.Types
-
-/**
- * Merges together all HEEx opening, body, and closing tokens into a single HEEx type
- */
-class TemplateData : MergingLexerAdapterBase(LookAhead()) {
- private val mergeFunction: MergeFunction = MergeFunction()
-
- override fun getMergeFunction(): com.intellij.lexer.MergeFunction = mergeFunction
-
- private inner class MergeFunction : com.intellij.lexer.MergeFunction {
- override fun merge(type: IElementType, originalLexer: Lexer): IElementType = if (type !== Types.DATA) {
- while (true) {
- val originalTokenType = originalLexer.tokenType
-
- if (originalTokenType != null && originalTokenType !== Types.DATA) {
- originalLexer.advance()
- } else {
- break
- }
- }
-
- HEEX
- } else {
- type
- }
- }
-
- companion object {
- @JvmField
- val HEEX: IElementType = OuterLanguageElementType("HEEx", Language.INSTANCE)
- }
-}
\ No newline at end of file
diff --git a/src/org/elixir_lang/heex/psi/ElementType.java b/src/org/elixir_lang/heex/psi/ElementType.java
index b6aa36ddd..972cd0410 100644
--- a/src/org/elixir_lang/heex/psi/ElementType.java
+++ b/src/org/elixir_lang/heex/psi/ElementType.java
@@ -1,11 +1,11 @@
package org.elixir_lang.heex.psi;
import com.intellij.psi.tree.IElementType;
-import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.HeexLanguage;
import org.jetbrains.annotations.NotNull;
public class ElementType extends IElementType {
public ElementType(@NotNull String debugName) {
- super(debugName, Language.INSTANCE);
+ super(debugName, HeexLanguage.INSTANCE);
}
}
diff --git a/src/org/elixir_lang/heex/psi/TokenType.java b/src/org/elixir_lang/heex/psi/TokenType.java
index d90c274ce..9bcd7a5c0 100644
--- a/src/org/elixir_lang/heex/psi/TokenType.java
+++ b/src/org/elixir_lang/heex/psi/TokenType.java
@@ -1,11 +1,11 @@
package org.elixir_lang.heex.psi;
import com.intellij.psi.tree.IElementType;
-import org.elixir_lang.heex.Language;
+import org.elixir_lang.heex.HeexLanguage;
import org.jetbrains.annotations.NotNull;
public class TokenType extends IElementType {
public TokenType(@NotNull String debugName) {
- super(debugName, Language.INSTANCE);
+ super(debugName, HeexLanguage.INSTANCE);
}
}
diff --git a/src/org/elixir_lang/heex/psi/Types.java b/src/org/elixir_lang/heex/psi/Types.java
index 4ae5a3a9c..767f10d8a 100644
--- a/src/org/elixir_lang/heex/psi/Types.java
+++ b/src/org/elixir_lang/heex/psi/Types.java
@@ -1,9 +1,12 @@
// 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 {
@@ -25,6 +28,14 @@ public interface Types {
IElementType OPENING = new TokenType("<%");
IElementType PIPE_MARKER = new TokenType("|");
+ IElementType HEEX_OUTER_ELEMENT = new OuterLanguageElementType("HEEx", HeexLanguage.INSTANCE);
+ IElementType HEEX_TEMPLATE_ELEMENT = new TemplateDataElementType(
+ "HEEX_TEMPLATE_DATA",
+ HeexLanguage.INSTANCE,
+ Types.DATA,
+ HEEX_OUTER_ELEMENT
+ );
+
class Factory {
public static PsiElement createElement(ASTNode node) {
IElementType type = node.getElementType();
From 5c1836daaa3f560192d6a5ec27a632edec8da875 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Mon, 30 Jun 2025 19:22:04 +0100
Subject: [PATCH 06/12] Add custom highlighter supporting tags beginning with
"."
---
resources/META-INF/plugin.xml | 4 ++++
src/org/elixir_lang/heex/TemplateHighlighter.java | 12 +++++++++---
src/org/elixir_lang/heex/file/Type.kt | 5 +++++
.../heex/html/HeexHTMLFileHighlighter.java | 11 +++++++++++
.../heex/html/HeexHTMLFileHighlighterFactory.java | 11 +++++++++++
5 files changed, 40 insertions(+), 3 deletions(-)
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java
diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml
index 4bcb3dd4b..4815ccfc1 100644
--- a/resources/META-INF/plugin.xml
+++ b/resources/META-INF/plugin.xml
@@ -98,6 +98,10 @@
implementationClass="org.elixir_lang.heex.html.HeexHTMLFileType"
fieldName="INSTANCE"
language="HEExHTML"/>
+
if (type === FileTypes.UNKNOWN) {
null
+ } else if (type == HtmlFileType.INSTANCE) {
+ Optional.of(HeexHTMLFileType.INSTANCE)
} else {
Optional.of(type)
}
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..2fde4ded2
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java
@@ -0,0 +1,11 @@
+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 {
+ public @NotNull Lexer getHighlightingLexer() {
+ return new HeexHTMLLexer(true);
+ }
+}
diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java
new file mode 100644
index 000000000..b8d0015bc
--- /dev/null
+++ b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java
@@ -0,0 +1,11 @@
+package org.elixir_lang.heex.html;
+
+import com.intellij.openapi.fileTypes.SingleLazyInstanceSyntaxHighlighterFactory;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import org.jetbrains.annotations.NotNull;
+
+public class HeexHTMLFileHighlighterFactory extends SingleLazyInstanceSyntaxHighlighterFactory {
+ protected @NotNull SyntaxHighlighter createHighlighter() {
+ return new HeexHTMLFileHighlighter();
+ }
+}
From 585a5d211a8e2a3b3482a4bb417e9a5cf7d696a5 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Mon, 30 Jun 2025 19:35:39 +0100
Subject: [PATCH 07/12] Disable HEEx brace interpolation within script and
style tags
---
gen/org/elixir_lang/heex/lexer/Flex.java | 118 +++++++++++++++--------
src/org/elixir_lang/HEEx.flex | 26 ++++-
2 files changed, 100 insertions(+), 44 deletions(-)
diff --git a/gen/org/elixir_lang/heex/lexer/Flex.java b/gen/org/elixir_lang/heex/lexer/Flex.java
index 1f86ec9ac..097163bf0 100644
--- a/gen/org/elixir_lang/heex/lexer/Flex.java
+++ b/gen/org/elixir_lang/heex/lexer/Flex.java
@@ -5,7 +5,7 @@
import com.intellij.psi.TokenType;
import com.intellij.psi.tree.IElementType;
-import org.elixir_lang.heex.psi.Types;
+import kotlinx.html.SCRIPT;import org.elixir_lang.heex.psi.Types;
public class Flex implements com.intellij.lexer.FlexLexer {
@@ -24,6 +24,8 @@ public class Flex implements com.intellij.lexer.FlexLexer {
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
@@ -32,7 +34,8 @@ public class Flex implements com.intellij.lexer.FlexLexer {
* 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
+ 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7,
+ 8, 8
};
/**
@@ -41,7 +44,7 @@ public class Flex implements com.intellij.lexer.FlexLexer {
private static final int [] ZZ_CMAP_TOP = zzUnpackcmap_top();
private static final String ZZ_CMAP_TOP_PACKED_0 =
- "\1\0\u10ff\u0100";
+ "\1\0\1\u0100\u10fe\u0200";
private static int [] zzUnpackcmap_top() {
int [] result = new int[4352];
@@ -71,10 +74,15 @@ private static int zzUnpackcmap_top(String packed, int offset, int [] result) {
private static final String ZZ_CMAP_BLOCKS_PACKED_0 =
"\11\0\2\1\1\0\2\1\22\0\1\2\2\0\1\3"+
"\1\0\1\4\11\0\1\5\14\0\1\6\1\7\1\10"+
- "\74\0\1\11\1\12\1\13\u0182\0";
+ "\4\0\1\11\1\0\1\12\3\0\1\13\2\0\1\14"+
+ "\3\0\1\15\1\0\1\16\1\17\1\20\4\0\1\21"+
+ "\11\0\1\11\1\0\1\12\3\0\1\13\2\0\1\14"+
+ "\3\0\1\15\1\0\1\16\1\17\1\20\4\0\1\21"+
+ "\1\0\1\22\1\23\1\24\262\0\2\13\115\0\1\17"+
+ "\u0180\0";
private static int [] zzUnpackcmap_blocks() {
- int [] result = new int[512];
+ int [] result = new int[768];
int offset = 0;
offset = zzUnpackcmap_blocks(ZZ_CMAP_BLOCKS_PACKED_0, offset, result);
return result;
@@ -98,12 +106,13 @@ private static int zzUnpackcmap_blocks(String packed, int offset, int [] result)
private static final int [] ZZ_ACTION = zzUnpackAction();
private static final String ZZ_ACTION_PACKED_0 =
- "\7\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\1\16\2\0"+
- "\1\17\1\20\1\0\1\21";
+ "\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\7\0\1\22"+
+ "\2\0\1\23\2\0\1\24";
private static int [] zzUnpackAction() {
- int [] result = new int[31];
+ int [] result = new int[58];
int offset = 0;
offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result);
return result;
@@ -128,13 +137,17 @@ private static int zzUnpackAction(String packed, int offset, int [] result) {
private static final int [] ZZ_ROWMAP = zzUnpackRowMap();
private static final String ZZ_ROWMAP_PACKED_0 =
- "\0\0\0\14\0\30\0\44\0\60\0\74\0\110\0\124"+
- "\0\140\0\124\0\124\0\154\0\124\0\170\0\124\0\170"+
- "\0\124\0\124\0\124\0\124\0\124\0\124\0\124\0\124"+
- "\0\204\0\154\0\220\0\124\0\124\0\234\0\124";
+ "\0\0\0\25\0\52\0\77\0\124\0\151\0\176\0\223"+
+ "\0\250\0\275\0\322\0\275\0\275\0\347\0\275\0\374"+
+ "\0\275\0\374\0\275\0\275\0\275\0\275\0\275\0\275"+
+ "\0\275\0\275\0\u0111\0\u0126\0\u013b\0\u0150\0\347\0\u0165"+
+ "\0\275\0\u017a\0\u018f\0\275\0\u01a4\0\u01b9\0\u01ce\0\u01e3"+
+ "\0\u01f8\0\u020d\0\u0222\0\275\0\u0237\0\u024c\0\u0261\0\u0276"+
+ "\0\u028b\0\u02a0\0\u02b5\0\275\0\u02ca\0\u02df\0\275\0\u02f4"+
+ "\0\u0309\0\275";
private static int [] zzUnpackRowMap() {
- int [] result = new int[31];
+ int [] result = new int[58];
int offset = 0;
offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result);
return result;
@@ -157,15 +170,22 @@ private static int zzUnpackRowMap(String packed, int offset, int [] result) {
private static final int [] ZZ_TRANS = zzUnpacktrans();
private static final String ZZ_TRANS_PACKED_0 =
- "\6\10\1\11\2\10\1\12\2\10\1\13\2\14\11\13"+
- "\4\15\1\16\7\15\4\17\1\20\7\17\3\21\1\22"+
- "\1\21\1\23\1\21\1\24\2\21\1\25\1\21\14\26"+
- "\11\17\1\27\1\17\1\30\20\0\1\31\10\0\2\32"+
- "\3\0\1\33\15\0\1\34\7\0\1\35\13\0\1\36"+
- "\11\0\1\37\11\0";
+ "\6\12\1\13\13\12\1\14\2\12\1\15\2\16\22\15"+
+ "\4\17\1\20\20\17\4\21\1\22\20\21\3\23\1\24"+
+ "\1\23\1\25\1\23\1\26\13\23\1\27\1\23\25\30"+
+ "\22\21\1\31\1\21\1\32\6\12\1\33\24\12\1\34"+
+ "\16\12\31\0\1\35\12\0\1\36\6\0\2\37\3\0"+
+ "\1\40\26\0\1\41\20\0\1\35\1\42\23\0\1\35"+
+ "\1\43\23\0\1\44\31\0\1\45\6\0\1\46\10\0"+
+ "\1\47\37\0\1\50\24\0\1\51\23\0\1\52\27\0"+
+ "\1\53\5\0\1\54\42\0\1\55\15\0\1\56\26\0"+
+ "\1\57\25\0\1\60\31\0\1\61\21\0\1\62\23\0"+
+ "\1\63\21\0\1\64\26\0\1\65\23\0\1\66\31\0"+
+ "\1\67\16\0\1\70\27\0\1\71\17\0\1\72\34\0"+
+ "\1\70\4\0";
private static int [] zzUnpacktrans() {
- int [] result = new int[168];
+ int [] result = new int[798];
int offset = 0;
offset = zzUnpacktrans(ZZ_TRANS_PACKED_0, offset, result);
return result;
@@ -203,11 +223,12 @@ private static int zzUnpacktrans(String packed, int offset, int [] result) {
private static final int [] ZZ_ATTRIBUTE = zzUnpackAttribute();
private static final String ZZ_ATTRIBUTE_PACKED_0 =
- "\7\0\1\11\1\1\2\11\1\1\1\11\1\1\1\11"+
- "\1\1\10\11\1\1\2\0\2\11\1\0\1\11";
+ "\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\7\0\1\11\2\0\1\11\2\0\1\11";
private static int [] zzUnpackAttribute() {
- int [] result = new int[31];
+ int [] result = new int[58];
int offset = 0;
offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result);
return result;
@@ -540,70 +561,70 @@ else if (zzAtEOF) {
{ return Types.DATA;
}
// fall through
- case 18: break;
+ case 21: break;
case 2:
{ yybegin(BEGIN_MATCHED_BRACES);
return Types.BRACE_OPENING;
}
// fall through
- case 19: break;
+ case 22: break;
case 3:
{ handleInState(YYINITIAL);
}
// fall through
- case 20: break;
+ case 23: break;
case 4:
{ return Types.COMMENT;
}
// fall through
- case 21: break;
+ case 24: break;
case 5:
{ return Types.ELIXIR;
}
// fall through
- case 22: break;
+ case 25: break;
case 6:
{ handleInState(ELIXIR);
return Types.EMPTY_MARKER;
}
// fall through
- case 23: break;
+ case 26: break;
case 7:
{ yybegin(COMMENT);
return Types.COMMENT_MARKER;
}
// fall through
- case 24: break;
+ case 27: break;
case 8:
{ yybegin(ELIXIR);
return Types.FORWARD_SLASH_MARKER;
}
// fall through
- case 25: break;
+ case 28: break;
case 9:
{ yybegin(ELIXIR);
return Types.EQUALS_MARKER;
}
// fall through
- case 26: break;
+ case 29: break;
case 10:
{ yybegin(ELIXIR);
return Types.PIPE_MARKER;
}
// fall through
- case 27: break;
+ case 30: break;
case 11:
{ handleInState(MATCHED_BRACES);
return Types.EQUALS_MARKER;
}
// fall through
- case 28: break;
+ case 31: break;
case 12:
{ openBraceCount++;
return Types.ELIXIR;
}
// fall through
- case 29: break;
+ case 32: break;
case 13:
{ if (openBraceCount > 0) {
openBraceCount--;
@@ -614,24 +635,24 @@ else if (zzAtEOF) {
}
}
// fall through
- case 30: break;
+ case 33: break;
case 14:
{ yybegin(MARKER_MAYBE);
return Types.OPENING;
}
// fall through
- case 31: break;
+ case 34: break;
case 15:
{ yybegin(WHITESPACE_MAYBE);
return Types.CLOSING;
}
// fall through
- case 32: break;
+ case 35: break;
case 16:
{ return Types.ESCAPED_OPENING;
}
// fall through
- case 33: break;
+ case 36: break;
case 17:
// lookahead expression with fixed lookahead length
zzMarkedPos = Character.offsetByCodePoints
@@ -640,7 +661,22 @@ else if (zzAtEOF) {
return TokenType.WHITE_SPACE;
}
// fall through
- case 34: break;
+ 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/src/org/elixir_lang/HEEx.flex b/src/org/elixir_lang/HEEx.flex
index 87daa2537..0300f087c 100644
--- a/src/org/elixir_lang/HEEx.flex
+++ b/src/org/elixir_lang/HEEx.flex
@@ -2,7 +2,7 @@ package org.elixir_lang.heex.lexer;
import com.intellij.psi.TokenType;
import com.intellij.psi.tree.IElementType;
-import org.elixir_lang.heex.psi.Types;
+import kotlinx.html.SCRIPT;import org.elixir_lang.heex.psi.Types;
%%
@@ -11,6 +11,7 @@ import org.elixir_lang.heex.psi.Types;
%class Flex
%implements com.intellij.lexer.FlexLexer
%unicode
+%ignorecase
%function advance
%type IElementType
%eof{ return;
@@ -42,20 +43,39 @@ PROCEDURAL_OPENING = {OPENING} " "
WHITE_SPACE = [\ \t\f\r\n]+
ANY = [^]
+START_SCRIPT_TAG = ""
-START_STYLE_TAG = ""
%state WHITESPACE_MAYBE
From f078f88436f73b30448b9d91154d4693f5c9d089 Mon Sep 17 00:00:00 2001
From: Simon J <2857218+mwnciau@users.noreply.github.com>
Date: Tue, 8 Jul 2025 07:09:49 +0100
Subject: [PATCH 12/12] Add custom highlighter for HTML in HEEx templates that
uses the HEEx HTML lexer
---
src/org/elixir_lang/heex/TemplateHighlighter.java | 9 ++++++++-
.../heex/html/HeexHTMLFileHighlighter.java | 12 ++++++++++++
2 files changed, 20 insertions(+), 1 deletion(-)
create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java
diff --git a/src/org/elixir_lang/heex/TemplateHighlighter.java b/src/org/elixir_lang/heex/TemplateHighlighter.java
index a0d199878..4480b33ed 100644
--- a/src/org/elixir_lang/heex/TemplateHighlighter.java
+++ b/src/org/elixir_lang/heex/TemplateHighlighter.java
@@ -1,5 +1,6 @@
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;
@@ -11,6 +12,7 @@
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;
@@ -47,7 +49,12 @@ public TemplateHighlighter(@Nullable Project project,
}
}
- SyntaxHighlighter dataHighlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(type, project, virtualFile);
+ 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);
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);
+ }
+}