Skip to content

#769 provided related twig symbols navigation #1927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 9, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package fr.adrienbrault.idea.symfony2plugin.navigation;

import com.intellij.lang.Language;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.navigation.GotoRelatedItem;
import com.intellij.navigation.GotoRelatedProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.twig.TwigFile;
import com.jetbrains.twig.TwigLanguage;
import com.jetbrains.twig.elements.TwigBlockStatement;
import com.jetbrains.twig.elements.TwigBlockTag;
import com.jetbrains.twig.elements.TwigElementTypes;
import com.jetbrains.twig.elements.TwigTagWithFileReference;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.dic.RelatedPopupGotoLineMarker;
import fr.adrienbrault.idea.symfony2plugin.templating.TwigPattern;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil;
import fr.adrienbrault.idea.symfony2plugin.twig.utils.TwigBlockUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import icons.TwigIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class TwigGotoRelatedProvider extends GotoRelatedProvider {
@NotNull
@Override
public List<? extends GotoRelatedItem> getItems(@NotNull PsiElement psiElement2) {
if (!Symfony2ProjectComponent.isEnabled(psiElement2)) {
return Collections.emptyList();
}

Language language = psiElement2.getLanguage();
if (language != TwigLanguage.INSTANCE && language != HTMLLanguage.INSTANCE) {
return Collections.emptyList();
}

List<GotoRelatedItem> gotoRelatedItems = new ArrayList<>();

PsiElement psiElement = TwigUtil.getElementOnTwigViewProvider(psiElement2);
if (psiElement == null) {
return Collections.emptyList();
}

PsiFile psiFile = psiElement.getContainingFile();
if (psiFile instanceof TwigFile) {
// extends
Set<String> templates = new HashSet<>();
TwigUtil.visitTemplateExtends((TwigFile) psiFile, pair -> templates.add(pair.getFirst()));
Set<VirtualFile> virtualFiles = new HashSet<>();
for (String template : templates) {
for (PsiFile templatePsiElement : TwigUtil.getTemplatePsiElements(psiElement.getProject(), template)) {
VirtualFile virtualFile = templatePsiElement.getVirtualFile();
if (!virtualFiles.contains(virtualFile)) {
virtualFiles.add(virtualFile);
gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(templatePsiElement, "extends").withIcon(TwigIcons.TwigFileIcon, Symfony2Icons.TWIG_LINE_MARKER));
}
}
}

// twig blocks up
@Nullable TwigBlockStatement parentOfType = PsiTreeUtil.getParentOfType(psiElement, TwigBlockStatement.class);
if (parentOfType != null) {
TwigBlockTag twigBlockTag = PsiTreeUtil.findChildOfType(parentOfType, TwigBlockTag.class);
if (twigBlockTag != null) {
PsiElement childrenOfType = PsiElementUtils.getChildrenOfType(twigBlockTag, TwigPattern.getBlockTagPattern());
if (childrenOfType != null) {
String blockName = twigBlockTag.getName();

gotoRelatedItems.addAll(TwigBlockUtil.getBlockOverwriteTargets(childrenOfType).stream().map((Function<PsiElement, GotoRelatedItem>) psiElement1 ->
new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(psiElement1, blockName).withIcon(Symfony2Icons.TWIG_BLOCK_OVERWRITE, Symfony2Icons.TWIG_BLOCK_OVERWRITE)
).collect(Collectors.toList()));

gotoRelatedItems.addAll(TwigBlockUtil.getBlockImplementationTargets(childrenOfType).stream().map((Function<PsiElement, GotoRelatedItem>) psiElement1 ->
new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(psiElement1, blockName).withIcon(Symfony2Icons.TWIG_BLOCK_OVERWRITE, Symfony2Icons.TWIG_BLOCK_OVERWRITE)
).collect(Collectors.toList()));
}
}
}

// "include" and other file tags
TwigTagWithFileReference twigTagWithFileReference = PsiTreeUtil.getParentOfType(psiElement, TwigTagWithFileReference.class);
if (twigTagWithFileReference != null) {
visitFileReferenceElement(psiElement.getProject(), gotoRelatedItems, twigTagWithFileReference);
}

// "embed" tag
PsiElement parentOfType1 = PsiElementUtils.getParentOfType(psiElement, TwigElementTypes.EMBED_STATEMENT);
if (parentOfType1 != null) {
PsiElement embedTag = PsiElementUtils.getChildrenOfType(parentOfType1, PlatformPatterns.psiElement().withElementType(TwigElementTypes.EMBED_TAG));
if (embedTag != null) {
visitFileReferenceElement(psiElement.getProject(), gotoRelatedItems, embedTag);
}
}
}

return gotoRelatedItems;
}

