package fr.adrienbrault.idea.symfony2plugin.form;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Processor;
import com.jetbrains.php.lang.PhpLanguage;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.elements.*;
import fr.adrienbrault.idea.symfony2plugin.Symfony2InterfacesUtil;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrar;
import fr.adrienbrault.idea.symfony2plugin.codeInsight.GotoCompletionRegistrarParameter;
import fr.adrienbrault.idea.symfony2plugin.form.gotoCompletion.TranslationDomainGotoCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.form.gotoCompletion.TranslationGotoCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.form.util.FormOptionsUtil;
import fr.adrienbrault.idea.symfony2plugin.form.util.FormUtil;
import fr.adrienbrault.idea.symfony2plugin.util.MethodMatcher;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.PhpMethodReferenceCall;
import fr.adrienbrault.idea.symfony2plugin.util.psi.PhpPsiMatcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class FormGotoCompletionRegistrar implements GotoCompletionRegistrar {
private static final PhpPsiMatcher.ArrayValueWithKeyAndMethod.Matcher CHOICE_TRANSLATION_DOMAIN_MATCHER = new PhpPsiMatcher.ArrayValueWithKeyAndMethod.Matcher(
new String[] {"choice_translation_domain", "translation_domain"},
new PhpMethodReferenceCall("Symfony\\Component\\Form\\FormBuilderInterface", 2, "add", "create"),
new PhpMethodReferenceCall("Symfony\\Component\\Form\\FormInterface", 2, "add", "create")
);
public void register(GotoCompletionRegistrarParameter registrar) {
// FormBuilderInterface:add("", "type")
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement parent = psiElement.getParent();
if(parent == null) {
return null;
}
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.StringParameterMatcher(parent, 1)
.withSignature(Symfony2InterfacesUtil.getFormBuilderInterface())
.match();
if(methodMatchParameter == null) {
return null;
}
return new FormBuilderAddGotoCompletionProvider(parent);
});
/**
* $options lookup
* public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array())
*/
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return null;
}
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.ArrayParameterMatcher(parent, 3)
.withSignature("\\Symfony\\Component\\Form\\FormFactoryInterface", "createNamedBuilder")
.withSignature("\\Symfony\\Component\\Form\\FormFactoryInterface", "createNamed")
.match();
if(methodMatchParameter == null) {
return null;
}
return getFormProvider((StringLiteralExpression) parent, methodMatchParameter.getParameters()[1]);
});
/**
* $this->createForm(new FormType(), $entity, array('<foo_key>' => ''));
* $this->createForm('foo', $entity, array('<foo_key>'));
*/
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
// @TODO: migrate to completion provider, because of performance
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return null;
}
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.ArrayParameterMatcher(parent, 2)
.withSignature("\\Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller", "createForm")
.withSignature("\\Symfony\\Component\\Form\\FormFactoryInterface", "create")
.withSignature("\\Symfony\\Component\\Form\\FormFactory", "createBuilder")
.match();
if(methodMatchParameter == null) {
return null;
}
return getFormProvider((StringLiteralExpression) parent, methodMatchParameter.getParameters()[0]);
});
/**
* FormTypeInterface::getParent
*/
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression) || !PhpElementsUtil.getMethodReturnPattern().accepts(parent)) {
return null;
}
Method method = PsiTreeUtil.getParentOfType(psiElement, Method.class);
if(method == null) {
return null;
}
if(!new Symfony2InterfacesUtil().isCallTo(method, "\\Symfony\\Component\\Form\\FormTypeInterface", "getParent")) {
return null;
}
return new FormBuilderAddGotoCompletionProvider(parent);
});
/**
* $type lookup
* public function createNamedBuilder($name, $type = 'form', $data = null, array $options = array())
*/
registrar.register(PlatformPatterns.psiElement().withParent(StringLiteralExpression.class).withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return null;
}
MethodMatcher.MethodMatchParameter methodMatchParameter = new MethodMatcher.StringParameterMatcher(parent, 1)
.withSignature("\\Symfony\\Component\\Form\\FormFactoryInterface", "createNamedBuilder")
.withSignature("\\Symfony\\Component\\Form\\FormFactoryInterface", "createNamed")
.match();
if(methodMatchParameter == null) {
return null;
}
return new FormBuilderAddGotoCompletionProvider(parent);
});
/*
* $builder->add('foo', null, [
* 'choice_translation_domain => '<caret>',
* 'translation_domain => '<caret>',
* ]);
*/
registrar.register(PhpPsiMatcher.ArrayValueWithKeyAndMethod.pattern().withLanguage(PhpLanguage.INSTANCE), psiElement -> {
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return null;
}
PhpPsiMatcher.ArrayValueWithKeyAndMethod.Result result = PhpPsiMatcher.match(parent, CHOICE_TRANSLATION_DOMAIN_MATCHER);
if(result == null) {
return null;
}
return new TranslationDomainGotoCompletionProvider(psiElement);
});
/*
* $builder->add('foo', null, [
* 'choice_translation_domain => 'foobar',
* 'choices => [
* '<caret>' => '<caret>',
* ],
* ]);
*/
registrar.register(PlatformPatterns.psiElement(), psiElement -> {
PsiElement parent = psiElement.getParent();
if(!(parent instanceof StringLiteralExpression)) {
return null;
}
// Symfony 2.x: choices as value
PsiElement choicesArrayValue1 = parent.getParent();
if(choicesArrayValue1.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
PsiElement parent1 = choicesArrayValue1.getParent();
if(parent1 instanceof ArrayHashElement) {
PsiElement choices = parent1.getParent();
if(choices instanceof ArrayCreationExpression) {
return createTranslationGotoCompletionWithLabelSwitch(psiElement, (ArrayCreationExpression) choices, arrayCreationExpression -> {
// <= 2.7 always choices are values
PhpPsiElement value = PhpElementsUtil.getArrayValue(arrayCreationExpression, "choices_as_values");
if(value == null) {
return SymfonyUtil.isVersionLessThenEquals(arrayCreationExpression.getProject(), "2.7");
}
return !(value instanceof ConstantReference && "false".equalsIgnoreCase(value.getName()));
});
}
}
}
// Symfony 3.x: choices as key
ArrayCreationExpression choices = PhpElementsUtil.getCompletableArrayCreationElement(parent);
if(choices != null) {
return createTranslationGotoCompletionWithLabelSwitch(psiElement, choices, arrayCreationExpression -> {
PhpPsiElement value = PhpElementsUtil.getArrayValue(arrayCreationExpression, "choices_as_values");
return !(value instanceof ConstantReference && "false".equalsIgnoreCase(value.getName()));
});
}
return null;
});
}
/**
* Form options on extension or form type default options
*/
private static class FormOptionsGotoCompletionProvider extends GotoCompletionProvider {
private final String formType;
private final Collection<FormOption> options;
public FormOptionsGotoCompletionProvider(@NotNull PsiElement element, @NotNull String formType, FormOption... options) {
super(element);
this.formType = formType;
this.options = Arrays.asList(options);
}
@NotNull
@Override
public Collection<LookupElement> getLookupElements() {
Collection<LookupElement> lookupElements = new ArrayList<>();
if(options.contains(FormOption.EXTENSION)) {
lookupElements.addAll(FormOptionsUtil.getFormExtensionKeysLookupElements(getElement().getProject(), this.formType));
}
if(options.contains(FormOption.DEFAULT_OPTIONS)) {
lookupElements.addAll(FormOptionsUtil.getDefaultOptionLookupElements(getElement().getProject(), this.formType));
}
return lookupElements;
}
@NotNull
@Override
public Collection<PsiElement> getPsiTargets(PsiElement psiElement) {
PsiElement element = psiElement.getParent();
if(!(element instanceof StringLiteralExpression)) {
return Collections.emptyList();
}
Collection<PsiElement> targets = new ArrayList<>();
if(options.contains(FormOption.EXTENSION)) {
targets.addAll(FormOptionsUtil.getFormExtensionsKeysTargets((StringLiteralExpression) element, this.formType));
}
if(options.contains(FormOption.DEFAULT_OPTIONS)) {
targets.addAll(FormOptionsUtil.getDefaultOptionTargets((StringLiteralExpression) element, this.formType));
}
return targets;
}
}
/**
* All registered form type with their getName() return alias name
*/
private static class FormBuilderAddGotoCompletionProvider extends GotoCompletionProvider {
public FormBuilderAddGotoCompletionProvider(PsiElement element) {
super(element);
}
@NotNull
@Override
public Collection<LookupElement> getLookupElements() {
return FormUtil.getFormTypeLookupElements(getElement().getProject());
}
@NotNull
@Override
public Collection<PsiElement> getPsiTargets(PsiElement psiElement) {
PsiElement element = psiElement.getParent();
if(!(element instanceof StringLiteralExpression)) {
return Collections.emptyList();
}
PhpClass formTypeToClass = FormUtil.getFormTypeToClass(getElement().getProject(), ((StringLiteralExpression) element).getContents());
if(formTypeToClass == null) {
return Collections.emptyList();
}
return Arrays.asList(new PsiElement[] { formTypeToClass });
}
}
private enum FormOption {
EXTENSION, DEFAULT_OPTIONS
}
private FormOptionsGotoCompletionProvider getFormProvider(StringLiteralExpression psiElement, PsiElement formType) {
PhpClass phpClass = FormUtil.getFormTypeClassOnParameter(formType);
if (phpClass == null) {
return new FormOptionsGotoCompletionProvider(psiElement, "form", FormOption.EXTENSION);
}
String presentableFQN = phpClass.getPresentableFQN();
return new FormOptionsGotoCompletionProvider(psiElement, presentableFQN, FormOption.EXTENSION, FormOption.DEFAULT_OPTIONS);
}
@Nullable
private GotoCompletionProvider createTranslationGotoCompletionWithLabelSwitch(@NotNull PsiElement origin, @NotNull ArrayCreationExpression choices, Processor<ArrayCreationExpression> processor) {
PsiElement choicesArrayValue = choices.getParent();
if(choicesArrayValue.getNode().getElementType() == PhpElementTypes.ARRAY_VALUE) {
PsiElement choicesValueHash = choicesArrayValue.getParent();
if(choicesValueHash instanceof ArrayHashElement) {
PhpPsiElement transKey = ((ArrayHashElement) choicesValueHash).getKey();
String stringValue = PhpElementsUtil.getStringValue(transKey);
if("choices".equals(stringValue)) {
PsiElement choicesKey = transKey.getParent();
if(choicesKey.getNode().getElementType() == PhpElementTypes.ARRAY_KEY) {
PsiElement formOptionsHash = choicesKey.getParent();
if(formOptionsHash instanceof ArrayHashElement) {
PsiElement arrayCreation = formOptionsHash.getParent();
if(arrayCreation instanceof ArrayCreationExpression) {
if(processor.process((ArrayCreationExpression) arrayCreation)) {
return createTranslationGotoCompletion(origin, arrayCreation);
}
}
}
}
}
}
}
return null;
}
@Nullable
private GotoCompletionProvider createTranslationGotoCompletion(@NotNull PsiElement psiElement, @NotNull PsiElement arrayCreation) {
int parameterIndexValue = PsiElementUtils.getParameterIndexValue(arrayCreation);
if(parameterIndexValue != 2) {
return null;
}
PsiElement parameterList = arrayCreation.getParent();
if(parameterList instanceof ParameterList) {
PsiElement methodReference = parameterList.getParent();
if(methodReference instanceof MethodReference) {
if(PhpElementsUtil.isMethodReferenceInstanceOf((MethodReference) methodReference, "\\Symfony\\Component\\Form\\FormBuilderInterface", "add") ||
PhpElementsUtil.isMethodReferenceInstanceOf((MethodReference) methodReference, "\\Symfony\\Component\\Form\\FormBuilderInterface", "create")
) {
return new TranslationGotoCompletionProvider(psiElement, extractTranslationDomainFromScope((ArrayCreationExpression) arrayCreation));
}
}
}
return null;
}
@NotNull
private String extractTranslationDomainFromScope(@NotNull ArrayCreationExpression arrayCreation) {
String domain = "messages";
PhpPsiElement value = PhpElementsUtil.getArrayValue(arrayCreation, "choice_translation_domain");
if(value instanceof StringLiteralExpression) {
String contents = PhpElementsUtil.getStringValue(value);
if(contents != null) {
domain = contents;
}
} else {
// translation_domain in current array block
String translationDomain = FormOptionsUtil.getTranslationFromScope(arrayCreation);
if(translationDomain != null) {
domain = translationDomain;
}
}
return domain;
}
}