Skip to content

Commit 4d648d3

Browse files
committed
Add support for built-in functions like i18n (fixes #163)
1 parent 80f7fb5 commit 4d648d3

File tree

7 files changed

+208
-6
lines changed

7 files changed

+208
-6
lines changed

src/main/kotlin/com/github/bjansen/intellij/pebble/lang/PebbleCore.kt

+13
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import com.intellij.psi.PsiMethod
99
object PebbleCore {
1010

1111
private val filtersKey = Key.create<PsiClass>("PEBBLE_FILTERS_CLASS")
12+
private val functionsKey = Key.create<PsiClass>("PEBBLE_FUNCTIONS_CLASS")
1213
private val testsKey = Key.create<PsiClass>("PEBBLE_TESTS_CLASS")
1314

1415
private val filtersByProject = hashMapOf<Project, Map<String, Filter>>()
16+
private val functionsByProject = hashMapOf<Project, Map<String, Filter>>()
1517
private val testsByProject = hashMapOf<Project, Map<String, Test>>()
1618

1719
fun getFilters(project: Project): Collection<Filter> {
@@ -22,6 +24,10 @@ object PebbleCore {
2224
return filtersByProject.computeIfAbsent(project, this::initFilters)[name]
2325
}
2426

27+
fun getFunctions(project: Project): Collection<Filter> {
28+
return functionsByProject.computeIfAbsent(project, this::initFunctions).values
29+
}
30+
2531
fun getTests(project: Project): Collection<Test> {
2632
return testsByProject.computeIfAbsent(project, this::initTests).values
2733
}
@@ -37,6 +43,13 @@ object PebbleCore {
3743
return filtersClass?.methods?.map { Filter(it) }?.map { it.name to it }?.toMap() ?: emptyMap()
3844
}
3945

46+
private fun initFunctions(project: Project): Map<String, Filter> {
47+
val functionsClass =
48+
ResourceUtil.loadPsiClassFromFile("/implicitCode/Functions.java", functionsKey, project)
49+
50+
return functionsClass?.methods?.map { Filter(it) }?.map { it.name to it }?.toMap() ?: emptyMap()
51+
}
52+
4053
private fun initTests(project: Project): Map<String, Test> {
4154
val testsClass =
4255
ResourceUtil.loadPsiClassFromFile("/implicitCode/Tests.java", testsKey, project)

src/main/kotlin/com/github/bjansen/intellij/pebble/psi/PebbleFile.kt

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.github.bjansen.intellij.pebble.psi
22

33
import com.github.bjansen.intellij.pebble.ext.SpringExtension
4+
import com.github.bjansen.intellij.pebble.lang.PebbleCore
45
import com.github.bjansen.intellij.pebble.lang.PebbleFileType
56
import com.github.bjansen.intellij.pebble.lang.PebbleLanguage
67
import com.github.bjansen.intellij.pebble.utils.ResourceUtil
@@ -55,6 +56,7 @@ class PebbleFile constructor(viewProvider: FileViewProvider) : PsiFileBase(viewP
5556
val list = arrayListOf<PsiNameIdentifierOwner>()
5657

5758
list.addAll(findLocalMacros())
59+
list.addAll(PebbleCore.getFunctions(project).map { it.source })
5860
list.addAll(SpringExtension.getImplicitFunctions(this))
5961

6062
return list

src/main/resources/META-INF/plugin.xml

+3-6
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,10 @@
99
]]></description>
1010

1111
<change-notes><![CDATA[
12-
<b>v0.10</b>
12+
<b>v0.11</b>
1313
<ul>
14-
<li>The plugin also recognizes the `.pebble` extension (<a href="https://github.com/bjansen/pebble-intellij/issues/56">#56</a>)</li>
15-
<li>Allow method calls on literals (<a href="https://github.com/bjansen/pebble-intellij/issues/62">#62</a>)</li>
16-
<li>Support built-in tests (<a href="https://github.com/bjansen/pebble-intellij/issues/51">#51</a>)</li>
17-
<li>Drop support for IDEA < 2022.2 (<a href="https://github.com/bjansen/pebble-intellij/issues/82">#82</a>)</li>
18-
<li>Fixed issue in EAP 2024.1 (<a href="https://github.com/bjansen/pebble-intellij/issues/174">#174</a>)</li>
14+
<li>Automatically show the completion popup after typing `{%`</li>
15+
<li>Add support for built-in functions like `i18n` (<a href="https://github.com/bjansen/pebble-intellij/issues/163">#163</a>)</li>
1916
</ul>
2017
2118
Full changelog at https://github.com/bjansen/pebble-intellij/milestone/10?closed=1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Lists all builtin filters and their arguments.
3+
*/
4+
interface Functions {
5+
6+
/**
7+
* <p>The block function is used to render the contents of a block more than once.
8+
* It is not to be confused with the block tag which is used to declare blocks.</p>
9+
*
10+
* <p>The following example will render the contents of the "post" block twice; once where it
11+
* was declared and again using the block function:</p>
12+
*
13+
* <code><pre>
14+
* {% block "post" %} content {% endblock %}
15+
*
16+
* {{ block("post") }}
17+
* </pre></code>
18+
*
19+
* The above example will output the following:
20+
*
21+
* <code><pre>
22+
* content
23+
*
24+
* content
25+
* </pre></code>
26+
* @param blockName
27+
*/
28+
block(String blockName);
29+
30+
/**
31+
* <p>The i18n function is used to retrieve messages from a locale-specific ResourceBundle.
32+
* Every PebbleTemplate is assigned a default locale from the PebbleEngine. At the point of evaluation,
33+
* this locale can be changed with an argument to the evaluate(...) method of the individual template.</p>
34+
*
35+
* <p>The i18n function wraps around ResourceBundle.getBundle(name, locale).getObject(key).
36+
* The first argument to the i18n function is the name of the bundle and the second argument is the key
37+
* within the bundle.</p>
38+
*
39+
* <code><pre>
40+
* {{ i18n("messages","greeting") }}
41+
* </pre></code>
42+
*
43+
* <p>The above example assumes you have messages.properties on your classpath and that that file contains
44+
* a key by the name of greeting. If the locale of that template was es_US for example, it would look
45+
* for a message_es_US.properties file instead.</p>
46+
*
47+
* <p>Going a little further, you can use variables within your message and pass a list of params
48+
* to this function which will replace your variables using MessageFormat:</p>
49+
*
50+
* <code><pre>
51+
* {# greeting.someone=Hello, {0} #}
52+
* {{ i18n("messages","greeting", "Jacob") }}
53+
*
54+
* {# output: Hello, Jacob #}
55+
* </pre></code>
56+
*
57+
* @param bundle
58+
* @param key
59+
* @param params
60+
*/
61+
i18n(String bundle, String key, Object... params);
62+
63+
/**
64+
* The max function will return the largest of it's numerical arguments.
65+
*
66+
* <code><pre>
67+
* {{ max(user.age, 80) }}
68+
* </pre></code>
69+
*
70+
* @param left
71+
* @param right
72+
*/
73+
max(Object left, Object right);
74+
75+
/**
76+
* The min function will return the smallest of it's numerical arguments.
77+
*
78+
* <code><pre>
79+
* {{ min(user.age, 80) }}
80+
* </pre></code>
81+
*
82+
* @param left
83+
* @param right
84+
*/
85+
min(Object left, Object right);
86+
87+
/**
88+
* <p>The parent function is used inside of a block to render the content that the parent template
89+
* would have rendered inside of the block had the current template not overriden it. It is similar
90+
* to Java's super keyword.</p>
91+
*
92+
* <p>Let's assume you have a template, "parent.peb" that looks something like this:</p>
93+
*
94+
* <code><pre>
95+
* {% block "content" %}
96+
* parent contents
97+
* {% endblock %}
98+
* </pre></code>
99+
*
100+
* <p>And then you have another template, "child.peb" that extends "parent.peb":</p>
101+
*
102+
* <code><pre>
103+
* {% extends "parent.peb" %}
104+
*
105+
* {% block "content" %}
106+
* child contents
107+
* {{ parent() }}
108+
* {% endblock %}
109+
* </pre></code>
110+
*
111+
* <p>The output will look something like the following:</p>
112+
*
113+
* <code><pre>
114+
* parent contents
115+
* child contents
116+
* </pre></code>
117+
*/
118+
parent();
119+
120+
/**
121+
* The range function will return a list containing an arithmetic progression of numbers:
122+
*
123+
* <code><pre>
124+
* {% for i in range(0, 3) %}
125+
* {{ i }},
126+
* {% endfor %}
127+
*
128+
* {# outputs 0, 1, 2, 3, #}
129+
* </pre></code>
130+
*
131+
* When step is given (as the third parameter), it specifies the increment (or decrement):
132+
*
133+
* <code><pre>
134+
* {% for i in range(0, 6, 2) %}
135+
* {{ i }},
136+
* {% endfor %}
137+
*
138+
* {# outputs 0, 2, 4, 6, #}
139+
* </pre></code>
140+
*
141+
* Pebble built-in .. operator is just a shortcut for the range function with a step of 1+
142+
*
143+
* <code><pre>
144+
* {% for i in 0..3 %}
145+
* {{ i }},
146+
* {% endfor %}
147+
*
148+
* {# outputs 0, 1, 2, 3, #}
149+
* </pre></code>
150+
*/
151+
range(int start, int end, int step);
152+
}

src/test/kotlin/com/github/bjansen/intellij/pebble/editor/completion/PebbleIdentifierCompletionTest.kt

+11
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ class PebbleIdentifierCompletionTest : AbstractCompletionTest() {
3030
assertLookupsContain(implicitFunctions)
3131
}
3232

33+
fun testCompletionOfBuiltinImplicitFunctions() {
34+
myFixture.configureByFile("file1.peb")
35+
myFixture.addClass("package java.util; interface Map<K, V> {}")
36+
myFixture.addClass("package javax.servlet.http; class HttpServletRequest {}")
37+
myFixture.addClass("package javax.servlet.http; class HttpServletResponse {}")
38+
myFixture.addClass("package javax.servlet.http; class HttpSession {}")
39+
myFixture.complete(CompletionType.BASIC)
40+
41+
assertLookupsContain(listOf("block", "i18n", "max", "min", "parent", "range"))
42+
}
43+
3344
fun testCompletionOfGettersAsProperties() {
3445
myFixture.configureByFile("file2.peb")
3546
myFixture.addClass(File("$testDataPath/MyClass.java").readText(Charsets.UTF_8))

src/test/kotlin/com/github/bjansen/intellij/pebble/psi/IdentifierReferencesTest.kt

+25
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,31 @@ class IdentifierReferencesTest : AbstractReferencesTest() {
300300
} else {
301301
fail("Reference resolved to nothing")
302302
}
303+
}
304+
305+
fun testReferenceToBuiltinFunction() {
306+
initFile("functions.peb")
307+
308+
moveCaret(5)
309+
310+
var resolved = resolveRefAtCaret()
311+
312+
if (resolved != null) {
313+
assert(resolved is PsiMethod)
314+
assert((resolved as PsiMethod).name == "i18n")
315+
} else {
316+
fail("Reference resolved to nothing")
317+
}
318+
319+
moveCaret(32)
303320

321+
resolved = resolveRefAtCaret()
322+
323+
if (resolved != null) {
324+
assert(resolved is PsiMethod)
325+
assert((resolved as PsiMethod).name == "max")
326+
} else {
327+
fail("Reference resolved to nothing")
328+
}
304329
}
305330
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{{ i18n("bundle", "key") }}
2+
{{ max(1, 2) }}

0 commit comments

Comments
 (0)