Skip to content

Commit

Permalink
Add support for built-in functions like i18n (fixes #163)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjansen committed Feb 29, 2024
1 parent 80f7fb5 commit 4d648d3
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import com.intellij.psi.PsiMethod
object PebbleCore {

private val filtersKey = Key.create<PsiClass>("PEBBLE_FILTERS_CLASS")
private val functionsKey = Key.create<PsiClass>("PEBBLE_FUNCTIONS_CLASS")
private val testsKey = Key.create<PsiClass>("PEBBLE_TESTS_CLASS")

private val filtersByProject = hashMapOf<Project, Map<String, Filter>>()
private val functionsByProject = hashMapOf<Project, Map<String, Filter>>()
private val testsByProject = hashMapOf<Project, Map<String, Test>>()

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

fun getFunctions(project: Project): Collection<Filter> {
return functionsByProject.computeIfAbsent(project, this::initFunctions).values
}

fun getTests(project: Project): Collection<Test> {
return testsByProject.computeIfAbsent(project, this::initTests).values
}
Expand All @@ -37,6 +43,13 @@ object PebbleCore {
return filtersClass?.methods?.map { Filter(it) }?.map { it.name to it }?.toMap() ?: emptyMap()
}

private fun initFunctions(project: Project): Map<String, Filter> {
val functionsClass =
ResourceUtil.loadPsiClassFromFile("/implicitCode/Functions.java", functionsKey, project)

return functionsClass?.methods?.map { Filter(it) }?.map { it.name to it }?.toMap() ?: emptyMap()
}

private fun initTests(project: Project): Map<String, Test> {
val testsClass =
ResourceUtil.loadPsiClassFromFile("/implicitCode/Tests.java", testsKey, project)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.github.bjansen.intellij.pebble.psi

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

list.addAll(findLocalMacros())
list.addAll(PebbleCore.getFunctions(project).map { it.source })
list.addAll(SpringExtension.getImplicitFunctions(this))

return list
Expand Down
9 changes: 3 additions & 6 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@
]]></description>

<change-notes><![CDATA[
<b>v0.10</b>
<b>v0.11</b>
<ul>
<li>The plugin also recognizes the `.pebble` extension (<a href="https://github.com/bjansen/pebble-intellij/issues/56">#56</a>)</li>
<li>Allow method calls on literals (<a href="https://github.com/bjansen/pebble-intellij/issues/62">#62</a>)</li>
<li>Support built-in tests (<a href="https://github.com/bjansen/pebble-intellij/issues/51">#51</a>)</li>
<li>Drop support for IDEA < 2022.2 (<a href="https://github.com/bjansen/pebble-intellij/issues/82">#82</a>)</li>
<li>Fixed issue in EAP 2024.1 (<a href="https://github.com/bjansen/pebble-intellij/issues/174">#174</a>)</li>
<li>Automatically show the completion popup after typing `{%`</li>
<li>Add support for built-in functions like `i18n` (<a href="https://github.com/bjansen/pebble-intellij/issues/163">#163</a>)</li>
</ul>
Full changelog at https://github.com/bjansen/pebble-intellij/milestone/10?closed=1
Expand Down
152 changes: 152 additions & 0 deletions src/main/resources/implicitCode/Functions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Lists all builtin filters and their arguments.
*/
interface Functions {

/**
* <p>The block function is used to render the contents of a block more than once.
* It is not to be confused with the block tag which is used to declare blocks.</p>
*
* <p>The following example will render the contents of the "post" block twice; once where it
* was declared and again using the block function:</p>
*
* <code><pre>
* {% block "post" %} content {% endblock %}
*
* {{ block("post") }}
* </pre></code>
*
* The above example will output the following:
*
* <code><pre>
* content
*
* content
* </pre></code>
* @param blockName
*/
block(String blockName);

/**
* <p>The i18n function is used to retrieve messages from a locale-specific ResourceBundle.
* Every PebbleTemplate is assigned a default locale from the PebbleEngine. At the point of evaluation,
* this locale can be changed with an argument to the evaluate(...) method of the individual template.</p>
*
* <p>The i18n function wraps around ResourceBundle.getBundle(name, locale).getObject(key).
* The first argument to the i18n function is the name of the bundle and the second argument is the key
* within the bundle.</p>
*
* <code><pre>
* {{ i18n("messages","greeting") }}
* </pre></code>
*
* <p>The above example assumes you have messages.properties on your classpath and that that file contains
* a key by the name of greeting. If the locale of that template was es_US for example, it would look
* for a message_es_US.properties file instead.</p>
*
* <p>Going a little further, you can use variables within your message and pass a list of params
* to this function which will replace your variables using MessageFormat:</p>
*
* <code><pre>
* {# greeting.someone=Hello, {0} #}
* {{ i18n("messages","greeting", "Jacob") }}
*
* {# output: Hello, Jacob #}
* </pre></code>
*
* @param bundle
* @param key
* @param params
*/
i18n(String bundle, String key, Object... params);

/**
* The max function will return the largest of it's numerical arguments.
*
* <code><pre>
* {{ max(user.age, 80) }}
* </pre></code>
*
* @param left
* @param right
*/
max(Object left, Object right);

/**
* The min function will return the smallest of it's numerical arguments.
*
* <code><pre>
* {{ min(user.age, 80) }}
* </pre></code>
*
* @param left
* @param right
*/
min(Object left, Object right);

/**
* <p>The parent function is used inside of a block to render the content that the parent template
* would have rendered inside of the block had the current template not overriden it. It is similar
* to Java's super keyword.</p>
*
* <p>Let's assume you have a template, "parent.peb" that looks something like this:</p>
*
* <code><pre>
* {% block "content" %}
* parent contents
* {% endblock %}
* </pre></code>
*
* <p>And then you have another template, "child.peb" that extends "parent.peb":</p>
*
* <code><pre>
* {% extends "parent.peb" %}
*
* {% block "content" %}
* child contents
* {{ parent() }}
* {% endblock %}
* </pre></code>
*
* <p>The output will look something like the following:</p>
*
* <code><pre>
* parent contents
* child contents
* </pre></code>
*/
parent();

/**
* The range function will return a list containing an arithmetic progression of numbers:
*
* <code><pre>
* {% for i in range(0, 3) %}
* {{ i }},
* {% endfor %}
*
* {# outputs 0, 1, 2, 3, #}
* </pre></code>
*
* When step is given (as the third parameter), it specifies the increment (or decrement):
*
* <code><pre>
* {% for i in range(0, 6, 2) %}
* {{ i }},
* {% endfor %}
*
* {# outputs 0, 2, 4, 6, #}
* </pre></code>
*
* Pebble built-in .. operator is just a shortcut for the range function with a step of 1+
*
* <code><pre>
* {% for i in 0..3 %}
* {{ i }},
* {% endfor %}
*
* {# outputs 0, 1, 2, 3, #}
* </pre></code>
*/
range(int start, int end, int step);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ class PebbleIdentifierCompletionTest : AbstractCompletionTest() {
assertLookupsContain(implicitFunctions)
}

fun testCompletionOfBuiltinImplicitFunctions() {
myFixture.configureByFile("file1.peb")
myFixture.addClass("package java.util; interface Map<K, V> {}")
myFixture.addClass("package javax.servlet.http; class HttpServletRequest {}")
myFixture.addClass("package javax.servlet.http; class HttpServletResponse {}")
myFixture.addClass("package javax.servlet.http; class HttpSession {}")
myFixture.complete(CompletionType.BASIC)

assertLookupsContain(listOf("block", "i18n", "max", "min", "parent", "range"))
}

fun testCompletionOfGettersAsProperties() {
myFixture.configureByFile("file2.peb")
myFixture.addClass(File("$testDataPath/MyClass.java").readText(Charsets.UTF_8))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,31 @@ class IdentifierReferencesTest : AbstractReferencesTest() {
} else {
fail("Reference resolved to nothing")
}
}

fun testReferenceToBuiltinFunction() {
initFile("functions.peb")

moveCaret(5)

var resolved = resolveRefAtCaret()

if (resolved != null) {
assert(resolved is PsiMethod)
assert((resolved as PsiMethod).name == "i18n")
} else {
fail("Reference resolved to nothing")
}

moveCaret(32)

resolved = resolveRefAtCaret()

if (resolved != null) {
assert(resolved is PsiMethod)
assert((resolved as PsiMethod).name == "max")
} else {
fail("Reference resolved to nothing")
}
}
}
2 changes: 2 additions & 0 deletions src/test/resources/references/functions.peb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ i18n("bundle", "key") }}
{{ max(1, 2) }}

0 comments on commit 4d648d3

Please sign in to comment.