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 f8a70f9e3..a3eed2daf 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/templating/TwigTemplateCompletionContributor.java @@ -10,7 +10,10 @@ import com.intellij.patterns.PatternCondition; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.StandardPatterns; -import com.intellij.psi.*; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.PlatformIcons; import com.intellij.util.ProcessingContext; @@ -36,6 +39,7 @@ import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; +import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.FormFieldResolver; import fr.adrienbrault.idea.symfony2plugin.templating.variable.resolver.holder.FormDataHolder; import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil; import fr.adrienbrault.idea.symfony2plugin.twig.utils.TwigFileUtil; @@ -53,6 +57,7 @@ import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory; import icons.TwigIcons; import org.apache.commons.lang.StringUtils; +import org.intellij.lang.annotations.RegExp; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -518,6 +523,21 @@ public void addCompletions(@NotNull CompletionParameters parameters, ProcessingC PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME), new IncompleteIfCompletionProvider() ); + + + // {{ form_wi => "form_wi(form.test)" + extend( + CompletionType.BASIC, + TwigPattern.getCompletablePattern(), + new IncompleteFormFieldPrintBlockCompletionProvider() + ); + + // {{ form_start => "form_start(form)" + extend( + CompletionType.BASIC, + TwigPattern.getCompletablePattern(), + new IncompleteFormPrintBlockCompletionProvider() + ); } private boolean isCompletionStartingMatch(@NotNull String fullText, @NotNull CompletionParameters completionParameters, int minLength) { @@ -538,6 +558,24 @@ private boolean isCompletionStartingMatch(@NotNull String fullText, @NotNull Com return false; } + private boolean isCompletionStartingRegexMatch(@RegExp String fullText, @NotNull CompletionParameters completionParameters, int minLength) { + PsiElement originalPosition = completionParameters.getOriginalPosition(); + if (originalPosition != null) { + String text = originalPosition.getText(); + if (text.length() >= minLength && text.matches(fullText)) { + return true; + } + } + + PsiElement position = completionParameters.getPosition(); + String text = position.getText().toLowerCase().replace("intellijidearulezzz", ""); + if (text.length() >= minLength && text.startsWith(fullText)) { + return true; + } + + return false; + } + private static class FilterCompletionProvider extends CompletionProvider { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { @@ -666,6 +704,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi } if(twigTypeContainer.getStringElement() != null) { + // LookupElementBuilder lookupElement = LookupElementBuilder.create(twigTypeContainer.getStringElement()); // form @@ -930,6 +969,100 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) { } } + private class IncompleteFormFieldPrintBlockCompletionProvider extends CompletionProvider { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { + if(!Symfony2ProjectComponent.isEnabled(completionParameters.getPosition())) { + return; + } + + resultSet.restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(1).with(new PatternCondition<>("include startsWith") { + @Override + public boolean accepts(@NotNull String s, ProcessingContext processingContext) { + return "for".startsWith(s); + } + })); + + if (!isCompletionStartingRegexMatch("fo\\w+", completionParameters, 3)) { + return; + } + + Set> entries = TwigTypeResolveUtil.collectScopeVariables(completionParameters.getOriginalPosition()).entrySet(); + Project project = completionParameters.getPosition().getProject(); + + Collection psiVariables = new ArrayList<>(); + for (Map.Entry entry : entries.stream().filter(stringPsiVariableEntry -> stringPsiVariableEntry.getKey().equals("form")).collect(Collectors.toList())) { + PhpType phpType = PhpIndex.getInstance(project).completeType(project, PhpType.from(entry.getValue().getTypes().toArray(new String[0])), new HashSet<>()); + if (phpType.types().noneMatch(s -> s.equals("\\Symfony\\Component\\Form\\FormView"))) { + continue; + } + + FormFieldResolver.visitFormReferencesFields(entry.getValue().getElement(), twigTypeContainers -> { + String typeText = null; + + if (twigTypeContainers.getDataHolder() instanceof FormDataHolder formDataHolder) { + typeText = formDataHolder.getPhpClass().getName(); + } + + for (String s : new String[]{"form_row", "form_widget", "form_label", "form_errors", "form_help"}) { + LookupElementBuilder element = LookupElementBuilder.create(s + "(" + entry.getKey() + "." + twigTypeContainers.getStringElement() + ")") + .withTypeText(typeText) + .withBoldness(true) + .withIcon(Symfony2Icons.FORM_TYPE); + + resultSet.addElement(element); + } + }); + + psiVariables.add(entry.getValue()); + } + } + } + + private class IncompleteFormPrintBlockCompletionProvider extends CompletionProvider { + @Override + protected void addCompletions(@NotNull CompletionParameters completionParameters, @NotNull ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { + if(!Symfony2ProjectComponent.isEnabled(completionParameters.getPosition())) { + return; + } + + resultSet.restartCompletionOnPrefixChange(StandardPatterns.string().longerThan(1).with(new PatternCondition<>("include startsWith") { + @Override + public boolean accepts(@NotNull String s, ProcessingContext processingContext) { + return "for".startsWith(s); + } + })); + + if (!isCompletionStartingRegexMatch("fo\\w+", completionParameters, 3)) { + return; + } + + Set> entries = TwigTypeResolveUtil.collectScopeVariables(completionParameters.getOriginalPosition()).entrySet(); + Project project = completionParameters.getPosition().getProject(); + + for (Map.Entry entry : entries.stream().filter(stringPsiVariableEntry -> stringPsiVariableEntry.getKey().equals("form")).collect(Collectors.toList())) { + PhpType phpType = PhpIndex.getInstance(project).completeType(project, PhpType.from(entry.getValue().getTypes().toArray(new String[0])), new HashSet<>()); + if (phpType.types().noneMatch(s -> s.equals("\\Symfony\\Component\\Form\\FormView"))) { + continue; + } + + String typeText = null; + Collection formTypeFromFormFactory = FormFieldResolver.getFormTypeFromFormFactory(entry.getValue().getElement()); + if (formTypeFromFormFactory.size() > 0) { + typeText = StringUtils.stripStart(formTypeFromFormFactory.iterator().next().getFQN(), "\\"); + } + + for (String s : new String[]{"form_start", "form_rest", "form_end", "form_errors"}) { + LookupElementBuilder element = LookupElementBuilder.create(s + "(" + entry.getKey() + ")") + .withTypeText(typeText) + .withIcon(Symfony2Icons.FORM_TYPE); + + resultSet.addElement(element); + } + } + } + } + /** * {% for => "for flash in app.flashes" */ 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 1a0925a4a..c485343ac 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 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; /** * @author Daniel Espendiller @@ -26,49 +27,57 @@ public class FormFieldResolver implements TwigTypeResolver { public void resolve(Collection targets, Collection previousElement, String typeName, Collection> previousElements, @Nullable Collection psiVariables) { - if(targets.size() == 0 || psiVariables == null || previousElements == null || previousElements.size() != 0) { + if (targets.size() == 0 || psiVariables == null || psiVariables.size() == 0 || previousElements == null || previousElements.size() != 0) { return; } TwigTypeContainer twigTypeContainer = targets.iterator().next(); + if (twigTypeContainer.getPhpNamedElement() instanceof PhpClass phpClass && PhpElementsUtil.isInstanceOf(phpClass, "\\Symfony\\Component\\Form\\FormView")) { + visitFormReferencesFields(psiVariables.iterator().next().getElement(), targets::add); + } + } - if(twigTypeContainer.getPhpNamedElement() instanceof PhpClass) { - if(PhpElementsUtil.isInstanceOf((PhpClass) twigTypeContainer.getPhpNamedElement(), "\\Symfony\\Component\\Form\\FormView")) { - if(psiVariables.size() > 0) { - PsiElement var = psiVariables.iterator().next().getElement(); - - // $form->createView() - if(var instanceof MethodReference) { - PsiElement form = var.getFirstChild(); - if(form instanceof Variable) { - PsiElement varDecl = ((Variable) form).resolve(); - if(varDecl instanceof Variable) { - MethodReference methodReference = PsiTreeUtil.getNextSiblingOfType(varDecl, MethodReference.class); - attachFormFields(methodReference, targets); - } + public static Collection getFormTypeFromFormFactory(@NotNull PsiElement formReference) { + Collection phpClasses = new ArrayList<>(); + + // $form->createView() + if (formReference instanceof MethodReference) { + PsiElement form = formReference.getFirstChild(); + if (form instanceof Variable) { + PsiElement varDecl = ((Variable) form).resolve(); + if (varDecl instanceof Variable) { + MethodReference methodReference = PsiTreeUtil.getNextSiblingOfType(varDecl, MethodReference.class); + if (methodReference != null) { + PhpClass phpClass = resolveCall(methodReference); + if (phpClass != null) { + phpClasses.add(phpClass); } } + } + } + } - - // nested resolve of form view; @TODO: should be some nicer - // 'foo2' => $form2 => $form2 = $form->createView() => $this->createForm(new Type(); - if(var instanceof Variable) { - PsiElement varDecl = ((Variable) var).resolve(); - if(varDecl instanceof Variable) { - MethodReference methodReference = PsiTreeUtil.getNextSiblingOfType(varDecl, MethodReference.class); - if(methodReference != null) { - PsiElement scopeVar = methodReference.getFirstChild(); - - // $form2 = $form->createView() - if(scopeVar instanceof Variable) { - PsiElement varDeclParent = ((Variable) scopeVar).resolve(); - if(varDeclParent instanceof Variable) { - - // "$form"->createView(); - PsiElement resolve = ((Variable) varDeclParent).resolve(); - if(resolve != null) { - attachFormFields(PsiTreeUtil.getNextSiblingOfType(resolve, MethodReference.class), targets); - } + // nested resolve of form view; @TODO: should be some nicer + // 'foo2' => $form2 => $form2 = $form->createView() => $this->createForm(new Type(); + if (formReference instanceof Variable) { + PsiElement varDecl = ((Variable) formReference).resolve(); + if (varDecl instanceof Variable) { + MethodReference methodReference = PsiTreeUtil.getNextSiblingOfType(varDecl, MethodReference.class); + if (methodReference != null) { + PsiElement scopeVar = methodReference.getFirstChild(); + + // $form2 = $form->createView() + if (scopeVar instanceof Variable) { + PsiElement varDeclParent = ((Variable) scopeVar).resolve(); + if (varDeclParent instanceof Variable) { + // "$form"->createView(); + PsiElement resolve = ((Variable) varDeclParent).resolve(); + if (resolve != null) { + MethodReference nextSiblingOfType = PsiTreeUtil.getNextSiblingOfType(resolve, MethodReference.class); + if (nextSiblingOfType != null) { + PhpClass phpClass = resolveCall(methodReference); + if (phpClass != null) { + phpClasses.add(phpClass); } } } @@ -77,13 +86,12 @@ public void resolve(Collection targets, Collection targets) { - if (methodReference == null) { - return; - } + return phpClasses; + } + @Nullable + private static PhpClass resolveCall(@NotNull MethodReference methodReference) { int index = -1; if (PhpElementsUtil.isMethodReferenceInstanceOf( @@ -102,24 +110,15 @@ private static void attachFormFields(@Nullable MethodReference methodReference, } if (index < 0) { - return; + return null; } PsiElement formType = PsiElementUtils.getMethodParameterPsiElementAt(methodReference, index); - if(formType != null) { - PhpClass phpClass = FormUtil.getFormTypeClassOnParameter(formType); - - if(phpClass == null) { - return; - } - - Method method = phpClass.findMethodByName("buildForm"); - if(method == null) { - return; - } - - targets.addAll(getTwigTypeContainer(method, phpClass)); + if (formType != null) { + return FormUtil.getFormTypeClassOnParameter(formType); } + + return null; } @NotNull @@ -146,4 +145,19 @@ private static List getTwigTypeContainer(@NotNull Method meth return twigTypeContainers; } + /** + * Search and resolve: "$form->createView()" + */ + public static void visitFormReferencesFields(PsiElement formReference, @NotNull Consumer consumer) { + for (PhpClass phpClass : getFormTypeFromFormFactory(formReference)) { + Method method = phpClass.findMethodByName("buildForm"); + if(method == null) { + return; + } + + for (TwigTypeContainer twigTypeContainer : getTwigTypeContainer(method, phpClass)) { + consumer.accept(twigTypeContainer); + } + } + } }