private void visitFileReferenceElement(@NotNull Project project, @NotNull List<GotoRelatedItem> gotoRelatedItems, @NotNull PsiElement psiElement) {
TwigUtil.visitTemplateIncludes(psiElement, templateInclude -> {
for (PsiFile templatePsiElement : TwigUtil.getTemplatePsiElements(project, templateInclude.getTemplateName())) {
gotoRelatedItems.add(new RelatedPopupGotoLineMarker.PopupGotoRelatedItem(templatePsiElement, templateInclude.getType().toString().toLowerCase()).withIcon(Symfony2Icons.TWIG_BLOCK_OVERWRITE, Symfony2Icons.TWIG_BLOCK_OVERWRITE));
}
});
}
}
Original file line number Diff line number Diff line change
@@ -2288,103 +2288,109 @@ public static Map<VirtualFile, Collection<String>> getBlockNamesForFiles(@NotNul
/**
* Visit all possible Twig include file pattern
*/
public static void visitTemplateIncludes(@NotNull TwigFile twigFile, @NotNull Consumer<TemplateInclude> consumer) {
PsiTreeUtil.collectElements(twigFile, psiElement -> {
if(psiElement instanceof TwigTagWithFileReference) {
// {% include %}
if(psiElement.getNode().getElementType() == TwigElementTypes.INCLUDE_TAG) {
for (String templateName : getIncludeTagStrings((TwigTagWithFileReference) psiElement)) {
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.INCLUDE));
}
public static void visitTemplateIncludes(@NotNull PsiElement psiElement, @NotNull Consumer<TemplateInclude> consumer) {
if(psiElement instanceof TwigTagWithFileReference) {
// {% include %}
if(psiElement.getNode().getElementType() == TwigElementTypes.INCLUDE_TAG) {
for (String templateName : getIncludeTagStrings((TwigTagWithFileReference) psiElement)) {
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.INCLUDE));
}
}
}

// {% import "foo.html.twig"
PsiElement importTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getTagNameParameterPattern(TwigElementTypes.IMPORT_TAG, "import"));
if(importTag != null) {
String templateName = importTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.IMPORT));
}
// {% import "foo.html.twig"
PsiElement importTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getTagNameParameterPattern(TwigElementTypes.IMPORT_TAG, "import"));
if(importTag != null) {
String templateName = importTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.IMPORT));
}
}

// {% from 'forms.html' import ... %}
PsiElement fromTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getTagNameParameterPattern(TwigElementTypes.IMPORT_TAG, "from"));
if(fromTag != null) {
String templateName = fromTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.IMPORT));
}
// {% from 'forms.html' import ... %}
PsiElement fromTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getTagNameParameterPattern(TwigElementTypes.IMPORT_TAG, "from"));
if(fromTag != null) {
String templateName = fromTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.IMPORT));
}
} else if(psiElement instanceof TwigCompositeElement) {
// {{ include() }}
// {{ source() }}
PsiElement includeTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getPrintBlockOrTagFunctionPattern("include", "source"));
if(includeTag != null) {
String templateName = includeTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.INCLUDE_FUNCTION));
}
}
} else if(psiElement instanceof TwigCompositeElement) {
// {{ include() }}
// {{ source() }}
PsiElement includeTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getPrintBlockOrTagFunctionPattern("include", "source"));
if(includeTag != null) {
String templateName = includeTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.INCLUDE_FUNCTION));
}
}

// {% embed "foo.html.twig"
PsiElement embedTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getEmbedPattern());
if(embedTag != null) {
String templateName = embedTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.EMBED));
}
// {% embed "foo.html.twig"
PsiElement embedTag = PsiElementUtils.getChildrenOfType(psiElement, TwigPattern.getEmbedPattern());
if(embedTag != null) {
String templateName = embedTag.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.EMBED));
}
}

