package fr.adrienbrault.idea.symfony2plugin; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileVisitor; import com.intellij.patterns.ElementPattern; import com.intellij.patterns.PatternCondition; import com.intellij.patterns.PlatformPatterns; import com.intellij.patterns.PsiElementPattern; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.*; import com.intellij.util.Consumer; import com.intellij.util.ProcessingContext; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.indexing.FileBasedIndexImpl; import com.jetbrains.twig.TwigFile; import com.jetbrains.twig.TwigFileType; import com.jetbrains.twig.TwigLanguage; import com.jetbrains.twig.TwigTokenTypes; import com.jetbrains.twig.elements.*; import fr.adrienbrault.idea.symfony2plugin.asset.dic.AssetDirectoryReader; import fr.adrienbrault.idea.symfony2plugin.asset.dic.AssetFile; import fr.adrienbrault.idea.symfony2plugin.extension.TwigNamespaceExtension; import fr.adrienbrault.idea.symfony2plugin.extension.TwigNamespaceExtensionParameter; import fr.adrienbrault.idea.symfony2plugin.stubs.SymfonyProcessors; import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.TwigMacroFunctionStubIndex; import fr.adrienbrault.idea.symfony2plugin.templating.TemplateLookupElement; import fr.adrienbrault.idea.symfony2plugin.templating.assets.TwigNamedAssetsServiceParser; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TemplateFileMap; import fr.adrienbrault.idea.symfony2plugin.templating.dict.TwigBlock; import fr.adrienbrault.idea.symfony2plugin.templating.path.TwigNamespaceSetting; import fr.adrienbrault.idea.symfony2plugin.templating.path.TwigPath; import fr.adrienbrault.idea.symfony2plugin.templating.path.TwigPathContentIterator; import fr.adrienbrault.idea.symfony2plugin.templating.path.TwigPathIndex; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils; import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil; import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle; import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory; import fr.adrienbrault.idea.symfony2plugin.util.yaml.YamlHelper; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.yaml.YAMLUtil; import org.jetbrains.yaml.psi.*; import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author Adrien Brault <adrien.brault@gmail.com> * @author Daniel Espendiller <daniel@espendiller.net> */ public class TwigHelper { private static final ExtensionPointName<TwigNamespaceExtension> EXTENSIONS = new ExtensionPointName<>( "fr.adrienbrault.idea.symfony2plugin.extension.TwigNamespaceExtension" ); public static String[] CSS_FILES_EXTENSIONS = new String[] { "css", "less", "sass", "scss" }; public static String[] JS_FILES_EXTENSIONS = new String[] { "js", "dart", "coffee" }; public static String[] IMG_FILES_EXTENSIONS = new String[] { "png", "jpg", "jpeg", "gif", "svg"}; public static String TEMPLATE_ANNOTATION_CLASS = "\\Sensio\\Bundle\\FrameworkExtraBundle\\Configuration\\Template"; private static final Key<CachedValue<TemplateFileMap>> TEMPLATE_CACHE_TWIG = new Key<>("TEMPLATE_CACHE_TWIG"); private static final Key<CachedValue<TemplateFileMap>> TEMPLATE_CACHE_ALL = new Key<>("TEMPLATE_CACHE_ALL"); public static final String DOC_SEE_REGEX = "\\{#[\\s]+@see[\\s]+([-@\\./\\:\\w\\\\\\[\\]]+)[\\s]*#}"; public static final String DOC_SEE_REGEX_WITHOUT_SEE = "\\{#[\\s]+([-@\\./\\:\\w\\\\\\[\\]]+)[\\s]*#}"; /** * ([) "FOO", 'FOO' (]) */ public static final ElementPattern<PsiElement> STRING_WRAP_PATTERN = PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE) ); private static final ElementPattern[] PARAMETER_WHITE_LIST = new ElementPattern[]{ PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.NUMBER), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.CONCAT), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT), PlatformPatterns.psiElement(TwigTokenTypes.DOT) }; @Deprecated public static Map<String, VirtualFile> getTemplateFilesByName(@NotNull Project project, boolean useTwig, boolean usePhp) { return getTemplateMap(project, useTwig, usePhp).getTemplates(); } @NotNull public static synchronized TemplateFileMap getTemplateMap(@NotNull Project project, boolean useTwig, final boolean usePhp) { TemplateFileMap templateMapProxy = null; // cache twig and all files, // only PHP files we dont need to cache if(useTwig && !usePhp) { // cache twig files only, most use case CachedValue<TemplateFileMap> cache = project.getUserData(TEMPLATE_CACHE_TWIG); if (cache == null) { cache = CachedValuesManager.getManager(project).createCachedValue(new MyTwigOnlyTemplateFileMapCachedValueProvider(project), false); project.putUserData(TEMPLATE_CACHE_TWIG, cache); } templateMapProxy = cache.getValue(); } else if(useTwig && usePhp) { // cache all files CachedValue<TemplateFileMap> cache = project.getUserData(TEMPLATE_CACHE_ALL); if (cache == null) { cache = CachedValuesManager.getManager(project).createCachedValue(new MyAllTemplateFileMapCachedValueProvider(project), false); project.putUserData(TEMPLATE_CACHE_ALL, cache); } templateMapProxy = cache.getValue(); } // cache-less calls if(templateMapProxy == null) { templateMapProxy = getTemplateMapProxy(project, useTwig, usePhp); } return templateMapProxy; } @NotNull private static TemplateFileMap getTemplateMapProxy(@NotNull Project project, boolean useTwig, boolean usePhp) { List<TwigPath> twigPaths = new ArrayList<>(); twigPaths.addAll(getTwigNamespaces(project)); if(twigPaths.size() == 0) { return new TemplateFileMap(); } // app/Resources/ParentBundle/Resources/views Map<String, SymfonyBundle> parentBundles = new SymfonyBundleUtil(project).getParentBundles(); if(parentBundles.size() > 0) { for (Map.Entry<String, SymfonyBundle> entry : parentBundles.entrySet()) { VirtualFile views = entry.getValue().getRelative("Resources/views"); if(views != null) { twigPaths.add(new TwigPath(views.getPath(), entry.getKey(), TwigPathIndex.NamespaceType.BUNDLE)); } } } // app/Resources/FooBundle/views VirtualFile relativeFile = VfsUtil.findRelativeFile(project.getBaseDir(), "app", "Resources"); if(relativeFile != null) { for (VirtualFile virtualFile : relativeFile.getChildren()) { if(!virtualFile.isDirectory() || !virtualFile.getName().endsWith("Bundle")) { continue; } VirtualFile views = virtualFile.findChild("views"); if(views == null) { continue; } twigPaths.add(new TwigPath(views.getPath(), virtualFile.getName(), TwigPathIndex.NamespaceType.BUNDLE)); } } TemplateFileMap container = new TemplateFileMap(); for (TwigPath twigPath : twigPaths) { if(twigPath.isEnabled()) { VirtualFile virtualDirectoryFile = twigPath.getDirectory(project); if(virtualDirectoryFile != null) { final TwigPathContentIterator iterator = new TwigPathContentIterator(project, twigPath).setWithPhp(usePhp).setWithTwig(useTwig); VfsUtil.visitChildrenRecursively(virtualDirectoryFile, new MyLimitedVirtualFileVisitor(iterator, 5, 150)); container.putAll(iterator.getResults()); } } } return container; } public static Map<String, VirtualFile> getTwigFilesByName(Project project) { return getTemplateFilesByName(project, true, false); } public static Map<String, VirtualFile> getTemplateFilesByName(Project project) { return getTemplateFilesByName(project, true, true); } @Nullable public static TwigNamespaceSetting findManagedTwigNamespace(Project project, TwigPath twigPath) { List<TwigNamespaceSetting> twigNamespaces = Settings.getInstance(project).twigNamespaces; if(twigNamespaces == null) { return null; } for(TwigNamespaceSetting twigNamespace: twigNamespaces) { if(twigNamespace.equals(project, twigPath)) { return twigNamespace; } } return null; } @Nullable public static PsiFile getTemplateFileByName(Project project, String templateName) { PsiFile[] templatePsiElements = TwigHelper.getTemplatePsiElements(project, templateName); if(templatePsiElements.length > 0) { return templatePsiElements[0]; } return null; } /** * both are valid names first is internal completion * BarBundle:Foo:steps/step_finish.html.twig * BarBundle:Foo/steps:step_finish.html.twig * * todo: provide setting for that */ public static String normalizeTemplateName(String templateName) { // force linux path style templateName = templateName.replace("\\", "/"); if(templateName.startsWith("@") || !templateName.matches("^.*?:.*?:.*?/.*?$")) { return templateName; } templateName = templateName.replace(":", "/"); int firstDoublePoint = templateName.indexOf("/"); int lastDoublePoint = templateName.lastIndexOf("/"); String bundle = templateName.substring(0, templateName.indexOf("/")); String subFolder = templateName.substring(firstDoublePoint, lastDoublePoint); String file = templateName.substring(templateName.lastIndexOf("/") + 1); return String.format("%s:%s:%s", bundle, StringUtils.strip(subFolder, "/"), file); } /** * Find file in a twig path collection * * @param project current project * @param templateName path known, should not be normalized * @return target files */ public static PsiFile[] getTemplatePsiElements(Project project, String templateName) { String normalizedTemplateName = normalizeTemplateName(templateName); Collection<PsiFile> psiFiles = new HashSet<>(); for (TwigPath twigPath : getTwigNamespaces(project)) { if(!twigPath.isEnabled()) { continue; } if(normalizedTemplateName.startsWith("@")) { // @Namespace/base.html.twig // @Namespace/folder/base.html.twig if(normalizedTemplateName.length() > 1 && twigPath.getNamespaceType() != TwigPathIndex.NamespaceType.BUNDLE) { int i = normalizedTemplateName.indexOf("/"); if(i > 0) { String templateNs = normalizedTemplateName.substring(1, i); if(twigPath.getNamespace().equals(templateNs)) { addFileInsideTwigPath(project, normalizedTemplateName.substring(i + 1), psiFiles, twigPath); } } } } else if(normalizedTemplateName.startsWith(":")) { // ::base.html.twig // :Foo:base.html.twig if(normalizedTemplateName.length() > 1 && twigPath.getNamespaceType() == TwigPathIndex.NamespaceType.BUNDLE && twigPath.isGlobalNamespace()) { String templatePath = StringUtils.strip(normalizedTemplateName.replace(":", "/"), "/"); addFileInsideTwigPath(project, templatePath, psiFiles, twigPath); } } else { // FooBundle::base.html.twig // FooBundle:Bar:base.html.twig if(twigPath.getNamespaceType() == TwigPathIndex.NamespaceType.BUNDLE) { int i = normalizedTemplateName.indexOf(":"); if(i > 0) { String templateNs = normalizedTemplateName.substring(0, i); if(twigPath.getNamespace().equals(templateNs)) { String templatePath = StringUtils.strip(normalizedTemplateName.substring(i + 1).replace(":", "/").replace("//", "/"), "/"); addFileInsideTwigPath(project, templatePath, psiFiles, twigPath); } } } // form_div_layout.html.twig if(twigPath.isGlobalNamespace() && twigPath.getNamespaceType() == TwigPathIndex.NamespaceType.ADD_PATH) { String templatePath = StringUtils.strip(normalizedTemplateName.replace(":", "/"), "/"); addFileInsideTwigPath(project, templatePath, psiFiles, twigPath); } // Bundle overwrite: // FooBundle:index.html -> app/views/FooBundle:index.html if(twigPath.isGlobalNamespace() && !normalizedTemplateName.startsWith(":") && !normalizedTemplateName.startsWith("@")) { String templatePath = StringUtils.strip(normalizedTemplateName.replace(":", "/").replace("//", "/"), "/"); addFileInsideTwigPath(project, templatePath, psiFiles, twigPath); } } } psiFiles.addAll(getTemplateOverwrites(project, normalizedTemplateName)); return psiFiles.toArray(new PsiFile[psiFiles.size()]); } /** * Collects overwritten templates * * app/Resources/MyUserBundle/views/layout.html.twig * src/Acme/UserBundle/Resources/views/layout.html.twig <- getParent = MyUserBundle */ private static Collection<PsiFile> getTemplateOverwrites(@NotNull Project project, @NotNull String normalizedTemplateName) { // Bundle overwrite: if(normalizedTemplateName.startsWith(":") || normalizedTemplateName.startsWith("@")) { return Collections.emptyList(); } String templatePath = StringUtils.strip(normalizedTemplateName.replace(":", "/").replace("//", "/"), "/"); int i = templatePath.indexOf("Bundle/"); if( i == -1) { return Collections.emptyList(); } Collection<VirtualFile> files = new HashSet<>(); String bundle = templatePath.substring(0, i + 6); // invalid Bundle in path condition if(bundle.contains("/")) { return Collections.emptyList(); } VirtualFile relativeFile = VfsUtil.findRelativeFile( project.getBaseDir(), String.format("app/Resources/%s/views/%s", bundle, templatePath.substring(i + 7)).split("/") ); if(relativeFile != null) { files.add(relativeFile); } // find parent bundles for (SymfonyBundle symfonyBundle : new SymfonyBundleUtil(project).getBundles()) { String parentBundle = symfonyBundle.getParentBundleName(); if(parentBundle != null && bundle.equals(parentBundle)) { relativeFile = symfonyBundle.getRelative(String.format("Resources/views/%s", templatePath.substring(i + 7))); if(relativeFile != null) { files.add(relativeFile); } } } return PsiElementUtils.convertVirtualFilesToPsiFiles(project, files); } private static void addFileInsideTwigPath(Project project, String templatePath, Collection<PsiFile> psiFiles, TwigPath twigPath) { String[] split = templatePath.split("/"); VirtualFile virtualFile = VfsUtil.findRelativeFile(twigPath.getDirectory(project), split); if(virtualFile != null) { PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); if(psiFile != null) { psiFiles.add(psiFile); } } } public static List<TwigPath> getTwigNamespaces(@NotNull Project project) { return getTwigNamespaces(project, true); } @NotNull public static List<TwigPath> getTwigNamespaces(@NotNull Project project, boolean includeSettings) { List<TwigPath> twigPaths = new ArrayList<>(); // load extension TwigNamespaceExtensionParameter parameter = new TwigNamespaceExtensionParameter(project); for (TwigNamespaceExtension namespaceExtension : EXTENSIONS.getExtensions()) { twigPaths.addAll(namespaceExtension.getNamespaces(parameter)); } // disable namespace explicitly disabled by user for(TwigPath twigPath: twigPaths) { TwigNamespaceSetting twigNamespaceSetting = findManagedTwigNamespace(project, twigPath); if(twigNamespaceSetting != null) { twigPath.setEnabled(false); } } twigPaths = getUniqueTwigTemplatesList(twigPaths); if(!includeSettings) { return twigPaths; } List<TwigNamespaceSetting> twigNamespaceSettings = Settings.getInstance(project).twigNamespaces; if(twigNamespaceSettings != null) { for(TwigNamespaceSetting twigNamespaceSetting: twigNamespaceSettings) { if(twigNamespaceSetting.isCustom()) { twigPaths.add(new TwigPath(twigNamespaceSetting.getPath(), twigNamespaceSetting.getNamespace(), twigNamespaceSetting.getNamespaceType(), true).setEnabled(twigNamespaceSetting.isEnabled())); } } } return twigPaths; } /** * Build a unique path + namespace + type list * normalize also windows linux path */ @NotNull public static List<TwigPath> getUniqueTwigTemplatesList(@NotNull Collection<TwigPath> origin) { List<TwigPath> twigPaths = new ArrayList<>(); Set<String> hashes = new HashSet<>(); for (TwigPath twigPath : origin) { // normalize hash; for same path element // TODO: move to path object itself String hash = twigPath.getNamespaceType() + twigPath.getNamespace() + twigPath.getPath().replace("\\", "/"); if(hashes.contains(hash)) { continue; } twigPaths.add(twigPath); hashes.add(hash); } return twigPaths; } @Nullable public static String getTwigMethodString(@Nullable PsiElement transPsiElement) { if (transPsiElement == null) return null; ElementPattern<PsiElement> pattern = PlatformPatterns.psiElement(TwigTokenTypes.RBRACE); String currentText = transPsiElement.getText(); for (PsiElement child = transPsiElement.getNextSibling(); child != null; child = child.getNextSibling()) { currentText = currentText + child.getText(); if (pattern.accepts(child)) { //noinspection unchecked return currentText; } } return null; } /** * Check for {{ include('|') }} * * @param functionName twig function name */ public static ElementPattern<PsiElement> getPrintBlockFunctionPattern(String... functionName) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% include ['', ~ '', ''] %} */ public static ElementPattern<PsiElement> getIncludeTagArrayPattern() { //noinspection unchecked return PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(TwigElementTypes.INCLUDE_TAG) ) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_SQ) ) ) .beforeLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_SQ) ) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% include foo ? '' : '' %} * {% extends foo ? '' : '' %} */ public static ElementPattern<PsiElement> getTagTernaryPattern(@NotNull IElementType type) { //noinspection unchecked return PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(type) ) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.QUESTION) ) .withLanguage(TwigLanguage.INSTANCE), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(type) ) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.COLON) ) .withLanguage(TwigLanguage.INSTANCE) ); } /** * Check for {{ include('|') }}, {% include('|') %} * * @param functionName twig function name */ public static ElementPattern<PsiElement> getPrintBlockOrTagFunctionPattern(String... functionName) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.or( PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK), PlatformPatterns.psiElement(TwigElementTypes.TAG), PlatformPatterns.psiElement(TwigElementTypes.IF_TAG), PlatformPatterns.psiElement(TwigElementTypes.SET_TAG), PlatformPatterns.psiElement(TwigElementTypes.ELSE_TAG), PlatformPatterns.psiElement(TwigElementTypes.ELSEIF_TAG) ) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) .withLanguage(TwigLanguage.INSTANCE); } /** * Literal are fine in lexer so just extract the parameter * * {{ foo({'foobar', 'foo<caret>bar'}) }} * {{ foo({'fo<caret>obar'}) }} */ public static ElementPattern<PsiElement> getFunctionWithFirstParameterAsLiteralPattern(@NotNull String... functionName) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_CURL), PlatformPatterns.psiElement(TwigTokenTypes.COMMA) ) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {{ foo({'foo<caret>bar': 'foo'}}) }} * {{ foo({'foobar': 'foo', 'foo<caret>bar': 'foo'}}) }} */ public static ElementPattern<PsiElement> getFunctionWithFirstParameterAsKeyLiteralPattern(@NotNull String... functionName) { return PlatformPatterns.or( PlatformPatterns // ",'foo'", {'foo'" .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_CURL).withParent( PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) ) ).withLanguage(TwigLanguage.INSTANCE), PlatformPatterns // ",'foo'", {'foo'" .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.psiElement().with(new MyBeforeColonAndInsideLiteralPatternCondition()), PlatformPatterns.psiElement(TwigTokenTypes.COLON) ) ) ); } /** * {{ foo(12, {'foo<caret>bar': 'foo'}}) }} * {{ foo(12, {'foobar': 'foo', 'foo<caret>bar': 'foo'}}) }} */ public static ElementPattern<PsiElement> getFunctionWithSecondParameterAsKeyLiteralPattern(@NotNull String... functionName) { //noinspection unchecked PsiElementPattern.Capture<PsiElement> parameterPattern = PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(PARAMETER_WHITE_LIST), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping(PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.NUMBER) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) ) ); return PlatformPatterns.or( // {{ foo({'foobar': 'foo', 'foo<caret>bar': 'foo'}}) }} PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).withParent(parameterPattern) ).withLanguage(TwigLanguage.INSTANCE), // {{ foo(12, {'foo<caret>bar': 'foo'}}) }} PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_CURL).withParent(parameterPattern) ) .withLanguage(TwigLanguage.INSTANCE) ); } /** * Array values are not detected by lexer, lets do the magic on our own * * {{ foo(['foobar', 'foo<caret>bar']) }} * {{ foo(['fo<caret>obar']) }} */ public static ElementPattern<PsiElement> getFunctionWithFirstParameterAsArrayPattern(@NotNull String... functionName) { //noinspection unchecked // "foo(<caret>" PsiElementPattern.Capture<PsiElement> functionPattern = PlatformPatterns .psiElement(TwigTokenTypes.LBRACE_SQ) .afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf(functionName)) ) ); return PlatformPatterns.or( // {{ foo(['fo<caret>obar']) }} PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement().withElementType(PlatformPatterns.elementType().or( TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE )).afterLeafSkipping( PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), functionPattern ) ).withLanguage(TwigLanguage.INSTANCE), // {{ foo(['foobar', 'foo<caret>bar']) }} PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT).afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement().withElementType(PlatformPatterns.elementType().or( TwigTokenTypes.SINGLE_QUOTE, TwigTokenTypes.DOUBLE_QUOTE )).afterLeafSkipping( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.COMMA) ), functionPattern ) ) ).withLanguage(TwigLanguage.INSTANCE) ); } /** * {% render "foo" * * @param tagName twig tag name */ public static ElementPattern<PsiElement> getStringAfterTagNamePattern(@NotNull String tagName) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(tagName) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.TAG) ) .withLanguage(TwigLanguage.INSTANCE); } /** * Check for {% if foo is "foo" %} */ public static ElementPattern<PsiElement> getAfterIsTokenPattern() { //noinspection unchecked return PlatformPatterns .psiElement() .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.IS), PlatformPatterns.psiElement(TwigTokenTypes.NOT) ) ) .withLanguage(TwigLanguage.INSTANCE); } /** * Check for {% if foo is "foo foo" %} */ public static ElementPattern<PsiElement> getAfterIsTokenWithOneIdentifierLeafPattern() { //noinspection unchecked return PlatformPatterns .psiElement() .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).afterLeafSkipping(PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.IS), PlatformPatterns.psiElement(TwigTokenTypes.NOT) )) ) .withLanguage(TwigLanguage.INSTANCE); } /** * Extract text {% if foo is "foo foo" %} */ public static ElementPattern<PsiElement> getAfterIsTokenTextPattern() { //noinspection unchecked return PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.IS), PlatformPatterns.psiElement(TwigTokenTypes.NOT) ); } /** * {% if foo <carpet> %} * {% if foo.bar <carpet> %} * {% if "foo.bar" <carpet> %} * {% if 'foo.bar' <carpet> %} */ public static ElementPattern<PsiElement> getAfterOperatorPattern() { // @TODO: make it some nicer. can wrap it with whitespace //noinspection unchecked ElementPattern<PsiElement> or = PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT), PlatformPatterns.psiElement(TwigTokenTypes.DOT), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_SQ), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_SQ), PlatformPatterns.psiElement(TwigTokenTypes.NUMBER), PlatformPatterns.psiElement(TwigTokenTypes.FILTER) ); //noinspection unchecked ElementPattern<PsiElement> anIf = PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("if"), PlatformPatterns.psiElement(TwigTokenTypes.AND), PlatformPatterns.psiElement(TwigTokenTypes.OR) ); return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeaf(PlatformPatterns.not( PlatformPatterns.psiElement(TwigTokenTypes.DOT) )) .withParent( PlatformPatterns.psiElement(TwigElementTypes.IF_TAG) ) .afterLeafSkipping(or, anIf) .withLanguage(TwigLanguage.INSTANCE); } /** * Twig tag pattern with some hack * because we have invalid psi elements after STATEMENT_BLOCK_START * * {% <carpet> %} */ public static ElementPattern<PsiElement> getTagTokenParserPattern() { //noinspection unchecked return PlatformPatterns .psiElement() .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.STATEMENT_BLOCK_START), PlatformPatterns.psiElement(PsiErrorElement.class) ) ) .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.STATEMENT_BLOCK_END) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% FOOBAR "WANTED.html.twig" %} */ public static ElementPattern<PsiElement> getTagNameParameterPattern(@NotNull IElementType elementType, @NotNull String tagName) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(elementType) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(tagName) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% embed "vertical_boxes_skeleton.twig" %} */ public static ElementPattern<PsiElement> getEmbedPattern() { return getTagNameParameterPattern(TwigElementTypes.EMBED_TAG, "embed"); } public static ElementPattern<PsiElement> getPrintBlockFunctionPattern() { return PlatformPatterns.psiElement().withParent(PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK)).withLanguage(TwigLanguage.INSTANCE); } /** * {{ form(foo) }}, {{ foo }} * NOT: {{ foo.bar }}, {{ 'foo.bar' }} */ public static ElementPattern<PsiElement> getCompletablePattern() { //noinspection unchecked return PlatformPatterns.psiElement() .andNot( PlatformPatterns.or( PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement(TwigTokenTypes.DOT)), PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE)), PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE)) ) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class) ), PlatformPatterns.psiElement() ) .withParent(PlatformPatterns.or( PlatformPatterns.psiElement(TwigElementTypes.PRINT_BLOCK), PlatformPatterns.psiElement(TwigElementTypes.SET_TAG) )) .withLanguage(TwigLanguage.INSTANCE); } /** * {% block 'foo' %} * {% block "foo" %} * {% block foo %} */ public static ElementPattern<PsiElement> getBlockTagPattern() { //noinspection unchecked return PlatformPatterns.or( // {% block "foo" %} PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ) .withParent( PlatformPatterns.psiElement(TwigBlockTag.class) ) .withLanguage(TwigLanguage.INSTANCE), // {% block foo %} PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ) .withParent( PlatformPatterns.psiElement(TwigBlockTag.class) ) .withLanguage(TwigLanguage.INSTANCE) ); } /** * {% filter foo %} */ public static ElementPattern<PsiElement> getFilterTagPattern() { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.FILTER_TAG) ) .withLanguage(TwigLanguage.INSTANCE) ; } /** * use getStringAfterTagNamePattern @TODO * * {% trans_default_domain '<carpet>' %} * {% trans_default_domain <carpet> %} */ public static ElementPattern<PsiElement> getTransDefaultDomainPattern() { //noinspection unchecked return PlatformPatterns.or( PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent( PlatformPatterns.psiElement(TwigElementTypes.TAG) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("trans_default_domain") ).withLanguage(TwigLanguage.INSTANCE), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(TwigElementTypes.TAG) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("trans_default_domain") ).withLanguage(TwigLanguage.INSTANCE) ); } /** * {% trans with {'%name%': 'Fabien'} from "app" %} * {% transchoice count with {'%name%': 'Fabien'} from "app" %} */ public static ElementPattern<PsiElement> getTranslationTokenTagFromPattern() { //noinspection unchecked // we need to use withText check, because twig tags dont have childrenAllowToVisit to search for tag name return PlatformPatterns.or( PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent( PlatformPatterns.psiElement(TwigElementTypes.TAG).withText( PlatformPatterns.string().matches("\\{%\\s+(trans|transchoice).*") ) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("from") ).withLanguage(TwigLanguage.INSTANCE), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .withParent( PlatformPatterns.psiElement(TwigElementTypes.TAG).withText( PlatformPatterns.string().matches("\\{%\\s+(trans|transchoice).*") ) ) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("from") ).withLanguage(TwigLanguage.INSTANCE) ); } /** * trans({}, 'bar') * trans(null, 'bar') * transchoice(2, null, 'bar') */ public static ElementPattern<PsiElement> getTransDomainPattern() { //noinspection unchecked ElementPattern[] whitespace = { PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) }; ElementPattern[] placeholder = { PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER), PlatformPatterns.psiElement(TwigTokenTypes.DOT), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_SQ), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_SQ) }; return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.or( // trans({}, 'bar') PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_CURL).withParent( PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans")) ) ) ) ), // trans(null, 'bar') // trans(, 'bar') PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(placeholder), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans")) ) ), // transchoice(2, {}, 'bar') PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_CURL).withParent( PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(PARAMETER_WHITE_LIST), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("transchoice")) ) ) ) ) ), // transchoice(2, null, 'bar') PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(placeholder), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or(PARAMETER_WHITE_LIST), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).afterLeafSkipping( PlatformPatterns.or(whitespace), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("transchoice")) ) ) ) ) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {{ path('_profiler_info', {'<caret>'}) }} * {{ path('_profiler_info', {'foobar': 'foobar', '<caret>'}) }} */ public static ElementPattern<PsiElement> getPathAfterLeafPattern() { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_CURL) ) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.LITERAL).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.COMMA).afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE).withParent( PlatformPatterns.psiElement().withText(PlatformPatterns.string().contains("path")) ) ) ) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getParentFunctionPattern() { return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withText("parent") .beforeLeaf( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {{ foo.fo<caret>o }} */ public static ElementPattern<PsiElement> getTypeCompletionPattern() { return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeaf( PlatformPatterns.psiElement(TwigTokenTypes.DOT) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiComment> getTwigTypeDocBlock() { return PlatformPatterns.or( PlatformPatterns.psiComment().withText(PlatformPatterns.string().matches(TwigTypeResolveUtil.DEPRECATED_DOC_TYPE_PATTERN)).withLanguage(TwigLanguage.INSTANCE), PlatformPatterns.psiComment().withText(PlatformPatterns.string().matches(TwigTypeResolveUtil.DOC_TYPE_PATTERN_SINGLE)).withLanguage(TwigLanguage.INSTANCE) ); } /** * {# @see Foo.html.twig #} * {# @see \Class #} * {# \Class #} */ @NotNull public static ElementPattern<PsiComment> getTwigDocSeePattern() { return PlatformPatterns.or( PlatformPatterns.psiComment().withText(PlatformPatterns.string().matches(DOC_SEE_REGEX)).withLanguage(TwigLanguage.INSTANCE), PlatformPatterns.psiComment().withText(PlatformPatterns.string().matches(DOC_SEE_REGEX_WITHOUT_SEE)).withLanguage(TwigLanguage.INSTANCE) ); } public static PsiElementPattern.Capture<PsiComment> getTwigDocBlockMatchPattern(String pattern) { return PlatformPatterns .psiComment().withText(PlatformPatterns.string().matches(pattern)) .withLanguage(TwigLanguage.INSTANCE); } public static PsiElementPattern.Capture<PsiElement> getFormThemeFileTag() { return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .withParent(PlatformPatterns.psiElement().withText(PlatformPatterns.string().matches("\\{%\\s+form_theme.*"))) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getRoutePattern() { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER).withText("path") .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class) ), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getAutocompletableRoutePattern() { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("path"), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("url") ) ) .withLanguage(TwigLanguage.INSTANCE) ; } /** * {{ asset('<caret>') }} * {{ asset("<caret>") }} * {{ absolute_url("<caret>") }} */ public static ElementPattern<PsiElement> getAutocompletableAssetPattern() { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("asset"), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText("absolute_url") ) ) .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE) ) .withLanguage(TwigLanguage.INSTANCE) ; } public static ElementPattern<PsiElement> getTranslationPattern(String... type) { //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.FILTER).beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class) ), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText( PlatformPatterns.string().oneOf(type) ) ) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getAutocompletableAssetTag(String tagName) { // @TODO: withChild is not working so we are filtering on text // pattern to match '..foo.css' but not match eg ='...' // // {% stylesheets filter='cssrewrite' // 'assets/css/foo.css' //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(PsiWhiteSpace.class) ) .withParent(PlatformPatterns .psiElement(TwigCompositeElement.class) .withText(PlatformPatterns.string().startsWith("{% " + tagName)) ); } public static ElementPattern<PsiElement> getTemplateFileReferenceTagPattern() { return getTemplateFileReferenceTagPattern("extends", "from", "include", "use", "import", "embed"); } public static ElementPattern<PsiElement> getTemplateFileReferenceTagPattern(String... tagNames) { // {% include '<xxx>' with {'foo' : bar, 'bar' : 'foo'} %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LBRACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(PlatformPatterns.string().oneOf(tagNames)) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getTemplateImportFileReferenceTagPattern() { // first: {% from '<xxx>' import foo, <|> %} // second: {% from '<xxx>' import <|> %} // and not: {% from '<xxx>' import foo as <|> %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent(PlatformPatterns.psiElement(TwigElementTypes.IMPORT_TAG)) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.AS_KEYWORD), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER) ), PlatformPatterns.psiElement(TwigTokenTypes.IMPORT_KEYWORD) ).andNot(PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.AS_KEYWORD) ) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getForTagVariablePattern() { // {% for "user" %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.IN) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {{ 'test'|<caret> }} */ public static ElementPattern<PsiElement> getFilterPattern() { //noinspection unchecked return PlatformPatterns.psiElement() .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement().withElementType(TwigTokenTypes.FILTER) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getForTagInVariablePattern() { // {% for key, user in "users" %} // {% for user in "users" %} // {% for user in "users"|slice(0, 10) %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.IN) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getIfVariablePattern() { // {% if "var" %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText( PlatformPatterns.string().oneOfIgnoreCase("if") ) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.IF_TAG) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getIfConditionVariablePattern() { // {% if var < "var1" %} // {% if var == "var1" %} // and so on //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.LE), PlatformPatterns.psiElement(TwigTokenTypes.LT), PlatformPatterns.psiElement(TwigTokenTypes.GE), PlatformPatterns.psiElement(TwigTokenTypes.GT), PlatformPatterns.psiElement(TwigTokenTypes.EQ_EQ), PlatformPatterns.psiElement(TwigTokenTypes.NOT_EQ) ) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.IF_TAG) ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getTwigMacroNamePattern() { // {% macro <foo>(user) %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .withParent(PlatformPatterns.psiElement( TwigElementTypes.MACRO_TAG )) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("macro") ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getTwigTagUseNamePattern() { // {% use '<foo>' %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .withParent(PlatformPatterns.psiElement( TwigElementTypes.TAG )) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("use") ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getTwigMacroNameKnownPattern(String macroName) { // {% macro <foo>(user) %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER).withText(macroName) .withParent(PlatformPatterns.psiElement( TwigElementTypes.MACRO_TAG )) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText("macro") ) .withLanguage(TwigLanguage.INSTANCE); } public static ElementPattern<PsiElement> getSetVariablePattern() { // {% set count1 = "var" %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.EQ) ) .withParent( PlatformPatterns.psiElement(TwigElementTypes.SET_TAG) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% include 'foo.html.twig' {'foo': 'foo'} only %} */ public static ElementPattern<PsiElement> getIncludeOnlyPattern() { // {% set count1 = "var" %} //noinspection unchecked return PlatformPatterns .psiElement(TwigTokenTypes.IDENTIFIER).withText("only") .beforeLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE) ), PlatformPatterns.psiElement(TwigTokenTypes.STATEMENT_BLOCK_END) ) .withLanguage(TwigLanguage.INSTANCE); } /** * {% from _self import foo %} * {% from 'template_name' import foo %} */ public static ElementPattern<PsiElement> getFromTemplateElement() { return PlatformPatterns.or( PlatformPatterns .psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(PlatformPatterns.string().oneOf("from")) ) .withLanguage(TwigLanguage.INSTANCE), PlatformPatterns .psiElement(TwigTokenTypes.RESERVED_ID) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME).withText(PlatformPatterns.string().oneOf("from")) ) .withLanguage(TwigLanguage.INSTANCE) ); } public static ElementPattern<PsiElement> getVariableTypePattern() { //noinspection unchecked return PlatformPatterns.or( TwigHelper.getForTagInVariablePattern(), TwigHelper.getIfVariablePattern(), TwigHelper.getIfConditionVariablePattern(), TwigHelper.getSetVariablePattern() ); } public static Set<VirtualFile> resolveAssetsFiles(Project project, String templateName, String... fileTypes) { Set<VirtualFile> virtualFiles = new HashSet<>(); // {% javascripts [...] @jquery_js2'%} if(templateName.startsWith("@") && templateName.length() > 1) { TwigNamedAssetsServiceParser twigPathServiceParser = ServiceXmlParserFactory.getInstance(project, TwigNamedAssetsServiceParser.class); String assetName = templateName.substring(1); if(twigPathServiceParser.getNamedAssets().containsKey(assetName)) { for (String s : twigPathServiceParser.getNamedAssets().get(assetName)) { VirtualFile fileByURL = VfsUtil.findFileByIoFile(new File(s), false); if(fileByURL != null) { virtualFiles.add(fileByURL); } } } } // dont matches wildcard: // {% javascripts '@SampleBundle/Resources/public/js/*' %} // {% javascripts 'assets/js/*' %} // {% javascripts 'assets/js/*.js' %} Matcher matcher = Pattern.compile("^(.*[/\\\\])\\*([.\\w+]*)$").matcher(templateName); if (!matcher.find()) { // directly resolve VirtualFile projectAssetRoot = AssetDirectoryReader.getProjectAssetRoot(project); if(projectAssetRoot != null) { VirtualFile relativeFile = VfsUtil.findRelativeFile(projectAssetRoot, templateName); if(relativeFile != null) { virtualFiles.add(relativeFile); } } for (final AssetFile assetFile : new AssetDirectoryReader().setFilterExtension(fileTypes).setIncludeBundleDir(true).setProject(project).getAssetFiles()) { if(assetFile.toString().equals(templateName)) { virtualFiles.add(assetFile.getFile()); } } return virtualFiles; } String pathName = matcher.group(1); String fileExtension = matcher.group(2).length() > 0 ? matcher.group(2) : null; for (final AssetFile assetFile : new AssetDirectoryReader().setFilterExtension(fileTypes).setIncludeBundleDir(true).setProject(project).getAssetFiles()) { if(fileExtension == null && assetFile.toString().matches(Pattern.quote(pathName) + "(?!.*[/\\\\]).*\\.\\w+")) { virtualFiles.add(assetFile.getFile()); } else if(fileExtension != null && assetFile.toString().matches(Pattern.quote(pathName) + "(?!.*[/\\\\]).*" + Pattern.quote(fileExtension))) { virtualFiles.add(assetFile.getFile()); } } return virtualFiles; } /** * twig lexer just giving use a flat psi list for a block. we need custom stuff to resolve this * path('route', {'<parameter>': * path('route', {'<parameter>': '', '<parameter2>': '' */ @Nullable public static String getMatchingRouteNameOnParameter(@NotNull PsiElement startPsiElement) { PsiElement parent = startPsiElement.getParent(); if(parent.getNode().getElementType() != TwigElementTypes.LITERAL) { return null; } final String[] text = {null}; PsiElementUtils.getPrevSiblingOnCallback(parent, psiElement -> { IElementType elementType = psiElement.getNode().getElementType(); if(elementType == TwigTokenTypes.STRING_TEXT) { // Only valid string parameter "('foobar'," if(getFirstFunctionParameterAsStringPattern().accepts(psiElement)){ text[0] = psiElement.getText(); } return false; } // exit on invalid items return psiElement instanceof PsiWhiteSpace || elementType == TwigTokenTypes.WHITE_SPACE || elementType == TwigTokenTypes.COMMA || elementType == TwigTokenTypes.SINGLE_QUOTE || elementType == TwigTokenTypes.DOUBLE_QUOTE ; }); return text[0]; } /** * Only a parameter is valid "('foobar'," */ @NotNull private static PsiElementPattern.Capture<PsiElement> getFirstFunctionParameterAsStringPattern() { // string wrapped elements ElementPattern[] elementPatterns = { PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) }; return PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .beforeLeafSkipping(PlatformPatterns.or(elementPatterns), PlatformPatterns.psiElement(TwigTokenTypes.COMMA)) .afterLeafSkipping(PlatformPatterns.or(elementPatterns), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE)); } /** * Only a parameter is valid ", 'foobar' [,)]" */ @NotNull public static PsiElementPattern.Capture<PsiElement> getParameterAsStringPattern() { // string wrapped elements ElementPattern[] elementPatterns = { PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE) }; return PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .beforeLeafSkipping(PlatformPatterns.or(elementPatterns), PlatformPatterns.or(PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE))) .afterLeafSkipping(PlatformPatterns.or(elementPatterns), PlatformPatterns.psiElement(TwigTokenTypes.COMMA)); } public static Set<String> getTwigMacroSet(Project project) { SymfonyProcessors.CollectProjectUniqueKeys ymlProjectProcessor = new SymfonyProcessors.CollectProjectUniqueKeys(project, TwigMacroFunctionStubIndex.KEY); FileBasedIndexImpl.getInstance().processAllKeys(TwigMacroFunctionStubIndex.KEY, ymlProjectProcessor, project); return ymlProjectProcessor.getResult(); } public static Collection<PsiElement> getTwigMacroTargets(final Project project, final String name) { final Collection<PsiElement> targets = new ArrayList<>(); FileBasedIndexImpl.getInstance().getFilesWithKey(TwigMacroFunctionStubIndex.KEY, new HashSet<>(Collections.singletonList(name)), virtualFile -> { PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); if (psiFile != null) { PsiTreeUtil.processElements(psiFile, psiElement -> { if (getTwigMacroNameKnownPattern(name).accepts(psiElement)) { targets.add(psiElement); } return true; }); } return true; }, GlobalSearchScope.getScopeRestrictedByFileTypes(GlobalSearchScope.allScope(project), TwigFileType.INSTANCE)); return targets; } public static Collection<LookupElement> getTwigLookupElements(Project project) { VirtualFile baseDir = project.getBaseDir(); Collection<LookupElement> lookupElements = new ArrayList<>(); for (Map.Entry<String, VirtualFile> entry : TwigHelper.getTwigFilesByName(project).entrySet()) { lookupElements.add( new TemplateLookupElement(entry.getKey(), entry.getValue(), baseDir) ); } return lookupElements; } public static Collection<LookupElement> getAllTemplateLookupElements(Project project) { VirtualFile baseDir = project.getBaseDir(); Collection<LookupElement> lookupElements = new ArrayList<>(); for (Map.Entry<String, VirtualFile> entry : TwigHelper.getTemplateFilesByName(project).entrySet()) { lookupElements.add( new TemplateLookupElement(entry.getKey(), entry.getValue(), baseDir) ); } return lookupElements; } /** * {% include 'foo.html.twig' %} * {% include ['foo.html.twig', 'foo_1.html.twig'] %} */ @NotNull public static Collection<String> getIncludeTagStrings(@NotNull TwigTagWithFileReference twigTagWithFileReference) { if(twigTagWithFileReference.getNode().getElementType() != TwigElementTypes.INCLUDE_TAG) { return Collections.emptySet(); } Collection<String> strings = new LinkedHashSet<>(); PsiElement firstChild = twigTagWithFileReference.getFirstChild(); if(firstChild == null) { return strings; } // {% include 'foo.html.twig' %} PsiElement psiSingleString = PsiElementUtils.getNextSiblingOfType(firstChild, PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ) ); // single match dont need to go deeper in conditional check, so stop here if(psiSingleString != null) { String text = psiSingleString.getText(); if(StringUtils.isNotBlank(text)) { strings.add(text); } return strings; } // {% include ['foo.html.twig', 'foo_1.html.twig'] %} PsiElement arrayMatch = PsiElementUtils.getNextSiblingOfType(firstChild, PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_SQ)); if(arrayMatch != null) { visitStringInArray(arrayMatch, pair -> strings.add(pair.getFirst()) ); } PsiElement psiQuestion = PsiElementUtils.getNextSiblingOfType(firstChild, PlatformPatterns.psiElement(TwigTokenTypes.QUESTION)); if(psiQuestion != null) { strings.addAll(getTernaryStrings(psiQuestion)); } return strings; } /** * Visit string values of given array start brace * ["foobar"] */ public static void visitStringInArray(@NotNull PsiElement arrayStartBrace, @NotNull Consumer<Pair<String, PsiElement>> pair) { // match: "([,)''(,])" Collection<PsiElement> questString = PsiElementUtils.getNextSiblingOfTypes(arrayStartBrace, PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.LBRACE_SQ) ) ) .beforeLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.or( PlatformPatterns.psiElement(TwigTokenTypes.COMMA), PlatformPatterns.psiElement(TwigTokenTypes.RBRACE_SQ) ) ) ); for (PsiElement psiElement : questString) { String text = psiElement.getText(); if(StringUtils.isNotBlank(text)) { pair.consume(Pair.create(text, psiElement)); } } } /** * Find "extends" template in twig TwigExtendsTag * * {% extends '::base.html.twig' %} * {% extends request.ajax ? "base_ajax.html" : "base.html" %} * * @param twigExtendsTag Extends tag * @return valid template names */ @NotNull public static Collection<String> getTwigExtendsTagTemplates(@NotNull TwigExtendsTag twigExtendsTag) { Collection<String> strings = new HashSet<>(); PsiElement firstChild = twigExtendsTag.getFirstChild(); if(firstChild == null) { return strings; } // single {% extends '::base.html.twig' PsiElement psiSingleString = PsiElementUtils.getNextSiblingOfType(firstChild, PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ) ); // single match dont need to go deeper in conditional check, so stop here if(psiSingleString != null) { String text = psiSingleString.getText(); if(StringUtils.isNotBlank(text)) { strings.add(text); } return strings; } PsiElement psiQuestion = PsiElementUtils.getNextSiblingOfType(firstChild, PlatformPatterns.psiElement(TwigTokenTypes.QUESTION)); if(psiQuestion != null) { strings.addAll(getTernaryStrings(psiQuestion)); } return strings; } /** * "foo ? 'foo' : 'bar'" */ private static Collection<String> getTernaryStrings(@NotNull PsiElement psiQuestion) { Collection<String> strings = new TreeSet<>(); // match ? "foo" : PsiElement questString = PsiElementUtils.getNextSiblingOfType(psiQuestion, PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.QUESTION) ) .beforeLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.COLON) ) ); if(questString != null) { String text = questString.getText(); if(StringUtils.isNotBlank(text)) { strings.add(text); } } // : "foo" PsiElement colonString = PsiElementUtils.getNextSiblingOfType(psiQuestion, PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( STRING_WRAP_PATTERN, PlatformPatterns.psiElement(TwigTokenTypes.COLON) ) ); if(colonString != null) { String text = colonString.getText(); if(StringUtils.isNotBlank(text)) { strings.add(text); } } return strings; } /** * Collect all block names in file * * {% block sds %}, {% block 'sds' %}, {% block "sds" %} * {%- block sds -%} */ @NotNull public static Collection<TwigBlock> getBlocksInFile(@NotNull TwigFile twigFile) { Collection<TwigBlock> block = new ArrayList<>(); PsiElementPattern.Capture<PsiElement> pattern = null; for (TwigBlockTag twigBlockTag : PsiTreeUtil.collectElementsOfType(twigFile, TwigBlockTag.class)) { String name = twigBlockTag.getName(); if(name != null && StringUtils.isNotBlank(name)) { block.add(new TwigBlock(name, twigBlockTag)); } PsiElement firstChild = twigBlockTag.getFirstChild(); if(firstChild == null) { continue; } // provide support for quote wrapping // {% block 'sds' %} if(pattern == null) { pattern = PlatformPatterns.psiElement(TwigTokenTypes.STRING_TEXT) .afterLeafSkipping( PlatformPatterns.or( PlatformPatterns.psiElement(PsiWhiteSpace.class), PlatformPatterns.psiElement(TwigTokenTypes.WHITE_SPACE), PlatformPatterns.psiElement(TwigTokenTypes.DOUBLE_QUOTE), PlatformPatterns.psiElement(TwigTokenTypes.SINGLE_QUOTE) ), PlatformPatterns.psiElement(TwigTokenTypes.TAG_NAME) ); } PsiElement psiString = PsiElementUtils.getNextSiblingOfType(firstChild, pattern); if(psiString != null) { String text = psiString.getText(); if(StringUtils.isNotBlank(text)) { block.add(new TwigBlock(text, twigBlockTag)); } } } return block; } private static class MyTwigOnlyTemplateFileMapCachedValueProvider implements CachedValueProvider<TemplateFileMap> { private final Project project; public MyTwigOnlyTemplateFileMapCachedValueProvider(Project project) { this.project = project; } @Nullable @Override public Result<TemplateFileMap> compute() { return Result.create(getTemplateMapProxy(project, true, false), PsiModificationTracker.MODIFICATION_COUNT); } } private static class MyAllTemplateFileMapCachedValueProvider implements CachedValueProvider<TemplateFileMap> { private final Project project; public MyAllTemplateFileMapCachedValueProvider(Project project) { this.project = project; } @Nullable @Override public Result<TemplateFileMap> compute() { return Result.create(getTemplateMapProxy(project, true, true), PsiModificationTracker.MODIFICATION_COUNT); } } /** * Find block scope "embed" with self search or file context with foreign extends search * * {% embed "template.twig" %}{% block <caret> %} * {% block <caret> %} */ @NotNull public static Pair<PsiFile[], Boolean> findScopedFile(@NotNull PsiElement psiElement) { // {% embed "template.twig" %}{% block <caret> %} PsiElement firstParent = TwigUtil.getTransDefaultDomainScope(psiElement); // {% embed "template.twig" %} if(firstParent != null && firstParent.getNode().getElementType() == TwigElementTypes.EMBED_STATEMENT) { PsiElement embedTag = firstParent.getFirstChild(); if(embedTag.getNode().getElementType() == TwigElementTypes.EMBED_TAG) { PsiElement fileReference = ContainerUtil.find(YamlHelper.getChildrenFix(embedTag), psiElement12 -> TwigHelper.getTemplateFileReferenceTagPattern().accepts(psiElement12) ); if(fileReference != null && TwigUtil.isValidTemplateString(fileReference)) { String text = fileReference.getText(); if(StringUtils.isNotBlank(text)) { return Pair.create( TwigHelper.getTemplatePsiElements(psiElement.getProject(), text), true ); } } } return Pair.create(new PsiFile[] {}, true); } return Pair.create(new PsiFile[] {psiElement.getContainingFile()}, false); } /** * Collects Twig path in given yaml configuration * * twig: * paths: * "%kernel.root_dir%/../src/vendor/bundle/Resources/views": core */ @NotNull public static Collection<Pair<String, String>> getTwigPathFromYamlConfig(@NotNull YAMLFile yamlFile) { YAMLKeyValue yamlKeyValue = YAMLUtil.getQualifiedKeyInFile(yamlFile, "twig", "paths"); if(yamlKeyValue == null) { return Collections.emptyList(); } YAMLValue value = yamlKeyValue.getValue(); if(!(value instanceof YAMLMapping)) { return Collections.emptyList(); } Collection<Pair<String, String>> pair = new ArrayList<>(); for (YAMLPsiElement element : value.getYAMLElements()) { if(!(element instanceof YAMLKeyValue)) { continue; } String keyText = ((YAMLKeyValue) element).getKeyText(); if(StringUtils.isBlank(keyText)) { continue; } keyText = keyText.replace("\\", "/").replaceAll("/+", "/"); String valueText = ((YAMLKeyValue) element).getValueText(); // normalize null value if(valueText.equals("~")) { valueText = ""; } pair.add(Pair.create(valueText, keyText)); } return pair; } /** * Replaces parameters and relative replaces strings * * "%kernel.root_dir%/../src/vendor/bundle/Resources/views": core * "%kernel.root_dir%" => "/app/../src/vendor/bundle/Resources/views" */ @NotNull public static Collection<Pair<String, String>> getTwigPathFromYamlConfigResolved(@NotNull YAMLFile yamlFile) { VirtualFile baseDir = yamlFile.getProject().getBaseDir(); VirtualFile appDir = VfsUtil.findRelativeFile(baseDir, "app"); if(appDir == null) { return Collections.emptyList(); } Collection<Pair<String, String>> paths = new ArrayList<>(); for (Pair<String, String> pair : getTwigPathFromYamlConfig(yamlFile)) { String second = pair.getSecond(); if(!second.startsWith("%kernel.root_dir%")) { continue; } String path = StringUtils.stripStart(second.substring("%kernel.root_dir%".length()), "/"); VirtualFile relativeFile = VfsUtil.findRelativeFile(appDir, path.split("/")); if(relativeFile != null) { String relativePath = VfsUtil.getRelativePath(relativeFile, baseDir, '/'); if(relativePath != null) { paths.add(Pair.create(pair.getFirst(), relativePath)); } } } return paths; } /** * {% trans with {'%name%': 'Fabien'} from "aa" %} */ @Nullable public static String getDomainFromTranslationTag(@NotNull TwigCompositeElement twigCompositeElement) { // getChildren fix PsiElement firstChild = twigCompositeElement.getFirstChild(); if(firstChild == null) { return null; } // with from identifier and get text value PsiElement childrenOfType = PsiElementUtils.getNextSiblingOfType(firstChild, getTranslationTokenTagFromPattern()); if(childrenOfType == null) { return null; } String text = childrenOfType.getText(); if(StringUtils.isBlank(text)) { return null; } return text; } private static class MyLimitedVirtualFileVisitor extends VirtualFileVisitor { @NotNull private final TwigPathContentIterator twigPathContentIterator; private int childrenAllowToVisit = 1000; MyLimitedVirtualFileVisitor(@NotNull TwigPathContentIterator twigPathContentIterator, int maxDepth, int maxDirs) { super(VirtualFileVisitor.limit(maxDepth)); this.twigPathContentIterator = twigPathContentIterator; this.childrenAllowToVisit = maxDirs; } @Override public boolean visitFile(@NotNull VirtualFile virtualFile) { // per path directory limit if(virtualFile.isDirectory()) { if(childrenAllowToVisit-- <= 0) { return false; } } twigPathContentIterator.processFile(virtualFile); return super.visitFile(virtualFile); } } /** * trans({ * %some%': "button.reserve"|trans, * %vars%': "button.reserve"|trans({}, '<caret>') * }) */ private static class MyBeforeColonAndInsideLiteralPatternCondition extends PatternCondition<PsiElement> { MyBeforeColonAndInsideLiteralPatternCondition() { super("BeforeColonAndInsideLiteralPattern"); } @Override public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext processingContext) { IElementType elementType = psiElement.getNode().getElementType(); return elementType != TwigTokenTypes.LBRACE_CURL && elementType != TwigTokenTypes.RBRACE_CURL && elementType != TwigTokenTypes.COLON; } } }