Skip to content

Commit 8584cd4

Browse files
committed
Add support for enum Twig function with completion, navigation, and insert handler for enum classes
1 parent 671eca0 commit 8584cd4

File tree

7 files changed

+150
-8
lines changed

7 files changed

+150
-8
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/insertHandler/TwigEscapedSlashInsertHandler.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
import com.intellij.openapi.editor.Document;
77
import com.intellij.psi.PsiElement;
88
import com.intellij.psi.SmartPsiElementPointer;
9+
import com.jetbrains.php.lang.psi.elements.PhpClass;
910
import com.jetbrains.php.lang.psi.elements.PhpClassMember;
1011
import org.apache.commons.lang3.StringUtils;
1112
import org.jetbrains.annotations.NotNull;
1213

14+
import java.util.Objects;
15+
1316
/**
1417
* "Foo\Foo" => "Foo\\Foo"
1518
*
@@ -24,27 +27,29 @@ public void handleInsert(@NotNull InsertionContext context, @NotNull LookupEleme
2427
return;
2528
}
2629

27-
String classFqn = null;
28-
String fieldName = null;
30+
String insertString = "";
31+
PsiElement element = smartPsiElementPointer.getElement();
32+
if (element instanceof PhpClassMember phpClassMember) {
33+
String classFqn = Objects.requireNonNull(phpClassMember.getContainingClass()).getFQN();
34+
String fieldName = phpClassMember.getName();
2935

30-
if (smartPsiElementPointer.getElement() instanceof PhpClassMember phpClassMember) {
31-
classFqn = phpClassMember.getContainingClass().getFQN();
32-
fieldName = phpClassMember.getName();
36+
insertString = StringUtils.stripStart(classFqn, "\\").replace("\\", "\\\\") + "::" + fieldName;
37+
} else if (element instanceof PhpClass phpClass) {
38+
insertString = StringUtils.stripStart(phpClass.getFQN(), "\\").replace("\\", "\\\\");
3339
}
3440

3541
Document document = context.getDocument();
3642
document.deleteString(context.getStartOffset(), context.getTailOffset());
3743

38-
String s = StringUtils.stripStart(classFqn, "\\").replace("\\", "\\\\") + "::" + fieldName;
39-
document.insertString(context.getStartOffset(), s);
44+
document.insertString(context.getStartOffset(), insertString);
4045
context.commitDocument();
4146

4247
PsiElement elementAt = context.getFile().findElementAt(context.getEditor().getCaretModel().getOffset());
4348
if (elementAt == null) {
4449
return;
4550
}
4651

47-
context.getEditor().getCaretModel().moveCaretRelatively(s.length(), 0, false, false, true);
52+
context.getEditor().getCaretModel().moveCaretRelatively(insertString.length(), 0, false, false, true);
4853
}
4954

5055
public static TwigEscapedSlashInsertHandler getInstance(){

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,13 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
437437
new ConstantCompletionParametersCompletionProvider()
438438
);
439439

440+
// {{ enum('App\\Config\\SomeOption') }}
441+
extend(
442+
CompletionType.BASIC,
443+
TwigPattern.getPrintBlockOrTagFunctionPattern("enum"),
444+
new EnumCompletionParametersCompletionProvider()
445+
);
446+
440447
// {% e => {% extends '...'
441448
extend(
442449
CompletionType.BASIC,
@@ -1632,6 +1639,73 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
16321639
}
16331640
}
16341641

1642+
/**
1643+
* {{ enum('<caret>') }}
1644+
* {{ enum('Foo\\<caret>') }}
1645+
*/
1646+
private static class EnumCompletionParametersCompletionProvider extends CompletionProvider<CompletionParameters> {
1647+
public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) {
1648+
PsiElement position = parameters.getPosition();
1649+
if (!Symfony2ProjectComponent.isEnabled(position)) {
1650+
return;
1651+
}
1652+
1653+
Project project = position.getProject();
1654+
PhpIndex instance = PhpIndex.getInstance(project);
1655+
1656+
PrefixMatcher prefixMatcher = resultSet.getPrefixMatcher();
1657+
String prefix = prefixMatcher.getPrefix();
1658+
1659+
if (prefix.contains("\\")) {
1660+
// 'FOO\\Foo<caret>'
1661+
int i = prefix.lastIndexOf("\\");
1662+
String substring = "\\" + StringUtils.stripStart(prefix.substring(0, i).replace("\\\\", "\\"), "\\");
1663+
String pre = prefix.substring(prefix.lastIndexOf("\\") + 1);
1664+
CompletionResultSet completionResultSet = resultSet.withPrefixMatcher(pre);
1665+
1666+
for (PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(project, substring)) {
1667+
// Only include enum classes
1668+
if (!phpClass.isEnum()) {
1669+
continue;
1670+
}
1671+
1672+
String fqn = phpClass.getFQN().substring(substring.length());
1673+
String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\");
1674+
1675+
LookupElementBuilder element = LookupElementBuilder
1676+
.create(fqn.replace("\\", "\\\\"))
1677+
.withInsertHandler(TwigEscapedSlashInsertHandler.getInstance())
1678+
.withTypeText(fqnNoLeadingSlash, true)
1679+
.withIcon(phpClass.getIcon());
1680+
1681+
completionResultSet.addElement(element);
1682+
}
1683+
} else {
1684+
// '<caret>'
1685+
Collection<PhpClass> phpClasses = new ArrayList<>();
1686+
for (String className : instance.getAllClassNames(resultSet.getPrefixMatcher())) {
1687+
phpClasses.addAll(instance.getClassesByName(className));
1688+
}
1689+
1690+
for (PhpClass phpClass : phpClasses) {
1691+
// Only include enum classes
1692+
if (!phpClass.isEnum()) {
1693+
continue;
1694+
}
1695+
1696+
String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\");
1697+
LookupElementBuilder element = LookupElementBuilder
1698+
.createWithSmartPointer(phpClass.getName(), phpClass)
1699+
.withInsertHandler(TwigEscapedSlashInsertHandler.getInstance())
1700+
.withTypeText(fqnNoLeadingSlash, true)
1701+
.withIcon(phpClass.getIcon());
1702+
1703+
resultSet.addElement(element);
1704+
}
1705+
}
1706+
}
1707+
}
1708+
16351709
@NotNull
16361710
private Collection<LookupElement> processVariables(@NotNull PsiElement psiElement, @NotNull Predicate<PhpType> filter, @NotNull Function<Map.Entry<String, Pair<String, LookupElement>>, String> map) {
16371711
Project project = psiElement.getProject();

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int offset,
168168
targets.addAll(getConstantGoto(psiElement));
169169
}
170170

