package de.espend.idea.laravel.blade; import com.intellij.codeInsight.daemon.LineMarkerInfo; import com.intellij.codeInsight.daemon.LineMarkerProvider; import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder; 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.util.Pair; 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.impl.source.tree.LeafPsiElement; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ConstantFunction; import com.intellij.util.indexing.FileBasedIndex; import com.intellij.util.indexing.ID; import com.jetbrains.php.PhpIcons; import com.jetbrains.php.blade.BladeFileType; import com.jetbrains.php.blade.psi.BladePsiDirectiveParameter; import com.jetbrains.php.blade.psi.BladeTokenTypes; import de.espend.idea.laravel.LaravelIcons; import de.espend.idea.laravel.LaravelProjectComponent; import de.espend.idea.laravel.blade.util.BladePsiUtil; import de.espend.idea.laravel.blade.util.BladeTemplateUtil; import de.espend.idea.laravel.stub.*; import de.espend.idea.laravel.util.PsiElementUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.stream.Collectors; /** * @author Daniel Espendiller <[email protected]> */ public class TemplateLineMarker implements LineMarkerProvider { @Nullable @Override public LineMarkerInfo getLineMarkerInfo(@NotNull PsiElement psiElement) { return null; } @Override public void collectSlowLineMarkers(@NotNull List<PsiElement> psiElements, @NotNull Collection<LineMarkerInfo> lineMarkers) { // we need project element; so get it from first item if(psiElements.size() == 0) { return; } Project project = psiElements.get(0).getProject(); if(!LaravelProjectComponent.isEnabled(project)) { return; } LazyVirtualFileTemplateResolver resolver = null; for(PsiElement psiElement: psiElements) { if(psiElement instanceof PsiFile) { // template file like rendering: like "extends" path if (resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectTemplateFileRelatedFiles((PsiFile) psiElement, resolver)); } else if(psiElement instanceof LeafPsiElement) { if (psiElement.getNode().getElementType() == BladeTokenTypes.SECTION_DIRECTIVE) { // @section() Pair<BladePsiDirectiveParameter, String> section = extractSectionParameter(psiElement); if(section != null) { if(resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectOverwrittenSection((LeafPsiElement) psiElement, section.getSecond(), resolver)); lineMarkers.addAll(collectImplementsSection((LeafPsiElement) psiElement, section.getSecond(), resolver)); } } else if(psiElement.getNode().getElementType() == BladeTokenTypes.YIELD_DIRECTIVE) { // @yield() Pair<BladePsiDirectiveParameter, String> section = extractSectionParameter(psiElement); if(section != null) { if(resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectImplementsSection((LeafPsiElement) psiElement, section.getSecond(), resolver)); } } else if(psiElement.getNode().getElementType() == BladeTokenTypes.STACK_DIRECTIVE) { // @stack() Pair<BladePsiDirectiveParameter, String> section = extractSectionParameter(psiElement); if(section != null) { if(resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectStackImplements((LeafPsiElement) psiElement, section.getSecond(), resolver)); } } else if(psiElement.getNode().getElementType() == BladeTokenTypes.PUSH_DIRECTIVE) { // @push('scripts') Pair<BladePsiDirectiveParameter, String> section = extractSectionParameter(psiElement); if(section != null) { if(resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectPushOverwrites((LeafPsiElement) psiElement, section.getSecond())); } } else if(psiElement.getNode().getElementType() == BladeTokenTypes.SLOT_DIRECTIVE) { // @slot('foobar') Pair<BladePsiDirectiveParameter, String> section = extractSectionParameter(psiElement); if (section != null) { if (resolver == null) { resolver = new LazyVirtualFileTemplateResolver(); } lineMarkers.addAll(collectSlotOverwrites((LeafPsiElement) psiElement, section.getFirst(), section.getSecond(), resolver)); } } } } } /** * Extract parameter: @foobar('my_value') */ @Nullable private Pair<BladePsiDirectiveParameter, String> extractSectionParameter(@NotNull PsiElement psiElement) { PsiElement nextSibling = psiElement.getNextSibling(); if(nextSibling instanceof BladePsiDirectiveParameter) { String sectionName = BladePsiUtil.getSection(nextSibling); if (sectionName != null && StringUtils.isNotBlank(sectionName)) { return Pair.create((BladePsiDirectiveParameter) nextSibling, sectionName); } } return null; } /** * Like this @section('sidebar') */ @NotNull private Collection<LineMarkerInfo> collectOverwrittenSection(@NotNull LeafPsiElement psiElement, @NotNull String sectionName, @NotNull LazyVirtualFileTemplateResolver resolver) { List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); for(PsiElement psiElement1 : psiElement.getContainingFile().getChildren()) { PsiElement extendDirective = psiElement1.getFirstChild(); if(extendDirective != null && extendDirective.getNode().getElementType() == BladeTokenTypes.EXTENDS_DIRECTIVE) { PsiElement bladeParameter = extendDirective.getNextSibling(); if(bladeParameter instanceof BladePsiDirectiveParameter) { String extendTemplate = BladePsiUtil.getSection(bladeParameter); if(extendTemplate != null) { for(VirtualFile virtualFile: resolver.resolveTemplateName(psiElement.getProject(), extendTemplate)) { PsiFile psiFile = PsiManager.getInstance(psiElement.getProject()).findFile(virtualFile); if(psiFile != null) { visitOverwrittenTemplateFile(psiFile, gotoRelatedItems, sectionName, resolver); } } } } } } if(gotoRelatedItems.size() == 0) { return Collections.emptyList(); } return Collections.singletonList( getRelatedPopover("Parent Section", "Blade Section", psiElement, gotoRelatedItems, PhpIcons.OVERRIDES) ); } @NotNull private Collection<LineMarkerInfo> collectTemplateFileRelatedFiles(@NotNull PsiFile psiFile, @NotNull LazyVirtualFileTemplateResolver resolver) { Collection<String> collectedTemplates = resolver.resolveTemplateName(psiFile); if(collectedTemplates.size() == 0) { return Collections.emptyList(); } // lowercase for index Set<String> templateNames = new HashSet<>(); for (String templateName : collectedTemplates) { templateNames.add(templateName); templateNames.add(templateName.toLowerCase()); } // normalize all template names and support both: "foo.bar" and "foo/bar" templateNames.addAll(new HashSet<>(templateNames) .stream().map(templateName -> templateName.replace(".", "/")) .collect(Collectors.toList()) ); AtomicBoolean includeLineMarker = new AtomicBoolean(false); for(ID<String, Void> key : Arrays.asList(BladeExtendsStubIndex.KEY, BladeSectionStubIndex.KEY, BladeIncludeStubIndex.KEY, BladeEachStubIndex.KEY)) { for(String templateName: templateNames) { FileBasedIndex.getInstance().getFilesWithKey(key, Collections.singleton(templateName), virtualFile -> { includeLineMarker.set(true); // stop on first file match return false; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(psiFile.getProject()), BladeFileType.INSTANCE)); } // found an element; stop iteration for all index keys if(includeLineMarker.get()) { break; } } Collection<LineMarkerInfo> lineMarkers = new ArrayList<>(); if(includeLineMarker.get()) { NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder .create(PhpIcons.IMPLEMENTED) .setTargets(new TemplateIncludeCollectionNotNullLazyValue(psiFile.getProject(), templateNames)) .setTooltipText("Navigate to Blade file"); lineMarkers.add(builder.createLineMarkerInfo(psiFile)); } // try to find at least von controller target; lazyly load target later via click boolean controllerLineMarker = false; for(String templateName: templateNames) { Collection<VirtualFile> files = FileBasedIndex.getInstance().getContainingFiles(PhpTemplateUsageStubIndex.KEY, templateName, GlobalSearchScope.allScope(psiFile.getProject())); if(files.size() > 0) { controllerLineMarker = true; break; } } if(controllerLineMarker) { NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder .create(LaravelIcons.TEMPLATE_CONTROLLER_LINE_MARKER) .setTargets(new ControllerRenderViewCollectionNotNullLazyValue(psiFile.getProject(), templateNames)) .setTooltipText("Navigate to controller"); lineMarkers.add(builder.createLineMarkerInfo(psiFile)); } return lineMarkers; } @NotNull private LineMarkerInfo getRelatedPopover(@NotNull String singleItemTitle, @NotNull String singleItemTooltipPrefix, @NotNull PsiElement lineMarkerTarget, @NotNull Collection<GotoRelatedItem> gotoRelatedItems, @NotNull Icon icon) { // single item has no popup String title = singleItemTitle; if(gotoRelatedItems.size() == 1) { String customName = gotoRelatedItems.iterator().next().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 ); } private void visitOverwrittenTemplateFile(@NotNull PsiFile psiFile, @NotNull List<GotoRelatedItem> gotoRelatedItems, @NotNull String sectionName, @NotNull LazyVirtualFileTemplateResolver resolver) { visitOverwrittenTemplateFile(psiFile, gotoRelatedItems, sectionName, 10, resolver); } private void visitOverwrittenTemplateFile(@NotNull PsiFile psiFile, @NotNull List<GotoRelatedItem> gotoRelatedItems, @NotNull String sectionName, int depth, @NotNull LazyVirtualFileTemplateResolver resolver) { // simple secure recursive calls if(depth-- <= 0) { return; } BladeTemplateUtil.DirectiveParameterVisitor visitor = parameter -> { if (sectionName.equalsIgnoreCase(parameter.getContent())) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(parameter.getPsiElement()).withIcon(LaravelIcons.LARAVEL, LaravelIcons.LARAVEL)); } }; BladeTemplateUtil.visitSection(psiFile, visitor); BladeTemplateUtil.visitYield(psiFile, visitor); final int finalDepth = depth; BladeTemplateUtil.visitExtends(psiFile, parameter -> { for (VirtualFile virtualFile : resolver.resolveTemplateName(psiFile.getProject(), parameter.getContent())) { PsiFile templatePsiFile = PsiManager.getInstance(psiFile.getProject()).findFile(virtualFile); if (templatePsiFile != null) { visitOverwrittenTemplateFile(templatePsiFile, gotoRelatedItems, sectionName, finalDepth, resolver); } } }); } /** * Find all sub implementations of a section that are overwritten by an extends tag * Possible targets are: @section('sidebar') */ @NotNull private Collection<LineMarkerInfo> collectImplementsSection(@NotNull LeafPsiElement psiElement, @NotNull String sectionName, @NotNull LazyVirtualFileTemplateResolver resolver) { Collection<String> templateNames = resolver.resolveTemplateName(psiElement.getContainingFile()); if(templateNames.size() == 0) { return Collections.emptyList(); } Collection<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); Set<VirtualFile> virtualFiles = BladeTemplateUtil.getExtendsImplementations(psiElement.getProject(), templateNames); if(virtualFiles.size() == 0) { return Collections.emptyList(); } for(VirtualFile virtualFile: virtualFiles) { PsiFile psiFile = PsiManager.getInstance(psiElement.getProject()).findFile(virtualFile); if(psiFile != null) { BladeTemplateUtil.visitSection(psiFile, parameter -> { if (sectionName.equalsIgnoreCase(parameter.getContent())) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(parameter.getPsiElement()).withIcon(LaravelIcons.LARAVEL, LaravelIcons.LARAVEL)); } }); } } if(gotoRelatedItems.size() == 0) { return Collections.emptyList(); } return Collections.singletonList( getRelatedPopover("Template", "Blade File", psiElement, gotoRelatedItems, PhpIcons.IMPLEMENTED) ); } /** * Support: @stack('foobar') */ @NotNull private Collection<LineMarkerInfo> collectStackImplements(@NotNull LeafPsiElement psiElement, @NotNull String sectionName, @NotNull LazyVirtualFileTemplateResolver resolver) { Collection<String> templateNames = resolver.resolveTemplateName(psiElement.getContainingFile()); if(templateNames.size() == 0) { return Collections.emptyList(); } List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); Set<VirtualFile> virtualFiles = BladeTemplateUtil.getExtendsImplementations(psiElement.getProject(), templateNames); if(virtualFiles.size() == 0) { return Collections.emptyList(); } for(VirtualFile virtualFile: virtualFiles) { PsiFile psiFile = PsiManager.getInstance(psiElement.getProject()).findFile(virtualFile); if(psiFile != null) { BladeTemplateUtil.visit(psiFile, BladeTokenTypes.PUSH_DIRECTIVE, parameter -> { if (sectionName.equalsIgnoreCase(parameter.getContent())) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(parameter.getPsiElement()).withIcon(LaravelIcons.LARAVEL, LaravelIcons.LARAVEL)); } }); } } if(gotoRelatedItems.size() == 0) { return Collections.emptyList(); } return Collections.singletonList( getRelatedPopover("Push Implementation", "Push Implementation", psiElement, gotoRelatedItems, PhpIcons.IMPLEMENTED) ); } /** * Support: @push('foobar') */ @NotNull private Collection<LineMarkerInfo> collectPushOverwrites(@NotNull LeafPsiElement psiElement, @NotNull String sectionName) { final List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); BladeTemplateUtil.visitUpPath(psiElement.getContainingFile(), 10, parameter -> { if(sectionName.equalsIgnoreCase(parameter.getContent())) { gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(parameter.getPsiElement()).withIcon(LaravelIcons.LARAVEL, LaravelIcons.LARAVEL)); } }, BladeTokenTypes.STACK_DIRECTIVE); if(gotoRelatedItems.size() == 0) { return Collections.emptyList(); } return Collections.singletonList( getRelatedPopover("Stack Section", "Stack Overwrites", psiElement, gotoRelatedItems, PhpIcons.OVERRIDES) ); } /** * Support: @slot('foobar') */ @NotNull private Collection<LineMarkerInfo> collectSlotOverwrites(@NotNull LeafPsiElement psiElement, @NotNull BladePsiDirectiveParameter parameter, @NotNull String sectionName, @NotNull LazyVirtualFileTemplateResolver resolver) { String component = BladePsiUtil.findComponentForSlotScope(parameter); if(component == null) { return Collections.emptyList(); } List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>(); for (VirtualFile virtualFile : resolver.resolveTemplateName(psiElement.getProject(), component)) { PsiFile file = PsiManager.getInstance(psiElement.getProject()).findFile(virtualFile); if(file == null) { continue; } gotoRelatedItems.addAll(BladePsiUtil.collectPrintBlockVariableTargets(file, sectionName).stream() .map((Function<PsiElement, GotoRelatedItem>) element -> new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(element).withIcon(LaravelIcons.LARAVEL, LaravelIcons.LARAVEL)) .collect(Collectors.toList() )); } if(gotoRelatedItems.size() == 0) { return Collections.emptyList(); } return Collections.singletonList( getRelatedPopover("Slot Overwrites", "Slot Overwrites", psiElement, gotoRelatedItems, PhpIcons.OVERRIDES) ); } /** * Provide navigation for all rendering calls in php controller of given template names */ private static class ControllerRenderViewCollectionNotNullLazyValue extends NotNullLazyValue<Collection<? extends PsiElement>> { @NotNull private final Project project; @NotNull private final Collection<String> templateNames; private ControllerRenderViewCollectionNotNullLazyValue(@NotNull Project project, @NotNull Collection<String> templateNames) { this.project = project; this.templateNames = templateNames; } @NotNull @Override protected Collection<? extends PsiElement> compute() { Collection<VirtualFile> files = new HashSet<>(); // find template usages of controller for (String templateName: templateNames) { files.addAll(FileBasedIndex.getInstance().getContainingFiles( PhpTemplateUsageStubIndex.KEY, templateName, GlobalSearchScope.allScope(project) )); } Collection<PsiElement> targets = new ArrayList<>(); for (PsiFile psiFile : PsiElementUtils.convertVirtualFilesToPsiFiles(project, files)) { Collection<Pair<String, PsiElement>> pairs = BladeTemplateUtil.getViewTemplatesPairScope(psiFile); for (String templateName : templateNames) { for (Pair<String, PsiElement> pair : pairs) { if (templateName.equalsIgnoreCase(pair.first)) { targets.add(pair.getSecond()); } } } } return targets; } } /** * Provide navigation for all rendering calls in php controller of given template names */ private static class TemplateIncludeCollectionNotNullLazyValue extends NotNullLazyValue<Collection<? extends PsiElement>> { @NotNull private final Project project; @NotNull private final Collection<String> templateNames; private TemplateIncludeCollectionNotNullLazyValue(@NotNull Project project, @NotNull Collection<String> templateNames) { this.project = project; this.templateNames = templateNames; } @NotNull @Override protected Collection<? extends PsiElement> compute() { Collection<VirtualFile> virtualFiles = new ArrayList<>(); for(ID<String, Void> key : Arrays.asList(BladeExtendsStubIndex.KEY, BladeSectionStubIndex.KEY, BladeIncludeStubIndex.KEY, BladeEachStubIndex.KEY)) { for(String templateName: templateNames) { FileBasedIndex.getInstance().getFilesWithKey(key, Collections.singleton(templateName), virtualFile -> { virtualFiles.add(virtualFile); return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), BladeFileType.INSTANCE)); } } return PsiElementUtils.convertVirtualFilesToPsiFiles(project, virtualFiles); } } }