Skip to content

Commit 6575f48

Browse files
committed
provide incomplete named argument complete for yaml with: "tagged_iterator", dotenv, service names and parameters
1 parent b5ae955 commit 6575f48

File tree

8 files changed

+194
-18
lines changed

8 files changed

+194
-18
lines changed

src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlCompletionContributor.java

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package fr.adrienbrault.idea.symfony2plugin.config.yaml;
22

33
import com.intellij.codeInsight.completion.*;
4+
import com.intellij.codeInsight.lookup.LookupElement;
45
import com.intellij.codeInsight.lookup.LookupElementBuilder;
6+
import com.intellij.openapi.util.Pair;
57
import com.intellij.openapi.vfs.VfsUtil;
68
import com.intellij.openapi.vfs.VirtualFile;
79
import com.intellij.openapi.vfs.VirtualFileVisitor;
@@ -14,14 +16,19 @@
1416
import com.intellij.util.ProcessingContext;
1517
import com.jetbrains.php.completion.PhpLookupElement;
1618
import com.jetbrains.php.lang.psi.elements.Method;
19+
import com.jetbrains.php.lang.psi.elements.Parameter;
20+
import com.jetbrains.php.lang.psi.elements.ParameterList;
1721
import com.jetbrains.php.lang.psi.elements.PhpClass;
22+
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
1823
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
1924
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
2025
import fr.adrienbrault.idea.symfony2plugin.config.component.ParameterLookupElement;
2126
import fr.adrienbrault.idea.symfony2plugin.config.doctrine.DoctrineStaticTypeLookupBuilder;
2227
import fr.adrienbrault.idea.symfony2plugin.config.yaml.completion.ConfigCompletionProvider;
2328
import fr.adrienbrault.idea.symfony2plugin.dic.ContainerParameter;
2429
import fr.adrienbrault.idea.symfony2plugin.dic.ServiceCompletionProvider;
30+
import fr.adrienbrault.idea.symfony2plugin.dic.container.dict.ServiceTypeHint;
31+
import fr.adrienbrault.idea.symfony2plugin.dic.container.suggestion.utils.ServiceSuggestionUtil;
2532
import fr.adrienbrault.idea.symfony2plugin.dic.container.util.DotEnvUtil;
2633
import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil;
2734
import fr.adrienbrault.idea.symfony2plugin.doctrine.DoctrineYamlAnnotationLookupBuilder;
@@ -33,6 +40,7 @@
3340
import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver;
3441
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
3542
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
43+
import fr.adrienbrault.idea.symfony2plugin.util.SimilarSuggestionUtil;
3644
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleFileCompletionProvider;
3745
import fr.adrienbrault.idea.symfony2plugin.util.completion.EventCompletionProvider;
3846
import fr.adrienbrault.idea.symfony2plugin.util.completion.PhpClassAndParameterCompletionProvider;
@@ -48,12 +56,10 @@
4856
import org.jetbrains.yaml.psi.YAMLKeyValue;
4957
import org.jetbrains.yaml.psi.YAMLScalar;
5058

51-
import java.util.Collections;
52-
import java.util.HashMap;
53-
import java.util.HashSet;
54-
import java.util.Map;
59+
import java.util.*;
5560
import java.util.regex.Matcher;
5661
import java.util.regex.Pattern;
62+
import java.util.stream.Collectors;
5763

