package fr.adrienbrault.idea.symfony2plugin.templating; import com.intellij.codeInsight.daemon.LineMarkerInfo; import com.intellij.codeInsight.daemon.LineMarkerProvider; import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo; import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder; import com.intellij.ide.util.PsiElementListCellRenderer; import com.intellij.navigation.GotoRelatedItem; import com.intellij.openapi.editor.markup.GutterIconRenderer; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NotNullLazyValue; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.presentation.java.SymbolPresentationUtil; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ConstantFunction; import com.intellij.util.indexing.FileBasedIndex; import com.jetbrains.php.PhpIcons; import com.jetbrains.php.lang.psi.elements.Function; import com.jetbrains.php.lang.psi.elements.Method; import com.jetbrains.twig.TwigFile; import com.jetbrains.twig.TwigFileType; import com.jetbrains.twig.elements.TwigElementTypes; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.dic.RelatedPopupGotoLineMarker; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigIncludeStubIndex; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TemplateFileMap; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import icons.TwigIcons; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class TwigLineMarkerProvider implements LineMarkerProvider { private TemplateFileMap templateMapCache = null; @Override public void collectSlowLineMarkers(@NotNull List<PsiElement> psiElements, @NotNull Collection<LineMarkerInfo> results) { if(psiElements.size() == 0 || !Symfony2ProjectComponent.isEnabled(psiElements.get(0))) { return; } for(PsiElement psiElement: psiElements) { // blocks if (TwigHelper.getBlockTagPattern().accepts(psiElement)) { LineMarkerInfo lineImpl = this.attachBlockImplements(psiElement); if(lineImpl != null) { results.add(lineImpl); } LineMarkerInfo lineOverwrites = this.attachBlockOverwrites(psiElement); if(lineOverwrites != null) { results.add(lineOverwrites); } } // controller if(psiElement instanceof TwigFile) { attachController((TwigFile) psiElement, results); // find foreign file references tags like: // include, embed, source, from, import, ... LineMarkerInfo lineIncludes = attachIncludes((TwigFile) psiElement); if(lineIncludes != null) { results.add(lineIncludes); } // eg bundle overwrites LineMarkerInfo overwrites = attachOverwrites((TwigFile) psiElement); if(overwrites != null) { results.add(overwrites); } } } // reset cache templateMapCache = null; } private void attachController(@NotNull TwigFile twigFile, @NotNull Collection<? super RelatedItemLineMarkerInfo> result) { Set<Function> methods = new HashSet<>(); Method method = TwigUtil.findTwigFileController(twigFile); if(method != null) { methods.add(method); } methods.addAll(TwigUtil.getTwigFileMethodUsageOnIndex(twigFile)); if(methods.size() == 0) { return; } NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(Symfony2Icons.TWIG_CONTROLLER_LINE_MARKER). setTargets(methods). setTooltipText("Navigate to controller"); result.add(builder.createLineMarkerInfo(twigFile)); } private LineMarkerInfo attachIncludes(@NotNull TwigFile twigFile) { TemplateFileMap files = getTemplateFilesByName(twigFile.getProject()); Set<String> templateNames = TwigUtil.getTemplateName(twigFile.getVirtualFile(), files); boolean found = false; for(String templateName: templateNames) { Project project = twigFile.getProject(); Collection<VirtualFile> containingFiles = FileBasedIndex.getInstance().getContainingFiles( TwigIncludeStubIndex.KEY, templateName, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), TwigFileType.INSTANCE) ); // stop on first target, we load them lazily afterwards if(containingFiles.size() > 0) { found = true; break; } } if(!found) { return null; } NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTED) .setTargets(new MyTemplateIncludeLazyValue(twigFile, templateNames)) .setTooltipText("Navigate to includes") .setCellRenderer(new MyFileReferencePsiElementListCellRenderer()); return builder.createLineMarkerInfo(twigFile); } @Nullable private LineMarkerInfo attachOverwrites(@NotNull TwigFile twigFile) { Collection<PsiFile> targets = new ArrayList<>(); TemplateFileMap files = getTemplateFilesByName(twigFile.getProject()); for (String templateName: TwigUtil.getTemplateName(twigFile.getVirtualFile(), files)) { for (PsiFile psiFile : TwigHelper.getTemplatePsiElements(twigFile.getProject(), templateName)) { if(!psiFile.getVirtualFile().equals(twigFile.getVirtualFile()) && !targets.contains(psiFile)) { targets.add(psiFile); } } } if(targets.size() == 0) { return null; } List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); for(PsiElement blockTag: targets) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem( blockTag, TwigUtil.getPresentableTemplateName(files.getTemplates(), blockTag, true) ).withIcon(TwigIcons.TwigFileIcon, Symfony2Icons.TWIG_LINE_OVERWRITE)); } return getRelatedPopover("Overwrites", "Overwrite", twigFile, gotoRelatedItems, Symfony2Icons.TWIG_LINE_OVERWRITE); } private TemplateFileMap getTemplateFilesByName(Project project) { return this.templateMapCache == null ? this.templateMapCache = TwigHelper.getTemplateMap(project, true, false) : this.templateMapCache; } private LineMarkerInfo getRelatedPopover(String singleItemTitle, String singleItemTooltipPrefix, PsiElement lineMarkerTarget, List<GotoRelatedItem> gotoRelatedItems) { return getRelatedPopover(singleItemTitle, singleItemTooltipPrefix, lineMarkerTarget, gotoRelatedItems, PhpIcons.IMPLEMENTED); } private LineMarkerInfo getRelatedPopover(String singleItemTitle, String singleItemTooltipPrefix, PsiElement lineMarkerTarget, List<GotoRelatedItem> gotoRelatedItems, Icon icon) { // single item has no popup String title = singleItemTitle; if(gotoRelatedItems.size() == 1) { String customName = gotoRelatedItems.get(0).getCustomName(); if(customName != null) { title = String.format(singleItemTooltipPrefix, customName); } } return new LineMarkerInfo<>( lineMarkerTarget, lineMarkerTarget.getTextRange(), icon, 6, new ConstantFunction<>(title), new RelatedPopupGotoLineMarker.NavigationHandler(gotoRelatedItems), GutterIconRenderer.Alignment.RIGHT ); } @Nullable private LineMarkerInfo attachBlockImplements(final PsiElement psiElement) { PsiFile psiFile = psiElement.getContainingFile(); if(psiFile == null) { return null; } TemplateFileMap files = getTemplateFilesByName(psiElement.getProject()); Collection<PsiFile> twigChild = TwigUtil.getTemplateFileReferences(psiFile, files); if(twigChild.size() == 0) { return null; } final String blockName = psiElement.getText(); List<PsiElement> blockTargets = new ArrayList<>(); for(PsiFile psiFile1: twigChild) { blockTargets.addAll(Arrays.asList(PsiTreeUtil.collectElements(psiFile1, psiElement1 -> TwigHelper.getBlockTagPattern().accepts(psiElement1) && blockName.equals(psiElement1.getText()))) ); } if(blockTargets.size() == 0) { return null; } List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); for(PsiElement blockTag: blockTargets) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(blockTag, TwigUtil.getPresentableTemplateName(files.getTemplates(), blockTag, true)).withIcon(TwigIcons.TwigFileIcon, Symfony2Icons.TWIG_LINE_MARKER)); } return getRelatedPopover("Implementations", "Impl: ", psiElement, gotoRelatedItems); } @Nullable private LineMarkerInfo attachBlockOverwrites(PsiElement psiElement) { PsiElement[] blocks = TwigTemplateGoToDeclarationHandler.getBlockGoTo(psiElement); if(blocks.length == 0) { return null; } List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); for(PsiElement blockTag: blocks) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(blockTag, TwigUtil.getPresentableTemplateName(getTemplateFilesByName(psiElement.getProject()).getTemplates(), blockTag, true)).withIcon(TwigIcons.TwigFileIcon, Symfony2Icons.TWIG_LINE_MARKER)); } // single item has no popup String title = "Overwrites"; if(gotoRelatedItems.size() == 1) { String customName = gotoRelatedItems.get(0).getCustomName(); if(customName != null) { title = title.concat(": ").concat(customName); } } return new LineMarkerInfo<>( psiElement, psiElement.getTextRange(), PhpIcons.OVERRIDES, 6, new ConstantFunction<>(title), new RelatedPopupGotoLineMarker.NavigationHandler(gotoRelatedItems), GutterIconRenderer.Alignment.RIGHT ); } @Nullable @Override public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) { return null; } private static class MyFileReferencePsiElementListCellRenderer extends PsiElementListCellRenderer { @Override public String getElementText(PsiElement psiElement) { String symbolPresentableText = SymbolPresentationUtil.getSymbolPresentableText(psiElement); return StringUtils.abbreviate(symbolPresentableText, 50); } @Nullable @Override protected String getContainerText(PsiElement psiElement, String s) { // relative path else fallback to default name extraction PsiFile containingFile = psiElement.getContainingFile(); String relativePath = VfsUtil.getRelativePath(containingFile.getVirtualFile(), psiElement.getProject().getBaseDir(), '/'); return relativePath != null ? relativePath : SymbolPresentationUtil.getSymbolContainerText(psiElement); } @Override protected int getIconFlags() { return 1; } @Override protected Icon getIcon(PsiElement psiElement) { if(psiElement.getNode().getElementType() == TwigElementTypes.INCLUDE_TAG) { return PhpIcons.IMPLEMENTED; } else if(psiElement.getNode().getElementType() == TwigElementTypes.EMBED_TAG) { return PhpIcons.OVERRIDEN; } return TwigIcons.TwigFileIcon; } } private static class MyTemplateIncludeLazyValue extends NotNullLazyValue<Collection<? extends PsiElement>> { @NotNull private final TwigFile twigFile; @NotNull private final Collection<String> templateNames; MyTemplateIncludeLazyValue(@NotNull TwigFile twigFile, @NotNull Collection<String> templateNames) { this.twigFile = twigFile; this.templateNames = templateNames; } @NotNull @Override protected Collection<? extends PsiElement> compute() { Collection<VirtualFile> twigFiles = new ArrayList<>(); Project project = twigFile.getProject(); for(String templateName: this.templateNames) { // collect files which contains given template name for inclusion twigFiles.addAll(FileBasedIndex.getInstance().getContainingFiles( TwigIncludeStubIndex.KEY, templateName, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), TwigFileType.INSTANCE)) ); } Collection<PsiElement> targets = new ArrayList<>(); for (VirtualFile virtualFile : twigFiles) { // resolve virtual file PsiFile myTwigFile = PsiManager.getInstance(project).findFile(virtualFile); if(!(myTwigFile instanceof TwigFile)) { continue; } Collection<PsiElement> fileTargets = new ArrayList<>(); TwigUtil.visitTemplateIncludes((TwigFile) myTwigFile, templateInclude -> { if(this.templateNames.contains(templateInclude.getTemplateName()) || this.templateNames.contains(TwigHelper.normalizeTemplateName(templateInclude.getTemplateName()))) { fileTargets.add(templateInclude.getPsiElement()); } } ); // navigate to include pattern; else fallback to file scope if(fileTargets.size() > 0) { targets.addAll(fileTargets); } else { targets.add(myTwigFile); } } return targets; } } }