Skip to content

Commit 75307d7

Browse files
committed
add FromType class constant completion and insert handler #623
1 parent 907402e commit 75307d7

File tree

8 files changed

+314
-0
lines changed

8 files changed

+314
-0
lines changed

META-INF/plugin.xml

+1
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@
380380
<completion.contributor language="yaml" implementationClass="fr.adrienbrault.idea.symfony2plugin.config.yaml.YamlCompletionContributor"/>
381381

382382
<completion.contributor language="XML" implementationClass="fr.adrienbrault.idea.symfony2plugin.doctrine.metadata.DoctrineXmlCompletionContributor"/>
383+
<completion.contributor language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.form.completion.FormCompletionContributor"/>
383384

384385
<annotator language="PHP" implementationClass="fr.adrienbrault.idea.symfony2plugin.templating.PhpTemplateAnnotator"/>
385386

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package fr.adrienbrault.idea.symfony2plugin.completion.insertHandler;
2+
3+
import com.intellij.codeInsight.completion.InsertHandler;
4+
import com.intellij.codeInsight.completion.InsertionContext;
5+
import com.intellij.codeInsight.lookup.LookupElement;
6+
import com.jetbrains.php.completion.insert.PhpInsertHandlerUtil;
7+
import com.jetbrains.php.completion.insert.PhpReferenceInsertHandler;
8+
import com.jetbrains.php.lang.psi.elements.PhpClass;
9+
import org.jetbrains.annotations.NotNull;
10+
11+
/**
12+
* @author Daniel Espendiller <daniel@espendiller.net>
13+
*/
14+
public class ClassConstantInsertHandler implements InsertHandler<LookupElement> {
15+
16+
private static final ClassConstantInsertHandler instance = new ClassConstantInsertHandler();
17+
18+
private ClassConstantInsertHandler() {}
19+
20+
@Override
21+
public void handleInsert(InsertionContext context, LookupElement lookupElement) {
22+
23+
if(!(lookupElement instanceof ClassConstantLookupElementInterface) || !(lookupElement.getObject() instanceof PhpClass)) {
24+
return;
25+
}
26+
27+
PhpReferenceInsertHandler.getInstance().handleInsert(context, lookupElement);
28+
PhpInsertHandlerUtil.insertStringAtCaret(context.getEditor(), "::class");
29+
}
30+
31+
public static ClassConstantInsertHandler getInstance(){
32+
return instance;
33+
}
34+
35+
public interface ClassConstantLookupElementInterface {
36+
@NotNull
37+
PhpClass getPhpClass();
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package fr.adrienbrault.idea.symfony2plugin.completion.lookup;
2+
3+
import com.intellij.codeInsight.completion.InsertionContext;
4+
import com.intellij.codeInsight.lookup.LookupElement;
5+
import com.jetbrains.php.lang.psi.elements.PhpClass;
6+
import fr.adrienbrault.idea.symfony2plugin.completion.insertHandler.ClassConstantInsertHandler;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
/**
10+
* @author Daniel Espendiller <daniel@espendiller.net>
11+
*/
12+
abstract public class ClassConstantLookupElementAbstract extends LookupElement implements ClassConstantInsertHandler.ClassConstantLookupElementInterface {
13+
14+
@NotNull
15+
protected final PhpClass phpClass;
16+
17+
public ClassConstantLookupElementAbstract(@NotNull PhpClass phpClass) {
18+
this.phpClass = phpClass;
19+
}
20+
21+
@NotNull
22+
@Override
23+
public String getLookupString() {
24+
return phpClass.getName();
25+
}
26+
27+
@NotNull
28+
public Object getObject() {
29+
return this.phpClass;
30+
}
31+
32+
@NotNull
33+
@Override
34+
public PhpClass getPhpClass() {
35+
return this.phpClass;
36+
}
37+
38+
@Override
39+
public void handleInsert(InsertionContext context) {
40+
ClassConstantInsertHandler.getInstance().handleInsert(context, this);
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package fr.adrienbrault.idea.symfony2plugin.form.completion;
2+
3+
import com.intellij.codeInsight.completion.InsertionContext;
4+
import com.intellij.codeInsight.lookup.LookupElementPresentation;
5+
import com.jetbrains.php.lang.psi.elements.PhpClass;
6+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
7+
import fr.adrienbrault.idea.symfony2plugin.completion.insertHandler.ClassConstantInsertHandler;
8+
import fr.adrienbrault.idea.symfony2plugin.completion.lookup.ClassConstantLookupElementAbstract;
9+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
10+
import org.apache.commons.lang.StringUtils;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
16+
/**
17+
* @author Daniel Espendiller <daniel@espendiller.net>
18+
*/
19+
public class FormClassConstantsLookupElement extends ClassConstantLookupElementAbstract {
20+
21+
public FormClassConstantsLookupElement(@NotNull PhpClass phpClass) {
22+
super(phpClass);
23+
}
24+
25+
@Override
26+
public void renderElement(LookupElementPresentation presentation) {
27+
28+
// provides parent and string alias name
29+
List<String> tailsText = new ArrayList<String>();
30+
31+
String getName = PhpElementsUtil.getMethodReturnAsString(phpClass, "getName");
32+
if(getName != null) {
33+
tailsText.add(getName);
34+
}
35+
36+
String getParent = PhpElementsUtil.getMethodReturnAsString(phpClass, "getParent");
37+
if(getParent != null && !getParent.equals("form")) {
38+
tailsText.add(getParent);
39+
}
40+
41+
if(tailsText.size() > 0) {
42+
presentation.setTailText("(" + StringUtils.join(tailsText, ",") + ")", true);
43+
}
44+
45+
presentation.setIcon(Symfony2Icons.FORM_TYPE);
46+
presentation.setItemText(phpClass.getName());
47+
presentation.setTypeText(phpClass.getPresentableFQN());
48+
presentation.setTypeGrayed(true);
49+
50+
super.renderElement(presentation);
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package fr.adrienbrault.idea.symfony2plugin.form.completion;
2+
3+
import com.intellij.codeInsight.completion.*;
4+
import com.intellij.codeInsight.lookup.LookupElement;
5+
import com.intellij.patterns.PlatformPatterns;
6+
import com.intellij.psi.PsiElement;
7+
import com.intellij.psi.util.PsiTreeUtil;
8+
import com.intellij.util.ProcessingContext;
9+
import com.jetbrains.php.PhpIndex;
10+
import com.jetbrains.php.lang.psi.elements.ConstantReference;
11+
import com.jetbrains.php.lang.psi.elements.MethodReference;
12+
import com.jetbrains.php.lang.psi.elements.ParameterList;
13+
import com.jetbrains.php.lang.psi.elements.PhpClass;
14+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
15+
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
16+
import org.jetbrains.annotations.NotNull;
17+
18+
/**
19+
* @author Daniel Espendiller <daniel@espendiller.net>
20+
*/
21+
public class FormCompletionContributor extends CompletionContributor {
22+
23+
public FormCompletionContributor() {
24+
extend(CompletionType.BASIC, PlatformPatterns.psiElement().withParent(ConstantReference.class), new CompletionProvider<CompletionParameters>() {
25+
@Override
26+
protected void addCompletions(@NotNull CompletionParameters completionParameters, ProcessingContext processingContext, @NotNull CompletionResultSet completionResultSet) {
27+
PsiElement psiElement = completionParameters.getOriginalPosition();
28+
if (!Symfony2ProjectComponent.isEnabled(psiElement)) {
29+
return;
30+
}
31+
32+
MethodReference methodReference = null;
33+
34+
// add('', <caret>)
35+
// add('', Foo<caret>)
36+
PsiElement parameterList = psiElement.getParent();
37+
if(parameterList instanceof ParameterList) {
38+
PsiElement psiElement2 = parameterList.getParent();
39+
if(psiElement2 instanceof MethodReference) {
40+
methodReference = (MethodReference) psiElement2;
41+
}
42+
} else if(parameterList instanceof ConstantReference) {
43+
methodReference = PsiTreeUtil.getParentOfType(parameterList, MethodReference.class);
44+
}
45+
46+
if(methodReference == null) {
47+
return;
48+
}
49+
50+
if(!(
51+
PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "Symfony\\Component\\Form\\FormBuilderInterface", "add") ||
52+
PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "Symfony\\Component\\Form\\FormBuilderInterface", "create") ||
53+
PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "Symfony\\Component\\Form\\FormFactoryInterface", "createNamedBuilder") ||
54+
PhpElementsUtil.isMethodReferenceInstanceOf(methodReference, "Symfony\\Component\\Form\\FormFactoryInterface", "createNamed")
55+
)) {
56+
return;
57+
}
58+
59+
for (PhpClass phpClass : PhpIndex.getInstance(psiElement.getProject()).getAllSubclasses("\\Symfony\\Component\\Form\\FormTypeInterface")) {
60+
if(phpClass.isAbstract() || phpClass.isInterface()) {
61+
continue;
62+
}
63+
64+
LookupElement elementBuilder = new FormClassConstantsLookupElement(phpClass);
65+
66+
// does this have an effect really?
67+
completionResultSet.addElement(
68+
PrioritizedLookupElement.withExplicitProximity(PrioritizedLookupElement.withPriority(elementBuilder, 1000), 1000)
69+
);
70+
}
71+
72+
}
73+
74+
});
75+
}
76+
77+
}

src/fr/adrienbrault/idea/symfony2plugin/util/PhpElementsUtil.java

+21
Original file line numberDiff line numberDiff line change
@@ -1231,4 +1231,25 @@ public static boolean isMethodReferenceInstanceOf(@NotNull MethodReference metho
12311231

12321232
return new Symfony2InterfacesUtil().isInstanceOf(containingClass, expectedClassName);
12331233
}
1234+
1235+
/**
1236+
* Resolves MethodReference and compare containing class against implementations instances
1237+
*/
1238+
public static boolean isMethodReferenceInstanceOf(@NotNull MethodReference methodReference, @NotNull String expectedClassName, @NotNull String methodName) {
1239+
if(!methodName.equals(methodReference.getName())) {
1240+
return false;
1241+
}
1242+
1243+
PsiElement resolve = methodReference.resolve();
1244+
if(!(resolve instanceof Method)) {
1245+
return false;
1246+
}
1247+
1248+
PhpClass containingClass = ((Method) resolve).getContainingClass();
1249+
if(containingClass == null) {
1250+
return false;
1251+
}
1252+
1253+
return new Symfony2InterfacesUtil().isInstanceOf(containingClass, expectedClassName);
1254+
}
12341255
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.form.completion;
2+
3+
import com.jetbrains.php.lang.PhpFileType;
4+
import com.jetbrains.twig.TwigFileType;
5+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
6+
7+
import java.io.File;
8+
9+
/**
10+
* @author Daniel Espendiller <daniel@espendiller.net>
11+
*
12+
* @see fr.adrienbrault.idea.symfony2plugin.form.completion.FormCompletionContributor
13+
*/
14+
public class FormCompletionContributorTest extends SymfonyLightCodeInsightFixtureTestCase {
15+
16+
public void setUp() throws Exception {
17+
super.setUp();
18+
myFixture.configureFromExistingVirtualFile(myFixture.copyFileToProject("classes.php"));
19+
}
20+
21+
protected String getTestDataPath() {
22+
return new File(this.getClass().getResource("fixtures").getFile()).getAbsolutePath();
23+
}
24+
25+
public void testClassConstantsCompletionWithoutNamespace() {
26+
assertCompletionResultEquals(PhpFileType.INSTANCE,
27+
"<?php\n /** @var $foo \\Symfony\\Component\\Form\\FormBuilderInterface */\n $foo->add('', HiddenType<caret>)",
28+
"<?php\n /** @var $foo \\Symfony\\Component\\Form\\FormBuilderInterface */\n $foo->add('', \\Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType::class)"
29+
);
30+
}
31+
32+
public void testClassConstantsCompletionWithNamespaceShouldInsertUseAndStripNamespace() {
33+
assertCompletionResultEquals(PhpFileType.INSTANCE,
34+
"<?php\n" +
35+
"namespace Foo {\n" +
36+
" /** @var $foo \\Symfony\\Component\\Form\\FormBuilderInterface */\n" +
37+
" $foo->add('', HiddenType<caret>);\n" +
38+
"};",
39+
"<?php\n" +
40+
"namespace Foo {\n\n" +
41+
" use Symfony\\Component\\Form\\Extension\\Core\\Type\\HiddenType;\n\n" +
42+
" /** @var $foo \\Symfony\\Component\\Form\\FormBuilderInterface */\n" +
43+
" $foo->add('', HiddenType::class);\n" +
44+
"};"
45+
);
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Symfony\Component\Form
4+
{
5+
interface FormBuilderInterface
6+
{
7+
public function add();
8+
public function create();
9+
}
10+
11+
interface FormFactoryInterface
12+
{
13+
public function createNamedBuilder();
14+
public function createNamed();
15+
}
16+
}
17+
18+
namespace Symfony\Component\Form
19+
{
20+
interface FormTypeInterface
21+
{
22+
}
23+
}
24+
25+
namespace Symfony\Component\Form\Extension\Core\Type
26+
{
27+
use Symfony\Component\Form\FormTypeInterface;
28+
29+
class HiddenType implements FormTypeInterface
30+
{
31+
}
32+
}
33+
34+

0 commit comments

Comments
 (0)