Skip to content

Commit b7c764c

Browse files
committed
HEEx work (from files I found locally)
1 parent 7feff75 commit b7c764c

File tree

9 files changed

+303
-0
lines changed

9 files changed

+303
-0
lines changed

resources/META-INF/plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
language="EEx" extensions="eex"/>
6969
<fileType name="Live Embedded Elixir" implementationClass="org.elixir_lang.leex.file.Type" fieldName="INSTANCE"
7070
language="EEx" extensions="leex"/>
71+
<fileType name="HEEx" implementationClass="org.elixir_lang.heex.file.Type" fieldName="INSTANCE"
72+
language="EEx" extensions="heex"/>
7173
<lang.fileViewProviderFactory language="EEx"
7274
implementationClass="org.elixir_lang.eex.file.view_provider.Factory"/>
7375
<lang.parserDefinition language="EEx" implementationClass="org.elixir_lang.eex.ParserDefinition"/>

resources/icons/file/heex.svg

Lines changed: 19 additions & 0 deletions
Loading

resources/icons/file/heex_dark.svg

Lines changed: 19 additions & 0 deletions
Loading

src/org/elixir_lang/eex/TemplateHighlighter.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ public TemplateHighlighter(@Nullable Project project,
5151
type = onlyTemplateDataFileType(virtualFile).orElse(null);
5252
}
5353

54+
if (type == null) {
55+
FileType fileType = virtualFile.getFileType();
56+
if (fileType instanceof org.elixir_lang.eex.file.Type eexType) {
57+
com.intellij.lang.Language defaultLang = eexType.defaultTemplateDataLanguage();
58+
if (defaultLang != null) {
59+
type = defaultLang.getAssociatedFileType();
60+
}
61+
}
62+
}
63+
5464
if (type == null) {
5565
type = Language.defaultTemplateLanguageFileType();
5666
}

