From 6575f483fc99687a92498b4ea693954aa0159f15 Mon Sep 17 00:00:00 2001 From: Daniel Espendiller Date: Wed, 22 Jun 2022 18:04:21 +0200 Subject: [PATCH] provide incomplete named argument complete for yaml with: "tagged_iterator", dotenv, service names and parameters --- .../yaml/YamlCompletionContributor.java | 149 ++++++++++++++++-- .../yaml/YamlGoToDeclarationHandler.java | 3 +- .../container/util/ServiceContainerUtil.java | 17 +- .../ui/MethodSignatureTypeSettingsForm.java | 1 - .../yaml/YamlCompletionContributorTest.java | 32 ++++ .../tests/config/yaml/fixtures/classes.php | 7 + .../tests/config/yaml/fixtures/services.xml | 1 + .../util/ServiceContainerUtilTest.java | 2 +- 8 files changed, 194 insertions(+), 18 deletions(-) diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlCompletionContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlCompletionContributor.java index 409419714..f6b7b7063 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlCompletionContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlCompletionContributor.java @@ -1,7 +1,9 @@ package fr.adrienbrault.idea.symfony2plugin.config.yaml; import com.intellij.codeInsight.completion.*; +import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileVisitor; @@ -14,7 +16,10 @@ import com.intellij.util.ProcessingContext; import com.jetbrains.php.completion.PhpLookupElement; import com.jetbrains.php.lang.psi.elements.Method; +import com.jetbrains.php.lang.psi.elements.Parameter; +import com.jetbrains.php.lang.psi.elements.ParameterList; import com.jetbrains.php.lang.psi.elements.PhpClass; +import com.jetbrains.php.lang.psi.resolve.types.PhpType; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.config.component.ParameterLookupElement; @@ -22,6 +27,8 @@ import fr.adrienbrault.idea.symfony2plugin.config.yaml.completion.ConfigCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.dic.ContainerParameter; import fr.adrienbrault.idea.symfony2plugin.dic.ServiceCompletionProvider; +import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ServiceTypeHint; +import fr.adrienbrault.idea.symfony2plugin.dic.container.suggestion.utils.ServiceSuggestionUtil; import fr.adrienbrault.idea.symfony2plugin.dic.container.util.DotEnvUtil; import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil; import fr.adrienbrault.idea.symfony2plugin.doctrine.DoctrineYamlAnnotationLookupBuilder; @@ -33,6 +40,7 @@ import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; +import fr.adrienbrault.idea.symfony2plugin.util.SimilarSuggestionUtil; import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleFileCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.util.completion.EventCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.util.completion.PhpClassAndParameterCompletionProvider; @@ -48,12 +56,10 @@ import org.jetbrains.yaml.psi.YAMLKeyValue; import org.jetbrains.yaml.psi.YAMLScalar; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; /** * @author Daniel Espendiller @@ -408,22 +414,143 @@ private static class NamedArgumentCompletionProvider extends CompletionProvider< protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) { HashSet uniqueParameters = new HashSet<>(); - ServiceContainerUtil.visitNamedArguments(parameters.getPosition().getContainingFile(), parameter -> { - String name = parameter.getName(); - if (uniqueParameters.contains(name)) { + PsiElement position = parameters.getPosition(); + boolean hasEmptyNextElement = position.getNextSibling() == null; + + ServiceContainerUtil.visitNamedArguments(position.getContainingFile(), pair -> { + Parameter parameter = pair.getFirst(); + String parameterName = parameter.getName(); + if (uniqueParameters.contains(parameterName)) { return; } - uniqueParameters.add(name); + uniqueParameters.add(parameterName); // create argument for yaml: $parameter result.addElement( - LookupElementBuilder.create("$" + name) - .withIcon(parameter.getIcon()) - .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true) + LookupElementBuilder.create("$" + parameterName) + .withIcon(parameter.getIcon()) + .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\")) ); + + if (hasEmptyNextElement) { + // iterable $handlers => can also provide "!tagged_iterator" + if (parameter.getType().getTypes().stream().anyMatch(s -> s.equalsIgnoreCase(PhpType._ITERABLE))) { + LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": !tagged_iterator") + .withIcon(parameter.getIcon()) + .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true); + + result.addElement(PrioritizedLookupElement.withPriority(element, -1000)); + } + + if (!parameter.getType().getTypes().stream().allMatch(PhpType::isPrimitiveType)) { + // $foobar: '@service' + result.addAllElements(getServiceSuggestion(position, pair, parameterName, new ContainerCollectionResolver.LazyServiceCollector(position.getProject()))); + } else { + String parameterNormalized = parameterName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", ""); + if (parameterNormalized.length() > 5) { + // $projectDir: '%kernel.project_dir%' + result.addAllElements(getParameterSuggestion(parameter, parameterName, parameterNormalized)); + + // $kernelClass: '%env(KERNEL_CLASS)%' + result.addAllElements(getDotEnvSuggestion(parameter, parameterName, parameterNormalized)); + } + } + } }); } + + @NotNull + private Collection getServiceSuggestion(@NotNull PsiElement position, @NotNull Pair pair, @NotNull String parameterName, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) { + Parameter parameter = pair.getFirst(); + + PsiElement parameterList = parameter.getParent(); + if (parameterList instanceof ParameterList) { + PsiElement parent = parameterList.getParent(); + if (parent instanceof Method) { + Collection suggestions = new ArrayList<>(ServiceSuggestionUtil.createSuggestions(new ServiceTypeHint( + (Method) parent, + pair.getSecond(), + position + ), lazyServiceCollector.getCollector().getServices().values())); + + return suggestions.stream() + .limit(3) + .map(service -> { + LookupElementBuilder element = LookupElementBuilder.create(String.format("$%s: '@%s'", parameterName, service)) + .withIcon(Symfony2Icons.SERVICE) + .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true); + + return PrioritizedLookupElement.withPriority(element, -1000); + }) + .collect(Collectors.toList()); + } + } + + return Collections.emptyList(); + } + + /** + * $projectDir: '%kernel.project_dir%' + */ + private Collection getParameterSuggestion(@NotNull Parameter parameter, @NotNull String parameterName, @NotNull String parameterNormalized) { + Set values = new HashSet<>(); + + for (String name : ContainerCollectionResolver.getParameterNames(parameter.getProject())) { + String symfonyParameterNormalized = name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", ""); + + if (symfonyParameterNormalized.contains(parameterNormalized)) { + values.add(name); + } + } + + // weight items: append all indirect matched, after them in case there they are not similar + List similarString = new ArrayList<>(SimilarSuggestionUtil.findSimilarString(parameterNormalized, values)); + similarString.addAll(values); + + return similarString.stream() + .distinct() + .limit(3) + .map(service -> { + LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": '%" + service + "%'") + .withIcon(Symfony2Icons.PARAMETER) + .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true); + + return PrioritizedLookupElement.withPriority(element, -1000); + }) + .collect(Collectors.toList()); + } + + /** + * "$kernelClass: '%env(KERNEL_CLASS)%'" + */ + @NotNull + private Collection getDotEnvSuggestion(@NotNull Parameter parameter, @NotNull String parameterName, @NotNull String parameterNormalized) { + Set dotEnv = new HashSet<>(); + for (String name : DotEnvUtil.getEnvironmentVariables(parameter.getProject())) { + String symfonyParameterNormalized = name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", ""); + + if (symfonyParameterNormalized.contains(parameterNormalized)) { + dotEnv.add(name); + } + } + + // weight items: append all indirect matched, after them in case there they are not similar + List similarString = new ArrayList<>(SimilarSuggestionUtil.findSimilarString(parameterNormalized, dotEnv)); + similarString.addAll(dotEnv); + + return similarString.stream() + .distinct() + .limit(3) + .map(service -> { + LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": '%env(" + service + ")%'") + .withIcon(Symfony2Icons.PARAMETER) + .withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true); + + return PrioritizedLookupElement.withPriority(element, -1000); + }) + .collect(Collectors.toList()); + } } /** diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlGoToDeclarationHandler.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlGoToDeclarationHandler.java index a82e8caf9..b1fadfb38 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlGoToDeclarationHandler.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlGoToDeclarationHandler.java @@ -214,7 +214,8 @@ private Collection namedDefaultBindArgumentGoto(@NotNull P Collection psiElements = new HashSet<>(); String argumentWithoutDollar = parameterName.substring(1); - ServiceContainerUtil.visitNamedArguments(psiElement.getContainingFile(), parameter -> { + ServiceContainerUtil.visitNamedArguments(psiElement.getContainingFile(), pair -> { + Parameter parameter = pair.getFirst(); if (parameter.getName().equals(argumentWithoutDollar)) { psiElements.add(parameter); } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java index 3c6f5b6ff..296919ee4 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java @@ -2,6 +2,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.patterns.PlatformPatterns; @@ -594,9 +595,9 @@ public static boolean hasMissingYamlNamedArgumentForInspection(@NotNull PsiEleme * arguments: * $ */ - public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consumer processor) { + public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consumer> processor) { if (psiFile instanceof YAMLFile) { - Collection parameters = new HashSet<>(); + Collection> parameters = new HashSet<>(); // direct service definition for (PhpClass phpClass : YamlHelper.getPhpClassesInYamlFile((YAMLFile) psiFile, new ContainerCollectionResolver.LazyServiceCollector(psiFile.getProject()))) { @@ -605,7 +606,10 @@ public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consum continue; } - parameters.addAll(Arrays.asList(constructor.getParameters())); + Parameter @NotNull [] methodParameters = constructor.getParameters(); + for (int i = 0, methodParametersLength = methodParameters.length; i < methodParametersLength; i++) { + parameters.add(Pair.create(methodParameters[i], i)); + } } for (YAMLKeyValue taggedService : YamlHelper.getTaggedServices((YAMLFile) psiFile, "controller.service_arguments")) { @@ -627,7 +631,12 @@ public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consum // maybe filter actions and public methods in a suitable way? phpClass.getMethods().stream() .filter(method -> method.getAccess().isPublic() && !method.getName().startsWith("set")) - .forEach(method -> Collections.addAll(parameters, method.getParameters())); + .forEach(method -> { + Parameter @NotNull [] methodParameters = method.getParameters(); + for (int i = 0, methodParametersLength = methodParameters.length; i < methodParametersLength; i++) { + parameters.add(Pair.create(methodParameters[i], i)); + } + }); } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/ui/MethodSignatureTypeSettingsForm.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/ui/MethodSignatureTypeSettingsForm.java index 3ea88dbb1..2d84b5b67 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/ui/MethodSignatureTypeSettingsForm.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/ui/MethodSignatureTypeSettingsForm.java @@ -20,7 +20,6 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.ArrayList; -import java.util.List; /** * @author Daniel Espendiller diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlCompletionContributorTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlCompletionContributorTest.java index 932123106..6bd3309b9 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlCompletionContributorTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlCompletionContributorTest.java @@ -316,5 +316,37 @@ public void testNamedArgumentCompletionForServiceArguments() { " $: ~\n", "$i" ); + + assertCompletionContains(YAMLFileType.YML, "" + + "services:\n" + + " Foo\\Car:\n" + + " arguments:\n" + + " $myDateTime\n", + "$myDateTime: '@foo'" + ); + + assertCompletionNotContains(YAMLFileType.YML, "" + + "services:\n" + + " Foo\\Car:\n" + + " arguments:\n" + + " $myDateTime: ~\n", + "$myDateTime: '@foo'" + ); + + assertCompletionContains(YAMLFileType.YML, "" + + "services:\n" + + " Foo\\Car:\n" + + " arguments:\n" + + " $foobarEnv\n", + "$foobarEnv: '%env(FOOBAR_ENV)%'" + ); + + assertCompletionContains(YAMLFileType.YML, "" + + "services:\n" + + " Foo\\Car:\n" + + " arguments:\n" + + " $projectDir\n", + "$projectDir: '%kernel.project_dir%'" + ); } } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/classes.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/classes.php index 9b6cc4413..d158dcdc9 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/classes.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/classes.php @@ -19,4 +19,11 @@ class Apple { function __construct($i, $z = null) { } } + + class Car + { + function __construct(\string $projectDir, \string $foobarEnv, \MyDateTime $myDateTime) + { + } + } } \ No newline at end of file diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/services.xml b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/services.xml index 18722fcad..9810385be 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/services.xml +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/services.xml @@ -3,6 +3,7 @@ bar + project_dir diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java index c11209496..bee00b497 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java @@ -349,7 +349,7 @@ public void testVisitNamedArguments() { ); Collection arguments = new HashSet<>(); - ServiceContainerUtil.visitNamedArguments(psiFile, parameter -> arguments.add(parameter.getName())); + ServiceContainerUtil.visitNamedArguments(psiFile, parameter -> arguments.add(parameter.getFirst().getName())); assertTrue(arguments.contains("foobar"));