diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java
index bc377d00c..d66b5d02d 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/form/util/FormUtil.java
@@ -251,21 +251,40 @@ public static Method resolveFormGetterCallMethod(MethodReference methodReference
*/
@Nullable
public static PhpClass getFormTypeClassOnParameter(@NotNull PsiElement psiElement) {
-
- if (psiElement instanceof StringLiteralExpression) {
- return getFormTypeToClass(psiElement.getProject(), ((StringLiteralExpression) psiElement).getContents());
+ switch (psiElement) {
+ case StringLiteralExpression stringLiteralExpression -> {
+ return getFormTypeToClass(psiElement.getProject(), stringLiteralExpression.getContents());
+ }
+ case ClassConstantReference classConstantReference -> {
+ return PhpElementsUtil.getClassConstantPhpClass(classConstantReference);
+ }
+ case PhpTypedElement phpTypedElement -> {
+ String typeName = phpTypedElement.getType().toString();
+ return getFormTypeToClass(psiElement.getProject(), typeName);
+ }
+ default -> {
+ }
}
- if (psiElement instanceof ClassConstantReference) {
- return PhpElementsUtil.getClassConstantPhpClass((ClassConstantReference) psiElement);
- }
+ return null;
+ }
- if (psiElement instanceof PhpTypedElement) {
- String typeName = ((PhpTypedElement) psiElement).getType().toString();
- return getFormTypeToClass(psiElement.getProject(), typeName);
+ /**
+ * Resolves a Symfony form type parameter to a normalized FQN string without exposing the resolved {@link PhpClass}.
+ *
+ *
Supported inputs match {@link #getFormTypeClassOnParameter(PsiElement)}: string literals, class constants,
+ * and typed expressions such as {@code new FooType()}.
+ *
+ * @return normalized class FQN with a leading backslash, or {@code null} when the parameter cannot be resolved
+ */
+ @Nullable
+ public static String getFormTypeFqnOnParameter(@NotNull PsiElement psiElement) {
+ PhpClass phpClass = getFormTypeClassOnParameter(psiElement);
+ if (phpClass == null) {
+ return null;
}
- return null;
+ return phpClass.getFQN();
}
@NotNull
diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java
index e83f17c6b..1bd3ad835 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigLineMarkerProvider.java
@@ -333,8 +333,11 @@ private LineMarkerInfo> attachFormType(@NotNull PsiElement psiElement) {
for (TwigTypeContainer twigTypeContainer : twigTypeContainers) {
Object dataHolder = twigTypeContainer.getDataHolder();
- if (dataHolder instanceof FormDataHolder && PhpElementsUtil.isInstanceOf(((FormDataHolder) dataHolder).getFormType(), "\\Symfony\\Component\\Form\\FormTypeInterface")) {
- phpClasses.add(((FormDataHolder) dataHolder).getFormType());
+ if (dataHolder instanceof FormDataHolder formDataHolder) {
+ PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), formDataHolder.ownerFormTypeFqn());
+ if (phpClass != null && PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormTypeInterface")) {
+ phpClasses.add(phpClass);
+ }
}
}
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 dfbb07235..fbebbf601 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java
@@ -57,6 +57,7 @@
import org.apache.commons.lang3.StringUtils;
import org.intellij.lang.annotations.RegExp;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Function;
@@ -755,11 +756,8 @@ protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull
// form
Object dataHolder = twigTypeContainer.getDataHolder();
- if (dataHolder instanceof FormDataHolder) {
- lookupElement = lookupElement.withIcon(Symfony2Icons.FORM_TYPE);
-
- lookupElement = lookupElement.withTypeText(((FormDataHolder) dataHolder).getPhpClass().getName());
- lookupElement = lookupElement.withTailText("(" + ((FormDataHolder) dataHolder).getFormType().getName() + ")", true);
+ if (dataHolder instanceof FormDataHolder formDataHolder) {
+ lookupElement = decorateFormFieldLookupElement(lookupElement, formDataHolder);
}
resultSet.addElement(lookupElement);
@@ -976,7 +974,7 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
String typeText = null;
if (twigTypeContainers.getDataHolder() instanceof FormDataHolder formDataHolder) {
- typeText = formDataHolder.getPhpClass().getName();
+ typeText = getFormTypeShortName(formDataHolder.fieldTypeFqn());
}
for (String s : new String[]{"form_row", "form_widget", "form_label", "form_errors", "form_help"}) {
@@ -992,6 +990,28 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}
}
+ @Nullable
+ private static String getFormTypeShortName(@Nullable String fqn) {
+ if (fqn == null || fqn.isBlank()) {
+ return null;
+ }
+
+ int index = fqn.lastIndexOf('\\');
+ return index >= 0 ? fqn.substring(index + 1) : fqn;
+ }
+
+ @NotNull
+ public static LookupElementBuilder decorateFormFieldLookupElement(@NotNull LookupElementBuilder lookupElement, @NotNull FormDataHolder formDataHolder) {
+ lookupElement = lookupElement.withIcon(Symfony2Icons.FORM_TYPE);
+
+ String fieldTypeShortName = getFormTypeShortName(formDataHolder.fieldTypeFqn());
+ if (fieldTypeShortName != null) {
+ lookupElement = lookupElement.withTypeText(fieldTypeShortName);
+ }
+
+ return lookupElement.withTailText("(" + getFormTypeShortName(formDataHolder.ownerFormTypeFqn()) + ")", true);
+ }
+
private class IncompleteFormPrintBlockCompletionProvider extends CompletionProvider {
@Override
protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) {
@@ -1028,9 +1048,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}
String typeText = null;
- Collection formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element);
- if (!formTypeFromFormFactory.isEmpty()) {
- typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\");
+ Set formTypeFqnsFromFormFactory = FormFieldResolver.getFormTypeFqnsFromFormFactory(element);
+ if (!formTypeFqnsFromFormFactory.isEmpty()) {
+ typeText = StringUtils.stripStart(formTypeFqnsFromFormFactory.iterator().next(), "\\");
}
for (String s : new String[]{"form_start", "form_rest", "form_end", "form_errors"}) {
@@ -1082,9 +1102,9 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}
String typeText = null;
- Collection formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(element);
- if (!formTypeFromFormFactory.isEmpty()) {
- typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\");
+ Set formTypeFqnsFromFormFactory = FormFieldResolver.getFormTypeFqnsFromFormFactory(element);
+ if (!formTypeFqnsFromFormFactory.isEmpty()) {
+ typeText = StringUtils.stripStart(formTypeFqnsFromFormFactory.iterator().next(), "\\");
}
if (orderedList == null) {
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 3a9d40a2e..b2418cb7d 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateGoToDeclarationHandler.java
@@ -539,9 +539,12 @@ public static Collection getTypeGoto(@NotNull PsiElement psiElement)
// @TODO: provide extension
if (text.equals(twigTypeContainer.getStringElement())) {
Object dataHolder = twigTypeContainer.getDataHolder();
- if (dataHolder instanceof FormDataHolder) {
+ if (dataHolder instanceof FormDataHolder formDataHolder) {
// @TODO: resolve the to field itself
- targetPsiElements.add(((FormDataHolder) dataHolder).getFormType());
+ PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), formDataHolder.ownerFormTypeFqn());
+ if (phpClass != null) {
+ targetPsiElements.add(phpClass);
+ }
}
}
}
diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java
index 9b18c1f5a..59e15c15a 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/FormFieldResolver.java
@@ -125,6 +125,23 @@ public static Collection getFormTypeFromFormFactory(@NotNull PsiElemen
return phpClasses;
}
+ /**
+ * Resolves form type FQNs for a form reference such as {@code $form->createView()}.
+ *
+ * This is the primitive counterpart of {@link #getFormTypeFromFormFactory(PsiElement)}. It may still use PSI
+ * internally, but it returns only normalized FQN strings with a leading backslash.
+ */
+ @NotNull
+ public static Set getFormTypeFqnsFromFormFactory(@NotNull PsiElement formReference) {
+ Set formTypeFqns = new LinkedHashSet<>();
+
+ for (PhpClass phpClass : getFormTypeFromFormFactory(formReference)) {
+ formTypeFqns.add(phpClass.getFQN());
+ }
+
+ return formTypeFqns;
+ }
+
@Nullable
private static PhpClass resolveCall(@NotNull MethodReference methodReference) {
int index = -1;
@@ -157,49 +174,55 @@ private static PhpClass resolveCall(@NotNull MethodReference methodReference) {
}
@NotNull
- private static List getTwigTypeContainer(@NotNull Method method, @NotNull PhpClass formTypClass) {
- List twigTypeContainers = new ArrayList<>();
+ private static List getTwigFormFields(@NotNull Method method, @NotNull PhpClass formTypeClass) {
+ List twigFormFields = new ArrayList<>();
for(MethodReference methodReference: FormUtil.getFormBuilderTypes(method)) {
String fieldName = PsiElementUtils.getMethodParameterAt(methodReference, 0);
+ if (fieldName == null) {
+ continue;
+ }
+
PsiElement psiElement = PsiElementUtils.getMethodParameterPsiElementAt(methodReference, 1);
- TwigTypeContainer twigTypeContainer = new TwigTypeContainer(fieldName);
+ String fieldTypeFqn = null;
// find form field type
if(psiElement != null) {
- PhpClass fieldType = FormUtil.getFormTypeClassOnParameter(psiElement);
- if(fieldType != null) {
- twigTypeContainer.withDataHolder(new FormDataHolder(fieldType, formTypClass));
- }
+ fieldTypeFqn = FormUtil.getFormTypeFqnOnParameter(psiElement);
}
- twigTypeContainers.add(twigTypeContainer);
+ twigFormFields.add(new TwigFormField(fieldName, fieldTypeFqn, formTypeClass.getFQN()));
}
- return twigTypeContainers;
+ return twigFormFields;
}
/**
* Search and resolve: "$form->createView()" to its PhpClass which is a form type
*/
public static void visitFormReferencesFields(@NotNull PsiElement formReference, @NotNull Consumer consumer) {
- visitFormReferencesFields(formReference.getProject(), getFormTypeFromFormFactory(formReference), consumer);
+ visitFormFields(formReference.getProject(), getFormTypeFqnsFromFormFactory(formReference), field -> consumer.accept(toTwigTypeContainer(field)));
}
/**
* Visit all form fields in given PhpClass which are already a form type
*/
public static void visitFormReferencesFields(@NotNull PhpClass phpClass, @NotNull Consumer consumer) {
- visitFormReferencesFields(phpClass.getProject(), Collections.singleton(phpClass), consumer);
+ visitFormFields(phpClass.getProject(), Collections.singleton(phpClass.getFQN()), field -> consumer.accept(toTwigTypeContainer(field)));
}
- private static void visitFormReferencesFields(@NotNull Project project, @NotNull Collection phpClasses, @NotNull Consumer consumer) {
+ public static void visitFormFields(@NotNull Project project, @NotNull Collection formTypeFqns, @NotNull Consumer consumer) {
FormUtil.FormTypeCollector collector = null;
Collection methods = new HashSet<>();
- for (PhpClass phpClass : phpClasses) {
+ for (String formTypeFqn : formTypeFqns) {
+ PhpClass phpClass = PhpElementsUtil.getClassInterface(project, formTypeFqn);
+ if (phpClass == null) {
+ continue;
+ }
+
Method method = phpClass.findMethodByName("buildForm");
if (method != null) {
methods.add(method);
@@ -225,14 +248,19 @@ private static void visitFormReferencesFields(@NotNull Project project, @NotNull
}
}
- private static void consumeFieldType(@NotNull PhpClass phpClass, @NotNull Consumer consumer) {
+ @NotNull
+ private static TwigTypeContainer toTwigTypeContainer(@NotNull TwigFormField field) {
+ return new TwigTypeContainer(field.name()).withDataHolder(new FormDataHolder(field.fieldTypeFqn(), field.ownerFormTypeFqn()));
+ }
+
+ private static void consumeFieldType(@NotNull PhpClass phpClass, @NotNull Consumer consumer) {
Method method = phpClass.findMethodByName("buildForm");
if (method == null) {
return;
}
- for (TwigTypeContainer twigTypeContainer : getTwigTypeContainer(method, phpClass)) {
- consumer.accept(twigTypeContainer);
+ for (TwigFormField twigFormField : getTwigFormFields(method, phpClass)) {
+ consumer.accept(twigFormField);
}
}
}
diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java
new file mode 100644
index 000000000..0fff71e18
--- /dev/null
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/TwigFormField.java
@@ -0,0 +1,20 @@
+package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Primitive metadata for a Symfony form field found in a form type.
+ *
+ * @param name field name passed to the form builder
+ * @param fieldTypeFqn explicit field form type FQN, or {@code null} when the builder call has no type parameter
+ * @param ownerFormTypeFqn FQN of the form type whose {@code buildForm()} contributed this field
+ *
+ * @author Daniel Espendiller
+ */
+public record TwigFormField(
+ @NotNull String name,
+ @Nullable String fieldTypeFqn,
+ @NotNull String ownerFormTypeFqn
+) {
+}
diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java
index 6b9027969..4f8039ebe 100644
--- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java
+++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/variable/resolver/holder/FormDataHolder.java
@@ -1,30 +1,27 @@
package fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder;
-import com.jetbrains.php.lang.psi.elements.PhpClass;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
/**
+ * UI metadata for a Symfony form field without holding PSI objects.
+ *
+ * @param fieldTypeFqn explicit normalized field form type FQN, or {@code null} when the builder call has no type parameter
+ * @param ownerFormTypeFqn normalized FQN of the form type whose {@code buildForm()} contributed this field
+ *
* @author Daniel Espendiller
*/
-public class FormDataHolder {
- @NotNull
- private final PhpClass phpClass;
+public record FormDataHolder(
+ @Nullable String fieldTypeFqn,
+ @NotNull String ownerFormTypeFqn
+) {
+ public FormDataHolder {
+ if (fieldTypeFqn != null && !fieldTypeFqn.startsWith("\\")) {
+ throw new IllegalArgumentException("fieldTypeFqn must be normalized with a leading backslash");
+ }
- @NotNull
- private final PhpClass formType;
-
- public FormDataHolder(@NotNull PhpClass phpClass, @NotNull PhpClass formType) {
- this.phpClass = phpClass;
- this.formType = formType;
- }
-
- @NotNull
- public PhpClass getPhpClass() {
- return phpClass;
- }
-
- @NotNull
- public PhpClass getFormType() {
- return formType;
+ if (!ownerFormTypeFqn.startsWith("\\")) {
+ throw new IllegalArgumentException("ownerFormTypeFqn must be normalized with a leading backslash");
+ }
}
}
diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java
index 8cebcb610..c60a6b31d 100644
--- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java
+++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/SymfonyLightCodeInsightFixtureTestCase.java
@@ -267,7 +267,7 @@ private void assertNavigationIsEmpty() {
}
}
- private void assertNavigationMatch(ElementPattern> pattern) {
+ public void assertNavigationMatch(ElementPattern> pattern) {
PsiElement psiElement = myFixture.getFile().findElementAt(myFixture.getCaretOffset());
diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java
index 52f86bdf4..fd00652ec 100644
--- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java
+++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/form/util/FormUtilTest.java
@@ -49,6 +49,24 @@ public void testGetFormTypeClassOnParameter() {
).getFQN());
}
+ public void testGetFormTypeFqnOnParameter() {
+ assertEquals("\\Form\\FormType\\Foo", FormUtil.getFormTypeFqnOnParameter(
+ PhpPsiElementFactory.createPhpPsiFromText(getProject(), PhpTypedElementImpl.class, " formTypeClasses = FormUtil.getFormTypeClasses(getProject());
diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java
index 6ab3d37a6..93b55f6f0 100644
--- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java
+++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/TwigLineMarkerProviderTest.java
@@ -13,6 +13,7 @@
import com.intellij.util.ProcessingContext;
import com.jetbrains.php.lang.PhpFileType;
import com.jetbrains.php.lang.psi.elements.Method;
+import com.jetbrains.php.lang.psi.elements.PhpClass;
import com.jetbrains.twig.TwigLanguage;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
@@ -168,6 +169,53 @@ public void testNoLineMarkerForStandaloneTemplate() {
assertFalse("Should not have extends/include navigation marker", hasNavigationMarker);
}
+ public void testFormStartLineMarkerNavigatesToFormType() {
+ myFixture.addFileToProject(
+ "src/Controller/ProductController.php",
+ "add('title');\n" +
+ " }\n" +
+ " }\n" +
+ "}\n" +
+ "namespace App\\Controller {\n" +
+ " class ProductController extends \\Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController {\n" +
+ " public function index() {\n" +
+ " $form = $this->createForm(\\App\\Form\\ProductType::class);\n" +
+ " return $this->render('form/line_marker.html.twig', ['form' => $form->createView()]);\n" +
+ " }\n" +
+ " }\n" +
+ "}\n"
+ );
+
+ PsiFile templateFile = myFixture.addFileToProject(
+ "templates/form/line_marker.html.twig",
+ "{{ form_start(form) }}"
+ );
+
+ assertLineMarker(
+ templateFile,
+ new LineMarker.TargetAcceptsPattern(
+ "Navigate to Form",
+ PlatformPatterns.psiElement(PhpClass.class).withName("ProductType")
+ )
+ );
+ }
+
/**
* Test embed line marker.
*/
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 8c873565d..93abeeb63 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
@@ -1,7 +1,12 @@
package fr.adrienbrault.idea.symfony2plugin.tests.templating;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.patterns.PlatformPatterns;
import com.jetbrains.twig.TwigFileType;
+import fr.adrienbrault.idea.symfony2plugin.templating.TwigTemplateCompletionContributor;
+import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormDataHolder;
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
/**
@@ -60,6 +65,20 @@ public void testThatTypesTagProvidesIncompleteForStatementCompletion() {
);
}
+ public void testFormFieldCompletionUsesPrimitiveFormDataHolderPresentation() {
+ LookupElement lookupElement = TwigTemplateCompletionContributor.decorateFormFieldLookupElement(
+ LookupElementBuilder.create("title"),
+ new FormDataHolder("\\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType", "\\App\\Form\\ProductType")
+ );
+
+ LookupElementPresentation presentation = new LookupElementPresentation();
+ lookupElement.renderElement(presentation);
+
+ assertEquals("title", lookupElement.getLookupString());
+ assertEquals("TextType", presentation.getTypeText());
+ assertEquals("(ProductType)", presentation.getTailText());
+ }
+
public void testThatConstantProvidesCompletionForClassConstant() {
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "CONST_FOO");
assertCompletionContains(TwigFileType.INSTANCE, "{{ constant('') }}", "FooConst::CAR", "FooEnum::FOOBAR");
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 eb0661348..ee6bb23bd 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
@@ -2,6 +2,7 @@
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
import com.jetbrains.php.lang.psi.elements.Field;
import com.jetbrains.php.lang.psi.elements.Function;
import com.jetbrains.php.lang.psi.elements.Method;
@@ -115,6 +116,49 @@ public void testTypeGotoIgnoresCoreTwigReferenceResolutionForPropertyAccess() {
assertContainsElements(methodNames, "getFoobar");
}
+ public void testFormFieldGotoUsesPrimitiveFormDataHolderOwnerFormTypeFqn() {
+ myFixture.copyFileToProject("ide-twig.json");
+
+ myFixture.addFileToProject(
+ "src/Controller/FormGotoController.php",
+ "add('title', \\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType::class);\n" +
+ " }\n" +
+ " }\n" +
+ "}\n" +
+ "namespace App\\Controller {\n" +
+ " class FormGotoController extends \\Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController {\n" +
+ " public function index() {\n" +
+ " $form = $this->createForm(\\App\\Form\\ProductType::class);\n" +
+ " return $this->render('form/goto.html.twig', ['form' => $form->createView()]);\n" +
+ " }\n" +
+ " }\n" +
+ "}\n"
+ );
+
+ PsiFile templateFile = myFixture.addFileToProject("templates/form/goto.html.twig", "{{ form.title }}");
+ myFixture.configureFromExistingVirtualFile(templateFile.getVirtualFile());
+ myFixture.getEditor().getCaretModel().moveToOffset("{{ form.ti".length());
+
+ assertNavigationMatch(PlatformPatterns.psiElement(PhpClass.class).withName("ProductType"));
+ }
+
public void testSeeTagGoto() {
assertNavigationMatch(TwigFileType.INSTANCE, "{# @see \\DateTime #}", PlatformPatterns.psiElement(PhpClass.class));
assertNavigationMatch(TwigFileType.INSTANCE, "{# @see DateTime #}", PlatformPatterns.psiElement(PhpClass.class));
diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java
new file mode 100644
index 000000000..a0cb3edc4
--- /dev/null
+++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/templating/variable/resolver/FormFieldResolverTest.java
@@ -0,0 +1,91 @@
+package fr.adrienbrault.idea.symfony2plugin.tests.templating.variable.resolver;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.jetbrains.php.lang.PhpFileType;
+import com.jetbrains.php.lang.psi.elements.MethodReference;
+import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormFieldResolver;
+import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.TwigFormField;
+import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Daniel Espendiller
+ */
+public class FormFieldResolverTest extends SymfonyLightCodeInsightFixtureTestCase {
+ public void testGetFormTypeFqnsFromFormFactory() {
+ myFixture.configureByText(PhpFileType.INSTANCE, "createForm(\\App\\Form\\ProductType::class);\n" +
+ " return ['form' => $form->createView()];\n" +
+ " }\n" +
+ " }\n" +
+ "}\n"
+ );
+
+ Set formTypeFqns = FormFieldResolver.getFormTypeFqnsFromFormFactory(findMethodReference("createView"));
+
+ assertContainsElements(formTypeFqns, "\\App\\Form\\ProductType");
+ }
+
+ public void testVisitFormFieldsProvidesPrimitiveFieldMetadata() {
+ myFixture.configureByText(PhpFileType.INSTANCE, "add('title', \\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType::class);\n" +
+ " $builder->add('plain');\n" +
+ " }\n" +
+ " }\n" +
+ "}\n"
+ );
+
+ Collection fields = new ArrayList<>();
+ FormFieldResolver.visitFormFields(getProject(), Collections.singleton("\\App\\Form\\ProductType"), fields::add);
+
+ Set fieldNames = fields.stream().map(TwigFormField::name).collect(Collectors.toSet());
+ assertContainsElements(fieldNames, "title", "plain");
+
+ TwigFormField title = fields.stream()
+ .filter(field -> "title".equals(field.name()))
+ .findFirst()
+ .orElseThrow();
+
+ assertEquals("\\Symfony\\Component\\Form\\Extension\\Core\\Type\\TextType", title.fieldTypeFqn());
+ assertEquals("\\App\\Form\\ProductType", title.ownerFormTypeFqn());
+
+ TwigFormField plain = fields.stream()
+ .filter(field -> "plain".equals(field.name()))
+ .findFirst()
+ .orElseThrow();
+
+ assertNull(plain.fieldTypeFqn());
+ assertEquals("\\App\\Form\\ProductType", plain.ownerFormTypeFqn());
+ }
+
+ @NotNull
+ private MethodReference findMethodReference(@NotNull String name) {
+ for (PsiElement psiElement : PsiTreeUtil.collectElementsOfType(myFixture.getFile(), MethodReference.class)) {
+ MethodReference methodReference = (MethodReference) psiElement;
+ if (name.equals(methodReference.getName())) {
+ return methodReference;
+ }
+ }
+
+ fail("Method reference not found: " + name);
+ throw new IllegalStateException(name);
+ }
+}