Skip to content

Commit 671eca0

Browse files
committed
Add completion and navigation support for Twig guard tag with function, filter, and test types
1 parent edd9ee5 commit 671eca0

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package fr.adrienbrault.idea.symfony2plugin.templating;
2+
3+
import com.intellij.codeInsight.lookup.LookupElement;
4+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
5+
import com.intellij.openapi.project.Project;
6+
import com.intellij.psi.PsiElement;
7+
import com.intellij.util.containers.ContainerUtil;
8+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
9+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
10+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
11+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
12+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
13+
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigExtension;
14+
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigExtensionLookupElement;
15+
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigExtensionParser;
16+
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
17+
import org.apache.commons.lang3.StringUtils;
18+
import org.jetbrains.annotations.NotNull;
19+
import org.jetbrains.annotations.Nullable;
20+
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
import java.util.Map;
25+
26+
/**
27+
* Provides completion and navigation for Twig guard tag
28+
* {% guard function importmap %}
29+
* {% guard filter upper %}
30+
* {% guard test even %}
31+
*
32+
* @author Daniel Espendiller <daniel@espendiller.net>
33+
*/
34+
public class GuardGotoCompletionRegistrar implements GotoCompletionRegistrar {
35+
@Override
36+
public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
37+
// {% guard <caret> %}
38+
// Completion for "function", "filter", "test" keywords
39+
registrar.register(TwigPattern.getGuardTypePattern(), psiElement -> {
40+
if (!Symfony2ProjectComponent.isEnabled(psiElement)) {
41+
return null;
42+
}
43+
44+
return new GuardTypeGotoCompletionProvider(psiElement);
45+
});
46+
47+
// {% guard function <caret> %}
48+
// Completion for callable name based on type
49+
registrar.register(TwigPattern.getGuardCallablePattern(), psiElement -> {
50+
if (!Symfony2ProjectComponent.isEnabled(psiElement)) {
51+
return null;
52+
}
53+
54+
return new GuardCallableGotoCompletionProvider(psiElement);
55+
});
56+
}
57+
58+
/**
59+
* {% guard <caret> %}
60+
* Provides completion for "function", "filter", "test" keywords
61+
*/
62+
private static class GuardTypeGotoCompletionProvider extends GotoCompletionProvider {
63+
GuardTypeGotoCompletionProvider(PsiElement psiElement) {
64+
super(psiElement);
65+
}
66+
67+
@NotNull
68+
@Override
69+
public Collection<LookupElement> getLookupElements() {
70+
Collection<LookupElement> lookupElements = new ArrayList<>();
71+
72+
lookupElements.add(LookupElementBuilder.create("function").withTypeText("Twig function", true).withIcon(Symfony2Icons.SYMFONY));
73+
lookupElements.add(LookupElementBuilder.create("filter").withTypeText("Twig filter", true).withIcon(Symfony2Icons.SYMFONY));
74+
lookupElements.add(LookupElementBuilder.create("test").withTypeText("Twig test", true).withIcon(Symfony2Icons.SYMFONY));
75+
76+
return lookupElements;
77+
}
78+
79+
@NotNull
80+
@Override
81+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
82+
return Collections.emptyList();
83+
}
84+
}
85+
86+
/**
87+
* {% guard function <caret> %}
88+
* {% guard filter <caret> %}
89+
* {% guard test <caret> %}
90+
*
91+
* Provides completion and navigation for callable names based on the guard type
92+
*/
93+
private static class GuardCallableGotoCompletionProvider extends GotoCompletionProvider {
94+
GuardCallableGotoCompletionProvider(PsiElement psiElement) {
95+
super(psiElement);
96+
}
97+
98+
@NotNull
99+
@Override
100+
public Collection<LookupElement> getLookupElements() {
101+
Collection<LookupElement> lookupElements = new ArrayList<>();
102+
103+
Project project = getProject();
104+
String guardType = getGuardType(getElement());
105+
106+
if ("function".equals(guardType)) {
107+
// Provide Twig functions
108+
for (Map.Entry<String, TwigExtension> entry : TwigExtensionParser.getFunctions(project).entrySet()) {
109+
lookupElements.add(new TwigExtensionLookupElement(project, entry.getKey(), entry.getValue()));
110+
}
111+
} else if ("filter".equals(guardType)) {
112+
// Provide Twig filters
113+
for (Map.Entry<String, TwigExtension> entry : TwigExtensionParser.getFilters(project).entrySet()) {
114+
lookupElements.add(new TwigExtensionLookupElement(project, entry.getKey(), entry.getValue()));
115+
}
116+
} else if ("test".equals(guardType)) {
117+
// Provide Twig tests
118+
for (Map.Entry<String, TwigExtension> entry : TwigExtensionParser.getSimpleTest(project).entrySet()) {
119+
lookupElements.add(new TwigExtensionLookupElement(project, entry.getKey(), entry.getValue()));
120+
}
121+
}
122+
123+
return lookupElements;
124+
}
125+
126+
@NotNull
127+
@Override
128+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
129+
String text = element.getText();
130+
if (StringUtils.isBlank(text)) {
131+
return Collections.emptyList();
132+
}
133+
134+
Collection<PsiElement> targets = new ArrayList<>();
135+
Project project = getProject();
136+
String guardType = getGuardType(element);
137+
138+
Map<String, TwigExtension> extensions = null;
139+
if ("function".equals(guardType)) {
140+
extensions = TwigExtensionParser.getFunctions(project);
141+
} else if ("filter".equals(guardType)) {
142+
extensions = TwigExtensionParser.getFilters(project);
143+
} else if ("test".equals(guardType)) {
144+
extensions = TwigExtensionParser.getSimpleTest(project);
145+
}
146+
147+
if (extensions != null) {
148+
for (Map.Entry<String, TwigExtension> entry : extensions.entrySet()) {
149+
if (text.equals(entry.getKey())) {
150+
ContainerUtil.addIfNotNull(
151+
targets,
152+
TwigExtensionParser.getExtensionTarget(project, entry.getValue())
153+
);
154+
}
155+
}
156+
}
157+
158+
return targets;
159+
}
160+
161+
/**
162+
* Get the guard type (function, filter, test) by looking at the previous IDENTIFIER
163+
* {% guard function importmap %}
164+
* ^-------^
165+
*/
166+
@Nullable
167+
private String getGuardType(@NotNull PsiElement element) {
168+
PsiElement prevIdentifier = PsiElementUtils.getPrevSiblingOfType(
169+
element,
170+
com.intellij.patterns.PlatformPatterns.psiElement(com.jetbrains.twig.TwigTokenTypes.IDENTIFIER)
171+
);
172+
173+
if (prevIdentifier != null) {
174+
String text = prevIdentifier.getText();
175+
if ("function".equals(text) || "filter".equals(text) || "test".equals(text)) {
176+
return text;
177+
}
178+
}
179+
180+
return null;
181+
}
182+
}
183+
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigPattern.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,4 +1640,52 @@ public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext process
16401640
elementType != TwigTokenTypes.COLON;
16411641
}
16421642
}
1643+
1644+
/**
1645+
* {% guard function importmap %}
1646+
* {% guard filter upper %}
1647+
* {% guard test even %}
1648+
*
1649+
* Pattern for the type keyword after "guard" tag name
1650+
*/
1651+
public static ElementPattern<PsiElement> getGuardTypePattern() {
1652+
return PlatformPatterns
1653+
.psiElement(TwigTokenTypes.IDENTIFIER)
1654+
.afterLeafSkipping(
1655+
PlatformPatterns.or(
1656+
PlatformPatterns.psiElement(PsiWhiteSpace.class),
1657+
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE)
1658+
),
1659+
PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("guard")
1660+
)
1661+
.withParent(
1662+
PlatformPatterns.psiElement(TwigElementTypes.TAG)
1663+
)
1664+
.withLanguage(TwigLanguage.INSTANCE);
1665+
}
1666+
1667+
/**
1668+
* {% guard function importmap %}
1669+
* {% guard filter upper %}
1670+
* {% guard test even %}
1671+
*
1672+
* Pattern for the callable name after "guard" type keyword
1673+
*/
1674+
public static ElementPattern<PsiElement> getGuardCallablePattern() {
1675+
return PlatformPatterns
1676+
.psiElement(TwigTokenTypes.IDENTIFIER)
1677+
.afterLeafSkipping(
1678+
PlatformPatterns.or(
1679+
PlatformPatterns.psiElement(PsiWhiteSpace.class),
1680+
PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE)
1681+
),
1682+
PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(
1683+
PlatformPatterns.string().oneOf("function", "filter", "test")
1684+
)
1685+
)
1686+
.withParent(
1687+
PlatformPatterns.psiElement(TwigElementTypes.TAG)
1688+
)
1689+
.withLanguage(TwigLanguage.INSTANCE);
1690+
}
16431691
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,7 @@
773773
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.form.FormOptionGotoCompletionRegistrar"/>
774774
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.BlockGotoCompletionRegistrar"/>
775775
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.FilterGotoCompletionRegistrar"/>
776+
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.GuardGotoCompletionRegistrar"/>
776777
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.TranslationTagGotoCompletionRegistrar"/>
777778
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.form.FormGotoCompletionRegistrar"/>
778779
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.config.php.PhpEventDispatcherGotoCompletionRegistrar"/>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.templating;
2+
3+
import com.jetbrains.twig.TwigFileType;
4+
import fr.adrienbrault.idea.symfony2plugin.templating.GuardGotoCompletionRegistrar;
5+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
6+
7+
/**
8+
* @author Daniel Espendiller <daniel@espendiller.net>
9+
* @see GuardGotoCompletionRegistrar
10+
*/
11+
public class GuardGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase {
12+
public void setUp() throws Exception {
13+
super.setUp();
14+
myFixture.copyFileToProject("GuardGotoCompletionRegistrarTest.php");
15+
}
16+
17+
public String getTestDataPath() {
18+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures";
19+
}
20+
21+
/**
22+
* Test completion for guard type keywords
23+
*/
24+
public void testCompletionForGuardTypeKeywords() {
25+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard <caret> %}", "function", "filter", "test");
26+
}
27+
28+
/**
29+
* Test completion for Twig function names after "guard function"
30+
*/
31+
public void testCompletionForGuardFunction() {
32+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard function <caret> %}", "test_function");
33+
}
34+
35+
/**
36+
* Test completion for Twig filter names after "guard filter"
37+
*/
38+
public void testCompletionForGuardFilter() {
39+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard filter <caret> %}", "test_filter");
40+
}
41+
42+
/**
43+
* Test completion for Twig test names after "guard test"
44+
*/
45+
public void testCompletionForGuardTest() {
46+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard test <caret> %}", "test_test");
47+
}
48+
49+
/**
50+
* Test navigation for guard function
51+
*/
52+
public void testNavigationForGuardFunction() {
53+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard function test_fun<caret>ction %}", "test_function");
54+
}
55+
56+
/**
57+
* Test navigation for guard filter
58+
*/
59+
public void testNavigationForGuardFilter() {
60+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard filter test_fil<caret>ter %}", "test_filter");
61+
}
62+
63+
/**
64+
* Test navigation for guard test
65+
*/
66+
public void testNavigationForGuardTest() {
67+
assertCompletionContains(TwigFileType.INSTANCE, "{% guard test test_te<caret>st %}", "test_test");
68+
}
69+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace
4+
{
5+
interface Twig_ExtensionInterface
6+
{
7+
public function getTokenParsers();
8+
public function getNodeVisitors();
9+
public function getFilters();
10+
public function getTests();
11+
public function getFunctions();
12+
public function getOperators();
13+
public function getGlobals();
14+
public function getName();
15+
}
16+
17+
class Twig_SimpleFilter
18+
{
19+
}
20+
21+
class Twig_SimpleFunction
22+
{
23+
}
24+
25+
class Twig_SimpleTest
26+
{
27+
}
28+
}
29+
30+
namespace Twig
31+
{
32+
class TestExtension implements \Twig_ExtensionInterface
33+
{
34+
public function getFunctions()
35+
{
36+
return [
37+
new \Twig_SimpleFunction('test_function', [$this, 'test_function']),
38+
];
39+
}
40+
41+
public function getFilters()
42+
{
43+
return [
44+
new \Twig_SimpleFilter('test_filter', [$this, 'test_filter']),
45+
];
46+
}
47+
48+
public function getTests()
49+
{
50+
return [
51+
new \Twig_SimpleTest('test_test', [$this, 'test_test']),
52+
];
53+
}
54+
55+
public function test_function()
56+
{
57+
}
58+
59+
public function test_filter()
60+
{
61+
}
62+
63+
public function test_test()
64+
{
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)