src/org/elixir_lang/eex/file/Type.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ open class Type protected constructor(lang: Language? = org.elixir_lang.eex.Lang
1919
override fun getDefaultExtension(): String = DEFAULT_EXTENSION
2020
override fun getIcon(): Icon? = Icons.FILE
2121

22+
open fun defaultTemplateDataLanguage(): Language? = null
23+
2224
companion object {
2325
private const val DEFAULT_EXTENSION = "eex"
2426

src/org/elixir_lang/eex/file/ViewProvider.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.intellij.lang.LanguageParserDefinitions;
44
import com.intellij.lang.ParserDefinition;
5+
import com.intellij.openapi.fileTypes.FileType;
56
import com.intellij.openapi.fileTypes.LanguageFileType;
67
import com.intellij.openapi.project.Project;
78
import com.intellij.openapi.vfs.VirtualFile;
@@ -85,6 +86,13 @@ private static com.intellij.lang.Language templateDataLanguage(@NotNull PsiManag
8586
.orElse(null);
8687
}
8788

89+
if (templateDataLanguage == null) {
90+
FileType fileType = virtualFile.getFileType();
91+
if (fileType instanceof org.elixir_lang.eex.file.Type eexType) {
92+
templateDataLanguage = eexType.defaultTemplateDataLanguage();
93+
}
94+
}
95+
8896
if (templateDataLanguage == null) {
8997
templateDataLanguage = Language.defaultTemplateLanguageFileType().getLanguage();
9098
}

src/org/elixir_lang/heex/Icons.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.elixir_lang.heex
2+
3+
import com.intellij.openapi.util.IconLoader
4+
5+
object Icons {
6+
@JvmField
7+
val FILE = IconLoader.getIcon("/icons/file/heex.svg", Icons.javaClass)
8+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.elixir_lang.heex.file
2+
3+
import com.intellij.lang.html.HTMLLanguage
4+
import com.intellij.openapi.fileTypes.LanguageFileType
5+
import org.elixir_lang.heex.Icons
6+
import javax.swing.Icon
7+
8+
class Type : org.elixir_lang.eex.file.Type() {
9+
override fun getName(): String = "HEEx"
10+
override fun getDisplayName(): String = "HEEx"
11+
override fun getDescription(): String = "HTML+EEx template file"
12+
override fun getDefaultExtension(): String = "heex"
13+
override fun getIcon(): Icon = Icons.FILE
14+
override fun defaultTemplateDataLanguage(): com.intellij.lang.Language = HTMLLanguage.INSTANCE
15+
16+
companion object {
17+
val INSTANCE: LanguageFileType = Type()
18+
}
19+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
package org.elixir_lang.heex
2+
3+
import com.intellij.lang.html.HTMLLanguage
4+
import com.intellij.openapi.fileTypes.FileTypeManager
5+
import com.intellij.psi.templateLanguages.ConfigurableTemplateLanguageFileViewProvider
6+
import org.elixir_lang.PlatformTestCase
7+
import org.elixir_lang.ElixirLanguage
8+
import org.elixir_lang.eex.Language as EExLanguage
9+
10+
class HEExFileTypeTest : PlatformTestCase() {
11+
12+
fun testFileTypeRegistered() {
13+
val fileType = FileTypeManager.getInstance().getFileTypeByExtension("heex")
14+
assertInstanceOf(fileType, org.elixir_lang.heex.file.Type::class.java)
15+
assertEquals("HEEx", fileType.name)
16+
}
17+
18+
fun testFileTypeProperties() {
19+
val fileType = org.elixir_lang.heex.file.Type.INSTANCE
20+
assertEquals("HEEx", fileType.name)
21+
assertEquals("heex", fileType.defaultExtension)
22+
assertNotNull(fileType.icon)
23+
}
24+
25+
fun testDefaultTemplateDataLanguageIsHtml() {
26+
val fileType = org.elixir_lang.heex.file.Type.INSTANCE as org.elixir_lang.heex.file.Type
27+
assertEquals(HTMLLanguage.INSTANCE, fileType.defaultTemplateDataLanguage())
28+
}
29+
30+
fun testEexDefaultTemplateDataLanguageIsNull() {
31+
val fileType = org.elixir_lang.eex.file.Type.INSTANCE as org.elixir_lang.eex.file.Type
32+
assertNull(fileType.defaultTemplateDataLanguage())
33+
}
34+
35+
fun testViewProviderUsesHtmlForHeexFile() {
36+
val file = myFixture.configureByText("template.heex", "<div><%= @name %></div>")
37+
val viewProvider = file.viewProvider
38+
39+
assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java)
40+
val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider
41+
assertEquals(HTMLLanguage.INSTANCE, templateViewProvider.templateDataLanguage)
42+
}
43+
44+
fun testViewProviderLanguagesIncludeElixir() {
45+
val file = myFixture.configureByText("template.heex", "<div><%= @name %></div>")
46+
val languages = file.viewProvider.languages
47+
48+
assertTrue("Should contain EEx language", languages.any { it.isKindOf(EExLanguage.INSTANCE) })
49+
assertTrue("Should contain HTML language", languages.any { it.isKindOf(HTMLLanguage.INSTANCE) })
50+
assertTrue("Should contain Elixir language", languages.any { it.isKindOf(ElixirLanguage.INSTANCE) })
51+
}
52+
53+
fun testEexFileDoesNotDefaultToHtml() {
54+
val file = myFixture.configureByText("template.eex", "<%= @name %>")
55+
val viewProvider = file.viewProvider
56+
57+
assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java)
58+
val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider
59+
// Plain .eex without double extension should NOT be HTML
60+
assertFalse(
61+
"Plain .eex should not default to HTML",
62+
templateViewProvider.templateDataLanguage == HTMLLanguage.INSTANCE
63+
)
64+
}
65+
66+
fun testHtmlEexFileUsesHtml() {
67+
val file = myFixture.configureByText("template.html.eex", "<div><%= @name %></div>")
68+
val viewProvider = file.viewProvider
69+
70+
assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java)
71+
val templateViewProvider = viewProvider as ConfigurableTemplateLanguageFileViewProvider
72+
assertEquals(HTMLLanguage.INSTANCE, templateViewProvider.templateDataLanguage)
73+
}
74+
75+
fun testHeexWithEexTags() {
76+
val file = myFixture.configureByText(
77+
"page.heex",
78+
"""
79+
<div class="container">
80+
<%= if @show do %>
81+
<p><%= @message %></p>
82+
<% end %>
83+
</div>
84+
""".trimIndent()
85+
)
86+
assertNotNull(file)
87+
assertFalse(file.text.isEmpty())
88+
}
89+
90+
// ------------------------------------------------------------------
91+
// HEEx-specific syntax patterns (from Phoenix LiveView assigns-eex guide).
92+
// These use curly-brace expressions that our EEx parser does not handle
93+
// natively -- they are treated as HTML text. These tests verify the files
94+
// still parse without exceptions.
95+
// ------------------------------------------------------------------
96+
97+
fun testHeexCurlyBraceAssignExpression() {
98+
// {@ ...} assign access syntax
99+
assertHeexParses(
100+
"""
101+
<div id={"user_#{@user.id}"}>
102+
{@user.name}
103+
</div>
104+
""".trimIndent()
105+
)
106+
}
107+
108+
fun testHeexCurlyBraceFunctionCall() {
109+
// {function(...)} expression syntax
110+
assertHeexParses(
111+
"""
112+
<h1>{expand_title(@title)}</h1>
113+
""".trimIndent()
114+
)
115+
}
116+
117+
fun testHeexFunctionComponent() {
118+
// <.component /> syntax
119+
assertHeexParses(
120+
"""
121+
<.show_name name={@user.name} />
122+
""".trimIndent()
123+
)
124+
}
125+
126+
fun testHeexFunctionComponentWithBody() {
127+
// <.component>...</.component> syntax with inner block
128+
assertHeexParses(
129+
"""
130+
<div class="card">
131+
<.card_header title={@title} class={@title_class} />
132+
<.card_body>
133+
{render_slot(@inner_block)}
134+
</.card_body>
135+
<.card_footer on_close={@on_close} />
136+
</div>
137+
""".trimIndent()
138+
)
139+
}
140+
141+
fun testHeexAssignsSpread() {
142+
// {assigns} spread syntax
143+
assertHeexParses(
144+
"""
145+
<div class="card">
146+
<.card_header {assigns} />
147+
</div>
148+
""".trimIndent()
149+
)
150+
}
151+
152+
fun testHeexForComprehension() {
153+
// :for special attribute
154+
assertHeexParses(
155+
"""
156+
<section :for={post <- @posts}>
157+
<h1>{expand_title(post.title)}</h1>
158+
</section>
159+
""".trimIndent()
160+
)
161+
}
162+
163+
fun testHeexForComprehensionWithKey() {
164+
// :for + :key special attributes
165+
assertHeexParses(
166+
"""
167+
<section :for={post <- @posts} :key={post.id}>
168+
<h1>{expand_title(post.title)}</h1>
169+
</section>
170+
""".trimIndent()
171+
)
172+
}
173+
174+
fun testHeexEexForComprehension() {
175+
// Traditional EEx for comprehension (still supported in HEEx)
176+
assertHeexParses(
177+
"""
178+
<%= for post <- @posts do %>
179+
<section>
180+
<h1>{expand_title(post.title)}</h1>
181+
</section>
182+
<% end %>
183+
""".trimIndent()
184+
)
185+
}
186+
187+
fun testHeexMixedSyntax() {
188+
// Realistic template mixing EEx tags and HEEx curly-brace expressions
189+
assertHeexParses(
190+
"""
191+
<div class="container">
192+
<%= if @show do %>
193+
<.header title={@page_title} />
194+
<section :for={item <- @items} :key={item.id}>
195+
<p>{item.name}</p>
196+
<p><%= item.description %></p>
197+
</section>
198+
<% end %>
199+
</div>
200+
""".trimIndent()
201+
)
202+
}
203+
204+
private fun assertHeexParses(content: String) {
205+
val file = myFixture.configureByText("template.heex", content)
206+
assertNotNull("File should be created", file)
207+
assertFalse("File should not be empty", file.text.isEmpty())
208+
209+
val viewProvider = file.viewProvider
210+
assertInstanceOf(viewProvider, ConfigurableTemplateLanguageFileViewProvider::class.java)
211+
assertEquals(
212+
HTMLLanguage.INSTANCE,
213+
(viewProvider as ConfigurableTemplateLanguageFileViewProvider).templateDataLanguage
214+
)
215+
}
216+
}

0 commit comments

Comments
 (0)