171+
// enum('App\\Config\\SomeOption')
172+
if (TwigPattern.getPrintBlockOrTagFunctionPattern("enum").accepts(psiElement)) {
173+
targets.addAll(getEnumGoto(psiElement));
174+
}
175+
171176
// {# @var user \Foo #}
172177
if (TwigPattern.getTwigTypeDocBlockPattern().accepts(psiElement)) {
173178
targets.addAll(getVarClassGoto(psiElement));
@@ -378,6 +383,21 @@ private Collection<PsiElement> getConstantGoto(@NotNull PsiElement psiElement) {
378383
return targetPsiElements;
379384
}
380385

386+
@NotNull
387+
private Collection<PsiElement> getEnumGoto(@NotNull PsiElement psiElement) {
388+
String contents = psiElement.getText();
389+
if(StringUtils.isBlank(contents)) {
390+
return Collections.emptyList();
391+
}
392+
393+
PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), contents.replace("\\\\", "\\"));
394+
if (phpClass == null || !phpClass.isEnum()) {
395+
return Collections.emptyList();
396+
}
397+
398+
return Collections.singletonList(phpClass);
399+
}
400+
381401
/**
382402
* Extract class from inline variables
383403
*

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public void setUp() throws Exception {
1515
myFixture.copyFileToProject("classes.php");
1616
myFixture.copyFileToProject("TwigTemplateCompletionContributorTest.php");
1717
myFixture.copyFileToProject("routing.xml");
18+
myFixture.copyFileToProject("TwigFilterExtension.php");
1819
}
1920

2021
public String getTestDataPath() {
@@ -128,4 +129,25 @@ public void testSelfMacroImport() {
128129
"foobar"
129130
);
130131
}
132+
133+
public void testThatEnumProvidesCompletionForEnumClasses() {
134+
// Test basic completion - enum should be available
135+
assertCompletionContains(TwigFileType.INSTANCE, "{{ enum('<caret>') }}", "FooEnum");
136+
137+
// Ensure non-enum classes are NOT included
138+
assertCompletionNotContains(TwigFileType.INSTANCE, "{{ enum('<caret>') }}", "FooConst");
139+
140+
// Test namespace-based completion
141+
assertCompletionContains(TwigFileType.INSTANCE, "{{ enum('App\\<caret>') }}", "\\\\Bike\\\\FooEnum");
142+
assertCompletionNotContains(TwigFileType.INSTANCE, "{{ enum('App\\<caret>') }}", "\\\\Bike\\\\FooConst");
143+
144+
// Test sub-namespace completion
145+
assertCompletionContains(TwigFileType.INSTANCE, "{{ enum('App\\\\Bike\\\\<caret>') }}", "FooEnum");
146+
assertCompletionNotContains(TwigFileType.INSTANCE, "{{ enum('App\\\\Bike\\\\<caret>') }}", "FooConst");
147+
148+
// Test that the insert handler properly escapes backslashes
149+
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ enum('<caret>') }}", "{{ enum('App\\\\Bike\\\\FooEnum') }}", l -> "FooEnum".equals(l.getLookupString()));
150+
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ enum('App\\<caret>') }}", "{{ enum('App\\\\\\Bike\\\\FooEnum') }}", l -> "\\\\Bike\\\\FooEnum".equals(l.getLookupString()));
151+
assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ enum('App\\\\Bike\\\\Foo<caret>') }}", "{{ enum('App\\\\Bike\\\\FooEnum') }}", l -> "FooEnum".equals(l.getLookupString()));
152+
}
131153
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,15 @@ public void testFoo() {
251251
PlatformPatterns.psiElement(Method.class)
252252
);
253253
}
254+
255+
public void testThatEnumProvidesNavigationToEnumClass() {
256+
assertNavigationMatch(TwigFileType.INSTANCE, "{{ enum('App\\\\Bike\\\\Foo<caret>Enum') }}", PlatformPatterns.psiElement(PhpClass.class).withName("FooEnum"));
257+
assertNavigationMatch(TwigFileType.INSTANCE, "{{ enum('\\\\App\\\\Bike\\\\Foo<caret>Enum') }}", PlatformPatterns.psiElement(PhpClass.class).withName("FooEnum"));
258+
259+
assertNavigationMatch(TwigFileType.INSTANCE, "{% if foo == enum('App\\\\Bike\\\\Foo<caret>Enum') %}", PlatformPatterns.psiElement(PhpClass.class).withName("FooEnum"));
260+
assertNavigationMatch(TwigFileType.INSTANCE, "{% set foo == enum('\\\\App\\\\Bike\\\\Foo<caret>Enum') %}", PlatformPatterns.psiElement(PhpClass.class).withName("FooEnum"));
261+
262+
// Should not navigate to non-enum classes
263+
assertNavigationIsEmpty(TwigFileType.INSTANCE, "{{ enum('App\\\\Bike\\\\Foo<caret>Const') }}");
264+
}
254265
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigFilterExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace{
44
function my_json_decode() {}
55
function twig_test_even() {}
6+
function constant($name) {}
7+
function enum($name) {}
68

79
interface Twig_ExtensionInterface {}
810
interface Twig_Environment {}
@@ -43,6 +45,8 @@ public function getFunctions()
4345
return array(
4446
new \Twig_SimpleFunction('foobar', array($this, 'foobar')),
4547
new \Twig_SimpleFunction('json_bar', 'my_json_decode'),
48+
new \Twig_SimpleFunction('constant', 'constant'),
49+
new \Twig_SimpleFunction('enum', 'enum'),
4650
);
4751
}
4852

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/classes.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,9 @@ class Request
8181
public function isMethod(string $method) {}
8282
}
8383
}
84+
85+
86+
namespace App\Bike
87+
{
88+
enum FooEnum {}
89+
}

0 commit comments

Comments
 (0)