Skip to content

Commit 8c1b743

Browse files
committed
Add support for autocompletion inside the MessageSubscriberInterface::getHandledMessages method
1 parent fcbb7e4 commit 8c1b743

File tree

5 files changed

+369
-0
lines changed

5 files changed

+369
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package fr.adrienbrault.idea.symfony2plugin.config.php;
2+
3+
import com.intellij.codeInsight.lookup.LookupElement;
4+
import com.intellij.codeInsight.lookup.LookupElementBuilder;
5+
import com.intellij.patterns.PatternCondition;
6+
import com.intellij.patterns.PlatformPatterns;
7+
import com.intellij.patterns.PsiElementPattern;
8+
import com.intellij.psi.PsiElement;
9+
import com.intellij.psi.util.PsiTreeUtil;
10+
import com.intellij.util.ProcessingContext;
11+
import com.jetbrains.php.lang.PhpLanguage;
12+
import com.jetbrains.php.lang.parser.PhpElementTypes;
13+
import com.jetbrains.php.lang.psi.elements.*;
14+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
15+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
16+
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
17+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
18+
import org.apache.commons.lang.StringUtils;
19+
import org.jetbrains.annotations.NotNull;
20+
21+
import java.util.ArrayList;
22+
import java.util.Collection;
23+
import java.util.Collections;
24+
25+
/**
26+
* \Symfony\Component\Messenger\Handler\MessageSubscriberInterface::getHandledMessages
27+
*
28+
* yield FirstMessage::class => ['method' => '<caret>']
29+
*
30+
* return array(
31+
* FirstMessage::class => '<caret>',
32+
* )
33+
*
34+
* @author Stefano Arlandini <sarlandini@alice.it>
35+
*/
36+
public class PhpMessageSubscriberGotoCompletionRegistrar implements GotoCompletionRegistrar {
37+
private static PsiElementPattern.Capture<PsiElement> REGISTRAR_PATTERN = PlatformPatterns.psiElement()
38+
.withLanguage(PhpLanguage.INSTANCE)
39+
.withParent(
40+
PlatformPatterns.psiElement(StringLiteralExpression.class).withParent(
41+
PlatformPatterns.psiElement()
42+
.withElementType(PhpElementTypes.ARRAY_VALUE)
43+
.withParent(
44+
PlatformPatterns.or(
45+
PlatformPatterns.psiElement(ArrayHashElement.class)
46+
.withParent(PlatformPatterns.psiElement(ArrayCreationExpression.class).withParent(PhpReturn.class))
47+
.with(new PatternCondition<ArrayHashElement>("Key Type") {
48+
@Override
49+
public boolean accepts(@NotNull ArrayHashElement arrayHashElement, ProcessingContext context) {
50+
PhpPsiElement keyElement = arrayHashElement.getKey();
51+
52+
return keyElement instanceof StringLiteralExpression || keyElement instanceof ClassConstantReference;
53+
}
54+
}),
55+
PlatformPatterns.psiElement(ArrayHashElement.class)
56+
.with(new PatternCondition<ArrayHashElement>("Key Text") {
57+
@Override
58+
public boolean accepts(@NotNull ArrayHashElement arrayHashElement, ProcessingContext context) {
59+
PhpPsiElement keyElement = arrayHashElement.getKey();
60+
61+
if (!(keyElement instanceof StringLiteralExpression)) {
62+
return false;
63+
}
64+
65+
return ((StringLiteralExpression) keyElement).getContents().equals("method");
66+
}
67+
})
68+
.withParent(
69+
PlatformPatterns.psiElement(ArrayCreationExpression.class).withParent(
70+
PlatformPatterns.psiElement(PhpYield.class)
71+
.with(new PatternCondition<PhpYield>("Yield Key Type") {
72+
@Override
73+
public boolean accepts(@NotNull PhpYield phpYield, ProcessingContext context) {
74+
PsiElement keyElement = phpYield.getArgument();
75+
76+
return keyElement instanceof StringLiteralExpression || keyElement instanceof ClassConstantReference;
77+
}
78+
})
79+
)
80+
)
81+
)
82+
)
83+
)
84+
);
85+
86+
@Override
87+
public void register(@NotNull GotoCompletionRegistrarParameter registrar) {
88+
registrar.register(REGISTRAR_PATTERN, psiElement -> {
89+
Method method = PsiTreeUtil.getParentOfType(psiElement, Method.class, true, Function.class);
90+
91+
if (method == null) {
92+
return null;
93+
}
94+
95+
if (!PhpElementsUtil.isMethodInstanceOf(method, "\\Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface", "getHandledMessages")) {
96+
return null;
97+
}
98+
99+
return new PhpClassPublicMethodProvider(method.getContainingClass());
100+
});
101+
}
102+
103+
private static class PhpClassPublicMethodProvider extends GotoCompletionProvider {
104+
private final PhpClass phpClass;
105+
106+
public PhpClassPublicMethodProvider(@NotNull PhpClass phpClass) {
107+
super(phpClass);
108+
109+
this.phpClass = phpClass;
110+
}
111+
112+
@NotNull
113+
@Override
114+
public Collection<LookupElement> getLookupElements() {
115+
Collection<LookupElement> elements = new ArrayList<>();
116+
117+
for (Method method : phpClass.getMethods()) {
118+
if (!method.getAccess().isPublic()) {
119+
continue;
120+
}
121+
122+
String methodName = method.getName();
123+
124+
if (methodName.equals("getHandledMessages") || methodName.startsWith("__")) {
125+
continue;
126+
}
127+
128+
elements.add(LookupElementBuilder.createWithIcon(method));
129+
}
130+
131+
return elements;
132+
}
133+
134+
@NotNull
135+
@Override
136+
public Collection<PsiElement> getPsiTargets(PsiElement element) {
137+
PsiElement parentElement = element.getParent();
138+
139+
if (!(parentElement instanceof StringLiteralExpression)) {
140+
return Collections.emptyList();
141+
}
142+
143+
String contents = ((StringLiteralExpression) parentElement).getContents();
144+
145+
if (StringUtils.isBlank(contents)) {
146+
return Collections.emptyList();
147+
}
148+
149+
Method method = phpClass.findMethodByName(contents);
150+
151+
if (method != null) {
152+
return Collections.singletonList(method);
153+
}
154+
155+
return Collections.emptyList();
156+
}
157+
}
158+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@
559559
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.templating.TranslationTagGotoCompletionRegistrar"/>
560560
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.form.FormGotoCompletionRegistrar"/>
561561
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.config.php.PhpEventDispatcherGotoCompletionRegistrar"/>
562+
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.config.php.PhpMessageSubscriberGotoCompletionRegistrar"/>
562563
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.completion.command.PhpCommandGotoCompletionRegistrar"/>
563564
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.completion.command.ConsoleHelperGotoCompletionRegistrar"/>
564565
<GotoCompletionRegistrar implementation="fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.DoctrineXmlGotoCompletionRegistrar"/>

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,22 @@ public void assertCompletionContains(String filename, String configureByText, St
103103
completionContainsAssert(lookupStrings);
104104
}
105105

106+
public void assertCompletionIsEmpty(LanguageFileType languageFileType, String configureByText) {
107+
108+
myFixture.configureByText(languageFileType, configureByText);
109+
myFixture.completeBasic();
110+
111+
List<String> lookupElements = myFixture.getLookupElementStrings();
112+
113+
if (lookupElements == null) {
114+
return;
115+
}
116+
117+
if (!lookupElements.isEmpty()) {
118+
fail("Failed that completion is empty.");
119+
}
120+
}
121+
106122
private void completionContainsAssert(String[] lookupStrings) {
107123
if(lookupStrings.length == 0) {
108124
fail("No lookup element given");
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.config.php;
2+
3+
import com.intellij.patterns.PlatformPatterns;
4+
import com.jetbrains.php.lang.PhpFileType;
5+
import com.jetbrains.php.lang.psi.elements.Method;
6+
import com.jetbrains.php.lang.psi.elements.PhpClass;
7+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
8+
9+
import java.util.Arrays;
10+
import java.util.List;
11+
12+
/**
13+
* @author Stefano Arlandini <sarlandini@alice.it>
14+
*
15+
* @see fr.adrienbrault.idea.symfony2plugin.config.php.PhpMessageSubscriberGotoCompletionRegistrar
16+
*/
17+
public class PhpMessageSubscriberGotoCompletionRegistrarTest extends SymfonyLightCodeInsightFixtureTestCase {
18+
@Override
19+
public void setUp() throws Exception {
20+
super.setUp();
21+
22+
myFixture.copyFileToProject("classes.php");
23+
}
24+
25+
@Override
26+
protected String getTestDataPath() {
27+
return "src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/php/fixtures";
28+
}
29+
30+
public void testAutocompletionForGetHandledMessagesMethodOfMessageSubscriberInterface() {
31+
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
32+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
33+
"{\n" +
34+
" public function handleFooEvent(): void\n" +
35+
" {\n" +
36+
" }\n" +
37+
" public function handleBarEvent(): void\n" +
38+
" {\n" +
39+
" }\n" +
40+
" private function handleBazEvent(): void\n" +
41+
" {\n" +
42+
" }\n" +
43+
" protected function handleQuxEvent(): void\n" +
44+
" {\n" +
45+
" }\n" +
46+
" public function __toString(): string\n" +
47+
" {\n" +
48+
" return '';\n" +
49+
" }\n" +
50+
" public function getHandledMessages(): iterable\n" +
51+
" {\n" +
52+
" yield FooEvent::class => ['method' => '<caret>'];\n" +
53+
" }\n" +
54+
"}",
55+
"handleFooEvent",
56+
"handleBarEvent"
57+
);
58+
59+
assertNavigationMatch(PhpFileType.INSTANCE, "<?php\n" +
60+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
61+
"{\n" +
62+
" public function handleFooEvent(): void\n" +
63+
" {\n" +
64+
" }\n" +
65+
" public function getHandledMessages(): iterable\n" +
66+
" {\n" +
67+
" yield FooEvent::class => ['method' => '<caret>handleFooEvent'];\n" +
68+
" }\n" +
69+
"}",
70+
PlatformPatterns.psiElement(Method.class).withName("handleFooEvent")
71+
);
72+
73+
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
74+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
75+
"{\n" +
76+
" public function handleFooEvent(): void\n" +
77+
" {\n" +
78+
" }\n" +
79+
" public function handleBarEvent(): void\n" +
80+
" {\n" +
81+
" }\n" +
82+
" private function handleBazEvent(): void\n" +
83+
" {\n" +
84+
" }\n" +
85+
" protected function handleQuxEvent(): void\n" +
86+
" {\n" +
87+
" }\n" +
88+
" public function __toString(): string\n" +
89+
" {\n" +
90+
" return '';\n" +
91+
" }\n" +
92+
" public function getHandledMessages(): iterable\n" +
93+
" {\n" +
94+
" yield 'FooEvent' => ['method' => '<caret>'];\n" +
95+
" }\n" +
96+
"}",
97+
"handleFooEvent",
98+
"handleBarEvent"
99+
);
100+
101+
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
102+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
103+
"{\n" +
104+
" public function handleFooEvent(): void\n" +
105+
" {\n" +
106+
" }\n" +
107+
" public function handleBarEvent(): void\n" +
108+
" {\n" +
109+
" }\n" +
110+
" private function handleBazEvent(): void\n" +
111+
" {\n" +
112+
" }\n" +
113+
" protected function handleQuxEvent(): void\n" +
114+
" {\n" +
115+
" }\n" +
116+
" public function __toString(): string\n" +
117+
" {\n" +
118+
" return '';\n" +
119+
" }\n" +
120+
" public function getHandledMessages(): iterable\n" +
121+
" {\n" +
122+
" return [FooEvent::class => '<caret>'];\n" +
123+
" }\n" +
124+
"}",
125+
"handleFooEvent",
126+
"handleBarEvent"
127+
);
128+
129+
assertCompletionContains(PhpFileType.INSTANCE, "<?php\n" +
130+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
131+
"{\n" +
132+
" public function handleFooEvent(): void\n" +
133+
" {\n" +
134+
" }\n" +
135+
" public function handleBarEvent(): void\n" +
136+
" {\n" +
137+
" }\n" +
138+
" private function handleBazEvent(): void\n" +
139+
" {\n" +
140+
" }\n" +
141+
" protected function handleQuxEvent(): void\n" +
142+
" {\n" +
143+
" }\n" +
144+
" public function __toString(): string\n" +
145+
" {\n" +
146+
" return '';\n" +
147+
" }\n" +
148+
" public function getHandledMessages(): iterable\n" +
149+
" {\n" +
150+
" return ['FooEvent' => '<caret>'];\n" +
151+
" }\n" +
152+
"}",
153+
"handleFooEvent",
154+
"handleBarEvent"
155+
);
156+
157+
assertNavigationMatch(PhpFileType.INSTANCE, "<?php\n" +
158+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
159+
"{\n" +
160+
" public function handleFooEvent(): void\n" +
161+
" {\n" +
162+
" }\n" +
163+
" public function getHandledMessages(): iterable\n" +
164+
" {\n" +
165+
" return [FooEvent::class => '<caret>handleFooEvent'];\n" +
166+
" }\n" +
167+
"}",
168+
PlatformPatterns.psiElement(Method.class).withName("handleFooEvent")
169+
);
170+
171+
assertCompletionIsEmpty(PhpFileType.INSTANCE, "<?php\n" +
172+
"final class FooMessageSubscriber implements Symfony\\Component\\Messenger\\Handler\\MessageSubscriberInterface\n" +
173+
"{\n" +
174+
" public function handleFooEvent(): void\n" +
175+
" {\n" +
176+
" }\n" +
177+
" public function getHandledMessages(): iterable\n" +
178+
" {\n" +
179+
" $foo = static function (): iterable {" +
180+
" return ['FooEvent' => '<caret>'];\n" +
181+
" };" +
182+
" }\n" +
183+
"}"
184+
);
185+
}
186+
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ public function clearTag($name)
1717
}
1818
}
1919
}
20+
21+
namespace Symfony\Component\Messenger\Handler
22+
{
23+
interface MessageSubscriberInterface
24+
{
25+
public static function getHandledMessages(): iterable;
26+
}
27+
}

0 commit comments

Comments
 (0)