5864
/**
5965
* @author Daniel Espendiller <daniel@espendiller.net>
@@ -408,22 +414,143 @@ private static class NamedArgumentCompletionProvider extends CompletionProvider<
408414
protected void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet result) {
409415
HashSet<String> uniqueParameters = new HashSet<>();
410416

411-
ServiceContainerUtil.visitNamedArguments(parameters.getPosition().getContainingFile(), parameter -> {
412-
String name = parameter.getName();
413-
if (uniqueParameters.contains(name)) {
417+
PsiElement position = parameters.getPosition();
418+
boolean hasEmptyNextElement = position.getNextSibling() == null;
419+
420+
ServiceContainerUtil.visitNamedArguments(position.getContainingFile(), pair -> {
421+
Parameter parameter = pair.getFirst();
422+
String parameterName = parameter.getName();
423+
if (uniqueParameters.contains(parameterName)) {
414424
return;
415425
}
416426

417-
uniqueParameters.add(name);
427+
uniqueParameters.add(parameterName);
418428

419429
// create argument for yaml: $parameter
420430
result.addElement(
421-
LookupElementBuilder.create("$" + name)
422-
.withIcon(parameter.getIcon())
423-
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true)
431+
LookupElementBuilder.create("$" + parameterName)
432+
.withIcon(parameter.getIcon())
433+
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"))
424434
);
435+
436+
if (hasEmptyNextElement) {
437+
// iterable $handlers => can also provide "!tagged_iterator"
438+
if (parameter.getType().getTypes().stream().anyMatch(s -> s.equalsIgnoreCase(PhpType._ITERABLE))) {
439+
LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": !tagged_iterator")
440+
.withIcon(parameter.getIcon())
441+
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true);
442+
443+
result.addElement(PrioritizedLookupElement.withPriority(element, -1000));
444+
}
445+
446+
if (!parameter.getType().getTypes().stream().allMatch(PhpType::isPrimitiveType)) {
447+
// $foobar: '@service'
448+
result.addAllElements(getServiceSuggestion(position, pair, parameterName, new ContainerCollectionResolver.LazyServiceCollector(position.getProject())));
449+
} else {
450+
String parameterNormalized = parameterName.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
451+
if (parameterNormalized.length() > 5) {
452+
// $projectDir: '%kernel.project_dir%'
453+
result.addAllElements(getParameterSuggestion(parameter, parameterName, parameterNormalized));
454+
455+
// $kernelClass: '%env(KERNEL_CLASS)%'
456+
result.addAllElements(getDotEnvSuggestion(parameter, parameterName, parameterNormalized));
457+
}
458+
}
459+
}
425460
});
426461
}
462+
463+
@NotNull
464+
private Collection<LookupElement> getServiceSuggestion(@NotNull PsiElement position, @NotNull Pair<Parameter, Integer> pair, @NotNull String parameterName, @NotNull ContainerCollectionResolver.LazyServiceCollector lazyServiceCollector) {
465+
Parameter parameter = pair.getFirst();
466+
467+
PsiElement parameterList = parameter.getParent();
468+
if (parameterList instanceof ParameterList) {
469+
PsiElement parent = parameterList.getParent();
470+
if (parent instanceof Method) {
471+
Collection<String> suggestions = new ArrayList<>(ServiceSuggestionUtil.createSuggestions(new ServiceTypeHint(
472+
(Method) parent,
473+
pair.getSecond(),
474+
position
475+
), lazyServiceCollector.getCollector().getServices().values()));
476+
477+
return suggestions.stream()
478+
.limit(3)
479+
.map(service -> {
480+
LookupElementBuilder element = LookupElementBuilder.create(String.format("$%s: '@%s'", parameterName, service))
481+
.withIcon(Symfony2Icons.SERVICE)
482+
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true);
483+
484+
return PrioritizedLookupElement.withPriority(element, -1000);
485+
})
486+
.collect(Collectors.toList());
487+
}
488+
}
489+
490+
return Collections.emptyList();
491+
}
492+
493+
/**
494+
* $projectDir: '%kernel.project_dir%'
495+
*/
496+
private Collection<LookupElement> getParameterSuggestion(@NotNull Parameter parameter, @NotNull String parameterName, @NotNull String parameterNormalized) {
497+
Set<String> values = new HashSet<>();
498+
499+
for (String name : ContainerCollectionResolver.getParameterNames(parameter.getProject())) {
500+
String symfonyParameterNormalized = name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
501+
502+
if (symfonyParameterNormalized.contains(parameterNormalized)) {
503+
values.add(name);
504+
}
505+
}
506+
507+
// weight items: append all indirect matched, after them in case there they are not similar
508+
List<String> similarString = new ArrayList<>(SimilarSuggestionUtil.findSimilarString(parameterNormalized, values));
509+
similarString.addAll(values);
510+
511+
return similarString.stream()
512+
.distinct()
513+
.limit(3)
514+
.map(service -> {
515+
LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": '%" + service + "%'")
516+
.withIcon(Symfony2Icons.PARAMETER)
517+
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true);
518+
519+
return PrioritizedLookupElement.withPriority(element, -1000);
520+
})
521+
.collect(Collectors.toList());
522+
}
523+
524+
/**
525+
* "$kernelClass: '%env(KERNEL_CLASS)%'"
526+
*/
527+
@NotNull
528+
private Collection<LookupElement> getDotEnvSuggestion(@NotNull Parameter parameter, @NotNull String parameterName, @NotNull String parameterNormalized) {
529+
Set<String> dotEnv = new HashSet<>();
530+
for (String name : DotEnvUtil.getEnvironmentVariables(parameter.getProject())) {
531+
String symfonyParameterNormalized = name.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9]", "");
532+
533+
if (symfonyParameterNormalized.contains(parameterNormalized)) {
534+
dotEnv.add(name);
535+
}
536+
}
537+
538+
// weight items: append all indirect matched, after them in case there they are not similar
539+
List<String> similarString = new ArrayList<>(SimilarSuggestionUtil.findSimilarString(parameterNormalized, dotEnv));
540+
similarString.addAll(dotEnv);
541+
542+
return similarString.stream()
543+
.distinct()
544+
.limit(3)
545+
.map(service -> {
546+
LookupElementBuilder element = LookupElementBuilder.create("$" + parameterName + ": '%env(" + service + ")%'")
547+
.withIcon(Symfony2Icons.PARAMETER)
548+
.withTypeText(StringUtils.stripStart(parameter.getType().toString(), "\\"), true);
549+
550+
return PrioritizedLookupElement.withPriority(element, -1000);
551+
})
552+
.collect(Collectors.toList());
553+
}
427554
}
428555

