diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/ServiceYamlReference.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/ServiceYamlReference.java new file mode 100644 index 000000000..1062d0169 --- /dev/null +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/ServiceYamlReference.java @@ -0,0 +1,21 @@ +package fr.adrienbrault.idea.symfony2plugin.config.yaml; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import fr.adrienbrault.idea.symfony2plugin.dic.AbstractServiceReference; +import org.jetbrains.annotations.NotNull; + +public class ServiceYamlReference extends AbstractServiceReference { + + public ServiceYamlReference(@NotNull PsiElement psiElement, @NotNull String serviceId) { + super(psiElement); + + this.serviceId = serviceId; + } + + public ServiceYamlReference(@NotNull PsiElement psiElement, @NotNull TextRange range, @NotNull String serviceId) { + super(psiElement, range); + + this.serviceId = serviceId; + } +} diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlReferenceContributor.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlReferenceContributor.java index 8da6fa869..ef44dbe41 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlReferenceContributor.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/config/yaml/YamlReferenceContributor.java @@ -1,12 +1,14 @@ package fr.adrienbrault.idea.symfony2plugin.config.yaml; +import com.intellij.openapi.util.TextRange; +import com.intellij.patterns.PatternCondition; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.StandardPatterns; import com.intellij.psi.*; import com.intellij.util.ProcessingContext; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import org.jetbrains.annotations.NotNull; -import org.jetbrains.yaml.psi.YAMLScalar; +import org.jetbrains.yaml.psi.*; public class YamlReferenceContributor extends PsiReferenceContributor { private static final String TAG_PHP_CONST = "!php/const"; @@ -26,7 +28,7 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) return PsiReference.EMPTY_ARRAY; } - var scalar = (YAMLScalar)element; + var scalar = (YAMLScalar) element; if (scalar.getTextValue().isEmpty()) { return PsiReference.EMPTY_ARRAY; } @@ -37,5 +39,245 @@ public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) } } ); + + // services: + // app.service.foo: + // arguments: + // - '@app.service.bar' + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequenceItem.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequence.class) + .withParent( + PlatformPatterns.psiElement(YAMLKeyValue.class).withName("arguments") + ) + ) + ), + new YAMLScalarServiceReferenceProvider() + ); + + // services: + // app.service.foo: + // arguments: + // $bar: '@app.service.bar' + + // services: + // app.service.foo: + // properties: + // bar: '@app.service.bar' + + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withParent( + PlatformPatterns + .psiElement(YAMLMapping.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withName("arguments", "properties") + ) + ) + ), + new YAMLScalarServiceReferenceProvider() + ); + + // services: + // app.service.foo: + // alias: app.service.bar + + // services: + // app.service.foo_decorator: + // decorates: app.service.foo + + // services: + // app.service.foo: + // parent: app.service.foo_parent + + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withName("alias", "decorates", "parent") + ), + new YAMLScalarServiceReferenceProvider(false) + ); + + // services: + // app.service.foo: + // configurator: @app.service.foo_configurator + + // services: + // app.service.foo: + // factory: @app.service.foo_factory + + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withName("configurator", "factory") + ), + new YAMLScalarServiceReferenceProvider() + ); + + // services: + // app.service.foo: + // factory: ['@app.service.foo_factory', 'create'] + + // services: + // app.service.foo: + // configurator: ['@app.service.foo_configurator', 'configure'] + + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequenceItem.class) + .with(new PatternCondition<>("is first sequence item") { + @Override + public boolean accepts(@NotNull YAMLSequenceItem element, ProcessingContext context) { + return element.getItemIndex() == 0; + } + }) + .withParent( + PlatformPatterns + .psiElement(YAMLSequence.class) + .withParent( + PlatformPatterns.psiElement(YAMLKeyValue.class).withName("factory", "configurator") + ) + ) + ), + new YAMLScalarServiceReferenceProvider() + ); + + // services: + // app.service.foo: '@app.service.bar' + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withParent( + PlatformPatterns + .psiElement(YAMLMapping.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withName("services") + ) + ) + ), + new YAMLScalarServiceReferenceProvider() + ); + + // services: + // app.service.foo: + // calls: + // - setBar: [ '@app.service.bar' ] + registrar.registerReferenceProvider( + PlatformPatterns + .psiElement(YAMLScalar.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequenceItem.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequence.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withParent( + PlatformPatterns + .psiElement(YAMLMapping.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequenceItem.class) + .withParent( + PlatformPatterns + .psiElement(YAMLSequence.class) + .withParent( + PlatformPatterns + .psiElement(YAMLKeyValue.class) + .withName("calls") + ) + ) + ) + ) + + ) + ) + ), + new YAMLScalarServiceReferenceProvider() + ); + } + + private static class YAMLScalarServiceReferenceProvider extends PsiReferenceProvider { + + private static final String PREFIX = "@"; + private static final String ESCAPED_PREFIX = "@@"; + + /** + * Flag indicating whenever YAMLScalar value start with `@` prefix + */ + private boolean isPrefixed = true; + + public YAMLScalarServiceReferenceProvider() { + } + + public YAMLScalarServiceReferenceProvider(boolean isPrefixed) { + this.isPrefixed = isPrefixed; + } + + @Override + public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + if (!Symfony2ProjectComponent.isEnabled(element)) { + return PsiReference.EMPTY_ARRAY; + } + + if (element instanceof YAMLScalar) { + var serviceName = ((YAMLScalar) element).getTextValue(); + if (serviceName.isEmpty()) { + return PsiReference.EMPTY_ARRAY; + } + + if (!isPrefixed) { + return new PsiReference[]{ + new ServiceYamlReference(element, serviceName) + }; + } + + if (isValidServiceNameWithPrefix(serviceName)) { + var range = TextRange.from(serviceName.indexOf(PREFIX) + 1, serviceName.length() - 1); + if (element instanceof YAMLQuotedText) { + // Skip quotes + range = range.shiftRight(1); + } + + return new PsiReference[]{ + new ServiceYamlReference(element, range, serviceName.substring(1)) + }; + } + } + + return PsiReference.EMPTY_ARRAY; + } + + private boolean isValidServiceNameWithPrefix(@NotNull String serviceName) { + return serviceName.length() > 1 && serviceName.startsWith(PREFIX) && !serviceName.startsWith(ESCAPED_PREFIX); + } } } diff --git a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/AbstractServiceReference.java b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/AbstractServiceReference.java index 8fbdb69cb..4650c6cfb 100644 --- a/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/AbstractServiceReference.java +++ b/src/main/java/fr/adrienbrault/idea/symfony2plugin/dic/AbstractServiceReference.java @@ -1,6 +1,7 @@ package fr.adrienbrault.idea.symfony2plugin.dic; import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementResolveResult; import com.intellij.psi.PsiPolyVariantReferenceBase; @@ -8,9 +9,11 @@ import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.PhpIndex; import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; +import fr.adrienbrault.idea.symfony2plugin.stubs.ServiceIndexUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -26,19 +29,34 @@ public AbstractServiceReference(PsiElement psiElement) { super(psiElement); } - @NotNull + public AbstractServiceReference(PsiElement psiElement, TextRange range) { + super(psiElement, range); + } + @Override - public ResolveResult[] multiResolve(boolean incompleteCode) { + public ResolveResult @NotNull [] multiResolve(boolean incompleteCode) { + var definitions = ServiceIndexUtil.findServiceDefinitions( + getElement().getProject(), + serviceId + ); + + // Return the PsiElement for the service definition corresponding to the serviceId + var results = new ArrayList(); + for (var definition : definitions) { + results.add(new PsiElementResolveResult(definition)); + } + ContainerCollectionResolver.ServiceCollector collector = ContainerCollectionResolver .ServiceCollector.create(getElement().getProject()); // Return the PsiElement for the class corresponding to the serviceId String serviceClass = collector.resolve(serviceId); - if (serviceClass == null) { - return new ResolveResult[0]; + if (serviceClass != null) { + var classes = PsiElementResolveResult.createResults(PhpIndex.getInstance(getElement().getProject()).getAnyByFQN(serviceClass)); + results.addAll(Arrays.asList(classes)); } - return PsiElementResolveResult.createResults(PhpIndex.getInstance(getElement().getProject()).getAnyByFQN(serviceClass)); + return results.toArray(ResolveResult.EMPTY_ARRAY); } @NotNull diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlReferenceContributorTest.java b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlReferenceContributorTest.java index 6d25947e6..e7f0610e8 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlReferenceContributorTest.java +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/YamlReferenceContributorTest.java @@ -1,10 +1,13 @@ package fr.adrienbrault.idea.symfony2plugin.tests.config.yaml; import com.intellij.patterns.PlatformPatterns; +import com.intellij.patterns.PsiElementPattern; import com.jetbrains.php.lang.psi.elements.Field; import com.jetbrains.php.lang.psi.elements.PhpDefine; import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase; +import org.jetbrains.annotations.NotNull; import org.jetbrains.yaml.YAMLFileType; +import org.jetbrains.yaml.psi.YAMLKeyValue; public class YamlReferenceContributorTest extends SymfonyLightCodeInsightFixtureTestCase { @@ -36,4 +39,153 @@ public void testConstantProvidesReferences() { PlatformPatterns.psiElement(Field.class).withName("FOO") ); } + + public void testArgumentsSequenceProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.bar:\n" + + " class: App\\BarService\n" + + "\n" + + " app.service.foo:\n" + + " class: App\\FooService\n" + + " arguments:\n"+ + " - '@app.service.bar'\n", + getExpectedReferencePattern("app.service.bar") + ); + } + + public void testArgumentsMapProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.bar:\n" + + " class: App\\BarService\n" + + "\n" + + " app.service.foo:\n" + + " class: App\\FooService\n" + + " arguments:\n"+ + " - '@app.service.bar'\n", + getExpectedReferencePattern("app.service.bar") + ); + } + + public void testPropertiesProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.bar:\n" + + " class: App\\BarService\n" + + "\n" + + " app.service.foo:\n" + + " class: App\\FooService\n" + + " properties:\n"+ + " bar: '@app.service.bar'\n", + getExpectedReferencePattern("app.service.bar") + ); + } + + public void testAliasProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.bar:\n" + + " class: App\\BarService\n" + + "\n" + + " app.service.foo:\n" + + " alias: app.service.bar\n", + getExpectedReferencePattern("app.service.bar") + ); + } + + public void testAliasShortcutProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.bar:\n" + + " class: App\\BarService\n" + + "\n" + + " app.service.foo: '@app.service.bar'\n", + getExpectedReferencePattern("app.service.bar") + ); + } + + public void testDecoratesProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo:\n" + + " class: App\\FooService\n" + + "\n" + + " app.service.foo_decorator:\n" + + " decorates: app.service.foo\n", + getExpectedReferencePattern("app.service.foo") + ); + } + + public void testConfiguratorInvokableProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo_configurator: ~\n" + + "\n" + + " app.service.foo:\n" + + " configurator: '@app.service.foo_configurator'\n", + getExpectedReferencePattern("app.service.foo_configurator") + ); + } + + public void testFactoryInvokableProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo_factory: ~\n" + + "\n" + + " app.service.foo:\n" + + " factory: '@app.service.foo_factory'\n", + getExpectedReferencePattern("app.service.foo_factory") + ); + } + + public void testFactoryProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo_factory: ~\n" + + "\n" + + " app.service.foo:\n" + + " factory: ['@app.service.foo_factory', 'create']\n", + getExpectedReferencePattern("app.service.foo_factory") + ); + } + + public void testConfiguratorProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo_configurator: ~\n" + + "\n" + + " app.service.foo:\n" + + " configurator: ['@app.service.foo_configurator', 'configure']\n", + getExpectedReferencePattern("app.service.foo_configurator") + ); + } + + public void testCallsArgsProvidesReferences() { + assertReferenceMatchOnParent( + YAMLFileType.YML, + "services:\n" + + " app.service.foo: ~\n" + + "\n" + + " app.service.bar:\n" + + " calls: " + + " - setFoo: ['@app.service.foo']\n", + getExpectedReferencePattern("app.service.foo") + ); + } + + @NotNull + private PsiElementPattern.Capture getExpectedReferencePattern(@NotNull String serviceName) { + return PlatformPatterns.psiElement(YAMLKeyValue.class).withName(serviceName); + } } diff --git a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/YamlReferenceContributor.php b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/YamlReferenceContributor.php index 435a4d6f6..d42cde3e6 100644 --- a/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/YamlReferenceContributor.php +++ b/src/test/java/fr/adrienbrault/idea/symfony2plugin/tests/config/yaml/fixtures/YamlReferenceContributor.php @@ -1,14 +1,71 @@ \n" + " parent: \n" , "data_collector.router" ); - - assertCompletionContains(YAMLFileType.YML, "services:\n" + - " newsletter_manager:\n" + - " parent: @\n" - , "data_collector.router" - ); - } public void testServiceStaticCompletion() {