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 = " { + {BRACE_OPENING} { yybegin(BEGIN_MATCHED_BRACES); + return Types.BRACE_OPENING; } + {START_SCRIPT_TAG} { yybegin(SCRIPT_TAG); return Types.DATA; } + {START_STYLE_TAG} { yybegin(STYLE_TAG); return Types.DATA; } +} + + { + {END_SCRIPT_TAG} { yybegin(YYINITIAL); return Types.DATA; } +} + + { + {END_STYLE_TAG} { yybegin(YYINITIAL); return Types.DATA; } +} + + { {ESCAPED_OPENING} { return Types.ESCAPED_OPENING; } {OPENING} { yybegin(MARKER_MAYBE); return Types.OPENING; } - {BRACE_OPENING} { yybegin(BEGIN_MATCHED_BRACES); - return Types.BRACE_OPENING; } {ANY} { return Types.DATA; } } From fa14638f2be56822eaaee030553e1c2897331ebc Mon Sep 17 00:00:00 2001 From: Simon J <2857218+mwnciau@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:42:06 +0100 Subject: [PATCH 08/12] Add back in all the HTML functionality to our custom HTML language --- resources/META-INF/plugin.xml | 42 +++++++++++++++---- .../heex/html/HeexHTMLAutoPopupHandler.java | 36 ++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 4815ccfc1..2841e638e 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -10,9 +10,6 @@ on how to target different products --> com.intellij.modules.lang org.intellij.plugins.markdown - - - com.intellij.modules.java @@ -90,24 +87,51 @@ implementationClass="org.elixir_lang.heex.ParserDefinition" /> - + - + implementationClass="org.elixir_lang.heex.html.HeexHTMLParserDefinition"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java b/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java new file mode 100644 index 000000000..548418f62 --- /dev/null +++ b/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java @@ -0,0 +1,36 @@ +package org.elixir_lang.heex.html; + +import com.intellij.codeInsight.AutoPopupController; +import com.intellij.codeInsight.editorActions.HtmlAutoPopupHandler; +import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.xml.XmlText; +import com.intellij.psi.xml.XmlTokenType; +import org.jetbrains.annotations.NotNull; + +public class HeexHTMLAutoPopupHandler extends HtmlAutoPopupHandler { + @NotNull + public TypedHandlerDelegate.Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { + if (charTyped == '&' && file instanceof HeexHTMLFileImpl) { + PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + if (element == null) { + return Result.CONTINUE; + } else { + IElementType elementType = element.getNode().getElementType(); + PsiElement parent = element.getParent(); + if (elementType == XmlTokenType.XML_END_TAG_START || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN || (elementType == XmlTokenType.XML_DATA_CHARACTERS || elementType == XmlTokenType.XML_WHITE_SPACE) && parent instanceof XmlText) { + AutoPopupController.getInstance(project).scheduleAutoPopup(editor); + return Result.STOP; + } else { + return Result.CONTINUE; + } + } + } else { + return Result.CONTINUE; + } + } +} From ce6141e7d94ec04f81e283b3c9038f0cefe735ed Mon Sep 17 00:00:00 2001 From: Simon J <2857218+mwnciau@users.noreply.github.com> Date: Sat, 5 Jul 2025 15:29:50 +0100 Subject: [PATCH 09/12] Add an OuterLanguageRangePatcher for HeexHTML to fix issues with the HTML Lexer being in the wrong state during injections --- resources/META-INF/plugin.xml | 4 +++ .../elixir_lang/heex/file/ViewProvider.java | 17 ++++------- .../heex/file/psi/TemplateData.java | 28 +++++++++++++++++++ .../HeexHTMLOuterLanguageRangePatcher.java | 12 ++++++++ src/org/elixir_lang/heex/psi/Types.java | 6 ---- 5 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 src/org/elixir_lang/heex/file/psi/TemplateData.java create mode 100644 src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 2841e638e..02f72bb3c 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -96,6 +96,10 @@ + { - IElementType elementType; - if (language == ElixirLanguage.INSTANCE) { - elementType = new HTMLEmbeddedElixir(); + return new HTMLEmbeddedElixir(); } else { - elementType = HEEX_TEMPLATE_ELEMENT; + return TemplateData.INSTANCE; } - - return elementType; } ); } @@ -149,7 +142,7 @@ public com.intellij.lang.Language getBaseLanguage() { @NotNull @Override public Set getLanguages() { - return new THashSet<>(Arrays.asList(getTemplateDataLanguage(), getBaseLanguage(), ElixirLanguage.INSTANCE)); + return Set.of(getTemplateDataLanguage(), getBaseLanguage(), ElixirLanguage.INSTANCE); } @NotNull @@ -163,7 +156,7 @@ public com.intellij.lang.Language getTemplateDataLanguage() { } @Override - protected MultiplePsiFilesPerDocumentFileViewProvider cloneInner(VirtualFile fileCopy) { + protected @NotNull MultiplePsiFilesPerDocumentFileViewProvider cloneInner(@NotNull VirtualFile fileCopy) { return new ViewProvider(getManager(), fileCopy, false, baseLanguage, templateDataLanguage); } diff --git a/src/org/elixir_lang/heex/file/psi/TemplateData.java b/src/org/elixir_lang/heex/file/psi/TemplateData.java new file mode 100644 index 000000000..712d5c9ac --- /dev/null +++ b/src/org/elixir_lang/heex/file/psi/TemplateData.java @@ -0,0 +1,28 @@ +package org.elixir_lang.heex.file.psi; + +import com.intellij.lang.Language; +import com.intellij.psi.templateLanguages.TemplateDataElementType; +import com.intellij.psi.tree.IElementType; +import org.elixir_lang.heex.HeexLanguage; +import org.elixir_lang.heex.psi.Types; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class TemplateData extends TemplateDataElementType { + public static final TemplateData INSTANCE = new TemplateData( + "HEEX_TEMPLATE_DATA", + HeexLanguage.INSTANCE, + Types.DATA, + Types.HEEX_OUTER_ELEMENT + ); + + protected TemplateData(@NonNls String debugName, Language language, @NotNull IElementType templateElementType, @NotNull IElementType outerElementType) { + super(debugName, language, templateElementType, outerElementType); + } + + @Override + protected boolean isInsertionToken(@Nullable IElementType tokenType, @NotNull CharSequence tokenSequence) { + return true; + } +} diff --git a/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java new file mode 100644 index 000000000..5fa39e825 --- /dev/null +++ b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java @@ -0,0 +1,12 @@ +package org.elixir_lang.heex.html; + +import com.intellij.psi.templateLanguages.TemplateDataElementType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class HeexHTMLOuterLanguageRangePatcher implements TemplateDataElementType.OuterLanguageRangePatcher { + @Override + public @Nullable String getTextForOuterLanguageInsertionRange(@NotNull TemplateDataElementType templateDataElementType, @NotNull CharSequence charSequence) { + return "HEExInjection"; + } +} diff --git a/src/org/elixir_lang/heex/psi/Types.java b/src/org/elixir_lang/heex/psi/Types.java index 767f10d8a..4f22f0cd3 100644 --- a/src/org/elixir_lang/heex/psi/Types.java +++ b/src/org/elixir_lang/heex/psi/Types.java @@ -29,12 +29,6 @@ public interface Types { 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) { From 40697930385d06ac7a1d7b6a642e016c558ab37c Mon Sep 17 00:00:00 2001 From: Simon J <2857218+mwnciau@users.noreply.github.com> Date: Sun, 6 Jul 2025 19:37:05 +0100 Subject: [PATCH 10/12] Remove custom HeexHTML language and instead override the default Html Lexer in out TemplateData class --- resources/META-INF/plugin.xml | 43 ++----------------- .../elixir_lang/heex/TemplateHighlighter.java | 6 --- src/org/elixir_lang/heex/file/Type.kt | 5 --- .../elixir_lang/heex/file/ViewProvider.java | 14 +----- .../heex/file/psi/TemplateData.java | 30 +++++++++++++ .../heex/html/HeexHTMLAutoPopupHandler.java | 36 ---------------- .../heex/html/HeexHTMLFileElementType.java | 11 ++++- .../heex/html/HeexHTMLFileHighlighter.java | 11 ----- .../html/HeexHTMLFileHighlighterFactory.java | 11 ----- .../heex/html/HeexHTMLFileImpl.java | 22 ---------- .../heex/html/HeexHTMLFileType.java | 31 ------------- .../HeexHTMLOuterLanguageRangePatcher.java | 2 +- .../heex/html/HeexHTMLParserDefinition.java | 20 --------- .../inspections/HTMLInspectionSuppressor.java | 6 +-- 14 files changed, 47 insertions(+), 201 deletions(-) delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLFileType.java delete mode 100644 src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 02f72bb3c..cee5617a9 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -87,55 +87,18 @@ implementationClass="org.elixir_lang.heex.ParserDefinition" /> - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/org/elixir_lang/heex/TemplateHighlighter.java b/src/org/elixir_lang/heex/TemplateHighlighter.java index 377272f60..a0d199878 100644 --- a/src/org/elixir_lang/heex/TemplateHighlighter.java +++ b/src/org/elixir_lang/heex/TemplateHighlighter.java @@ -1,6 +1,5 @@ package org.elixir_lang.heex; -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; @@ -12,7 +11,6 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.templateLanguages.TemplateDataLanguageMappings; import org.elixir_lang.ElixirFileType; -import org.elixir_lang.heex.html.HeexHTMLLanguage; import org.elixir_lang.heex.psi.Types; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -37,10 +35,6 @@ public TemplateHighlighter(@Nullable Project project, TemplateDataLanguageMappings.getInstance(project).getMapping(virtualFile); if (language != null) { - if (language.is(HTMLLanguage.INSTANCE)) { - language = HeexHTMLLanguage.INSTANCE; - } - type = language.getAssociatedFileType(); } diff --git a/src/org/elixir_lang/heex/file/Type.kt b/src/org/elixir_lang/heex/file/Type.kt index 7d1d719ba..248258a53 100644 --- a/src/org/elixir_lang/heex/file/Type.kt +++ b/src/org/elixir_lang/heex/file/Type.kt @@ -1,16 +1,13 @@ package org.elixir_lang.heex.file -import com.intellij.ide.highlighter.HtmlFileType 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 com.intellij.psi.xml.HtmlFileElementType import org.elixir_lang.heex.HeexLanguage import org.elixir_lang.heex.Icons import org.elixir_lang.heex.TemplateHighlighter -import org.elixir_lang.heex.html.HeexHTMLFileType import java.util.* import java.util.stream.Collectors import javax.swing.Icon @@ -52,8 +49,6 @@ open class Type protected constructor(lang: Language? = HeexLanguage.INSTANCE) : ?.let { type -> 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/file/ViewProvider.java b/src/org/elixir_lang/heex/file/ViewProvider.java index 039067f86..9def4f387 100644 --- a/src/org/elixir_lang/heex/file/ViewProvider.java +++ b/src/org/elixir_lang/heex/file/ViewProvider.java @@ -2,7 +2,6 @@ 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; @@ -14,16 +13,13 @@ 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.HeexLanguage; import org.elixir_lang.heex.element_type.HTMLEmbeddedElixir; import org.elixir_lang.heex.file.psi.TemplateData; -import org.elixir_lang.heex.html.HeexHTMLLanguage; 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; @@ -104,11 +100,7 @@ protected PsiFile createFile(@NotNull com.intellij.lang.Language language) { ParserDefinition parserDefinition; PsiFileImpl psiFileImpl; - if (language.isKindOf(HTMLLanguage.INSTANCE)) { - parserDefinition = getDefinition(HeexHTMLLanguage.INSTANCE); - } else { - parserDefinition = getDefinition(language); - } + parserDefinition = getDefinition(language); if (parserDefinition == null) { psiFileImpl = null; @@ -148,10 +140,6 @@ 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/psi/TemplateData.java b/src/org/elixir_lang/heex/file/psi/TemplateData.java index 712d5c9ac..c49645d89 100644 --- a/src/org/elixir_lang/heex/file/psi/TemplateData.java +++ b/src/org/elixir_lang/heex/file/psi/TemplateData.java @@ -1,9 +1,18 @@ package org.elixir_lang.heex.file.psi; import com.intellij.lang.Language; +import com.intellij.lang.html.HTMLLanguage; +import com.intellij.psi.FileViewProvider; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.SingleRootFileViewProvider; +import com.intellij.psi.impl.source.html.HtmlFileImpl; import com.intellij.psi.templateLanguages.TemplateDataElementType; import com.intellij.psi.tree.IElementType; +import com.intellij.testFramework.LightVirtualFile; import org.elixir_lang.heex.HeexLanguage; +import org.elixir_lang.heex.html.HeexHTMLFileElementType; +import org.elixir_lang.heex.html.HeexHTMLLanguage; import org.elixir_lang.heex.psi.Types; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -25,4 +34,25 @@ protected TemplateData(@NonNls String debugName, Language language, @NotNull IEl protected boolean isInsertionToken(@Nullable IElementType tokenType, @NotNull CharSequence tokenSequence) { return true; } + + @Override + protected PsiFile createPsiFileFromSource(final Language language, CharSequence sourceCode, PsiManager manager) { + if (language == HTMLLanguage.INSTANCE) { + return createSpoofedPsiFileForHTML(sourceCode, manager); + } + + return super.createPsiFileFromSource(language, sourceCode, manager); + } + + /** For HTML, we manually create the PSI file so we can force it to use our custom lexer */ + private PsiFile createSpoofedPsiFileForHTML(CharSequence sourceCode, PsiManager manager) { + LightVirtualFile virtualFile = new LightVirtualFile("HEExHTML", this.createTemplateFakeFileType(HeexHTMLLanguage.INSTANCE), sourceCode); + FileViewProvider viewProvider = new SingleRootFileViewProvider(manager, virtualFile, false) { + public @NotNull Language getBaseLanguage() { + return HTMLLanguage.INSTANCE; + } + }; + + return new HtmlFileImpl(viewProvider, HeexHTMLFileElementType.INSTANCE); + } } diff --git a/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java b/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java deleted file mode 100644 index 548418f62..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLAutoPopupHandler.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.elixir_lang.heex.html; - -import com.intellij.codeInsight.AutoPopupController; -import com.intellij.codeInsight.editorActions.HtmlAutoPopupHandler; -import com.intellij.codeInsight.editorActions.TypedHandlerDelegate; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.tree.IElementType; -import com.intellij.psi.xml.XmlText; -import com.intellij.psi.xml.XmlTokenType; -import org.jetbrains.annotations.NotNull; - -public class HeexHTMLAutoPopupHandler extends HtmlAutoPopupHandler { - @NotNull - public TypedHandlerDelegate.Result checkAutoPopup(char charTyped, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) { - if (charTyped == '&' && file instanceof HeexHTMLFileImpl) { - PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); - if (element == null) { - return Result.CONTINUE; - } else { - IElementType elementType = element.getNode().getElementType(); - PsiElement parent = element.getParent(); - if (elementType == XmlTokenType.XML_END_TAG_START || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER || elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN || (elementType == XmlTokenType.XML_DATA_CHARACTERS || elementType == XmlTokenType.XML_WHITE_SPACE) && parent instanceof XmlText) { - AutoPopupController.getInstance(project).scheduleAutoPopup(editor); - return Result.STOP; - } else { - return Result.CONTINUE; - } - } - } else { - return Result.CONTINUE; - } - } -} diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java index 0479100b2..c20c7550d 100644 --- a/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java +++ b/src/org/elixir_lang/heex/html/HeexHTMLFileElementType.java @@ -1,9 +1,11 @@ package org.elixir_lang.heex.html; import com.intellij.lang.*; +import com.intellij.lang.html.HTMLLanguage; import com.intellij.openapi.project.Project; import com.intellij.psi.PsiElement; import com.intellij.psi.xml.HtmlFileElementType; +import org.jetbrains.annotations.NotNull; public class HeexHTMLFileElementType extends HtmlFileElementType { public static final HeexHTMLFileElementType INSTANCE = new HeexHTMLFileElementType(); @@ -16,10 +18,15 @@ public ASTNode parseContents(ASTNode chameleon) { assert psi != null : "Bad chameleon: " + chameleon; Project project = psi.getProject(); - PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, null, HeexHTMLLanguage.INSTANCE, chameleon.getChars()); - PsiParser parser = (LanguageParserDefinitions.INSTANCE.forLanguage(HeexHTMLLanguage.INSTANCE)).createParser(project); + PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, new HeexHTMLLexer(), HTMLLanguage.INSTANCE, chameleon.getChars()); + PsiParser parser = (LanguageParserDefinitions.INSTANCE.forLanguage(HTMLLanguage.INSTANCE)).createParser(project); ASTNode node = parser.parse(this, builder); return node.getFirstChildNode(); } + + @Override + public @NotNull Language getLanguage() { + return HTMLLanguage.INSTANCE; + } } diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java b/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java deleted file mode 100644 index 2fde4ded2..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighter.java +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index b8d0015bc..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLFileHighlighterFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -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(); - } -} diff --git a/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java b/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java deleted file mode 100644 index 91c4326e3..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLFileImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index bea7b22e7..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLFileType.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.elixir_lang.heex.html; - -import com.intellij.icons.AllIcons.FileTypes; -import com.intellij.ide.highlighter.HtmlFileType; -import org.jetbrains.annotations.NotNull; - -import javax.swing.*; - -public class HeexHTMLFileType extends HtmlFileType { - 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/HeexHTMLOuterLanguageRangePatcher.java b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java index 5fa39e825..f888b7eaf 100644 --- a/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java +++ b/src/org/elixir_lang/heex/html/HeexHTMLOuterLanguageRangePatcher.java @@ -7,6 +7,6 @@ public class HeexHTMLOuterLanguageRangePatcher implements TemplateDataElementType.OuterLanguageRangePatcher { @Override public @Nullable String getTextForOuterLanguageInsertionRange(@NotNull TemplateDataElementType templateDataElementType, @NotNull CharSequence charSequence) { - return "HEExInjection"; + return "Injection"; } } diff --git a/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java b/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java deleted file mode 100644 index 934005732..000000000 --- a/src/org/elixir_lang/heex/html/HeexHTMLParserDefinition.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.elixir_lang.heex.html; - -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 { - @Override - 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 c1c8ee333..40f904901 100644 --- a/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java +++ b/src/org/elixir_lang/heex/inspections/HTMLInspectionSuppressor.java @@ -8,7 +8,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlTag; import com.intellij.xml.util.CheckEmptyTagInspection; -import org.elixir_lang.heex.html.HeexHTMLLanguage; +import org.elixir_lang.heex.HeexLanguage; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -21,13 +21,13 @@ public class HTMLInspectionSuppressor implements InspectionSuppressor { ); - public boolean isSuppressedFor(PsiElement element, String toolId) { + public boolean isSuppressedFor(@NotNull PsiElement element, @NotNull String toolId) { if (!SUPPRESSED_INSPECTIONS.contains(toolId)) { return false; } PsiFile file = element.getContainingFile(); - if (file != null && file.getViewProvider().hasLanguage(HeexHTMLLanguage.INSTANCE)) { + if (file != null && file.getViewProvider().hasLanguage(HeexLanguage.INSTANCE)) { XmlTag xmlTag = PsiTreeUtil.getParentOfType(element, XmlTag.class, false); // Tag names that contain dots are HEEx components From 6cb611f1c6384e4df65dde36cc4cccfdfb18c845 Mon Sep 17 00:00:00 2001 From: Simon J <2857218+mwnciau@users.noreply.github.com> Date: Sun, 6 Jul 2025 19:42:06 +0100 Subject: [PATCH 11/12] Fix edge case where components beginning with "Script" or "Style" were not being parsed in HEEx --- gen/org/elixir_lang/heex/lexer/Flex.java | 80 +++++++++++++----------- src/org/elixir_lang/HEEx.flex | 4 +- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/gen/org/elixir_lang/heex/lexer/Flex.java b/gen/org/elixir_lang/heex/lexer/Flex.java index 097163bf0..12502d6bf 100644 --- a/gen/org/elixir_lang/heex/lexer/Flex.java +++ b/gen/org/elixir_lang/heex/lexer/Flex.java @@ -44,7 +44,8 @@ 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\1\u0100\u10fe\u0200"; + "\1\0\1\u0100\24\u0200\1\u0300\11\u0200\1\u0400\17\u0200\1\u0500"+ + "\u10cf\u0200"; private static int [] zzUnpackcmap_top() { int [] result = new int[4352]; @@ -72,17 +73,19 @@ private static int zzUnpackcmap_top(String packed, int offset, int [] result) { 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"+ - "\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"; + "\11\0\2\1\1\2\2\1\22\0\1\3\2\0\1\4"+ + "\1\0\1\5\11\0\1\6\14\0\1\7\1\10\1\11"+ + "\4\0\1\12\1\0\1\13\3\0\1\14\2\0\1\15"+ + "\3\0\1\16\1\0\1\17\1\20\1\21\4\0\1\22"+ + "\11\0\1\12\1\0\1\13\3\0\1\14\2\0\1\15"+ + "\3\0\1\16\1\0\1\17\1\20\1\21\4\0\1\22"+ + "\1\0\1\23\1\24\1\25\7\0\1\2\32\0\1\2"+ + "\217\0\2\14\115\0\1\20\u0200\0\1\2\177\0\13\2"+ + "\35\0\2\2\5\0\1\2\57\0\1\2\240\0\1\2"+ + "\377\0"; private static int [] zzUnpackcmap_blocks() { - int [] result = new int[768]; + int [] result = new int[1536]; int offset = 0; offset = zzUnpackcmap_blocks(ZZ_CMAP_BLOCKS_PACKED_0, offset, result); return result; @@ -108,11 +111,11 @@ private static int zzUnpackcmap_blocks(String packed, int offset, int [] result) private static final String ZZ_ACTION_PACKED_0 = "\11\0\2\1\1\2\2\3\2\4\2\5\1\6\1\7"+ "\1\10\1\11\1\12\1\13\1\14\1\15\2\1\1\16"+ - "\3\0\1\17\2\0\1\20\7\0\1\21\7\0\1\22"+ - "\2\0\1\23\2\0\1\24"; + "\3\0\1\17\2\0\1\20\7\0\1\21\13\0\1\22"+ + "\2\0\1\23\1\24"; private static int [] zzUnpackAction() { - int [] result = new int[58]; + int [] result = new int[60]; int offset = 0; offset = zzUnpackAction(ZZ_ACTION_PACKED_0, offset, result); return result; @@ -137,17 +140,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\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"; + "\0\0\0\26\0\54\0\102\0\130\0\156\0\204\0\232"+ + "\0\260\0\306\0\334\0\306\0\306\0\362\0\306\0\u0108"+ + "\0\306\0\u0108\0\306\0\306\0\306\0\306\0\306\0\306"+ + "\0\306\0\306\0\u011e\0\u0134\0\u014a\0\u0160\0\362\0\u0176"+ + "\0\306\0\u018c\0\u01a2\0\306\0\u01b8\0\u01ce\0\u01e4\0\u01fa"+ + "\0\u0210\0\u0226\0\u023c\0\306\0\u0252\0\u0268\0\u027e\0\u0294"+ + "\0\u02aa\0\u02c0\0\u02d6\0\u02ec\0\u0302\0\u0318\0\u032e\0\306"+ + "\0\u0344\0\u035a\0\306\0\306"; private static int [] zzUnpackRowMap() { - int [] result = new int[58]; + int [] result = new int[60]; int offset = 0; offset = zzUnpackRowMap(ZZ_ROWMAP_PACKED_0, offset, result); return result; @@ -170,22 +173,23 @@ 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\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"; + "\7\12\1\13\13\12\1\14\2\12\1\15\1\16\1\15"+ + "\1\16\22\15\5\17\1\20\20\17\5\21\1\22\20\21"+ + "\4\23\1\24\1\23\1\25\1\23\1\26\13\23\1\27"+ + "\1\23\26\30\23\21\1\31\1\21\1\32\7\12\1\33"+ + "\25\12\1\34\16\12\33\0\1\35\12\0\1\36\6\0"+ + "\1\37\1\0\1\37\3\0\1\40\27\0\1\41\21\0"+ + "\1\35\1\42\24\0\1\35\1\43\24\0\1\44\32\0"+ + "\1\45\6\0\1\46\11\0\1\47\40\0\1\50\25\0"+ + "\1\51\24\0\1\52\30\0\1\53\6\0\1\54\43\0"+ + "\1\55\16\0\1\56\27\0\1\57\26\0\1\60\32\0"+ + "\1\61\22\0\1\62\24\0\1\63\22\0\1\64\27\0"+ + "\1\65\24\0\1\66\32\0\1\67\5\0\3\70\5\0"+ + "\1\70\27\0\1\71\30\0\1\72\10\0\3\73\5\0"+ + "\1\73\25\0\1\74\35\0\1\71\4\0"; private static int [] zzUnpacktrans() { - int [] result = new int[798]; + int [] result = new int[880]; int offset = 0; offset = zzUnpacktrans(ZZ_TRANS_PACKED_0, offset, result); return result; @@ -225,10 +229,10 @@ private static int zzUnpacktrans(String packed, int offset, int [] result) { private static final String ZZ_ATTRIBUTE_PACKED_0 = "\11\0\1\11\1\1\2\11\1\1\1\11\1\1\1\11"+ "\1\1\10\11\3\1\3\0\1\11\2\0\1\11\7\0"+ - "\1\11\7\0\1\11\2\0\1\11\2\0\1\11"; + "\1\11\13\0\1\11\2\0\2\11"; private static int [] zzUnpackAttribute() { - int [] result = new int[58]; + int [] result = new int[60]; int offset = 0; offset = zzUnpackAttribute(ZZ_ATTRIBUTE_PACKED_0, offset, result); return result; diff --git a/src/org/elixir_lang/HEEx.flex b/src/org/elixir_lang/HEEx.flex index 0300f087c..b80a24952 100644 --- a/src/org/elixir_lang/HEEx.flex +++ b/src/org/elixir_lang/HEEx.flex @@ -43,9 +43,9 @@ PROCEDURAL_OPENING = {OPENING} " " WHITE_SPACE = [\ \t\f\r\n]+ ANY = [^] -START_SCRIPT_TAG = "] END_SCRIPT_TAG = "" -START_STYLE_TAG = "] END_STYLE_TAG = "" %state WHITESPACE_MAYBE 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); + } +}