429556
/**

src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlGoToDeclarationHandler.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ private Collection<? extends PsiElement> namedDefaultBindArgumentGoto(@NotNull P
214214
Collection<PsiElement> psiElements = new HashSet<>();
215215

216216
String argumentWithoutDollar = parameterName.substring(1);
217-
ServiceContainerUtil.visitNamedArguments(psiElement.getContainingFile(), parameter -> {
217+
ServiceContainerUtil.visitNamedArguments(psiElement.getContainingFile(), pair -> {
218+
Parameter parameter = pair.getFirst();
218219
if (parameter.getName().equals(argumentWithoutDollar)) {
219220
psiElements.add(parameter);
220221
}

src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/container/util/ServiceContainerUtil.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.intellij.openapi.project.Project;
44
import com.intellij.openapi.util.Key;
5+
import com.intellij.openapi.util.Pair;
56
import com.intellij.openapi.vfs.VfsUtil;
67
import com.intellij.openapi.vfs.VirtualFile;
78
import com.intellij.patterns.PlatformPatterns;
@@ -594,9 +595,9 @@ public static boolean hasMissingYamlNamedArgumentForInspection(@NotNull PsiEleme
594595
* arguments:
595596
* $<caret>
596597
*/
597-
public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consumer<Parameter> processor) {
598+
public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consumer<Pair<Parameter, Integer>> processor) {
598599
if (psiFile instanceof YAMLFile) {
599-
Collection<Parameter> parameters = new HashSet<>();
600+
Collection<Pair<Parameter, Integer>> parameters = new HashSet<>();
600601

601602
// direct service definition
602603
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
605606
continue;
606607
}
607608

608-
parameters.addAll(Arrays.asList(constructor.getParameters()));
609+
Parameter @NotNull [] methodParameters = constructor.getParameters();
610+
for (int i = 0, methodParametersLength = methodParameters.length; i < methodParametersLength; i++) {
611+
parameters.add(Pair.create(methodParameters[i], i));
612+
}
609613
}
610614

