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.util.Pair;
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.PsiWhiteSpace;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.twig.TwigLanguage;
import com.jetbrains.twig.TwigTokenTypes;
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.TwigHelper;
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigBlock;
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigBlockParser;
import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigExtension;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigExtensionParser;
import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil;
import fr.adrienbrault.idea.symfony2plugin.translation.dict.TranslationUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
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.*;
/**
* @author Adrien Brault <adrien.brault@gmail.com>
*/
public class TwigTemplateGoToDeclarationHandler implements GotoDeclarationHandler {
@Nullable
@Override
public PsiElement[] getGotoDeclarationTargets(PsiElement psiElement, int i, Editor editor) {
if(!Symfony2ProjectComponent.isEnabled(psiElement) || !PlatformPatterns.psiElement().withLanguage(TwigLanguage.INSTANCE).accepts(psiElement)) {
return null;
}
if (TwigHelper.getBlockTagPattern().accepts(psiElement)) {
return getBlockGoTo(psiElement);
}
if (TwigHelper.getPathAfterLeafPattern().accepts(psiElement)) {
PsiElement[] psiElements = this.getRouteParameterGoTo(psiElement);
if(psiElements.length > 0) {
return psiElements;
}
}
// support: {% include() %}, {{ include() }}
if(TwigHelper.getTemplateFileReferenceTagPattern().accepts(psiElement) || TwigHelper.getPrintBlockFunctionPattern("include", "source").accepts(psiElement)) {
return this.getTwigFiles(psiElement);
}
if(TwigHelper.getAutocompletableRoutePattern().accepts(psiElement)) {
return this.getRouteGoTo(psiElement);
}
// find trans('', {}, '|')
// tricky way to get the function string trans(...)
if (TwigHelper.getTransDomainPattern().accepts(psiElement)) {
PsiElement psiElementTrans = PsiElementUtils.getPrevSiblingOfType(psiElement, PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice")));
if(psiElementTrans != null && TwigHelper.getTwigMethodString(psiElementTrans) != null) {
return getTranslationDomainGoto(psiElement);
}
}
// {% trans from "app" %}
// {% transchoice from "app" %}
if (TwigHelper.getTranslationTokenTagFromPattern().accepts(psiElement)) {
return getTranslationDomainGoto(psiElement);
}
if (TwigHelper.getTranslationPattern("trans", "transchoice").accepts(psiElement)) {
return getTranslationKeyGoTo(psiElement);
}
// provide global twig file resolving
if (PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT)
.withText(PlatformPatterns.string().endsWith(".twig")).accepts(psiElement)) {
return this.getTwigFiles(psiElement);
}
if(TwigHelper.getPrintBlockOrTagFunctionPattern("controller").accepts(psiElement) || TwigHelper.getStringAfterTagNamePattern("render").accepts(psiElement)) {
PsiElement controllerMethod = this.getControllerGoTo(psiElement);
if(controllerMethod != null) {
return new PsiElement[] { controllerMethod };
}
}
if(TwigHelper.getTransDefaultDomainPattern().accepts(psiElement)) {
List<PsiFile> domainPsiFiles = TranslationUtil.getDomainPsiFiles(psiElement.getProject(), psiElement.getText());
return domainPsiFiles.toArray(new PsiElement[domainPsiFiles.size()]);
}
if(TwigHelper.getFilterPattern().accepts(psiElement)) {
return getFilterGoTo(psiElement);
}
// {% if foo is ... %}
// {% if foo is not ... %}
if(PlatformPatterns.or(TwigHelper.getAfterIsTokenPattern(), TwigHelper.getAfterIsTokenWithOneIdentifierLeafPattern()).accepts(psiElement)) {
return getAfterIsToken(psiElement);
}
return null;
}
/**
* {% if foo is ... %}
*/
private PsiElement[] getAfterIsToken(@NotNull PsiElement psiElement) {
// find text after if statement
String text = StringUtils.trim(
PhpElementsUtil.getPrevSiblingAsTextUntil(psiElement, TwigHelper.getAfterIsTokenTextPattern(), false) + psiElement.getText()
);
if(StringUtils.isBlank(text)) {
return new PsiElement[0];
}
Set<String> items = new HashSet<>(
Collections.singletonList(text)
);
// support atleat one identifier after current caret position
// "divisi<caret>ble by"
PsiElement whitespace = psiElement.getNextSibling();
if(whitespace instanceof PsiWhiteSpace) {
PsiElement nextSibling = whitespace.getNextSibling();
if(nextSibling != null && nextSibling.getNode().getElementType() == TwigTokenTypes.IDENTIFIER) {
String identifier = nextSibling.getText();
if(StringUtils.isNotBlank(identifier)) {
items.add(text + " " + identifier);
}
}
}
Collection<PsiElement> psiElements = new ArrayList<>();
for (Map.Entry<String, TwigExtension> entry : new TwigExtensionParser(psiElement.getProject()).getSimpleTest().entrySet()) {
for (String item : items) {
if(entry.getKey().equalsIgnoreCase(item)) {
psiElements.addAll(Arrays.asList(
PhpElementsUtil.getPsiElementsBySignature(psiElement.getProject(), entry.getValue().getSignature()))
);
}
}
}
return psiElements.toArray(new PsiElement[psiElements.size()]);
}
private PsiElement[] getRouteParameterGoTo(PsiElement psiElement) {
String routeName = TwigHelper.getMatchingRouteNameOnParameter(psiElement);
if(routeName == null) {
return new PsiElement[0];
}
return RouteHelper.getRouteParameterPsiElements(psiElement.getProject(), routeName, psiElement.getText());
}
private PsiElement getControllerGoTo(PsiElement psiElement) {
String text = PsiElementUtils.trimQuote(psiElement.getText());
return ControllerIndex.getControllerMethod(psiElement.getProject(), text);
}
@Nullable
private PsiElement[] getTwigFiles(PsiElement psiElement) {
String templateName = psiElement.getText();
PsiElement[] psiElements = TwigHelper.getTemplatePsiElements(psiElement.getProject(), templateName);
if(psiElements.length == 0) {
return null;
}
return psiElements;
}
private PsiElement[] getFilterGoTo(PsiElement psiElement) {
Map<String, TwigExtension> filters = new TwigExtensionParser(psiElement.getProject()).getFilters();
if(!filters.containsKey(psiElement.getText())) {
return new PsiElement[0];
}
String signature = filters.get(psiElement.getText()).getSignature();
if(signature == null) {
return new PsiElement[0];
}
return PhpElementsUtil.getPsiElementsBySignature(psiElement.getProject(), signature);
}
@NotNull
public static PsiElement[] getBlockGoTo(@NotNull PsiElement psiElement) {
String blockName = psiElement.getText();
if(StringUtils.isBlank(blockName)) {
return new PsiElement[0];
}
Collection<PsiElement> psiElements = new HashSet<>();
Pair<PsiFile[], Boolean> scopedFile = TwigHelper.findScopedFile(psiElement);
for (PsiFile psiFile : scopedFile.getFirst()) {
ContainerUtil.addAll(psiElements, getBlockNameGoTo(psiFile, blockName, scopedFile.getSecond()));
}
return psiElements.toArray(new PsiElement[psiElements.size()]);
}
@NotNull
public static PsiElement[] getBlockNameGoTo(@NotNull PsiFile psiFile, @NotNull String blockName) {
return getBlockNameGoTo(psiFile, blockName, false);
}
@NotNull
public static PsiElement[] getBlockNameGoTo(PsiFile psiFile, String blockName, boolean withSelfBlocks) {
Map<String, VirtualFile> twigFilesByName = TwigHelper.getTwigFilesByName(psiFile.getProject());
List<TwigBlock> blocks = new TwigBlockParser(twigFilesByName).withSelfBlocks(withSelfBlocks).walk(psiFile);
List<PsiElement> psiElements = new ArrayList<>();
for (TwigBlock block : blocks) {
if(block.getName().equals(blockName)) {
Collections.addAll(psiElements, block.getBlock());
}
}
return psiElements.toArray(new PsiElement[psiElements.size()]);
}
private PsiElement[] getRouteGoTo(PsiElement psiElement) {
String text = PsiElementUtils.getText(psiElement);
if(StringUtils.isBlank(text)) {
return new PsiElement[0];
}
PsiElement[] methods = RouteHelper.getMethods(psiElement.getProject(), text);
if(methods.length > 0) {
return methods;
}
List<PsiElement> psiElementList = RouteHelper.getRouteDefinitionTargets(psiElement.getProject(), text);
return psiElementList.toArray(new PsiElement[psiElementList.size()]);
}
private PsiElement[] getTranslationKeyGoTo(PsiElement psiElement) {
String translationKey = psiElement.getText();
return TranslationUtil.getTranslationPsiElements(psiElement.getProject(), translationKey, TwigUtil.getPsiElementTranslationDomain(psiElement));
}
private PsiElement[] getTranslationDomainGoto(PsiElement psiElement) {
String text = PsiElementUtils.trimQuote(psiElement.getText());
if(StringUtils.isNotBlank(text)) {
List<PsiFile> domainPsiFiles = TranslationUtil.getDomainPsiFiles(psiElement.getProject(), text);
return domainPsiFiles.toArray(new PsiElement[domainPsiFiles.size()]);
}
return new PsiElement[0];
}
@Nullable
@Override
public String getActionText(DataContext dataContext) {
return null;
}
}