From d4c3f8fff1ef7425bc5e44c1f88fe71304da7476 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Sun, 7 Apr 2024 13:37:12 +0200 Subject: [PATCH] #2249 #2049 rebuild Twig constant completion and navigation and supporting enums --- .../TwigEscapedSlashInsertHandler.java | 53 ++++++++ .../TwigTemplateCompletionContributor.java | 120 ++++++++++++++---- .../TwigTemplateGoToDeclarationHandler.java | 18 +-- ...TwigTemplateCompletionContributorTest.java | 21 ++- ...wigTemplateGoToDeclarationHandlerTest.java | 4 +- .../TwigTemplateCompletionContributorTest.php | 20 +++ ...wigTemplateGoToLocalDeclarationHandler.php | 14 +- 7 files changed, 210 insertions(+), 40 deletions(-) create mode 100644 src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/insertHandler/TwigEscapedSlashInsertHandler.java diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/insertHandler/TwigEscapedSlashInsertHandler.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/insertHandler/TwigEscapedSlashInsertHandler.java new file mode 100644 index 000000000..1cbcb80d4 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/completion/insertHandler/TwigEscapedSlashInsertHandler.java @@ -0,0 +1,53 @@ +package fr.adrienbrault.idea.symfony2plugin.completion.insertHandler; + +import com.intellij.codeInsight.completion.InsertHandler; +import com.intellij.codeInsight.completion.InsertionContext; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.openapi.editor.Document; +import com.intellij.psi.PsiElement; +import com.intellij.psi.SmartPsiElementPointer; +import com.jetbrains.php.lang.psi.elements.PhpClassMember; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +/** + * "Foo\Foo" => "Foo\\Foo" + * + * @author Daniel Espendiller + */ +public class TwigEscapedSlashInsertHandler implements InsertHandler { + private static final TwigEscapedSlashInsertHandler instance = new TwigEscapedSlashInsertHandler(); + + @Override + public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) { + if (!(item.getObject() instanceof SmartPsiElementPointer smartPsiElementPointer)) { + return; + } + + String classFqn = null; + String fieldName = null; + + if (smartPsiElementPointer.getElement() instanceof PhpClassMember phpClassMember) { + classFqn = phpClassMember.getContainingClass().getFQN(); + fieldName = phpClassMember.getName(); + } + + Document document = context.getDocument(); + document.deleteString(context.getStartOffset(), context.getTailOffset()); + + String s = StringUtils.stripStart(classFqn, "\\").replace("\\", "\\\\") + "::" + fieldName; + document.insertString(context.getStartOffset(), s); + context.commitDocument(); + + PsiElement elementAt = context.getFile().findElementAt(context.getEditor().getCaretModel().getOffset()); + if (elementAt == null) { + return; + } + + context.getEditor().getCaretModel().moveCaretRelatively(s.length(), 0, false, false, true); + } + + public static TwigEscapedSlashInsertHandler getInstance(){ + return instance; + } +} diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java index b7121b660..6329a21b2 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java @@ -30,6 +30,7 @@ import fr.adrienbrault.idea.symfony2plugin.asset.AssetDirectoryReader; import fr.adrienbrault.idea.symfony2plugin.asset.provider.AssetCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.assetMapper.AssetMapperUtil; +import fr.adrienbrault.idea.symfony2plugin.completion.insertHandler.TwigEscapedSlashInsertHandler; import fr.adrienbrault.idea.symfony2plugin.dic.MethodReferenceBag; import fr.adrienbrault.idea.symfony2plugin.dic.ServiceCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; @@ -434,32 +435,7 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr extend( CompletionType.BASIC, TwigPattern.getPrintBlockOrTagFunctionPattern("constant"), - new CompletionProvider<>() { - public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) { - PsiElement position = parameters.getPosition(); - if (!Symfony2ProjectComponent.isEnabled(position)) { - return; - } - - PhpIndex instance = PhpIndex.getInstance(position.getProject()); - for (String constant : instance.getAllConstantNames(PrefixMatcher.ALWAYS_TRUE)) { - resultSet.addElement(LookupElementBuilder.create(constant).withIcon(PhpIcons.CONSTANT)); - } - - int foo = parameters.getOffset() - position.getTextRange().getStartOffset(); - String before = position.getText().substring(0, foo); - String[] parts = before.split("::"); - - if (parts.length >= 1) { - PhpClass phpClass = PhpElementsUtil.getClassInterface(position.getProject(), parts[0].replace("\\\\", "\\")); - if (phpClass != null) { - phpClass.getFields().stream().filter(Field::isConstant).forEach(field -> - resultSet.addElement(LookupElementBuilder.create(phpClass.getPresentableFQN().replace("\\", "\\\\") + "::" + field.getName()).withIcon(PhpIcons.CONSTANT)) - ); - } - } - } - } + new ConstantCompletionParametersCompletionProvider() ); // {% e => {% extends '...' @@ -1574,6 +1550,98 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull } } + /** + * {% constant('') %} + * {% constant('Foo\\') %} + * {% constant('FOO::') %} + */ + private static class ConstantCompletionParametersCompletionProvider extends CompletionProvider { + public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) { + PsiElement position = parameters.getPosition(); + if (!Symfony2ProjectComponent.isEnabled(position)) { + return; + } + + Project project = position.getProject(); + PhpIndex instance = PhpIndex.getInstance(project); + + PrefixMatcher prefixMatcher = resultSet.getPrefixMatcher(); + String prefix = prefixMatcher.getPrefix(); + if (prefix.contains(":")) { + // 'FOO::foo' + String[] parts = prefix.replace("::", ":").split(":"); + String substring = prefix.substring(prefix.lastIndexOf(":") + 1); + CompletionResultSet completionResultSet = resultSet.withPrefixMatcher(substring); + + PhpClass phpClass = PhpElementsUtil.getClassInterface(project, parts[0].replace("\\\\", "\\")); + if (phpClass != null) { + String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\"); + + for (PhpNamedElement item : getFieldsAndEnums(phpClass)) { + LookupElementBuilder element = LookupElementBuilder + .createWithSmartPointer(item.getName(), item) + .withTypeText(fqnNoLeadingSlash, true) + .withIcon(item.getIcon()); + + completionResultSet.addElement(element); + } + } + } else if (prefix.contains("\\")) { + // 'FOO\\Foo' + int i = prefix.lastIndexOf("\\"); + String substring = "\\" + StringUtils.stripStart(prefix.substring(0, i).replace("\\\\", "\\"), "\\"); + String pre = prefix.substring(prefix.lastIndexOf("\\") + 1); + CompletionResultSet completionResultSet = resultSet.withPrefixMatcher(pre); + + for (PhpClass phpClass: PhpIndexUtil.getPhpClassInsideNamespace(project, substring)) { + String fqn = phpClass.getFQN().substring(substring.length()); + String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\"); + + for (PhpNamedElement item : getFieldsAndEnums(phpClass)) { + LookupElementBuilder element = LookupElementBuilder + .create(fqn.replace("\\", "\\\\") + "::" + item.getName()) + .withTypeText(fqnNoLeadingSlash, true) + .withIcon(item.getIcon()); + + completionResultSet.addElement(element); + } + } + } else { + // '' + for (String constant : instance.getAllConstantNames(prefixMatcher)) { + resultSet.addElement(LookupElementBuilder.create(constant).withIcon(PhpIcons.CONSTANT)); + } + + Collection phpClasses = new ArrayList<>(); + for (String className : instance.getAllClassNames(resultSet.getPrefixMatcher())) { + phpClasses.addAll(instance.getClassesByName(className)); + } + + for (PhpClass phpClass : phpClasses) { + String fqnNoLeadingSlash = StringUtils.stripStart(phpClass.getFQN(), "\\"); + for (PhpNamedElement field : getFieldsAndEnums(phpClass)) { + LookupElementBuilder element = LookupElementBuilder + .createWithSmartPointer(phpClass.getName() + "::" + field.getName(), field) + .withInsertHandler(TwigEscapedSlashInsertHandler.getInstance()) + .withTypeText(fqnNoLeadingSlash, true) + .withIcon(field.getIcon()); + + resultSet.addElement(element); + } + } + } + } + + private static @NotNull Collection getFieldsAndEnums(@NotNull PhpClass phpClass) { + Collection items = new ArrayList<>(); + + items.addAll(Arrays.stream(phpClass.getOwnFields()).filter(field -> field.isConstant() && field.getModifier().isPublic()).toList()); + items.addAll(phpClass.getEnumCases()); + + return items; + } + } + @NotNull private Collection processVariables(@NotNull PsiElement psiElement, @NotNull Predicate filter, @NotNull Function>, String> map) { Project project = psiElement.getProject(); diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java index ad86ea49c..040cb4508 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java @@ -13,6 +13,7 @@ import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.Field; import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.elements.PhpEnumCase; import com.jetbrains.twig.TwigLanguage; import com.jetbrains.twig.TwigTokenTypes; import com.jetbrains.twig.elements.TwigBlockTag; @@ -39,6 +40,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -346,35 +348,35 @@ private Collection getTranslationDomainGoto(@NotNull PsiElement psiE @NotNull private Collection getConstantGoto(@NotNull PsiElement psiElement) { - Collection targetPsiElements = new ArrayList<>(); - String contents = psiElement.getText(); if(StringUtils.isBlank(contents)) { - return targetPsiElements; + return Collections.emptyList(); } // global constant if(!contents.contains(":")) { - targetPsiElements.addAll(PhpIndex.getInstance(psiElement.getProject()).getConstantsByName(contents)); - return targetPsiElements; + return new ArrayList<>(PhpIndex.getInstance(psiElement.getProject()).getConstantsByName(contents)); } // resolve class constants String[] parts = contents.split("::"); if(parts.length != 2) { - return targetPsiElements; + return Collections.emptyList(); } PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), parts[0].replace("\\\\", "\\")); - if(phpClass == null) { - return targetPsiElements; + if (phpClass == null) { + return Collections.emptyList(); } + Collection targetPsiElements = new ArrayList<>(); Field field = phpClass.findFieldByName(parts[1], true); if(field != null) { targetPsiElements.add(field); } + targetPsiElements.addAll(phpClass.getEnumCases().stream().filter(e -> parts[1].equals(e.getName())).toList()); + return targetPsiElements; } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java index c657fc429..aa82420cf 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateCompletionContributorTest.java @@ -71,15 +71,30 @@ public void testBlockCompletionForEmbed() { public void testThatInlineVarProvidesClassCompletion() { assertCompletionContains(TwigFileType.INSTANCE, "{# @var bar F #}", "Foobar"); - assertCompletionContains(TwigFileType.INSTANCE, "{# @var bar MyFoo\\Ca #}", "Car\\Bike\\Foobar"); - } + assertCompletionContains(TwigFileType.INSTANCE, "{# @var bar MyFoo\\Ca #}", "Car\\Bike\\Foobar"); } public void testThatInlineVarProvidesClassCompletionDeprecated() { assertCompletionContains(TwigFileType.INSTANCE, "{# bar F #}", "Foobar"); } - public void testThatConstantProvidesCompletionForClassAndDefine() { + public void testThatConstantProvidesCompletionForClassConstant() { assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "CONST_FOO"); + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "FooConst::CAR", "FooEnum::FOOBAR"); + + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\') }}", "\\\\Bike\\\\FooConst::CAR", "\\\\Bike\\\\FooEnum::FOOBAR"); + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\') }}", "FooConst::CAR", "FooEnum::FOOBAR"); + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\Foo') }}", "FooEnum::FOOBAR"); + + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\Bike\\\\Foo') }}", "FooEnum::FOOBAR"); + + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooConst::C') }}", "CAR"); + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooEnum::F') }}", "FOOBAR"); + assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\Bike\\\\FooEnum::F') }}", "FOOBAR"); + + assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FooEnum::FOOBAR".equals(l.getLookupString())); + assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\') }}", "{{ constant('App\\\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "\\\\Bike\\\\FooEnum::FOOBAR".equals(l.getLookupString())); + assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\Foo') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FooEnum::FOOBAR".equals(l.getLookupString())); + assertCompletionResultEquals(TwigFileType.INSTANCE, "{{ constant('App\\\\Bike\\\\FooEnum::F') }}", "{{ constant('App\\\\Bike\\\\FooEnum::FOOBAR') }}", l -> "FOOBAR".equals(l.getLookupString())); } public void testCompletionForRoutingParameter() { diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java index 59f0d20b1..a0a8f9243 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigTemplateGoToDeclarationHandlerTest.java @@ -159,14 +159,14 @@ public void testSeeTagGotoRegexMatch() { } } - public void testThatConstantProvidesNavigation() { + public void testThatConstantAndEnumProvidesNavigation() { assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\Foo\\ConstantBar\\Foo::FOO') }}", PlatformPatterns.psiElement(Field.class).withName("FOO")); assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\\\Foo\\\\ConstantBar\\\\Foo::FOO') }}", PlatformPatterns.psiElement(Field.class).withName("FOO")); assertNavigationMatch(TwigFileType.INSTANCE, "{% if foo == constant('\\Foo\\ConstantBar\\Foo::FOO') %}", PlatformPatterns.psiElement(Field.class).withName("FOO")); assertNavigationMatch(TwigFileType.INSTANCE, "{% set foo == constant('\\Foo\\ConstantBar\\Foo::FOO') %}", PlatformPatterns.psiElement(Field.class).withName("FOO")); - assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('CONST_FOO') }}", PlatformPatterns.psiElement()); + assertNavigationMatch(TwigFileType.INSTANCE, "{{ constant('\\\\App\\\\FooEnum::FOO') }}", PlatformPatterns.psiElement()); } public void testTestControllerActionsProvidesReferences() { diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateCompletionContributorTest.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateCompletionContributorTest.php index 2c5aa6a34..53524704b 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateCompletionContributorTest.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateCompletionContributorTest.php @@ -125,3 +125,23 @@ class Foobar2 } } +namespace App\Bike +{ + enum FooEnum + { + case FOOBAR; + case FOOBAR1; + case FOOBAR2; + case FOOBAR3; + } + + class FooConst + { + public const CAR = ''; + public const CAR1 = ''; + public const CAR2 = ''; + public const CAR3 = ''; + } +} + + diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php index 2dcfe18c4..33cfa6706 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/fixtures/TwigTemplateGoToLocalDeclarationHandler.php @@ -48,4 +48,16 @@ public function getTag(); class Alert { } -} \ No newline at end of file +} + +namespace App +{ + enum FooEnum + { + case FOO; + case FOO1; + + case BAR; + case BAR1; + } +}