if(psiElement.getNode().getElementType() == TwigElementTypes.TAG) {
PsiElement tagElement = PsiElementUtils.getChildrenOfType(psiElement, PlatformPatterns.psiElement().withElementType(TwigTokenTypes.TAG_NAME));
if(tagElement != null) {
String text = tagElement.getText();
if("form_theme".equals(text)) {
// {% form_theme form.child 'form/fields_child.html.twig' %}
PsiElement childrenOfType = PsiElementUtils.getNextSiblingAndSkip(tagElement, TwigTokenTypes.STRING_TEXT,
TwigTokenTypes.IDENTIFIER, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.DOT
);
if(psiElement.getNode().getElementType() == TwigElementTypes.TAG) {
PsiElement tagElement = PsiElementUtils.getChildrenOfType(psiElement, PlatformPatterns.psiElement().withElementType(TwigTokenTypes.TAG_NAME));
if(tagElement != null) {
String text = tagElement.getText();
if("form_theme".equals(text)) {
// {% form_theme form.child 'form/fields_child.html.twig' %}
PsiElement childrenOfType = PsiElementUtils.getNextSiblingAndSkip(tagElement, TwigTokenTypes.STRING_TEXT,
TwigTokenTypes.IDENTIFIER, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.DOT
);

if(childrenOfType != null) {
String templateName = childrenOfType.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.FORM_THEME));
}
if(childrenOfType != null) {
String templateName = childrenOfType.getText();
if(StringUtils.isNotBlank(templateName)) {
consumer.consume(new TemplateInclude(psiElement, templateName, TemplateInclude.TYPE.FORM_THEME));
}
}

// {% form_theme form.child with ['form/fields_child.html.twig'] %}
PsiElement withElement = PsiElementUtils.getNextSiblingOfType(tagElement, PlatformPatterns.psiElement().withElementType(TwigTokenTypes.IDENTIFIER).withText("with"));
if(withElement != null) {
// find LITERAL "[", "{"
PsiElement arrayStart = PsiElementUtils.getNextSiblingAndSkip(tagElement, TwigElementTypes.LITERAL,
TwigTokenTypes.IDENTIFIER, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.DOT
);

if(arrayStart != null) {
PsiElement firstChild = arrayStart.getFirstChild();
if(firstChild != null) {
visitStringInArray(firstChild, pair ->
consumer.consume(new TemplateInclude(psiElement, pair.getFirst(), TemplateInclude.TYPE.FORM_THEME))
);
}
// {% form_theme form.child with ['form/fields_child.html.twig'] %}
PsiElement withElement = PsiElementUtils.getNextSiblingOfType(tagElement, PlatformPatterns.psiElement().withElementType(TwigTokenTypes.IDENTIFIER).withText("with"));
if(withElement != null) {
// find LITERAL "[", "{"
PsiElement arrayStart = PsiElementUtils.getNextSiblingAndSkip(tagElement, TwigElementTypes.LITERAL,
TwigTokenTypes.IDENTIFIER, TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE, TwigTokenTypes.DOT
);

if(arrayStart != null) {
PsiElement firstChild = arrayStart.getFirstChild();
if(firstChild != null) {
visitStringInArray(firstChild, pair ->
consumer.consume(new TemplateInclude(psiElement, pair.getFirst(), TemplateInclude.TYPE.FORM_THEME))
);
}
}
}
}
}
}

for (TwigFileUsage extension : TWIG_FILE_USAGE_EXTENSIONS.getExtensions()) {
if (extension.isIncludeTemplate(psiElement)) {
for (String template : extension.getIncludeTemplate(psiElement)) {
consumer.consume(new TemplateInclude(psiElement, template, TemplateInclude.TYPE.INCLUDE));
}
for (TwigFileUsage extension : TWIG_FILE_USAGE_EXTENSIONS.getExtensions()) {
if (extension.isIncludeTemplate(psiElement)) {
for (String template : extension.getIncludeTemplate(psiElement)) {
consumer.consume(new TemplateInclude(psiElement, template, TemplateInclude.TYPE.INCLUDE));
}
}
}
}
}

return false;
/**
* Visit all possible Twig include file pattern
*/
public static void visitTemplateIncludes(@NotNull TwigFile twigFile, @NotNull Consumer<TemplateInclude> consumer) {
PsiTreeUtil.collectElements(twigFile, psiElement -> {
visitTemplateIncludes(psiElement, consumer);
return true;
});
}

1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
@@ -249,6 +249,7 @@
<gotoFileContributor implementation="fr.adrienbrault.idea.symfony2plugin.navigation.TemplateFileContributor"/>

<gotoRelatedProvider implementation="fr.adrienbrault.idea.symfony2plugin.navigation.PhpGotoRelatedProvider"/>
<gotoRelatedProvider implementation="fr.adrienbrault.idea.symfony2plugin.navigation.TwigGotoRelatedProvider"/>

<directoryProjectGenerator implementation="fr.adrienbrault.idea.symfony2plugin.installer.SymfonyInstallerProjectGenerator"/>
<projectTemplatesFactory implementation="fr.adrienbrault.idea.symfony2plugin.installer.SymfonyInstallerTemplatesFactory"/>