611615
for (YAMLKeyValue taggedService : YamlHelper.getTaggedServices((YAMLFile) psiFile, "controller.service_arguments")) {
@@ -627,7 +631,12 @@ public static void visitNamedArguments(@NotNull PsiFile psiFile, @NotNull Consum
627631
// maybe filter actions and public methods in a suitable way?
628632
phpClass.getMethods().stream()
629633
.filter(method -> method.getAccess().isPublic() && !method.getName().startsWith("set"))
630-
.forEach(method -> Collections.addAll(parameters, method.getParameters()));
634+
.forEach(method -> {
635+
Parameter @NotNull [] methodParameters = method.getParameters();
636+
for (int i = 0, methodParametersLength = methodParameters.length; i < methodParametersLength; i++) {
637+
parameters.add(Pair.create(methodParameters[i], i));
638+
}
639+
});
631640
}
632641
}
633642

src/main/java/fr/adrienbrault/idea/symfony2plugin/ui/MethodSignatureTypeSettingsForm.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.awt.event.MouseAdapter;
2121
import java.awt.event.MouseEvent;
2222
import java.util.ArrayList;
23-
import java.util.List;
2423

2524
/**
2625
* @author Daniel Espendiller <daniel@espendiller.net>

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlCompletionContributorTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,5 +316,37 @@ public void testNamedArgumentCompletionForServiceArguments() {
316316
" $<caret>: ~\n",
317317
"$i"
318318
);
319+
320+
assertCompletionContains(YAMLFileType.YML, "" +
321+
"services:\n" +
322+
" Foo\\Car:\n" +
323+
" arguments:\n" +
324+
" $myDateT<caret>ime\n",
325+
"$myDateTime: '@foo'"
326+
);
327+
328+
assertCompletionNotContains(YAMLFileType.YML, "" +
329+
"services:\n" +
330+
" Foo\\Car:\n" +
331+
" arguments:\n" +
332+
" $myDateT<caret>ime: ~\n",
333+
"$myDateTime: '@foo'"
334+
);
335+
336+
assertCompletionContains(YAMLFileType.YML, "" +
337+
"services:\n" +
338+
" Foo\\Car:\n" +
339+
" arguments:\n" +
340+
" $foobarEn<caret>v\n",
341+
"$foobarEnv: '%env(FOOBAR_ENV)%'"
342+
);
343+
344+
assertCompletionContains(YAMLFileType.YML, "" +
345+
"services:\n" +
346+
" Foo\\Car:\n" +
347+
" arguments:\n" +
348+
" $project<caret>Dir\n",
349+
"$projectDir: '%kernel.project_dir%'"
350+
);
319351
}
320352
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/classes.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,11 @@ class Apple
1919
{
2020
function __construct($i, $z = null) { }
2121
}
22+
23+
class Car
24+
{
25+
function __construct(\string $projectDir, \string $foobarEnv, \MyDateTime $myDateTime)
26+
{
27+
}
28+
}
2229
}

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/services.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
<parameters>
55
<parameter key="foo_parameter">bar</parameter>
6+
<parameter key="kernel.project_dir">project_dir</parameter>
67
</parameters>
78

89
<services>

src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/dic/container/util/ServiceContainerUtilTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ public void testVisitNamedArguments() {
349349
);
350350

351351
Collection<String> arguments = new HashSet<>();
352-
ServiceContainerUtil.visitNamedArguments(psiFile, parameter -> arguments.add(parameter.getName()));
352+
ServiceContainerUtil.visitNamedArguments(psiFile, parameter -> arguments.add(parameter.getFirst().getName()));
353353

354354
assertTrue(arguments.contains("foobar"));
355355

0 commit comments

Comments
 (0)