Skip to content

Commit

Permalink
add "form_*" incomplete completion
Browse files Browse the repository at this point in the history
  • Loading branch information
Haehnchen committed Jun 24, 2023
1 parent f652c71 commit e60106e
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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.*;
Expand Down Expand Up @@ -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) {
Expand All @@ -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<CompletionParameters> {
public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) {
if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) {
Expand Down Expand Up @@ -666,6 +704,7 @@ protected void addCompletions(@NotNull CompletionParameters parameters, Processi
}

if(twigTypeContainer.getStringElement() != null) {
//
LookupElementBuilder lookupElement = LookupElementBuilder.create(twigTypeContainer.getStringElement());

// form
Expand Down Expand Up @@ -930,6 +969,100 @@ public boolean accepts(@NotNull String s, ProcessingContext processingContext) {
}
}

private class IncompleteFormFieldPrintBlockCompletionProvider extends CompletionProvider<CompletionParameters> {
@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<Map.Entry<String, PsiVariable>> entries = TwigTypeResolveUtil.collectScopeVariables(completionParameters.getOriginalPosition()).entrySet();
Project project = completionParameters.getPosition().getProject();

Collection<PsiVariable> psiVariables = new ArrayList<>();
for (Map.Entry<String, PsiVariable> 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<CompletionParameters> {
@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<Map.Entry<String, PsiVariable>> entries = TwigTypeResolveUtil.collectScopeVariables(completionParameters.getOriginalPosition()).entrySet();
Project project = completionParameters.getPosition().getProject();

for (Map.Entry<String, PsiVariable> 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<PhpClass> 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"
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,65 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class FormFieldResolver implements TwigTypeResolver {

public void resolve(Collection<TwigTypeContainer> targets, Collection<TwigTypeContainer> previousElement, String typeName, Collection<List<TwigTypeContainer>> previousElements, @Nullable Collection<PsiVariable> 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<PhpClass> getFormTypeFromFormFactory(@NotNull PsiElement formReference) {
Collection<PhpClass> 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);
}
}
}
Expand All @@ -77,13 +86,12 @@ public void resolve(Collection<TwigTypeContainer> targets, Collection<TwigTypeCo
}
}
}
}

private static void attachFormFields(@Nullable MethodReference methodReference, @NotNull Collection<TwigTypeContainer> targets) {
if (methodReference == null) {
return;
}
return phpClasses;
}

@Nullable
private static PhpClass resolveCall(@NotNull MethodReference methodReference) {
int index = -1;

if (PhpElementsUtil.isMethodReferenceInstanceOf(
Expand All @@ -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
Expand All @@ -146,4 +145,19 @@ private static List<TwigTypeContainer> getTwigTypeContainer(@NotNull Method meth
return twigTypeContainers;
}

/**
* Search and resolve: "$form->createView()"
*/
public static void visitFormReferencesFields(PsiElement formReference, @NotNull Consumer<TwigTypeContainer> consumer) {
for (PhpClass phpClass : getFormTypeFromFormFactory(formReference)) {
Method method = phpClass.findMethodByName("buildForm");
if(method == null) {
return;
}

for (TwigTypeContainer twigTypeContainer : getTwigTypeContainer(method, phpClass)) {
consumer.accept(twigTypeContainer);
}
}
}
}

0 comments on commit e60106e

Please sign in to comment.