package fr.adrienbrault.idea.symfony2plugin.templating; import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.Field; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.twig.TwigLanguage; import com.jetbrains.twig.TwigTokenTypes; import com.jetbrains.twig.elements.TwigBlockTag; import com.jetbrains.twig.elements.TwigElementTypes; import com.jetbrains.twig.elements.TwigTagWithFileReference; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigExtension; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigMacro; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigSet; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigExtensionParser; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.templating.variable.collector.ControllerDocVariableCollector; import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil; import fr.adrienbrault.idea.symfony2plugin.util.RegexPsiElementFilter; import fr.adrienbrault.idea.symfony2plugin.util.controller.ControllerIndex; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class TwigTemplateGoToLocalDeclarationHandler implements GotoDeclarationHandler { @Nullable @Override public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int i, Editor editor) { if(!Symfony2ProjectComponent.isEnabled(psiElement)) { return null; } List<PsiElement> psiElements = new ArrayList<>(); // {{ goto_me() }} if (TwigHelper.getPrintBlockFunctionPattern().accepts(psiElement)) { psiElements.addAll(this.getMacros(psiElement)); } // {% from 'boo.html.twig' import goto_me %} if (TwigHelper.getTemplateImportFileReferenceTagPattern().accepts(psiElement)) { psiElements.addAll(this.getMacros(psiElement)); } // {% set foo %} // {% set foo = bar %} if (PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent( PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK) ).withLanguage(TwigLanguage.INSTANCE).accepts(psiElement)) { psiElements.addAll(Arrays.asList(this.getSets(psiElement))); } // {{ function( }} // {{ function }} if (PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent(PlatformPatterns.or( PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK), PlatformPatterns.psiElement(TwigElementTypes.SET_TAG) )).withLanguage(TwigLanguage.INSTANCE).accepts(psiElement)) { psiElements.addAll(Arrays.asList(this.getFunctions(psiElement))); } // {{ foo.fo<caret>o }} if(TwigHelper.getTypeCompletionPattern().accepts(psiElement) || TwigHelper.getPrintBlockFunctionPattern().accepts(psiElement) || TwigHelper.getVariableTypePattern().accepts(psiElement)) { psiElements.addAll(Arrays.asList(this.getTypeGoto(psiElement))); } if(TwigHelper.getTwigDocBlockMatchPattern(ControllerDocVariableCollector.DOC_PATTERN).accepts(psiElement)) { psiElements.addAll(Arrays.asList(this.getControllerNameGoto(psiElement))); } // {{ parent() }} if(TwigHelper.getParentFunctionPattern().accepts(psiElement)) { psiElements.addAll(Arrays.asList(this.getParentGoto(psiElement))); } // constant('Post::PUBLISHED') if(TwigHelper.getPrintBlockOrTagFunctionPattern("constant").accepts(psiElement)) { psiElements.addAll(this.getConstantGoto(psiElement)); } // {# @var user \Foo #} if(TwigHelper.getTwigTypeDocBlock().accepts(psiElement)) { psiElements.addAll(this.getVarClassGoto(psiElement)); } // {# @see Foo.html.twig #} // {# @see \Class #} if(TwigHelper.getTwigDocSeePattern().accepts(psiElement)) { psiElements.addAll(this.getSeeDocTagTargets(psiElement)); } return psiElements.toArray(new PsiElement[psiElements.size()]); } private Collection<PsiElement> getConstantGoto(PsiElement psiElement) { Collection<PsiElement> targetPsiElements = new ArrayList<>(); String contents = psiElement.getText(); if(StringUtils.isBlank(contents)) { return targetPsiElements; } // global constant if(!contents.contains(":")) { targetPsiElements.addAll(PhpIndex.getInstance(psiElement.getProject()).getConstantsByName(contents)); return targetPsiElements; } // resolve class constants String[] parts = contents.split("::"); if(parts.length != 2) { return targetPsiElements; } PhpClass phpClass = PhpElementsUtil.getClassInterface(psiElement.getProject(), parts[0].replace("\\\\", "\\")); if(phpClass == null) { return targetPsiElements; } Field field = phpClass.findFieldByName(parts[1], true); if(field != null) { targetPsiElements.add(field); } return targetPsiElements; } private Collection<PhpClass> getVarClassGoto(PsiElement psiElement) { String comment = psiElement.getText(); if(StringUtils.isBlank(comment)) { return Collections.emptyList(); } for(String pattern: new String[] {TwigTypeResolveUtil.DEPRECATED_DOC_TYPE_PATTERN, TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE}) { Matcher matcher = Pattern.compile(pattern).matcher(comment); if (matcher.find()) { String className = matcher.group(2); if(StringUtils.isNotBlank(className)) { return PhpElementsUtil.getClassesInterface(psiElement.getProject(), className); } } } return Collections.emptyList(); } @NotNull private Collection<PsiElement> getSeeDocTagTargets(@NotNull PsiElement psiElement) { String comment = psiElement.getText(); if(StringUtils.isBlank(comment)) { return Collections.emptyList(); } Collection<PsiElement> psiElements = new ArrayList<>(); for(String pattern: new String[] {TwigHelper.DOC_SEE_REGEX, TwigHelper.DOC_SEE_REGEX_WITHOUT_SEE}) { Matcher matcher = Pattern.compile(pattern).matcher(comment); if (!matcher.find()) { continue; } String content = matcher.group(1); if(content.toLowerCase().endsWith(".twig")) { ContainerUtil.addAll(psiElements, TwigHelper.getTemplatePsiElements(psiElement.getProject(), content)); } psiElements.addAll(PhpElementsUtil.getClassesInterface(psiElement.getProject(), content)); ContainerUtil.addIfNotNull(psiElements, ControllerIndex.getControllerMethod(psiElement.getProject(), content)); PsiDirectory parent = psiElement.getContainingFile().getParent(); if(parent != null) { VirtualFile relativeFile = VfsUtil.findRelativeFile(parent.getVirtualFile(), content.replace("\\", "/").split("/")); if(relativeFile != null) { ContainerUtil.addIfNotNull(psiElements, PsiManager.getInstance(psiElement.getProject()).findFile(relativeFile)); } } Matcher methodMatcher = Pattern.compile("([\\w\\\\-]+):+([\\w_\\-]+)").matcher(content); if (methodMatcher.find()) { for (PhpClass phpClass : PhpIndex.getInstance(psiElement.getProject()).getAnyByFQN(methodMatcher.group(1))) { ContainerUtil.addIfNotNull(psiElements, phpClass.findMethodByName(methodMatcher.group(2))); } } } return psiElements; } private PsiElement[] getTypeGoto(PsiElement psiElement) { List<PsiElement> targetPsiElements = new ArrayList<>(); // class, class.method, class.method.method // click on first item is our class name String[] beforeLeaf = TwigTypeResolveUtil.formatPsiTypeName(psiElement); if(beforeLeaf.length == 0) { Collection<TwigTypeContainer> twigTypeContainers = TwigTypeResolveUtil.resolveTwigMethodName(psiElement, TwigTypeResolveUtil.formatPsiTypeName(psiElement, true)); for(TwigTypeContainer twigTypeContainer: twigTypeContainers) { if(twigTypeContainer.getPhpNamedElement() != null) { targetPsiElements.add(twigTypeContainer.getPhpNamedElement()); } } } else { Collection<TwigTypeContainer> types = TwigTypeResolveUtil.resolveTwigMethodName(psiElement, beforeLeaf); String text = psiElement.getText(); if(StringUtils.isNotBlank(text)) { // provide method / field goto for(TwigTypeContainer twigTypeContainer: types) { if(twigTypeContainer.getPhpNamedElement() != null) { targetPsiElements.addAll(TwigTypeResolveUtil.getTwigPhpNameTargets(twigTypeContainer.getPhpNamedElement(), text)); } } } } return targetPsiElements.toArray(new PsiElement[targetPsiElements.size()]); } private PsiElement[] getFunctions(PsiElement psiElement) { Map<String, TwigExtension> functions = new TwigExtensionParser(psiElement.getProject()).getFunctions(); String funcName = psiElement.getText(); if(!functions.containsKey(funcName)) { return new PsiElement[0]; } return PhpElementsUtil.getPsiElementsBySignature(psiElement.getProject(), functions.get(funcName).getSignature()); } private PsiElement[] getSets(PsiElement psiElement) { String funcName = psiElement.getText(); for(TwigSet twigSet: TwigUtil.getSetDeclaration(psiElement.getContainingFile())) { if(twigSet.getName().equals(funcName)) { return PsiTreeUtil.collectElements(psiElement.getContainingFile(), new RegexPsiElementFilter( TwigTagWithFileReference.class, "\\{%\\s?set\\s?" + Pattern.quote(funcName) + "\\s?.*") ); } } return new PsiElement[0]; } @NotNull private Collection<PsiElement> getMacros(@NotNull PsiElement psiElement) { String funcName = psiElement.getText(); // check for complete file as namespace import {% import "file" as foo %} // {% import _self as foobar %} // {{ foobar.bar }} PsiElement prevSibling = psiElement.getPrevSibling(); if(prevSibling != null && prevSibling.getNode().getElementType() == TwigTokenTypes.DOT) { PsiElement identifier = prevSibling.getPrevSibling(); if(identifier == null || identifier.getNode().getElementType() != TwigTokenTypes.IDENTIFIER) { return Collections.emptyList(); } return TwigUtil.getImportedMacrosNamespaces( psiElement.getContainingFile(), identifier.getText() + "." + funcName ); } // {% from _self import foobar as input, foobar %} return TwigUtil.getImportedMacros(psiElement.getContainingFile(), funcName); } private PsiElement[] getControllerNameGoto(PsiElement psiElement) { Pattern pattern = Pattern.compile(ControllerDocVariableCollector.DOC_PATTERN); Matcher matcher = pattern.matcher(psiElement.getText()); if (!matcher.find()) { return new PsiElement[0]; } String controllerName = matcher.group(1); Method method = ControllerIndex.getControllerMethod(psiElement.getProject(), controllerName); if(method == null) { return new PsiElement[0]; } return new PsiElement[] { method }; } private PsiElement[] getParentGoto(PsiElement psiElement) { // find printblock PsiElement printBlock = psiElement.getParent(); if(printBlock == null || !PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK).accepts(printBlock)) { return new PsiElement[0]; } // printblock need to be child block statement PsiElement blockStatement = printBlock.getParent(); if(blockStatement == null || !PlatformPatterns.psiElement(TwigElementTypes.BLOCK_STATEMENT).accepts(blockStatement)) { return new PsiElement[0]; } // BlockTag is first child of block statement PsiElement blockTag = blockStatement.getFirstChild(); if(!(blockTag instanceof TwigBlockTag)) { return new PsiElement[0]; } String blockName = ((TwigBlockTag) blockTag).getName(); return TwigTemplateGoToDeclarationHandler.getBlockNameGoTo(psiElement.getContainingFile(), blockName); } @Nullable @Override public String getActionText(DataContext dataContext) { return null; } }