package fr.adrienbrault.idea.symfony2plugin.templating.variable.collector; import com.intellij.openapi.project.Project; 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.PsiRecursiveElementWalkingVisitor; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.indexing.FileBasedIndexImpl; import com.jetbrains.twig.TwigFile; import com.jetbrains.twig.TwigFileType; import com.jetbrains.twig.TwigTokenTypes; import com.jetbrains.twig.elements.TwigCompositeElement; import com.jetbrains.twig.elements.TwigElementTypes; import com.jetbrains.twig.elements.TwigExtendsTag; import com.jetbrains.twig.elements.TwigTagWithFileReference; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigIncludeStubIndex; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigFileVariableCollector; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigFileVariableCollectorParameter; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Daniel Espendiller <daniel@espendiller.net> */ public class IncludeVariableCollector implements TwigFileVariableCollector, TwigFileVariableCollector.TwigFileVariableCollectorExt { @Override public void collectVars(final TwigFileVariableCollectorParameter parameter, final Map<String, PsiVariable> variables) { final PsiFile psiFile = parameter.getElement().getContainingFile(); if(!(psiFile instanceof TwigFile) || PsiTreeUtil.getChildOfType(psiFile, TwigExtendsTag.class) != null) { return; } Collection<VirtualFile> files = getImplements((TwigFile) psiFile); if(files.size() == 0) { return; } for(VirtualFile virtualFile: files) { PsiFile twigFile = PsiManager.getInstance(parameter.getProject()).findFile(virtualFile); if(!(twigFile instanceof TwigFile)) { continue; } twigFile.acceptChildren(new MyPsiRecursiveElementWalkingVisitor(psiFile, variables, parameter)); } } private void collectIncludeContextVars(IElementType iElementType, PsiElement tag, PsiElement templatePsiName, Map<String, PsiVariable> variables, Set<VirtualFile> visitedFiles) { boolean addContextVar = true; Map<String, String> varAliasMap = new HashMap<>(); if(iElementType == TwigElementTypes.INCLUDE_TAG || iElementType == TwigElementTypes.EMBED_TAG) { // {% include 'template.html' with {'foo': 'bar'} only %} // {% embed "template.html.twig" with {'foo': 'bar'} only %} PsiElement onlyElement = PsiElementUtils.getChildrenOfType(tag, TwigHelper.getIncludeOnlyPattern()); if(onlyElement != null) { addContextVar = false; } varAliasMap = getIncludeWithVarNames(tag.getText()); } else if(iElementType == TwigTokenTypes.IDENTIFIER) { // {{ include('template.html.twig', {'foo2': foo}, with_context = false) }} // not nice but its working :) // strip all whitespace psi elements String text = tag.getText(); text = text.replaceAll("\\r|\\n|\\s+", ""); String regex = "include\\((['|\"].*['|\"],(.*))\\)"; Matcher matcher = Pattern.compile(regex).matcher(text); if (matcher.find()) { String[] group = matcher.group(1).split(","); if(group.length > 1) { // json alias map: {'foo2': foo} if(group[1].startsWith("{")) { varAliasMap = getVariableAliasMap(group[1]); } // try to find context in one of the parameter: // include('template.html', with_context = false) // include('template.html', {foo: 'bar'}, with_context = false) for (int i = 1; i < group.length; i++) { if(group[i].equals("with_context=false")) { addContextVar = false; } } } } } // we dont need to collect foreign file variables if(!addContextVar && varAliasMap.size() == 0) { return; } Map<String, PsiVariable> stringPsiVariableHashMap = TwigTypeResolveUtil.collectScopeVariables(templatePsiName, visitedFiles); // add context vars if(addContextVar) { for(Map.Entry<String, PsiVariable> entry: stringPsiVariableHashMap.entrySet()) { variables.put(entry.getKey(), entry.getValue()); } } // add alias vars if(varAliasMap.size() > 0) { for(Map.Entry<String, String> entry: varAliasMap.entrySet()) { if(stringPsiVariableHashMap.containsKey(entry.getValue())) { variables.put(entry.getKey(), stringPsiVariableHashMap.get(entry.getValue())); } } } } public static Map<String, String> getIncludeWithVarNames(String includeText) { String regex = "with\\s*\\{\\s*(.*[^%])\\}\\s*"; Matcher matcher = Pattern.compile(regex).matcher(includeText.replace("\r\n", " ").replace("\n", " ")); if (matcher.find()) { String group = matcher.group(1); return getVariableAliasMap("{" + group + "}"); } return new HashMap<>(); } private static Map<String, String> getVariableAliasMap(String jsonLike) { Map<String, String> map = new HashMap<>(); String[] parts = jsonLike.replaceAll("^\\{|\\}$","").split("\"?(:|,)(?![^\\{]*\\})\"?"); for (int i = 0; i < parts.length -1; i+=2) { map.put(StringUtils.trim(parts[i]).replaceAll("^\"|\"$|\'|\'$", ""), StringUtils.trim(parts[i+1]).replaceAll("^\"|\"$|\'|\'$", "")); } return map; } @Override public void collect(TwigFileVariableCollectorParameter parameter, Map<String, Set<String>> variables) { } private Collection<VirtualFile> getImplements(TwigFile twigFile) { final Set<VirtualFile> targets = new HashSet<>(); for(String templateName: TwigUtil.getTemplateName(twigFile)) { final Project project = twigFile.getProject(); FileBasedIndexImpl.getInstance().getFilesWithKey(TwigIncludeStubIndex.KEY, new HashSet<>(Collections.singletonList(templateName)), virtualFile -> { targets.add(virtualFile); return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), TwigFileType.INSTANCE)); } return targets; } private class MyPsiRecursiveElementWalkingVisitor extends PsiRecursiveElementWalkingVisitor { private final PsiFile psiFile; private final Map<String, PsiVariable> variables; private final TwigFileVariableCollectorParameter parameter; public MyPsiRecursiveElementWalkingVisitor(PsiFile psiFile, Map<String, PsiVariable> variables, TwigFileVariableCollectorParameter parameter) { this.psiFile = psiFile; this.variables = variables; this.parameter = parameter; } @Override public void visitElement(PsiElement element) { // {% include 'template.html' %} if(element instanceof TwigTagWithFileReference && element.getNode().getElementType() == TwigElementTypes.INCLUDE_TAG) { PsiElement includeTag = PsiElementUtils.getChildrenOfType(element, TwigHelper.getTemplateFileReferenceTagPattern("include")); if(includeTag != null) { collectContextVars(TwigElementTypes.INCLUDE_TAG, element, includeTag); } } if(element instanceof TwigCompositeElement) { // {{ include('template.html') }} PsiElement includeTag = PsiElementUtils.getChildrenOfType(element, TwigHelper.getPrintBlockFunctionPattern("include")); if(includeTag != null) { collectContextVars(TwigTokenTypes.IDENTIFIER, element, includeTag); } // {% embed "foo.html.twig" PsiElement embedTag = PsiElementUtils.getChildrenOfType(element, TwigHelper.getEmbedPattern()); if(embedTag != null) { collectContextVars(TwigElementTypes.EMBED_TAG, element, embedTag); } } super.visitElement(element); } private void collectContextVars(IElementType iElementType, @NotNull PsiElement element, @NotNull PsiElement includeTag) { String templateName = includeTag.getText(); if(StringUtils.isNotBlank(templateName)) { for(PsiFile templateFile: TwigHelper.getTemplatePsiElements(element.getProject(), templateName)) { if(templateFile.equals(psiFile)) { collectIncludeContextVars(iElementType, element, includeTag, variables, parameter.getVisitedFiles()); } } } } } }