package fr.adrienbrault.idea.symfony2plugin.config.yaml; import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.editor.Editor; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PsiElementPattern; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IElementType; import com.jetbrains.php.lang.psi.elements.Method; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.dic.container.util.DotEnvUtil; import fr.adrienbrault.idea.symfony2plugin.dic.container.util.ServiceContainerUtil; import fr.adrienbrault.idea.symfony2plugin.stubs.ContainerCollectionResolver; import fr.adrienbrault.idea.symfony2plugin.stubs.ServiceIndexUtil; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.dict.ServiceUtil; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLTokenTypes; import org.jetbrains.yaml.psi.YAMLKeyValue; import org.jetbrains.yaml.psi.YAMLMapping; import org.jetbrains.yaml.psi.YAMLScalar; import org.jetbrains.yaml.psi.YAMLSequenceItem; import java.util.*; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class YamlGoToDeclarationHandler implements GotoDeclarationHandler { @Nullable @Override public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int i, Editor editor) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return null; } List<PsiElement> psiElements = new ArrayList<>(); // yaml Plugin BC: "!php/const:" is a tag IElementType elementType = psiElement.getNode().getElementType(); if(elementType == YAMLTokenTypes.TAG) { String psiText = PsiElementUtils.getText(psiElement); if(psiText != null && psiText.length() > 0 && psiText.startsWith("!php/const:")) { psiElements.addAll(constantGoto(psiElement, psiText)); } } // only string values like "foo", foo if (elementType != YAMLTokenTypes.TEXT && elementType != YAMLTokenTypes.SCALAR_DSTRING && elementType != YAMLTokenTypes.SCALAR_STRING) { return psiElements.toArray(new PsiElement[psiElements.size()]); } String psiText = PsiElementUtils.getText(psiElement); if(null == psiText || psiText.length() == 0) { return psiElements.toArray(new PsiElement[psiElements.size()]); } if(psiText.startsWith("@") && psiText.length() > 1) { psiElements.addAll(Arrays.asList((serviceGoToDeclaration(psiElement, psiText.substring(1))))); } // match: %annotations.reader.class% if(psiText.length() > 3 && psiText.startsWith("%") && psiText.endsWith("%")) { psiElements.addAll(Arrays.asList((parameterGoToDeclaration(psiElement, psiText)))); } if(psiText.startsWith("!php/const:")) { psiElements.addAll(constantGoto(psiElement, psiText)); } if(psiText.contains("\\")) { psiElements.addAll(classGoToDeclaration(psiElement, psiText)) ; } if(psiText.endsWith(".twig") || psiText.endsWith(".php")) { psiElements.addAll(templateGoto(psiElement, psiText)); } if(psiText.matches("^[\\w_.]+") && getGlobalServiceStringPattern().accepts(psiElement)) { psiElements.addAll(Arrays.asList((serviceGoToDeclaration(psiElement, psiText)))); } return psiElements.toArray(new PsiElement[psiElements.size()]); } private Collection<PsiElement> classGoToDeclaration(PsiElement psiElement, String className) { Collection<PsiElement> psiElements = new HashSet<>(); // Class::method // Class::FooAction // Class:Foo if(className.contains(":")) { String[] split = className.replaceAll("(:)\\1", "$1").split(":"); if(split.length == 2) { for(String append: new String[] {"", "Action"}) { Method classMethod = PhpElementsUtil.getClassMethod(psiElement.getProject(), split[0], split[1] + append); if(classMethod != null) { psiElements.add(classMethod); } } } return psiElements; } // ClassName psiElements.addAll(PhpElementsUtil.getClassesInterface(psiElement.getProject(), className)); return psiElements; } private PsiElement[] serviceGoToDeclaration(PsiElement psiElement, String serviceId) { serviceId = YamlHelper.trimSpecialSyntaxServiceName(serviceId).toLowerCase(); String serviceClass = ContainerCollectionResolver.resolveService(psiElement.getProject(), serviceId); if (serviceClass != null) { PsiElement[] targetElements = PhpElementsUtil.getClassInterfacePsiElements(psiElement.getProject(), serviceClass); if(targetElements.length > 0) { return targetElements; } } // get container target on indexes List<PsiElement> possibleServiceTargets = ServiceIndexUtil.findServiceDefinitions(psiElement.getProject(), serviceId); return possibleServiceTargets.toArray(new PsiElement[possibleServiceTargets.size()]); } private PsiElement[] parameterGoToDeclaration(@NotNull PsiElement psiElement, @NotNull String psiParameterName) { Collection<PsiElement> targets = new ArrayList<>(); targets.addAll( DotEnvUtil.getEnvironmentVariableTargetsForParameter(psiElement.getProject(), psiParameterName) ); if(!YamlHelper.isValidParameterName(psiParameterName)) { return targets.toArray(new PsiElement[targets.size()]); } targets.addAll(ServiceUtil.getServiceClassTargets(psiElement.getProject(), psiParameterName)); return targets.toArray(new PsiElement[targets.size()]); } private List<PsiFile> templateGoto(PsiElement psiElement, String templateName) { return Arrays.asList(TwigHelper.getTemplatePsiElements(psiElement.getProject(), templateName)); } private Collection<PsiElement> constantGoto(@NotNull PsiElement psiElement, @NotNull String tagName) { String constantName = tagName.substring(11); if(StringUtils.isBlank(constantName)) { return Collections.emptyList(); } return ServiceContainerUtil.getTargetsForConstant(psiElement.getProject(), constantName); } @Nullable @Override public String getActionText(DataContext dataContext) { return null; } /** * foo: b<caret>ar * foo: [ b<caret>ar ] * foo: { b<caret>ar } * foo: * - b<caret>ar */ private PsiElementPattern.Capture<PsiElement> getGlobalServiceStringPattern() { return PlatformPatterns.psiElement().withParent( PlatformPatterns.psiElement(YAMLScalar.class).withParent(PlatformPatterns.or( PlatformPatterns.psiElement(YAMLKeyValue.class), PlatformPatterns.psiElement(YAMLSequenceItem.class), PlatformPatterns.psiElement(YAMLMapping.class) ))); } }