package fr.adrienbrault.idea.symfony2plugin.templating; import com.intellij.codeInsight.completion.*; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.patterns.PlatformPatterns; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.ProcessingContext; import com.intellij.util.containers.ContainerUtil; import com.jetbrains.php.PhpIcons; import com.jetbrains.php.PhpIndex; import com.jetbrains.php.lang.psi.elements.*; import com.jetbrains.twig.TwigTokenTypes; import com.jetbrains.twig.elements.TwigElementTypes; import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons; import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent; import fr.adrienbrault.idea.symfony2plugin.TwigHelper; import fr.adrienbrault.idea.symfony2plugin.asset.dic.AssetDirectoryReader; import fr.adrienbrault.idea.symfony2plugin.asset.provider.AssetCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper; import fr.adrienbrault.idea.symfony2plugin.templating.completion.QuotedInsertionLookupElement; import fr.adrienbrault.idea.symfony2plugin.templating.dict.*; import fr.adrienbrault.idea.symfony2plugin.templating.globals.TwigGlobalEnum; import fr.adrienbrault.idea.symfony2plugin.templating.globals.TwigGlobalVariable; import fr.adrienbrault.idea.symfony2plugin.templating.globals.TwigGlobalsServiceParser; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigExtensionParser; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigTypeResolveUtil; import fr.adrienbrault.idea.symfony2plugin.templating.util.TwigUtil; import fr.adrienbrault.idea.symfony2plugin.templating.variable.TwigTypeContainer; import fr.adrienbrault.idea.symfony2plugin.templating.variable.collector.ControllerDocVariableCollector; import fr.adrienbrault.idea.symfony2plugin.templating.variable.dict.PsiVariable; 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.completion.FunctionInsertHandler; import fr.adrienbrault.idea.symfony2plugin.util.completion.PhpClassCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.util.controller.ControllerCompletionProvider; import fr.adrienbrault.idea.symfony2plugin.util.service.ServiceXmlParserFactory; import icons.TwigIcons; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; /** * @author Adrien Brault <adrien.brault@gmail.com> */ public class TwigTemplateCompletionContributor extends CompletionContributor { public TwigTemplateCompletionContributor() { extend(CompletionType.BASIC, PlatformPatterns.or( TwigHelper.getTemplateFileReferenceTagPattern(), TwigHelper.getTagTernaryPattern(TwigElementTypes.EXTENDS_TAG) ), new TemplateCompletionProvider()); // all file template "include" pattern extend(CompletionType.BASIC, PlatformPatterns.or( TwigHelper.getPrintBlockFunctionPattern("include", "source"), TwigHelper.getIncludeTagArrayPattern(), TwigHelper.getTagTernaryPattern(TwigElementTypes.INCLUDE_TAG) ), new TemplateCompletionProvider()); // provides support for 'a<xxx>'|trans({'%foo%' : bar|default}, 'Domain') // provides support for 'a<xxx>'|transchoice(2, {'%foo%' : bar|default}, 'Domain') extend( CompletionType.BASIC, TwigHelper.getTranslationPattern("trans", "transchoice"), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } PsiElement psiElement = parameters.getPosition(); String domainName = TwigUtil.getPsiElementTranslationDomain(psiElement); resultSet.addAllElements(TranslationUtil.getTranslationLookupElementsOnDomain(psiElement.getProject(), domainName)); } } ); // provides support for 'a'|trans({'%foo%' : bar|default}, '<xxx>') // provides support for 'a'|transchoice(2, {'%foo%' : bar|default}, '<xxx>') extend( CompletionType.BASIC, TwigHelper.getTransDomainPattern(), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } if(PsiElementUtils.getPrevSiblingOfType(parameters.getPosition(), PlatformPatterns.psiElement(TwigTokenTypes.IDENTIFIER).withText(PlatformPatterns.string().oneOf("trans", "transchoice"))) == null) { return; } resultSet.addAllElements( TranslationUtil.getTranslationDomainLookupElements(parameters.getPosition().getProject()) ); } } ); // provides support for {% block | extend(CompletionType.BASIC, TwigHelper.getBlockTagPattern(), new BlockCompletionProvider()); // provides support for {% from 'twig..' import | extend( CompletionType.BASIC, TwigHelper.getTemplateImportFileReferenceTagPattern(), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } // find {% from "<template.name>" PsiElement psiElement = PsiElementUtils.getPrevSiblingOfType(parameters.getPosition(), TwigHelper.getFromTemplateElement()); if(psiElement == null) { return; } // {% from _self if(psiElement.getNode().getElementType() == TwigTokenTypes.RESERVED_ID) { attachLookupElements(resultSet, new PsiFile[]{psiElement.getContainingFile()}); return; } String templateName = psiElement.getText(); if(StringUtils.isBlank(templateName)) { return; } PsiFile[] twigFilesByName = TwigHelper.getTemplatePsiElements(parameters.getPosition().getProject(), templateName); if(twigFilesByName.length == 0) { return; } attachLookupElements(resultSet, twigFilesByName); } private void attachLookupElements(@NotNull CompletionResultSet resultSet, PsiFile[] psiFiles) { for (PsiFile psiFile : psiFiles) { for (TwigMacroTagInterface entry: TwigUtil.getMacros(psiFile)) { resultSet.addElement(LookupElementBuilder.create(entry.getName()).withTypeText(entry.getParameters(), true).withIcon(TwigIcons.TwigFileIcon)); } } } } ); // {{ 'test'|<caret> }} extend( CompletionType.BASIC, TwigHelper.getFilterPattern(), new FilterCompletionProvider() ); // provides support for {{ '<xxx>' }} extend( CompletionType.BASIC, TwigHelper.getCompletablePattern(), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } PsiElement psiElement = parameters.getPosition().getOriginalElement(); for(Map.Entry<String, TwigExtension> entry : new TwigExtensionParser(parameters.getPosition().getProject()).getFunctions().entrySet()) { resultSet.addElement(new TwigExtensionLookupElement(psiElement.getProject(), entry.getKey(), entry.getValue())); } // {% import 'forms.html' as forms %} for(TwigMacro twigMacro: TwigUtil.getImportedMacros(psiElement.getContainingFile())) { resultSet.addElement(LookupElementBuilder.create(twigMacro.getName()).withTypeText(twigMacro.getTemplate(), true).withIcon(TwigIcons.TwigFileIcon).withInsertHandler(FunctionInsertHandler.getInstance())); } // {% from 'forms.html' import input as input_field, textarea %} for(TwigMacro twigMacro: TwigUtil.getImportedMacrosNamespaces(psiElement.getContainingFile())) { resultSet.addElement(LookupElementBuilder.create(twigMacro.getName()) .withTypeText(twigMacro.getTemplate(), true) .withIcon(TwigIcons.TwigFileIcon).withInsertHandler(FunctionInsertHandler.getInstance()) ); } for(TwigSet twigSet: TwigUtil.getSetDeclaration(psiElement.getContainingFile())) { resultSet.addElement(LookupElementBuilder.create(twigSet.getName()).withTypeText("set", true)); } for(Map.Entry<String, PsiVariable> entry: TwigTypeResolveUtil.collectScopeVariables(parameters.getOriginalPosition()).entrySet()) { resultSet.addElement(LookupElementBuilder.create(entry.getKey()).withTypeText(TwigTypeResolveUtil.getTypeDisplayName(psiElement.getProject(), entry.getValue().getTypes()), true).withIcon(PhpIcons.CLASS)); } for(Map.Entry<String, TwigGlobalVariable> entry: ServiceXmlParserFactory.getInstance(psiElement.getProject(), TwigGlobalsServiceParser.class).getTwigGlobals().entrySet()) { if(entry.getValue().getTwigGlobalEnum() == TwigGlobalEnum.TEXT) { resultSet.addElement(LookupElementBuilder.create(entry.getKey()).withTypeText(entry.getValue().getValue(), true).withIcon(PhpIcons.CONSTANT)); } } } } ); // {% for user in "users" %} extend( CompletionType.BASIC, TwigHelper.getVariableTypePattern(), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } PsiElement psiElement = parameters.getOriginalPosition(); if(psiElement == null) { return; } for(Map.Entry<String, PsiVariable> entry: TwigTypeResolveUtil.collectScopeVariables(parameters.getOriginalPosition()).entrySet()) { resultSet.addElement(LookupElementBuilder.create(entry.getKey()).withTypeText(TwigTypeResolveUtil.getTypeDisplayName(psiElement.getProject(), entry.getValue().getTypes())).withIcon(PhpIcons.CLASS)); } } } ); // {% trans_default_domain <> %} // {% trans_default_domain '<>' %} extend(CompletionType.BASIC, TwigHelper.getTransDefaultDomainPattern(), new TranslationDomainCompletionProvider()); // {% trans from "<carpet>" %} // {% transchoice from "<carpet>" %} extend(CompletionType.BASIC, TwigHelper.getTranslationTokenTagFromPattern(), new TranslationDomainCompletionProvider()); // {{ controller('<caret>') }} // {% render(controller('<caret>')) %} extend(CompletionType.BASIC, TwigHelper.getPrintBlockOrTagFunctionPattern("controller"), new ControllerCompletionProvider()); // {% render '<caret>' %}" extend(CompletionType.BASIC, TwigHelper.getStringAfterTagNamePattern("render"), new ControllerCompletionProvider()); // assets completion: // stylesheets and javascripts tags extend(CompletionType.BASIC, TwigHelper.getAutocompletableAssetPattern(), new AssetCompletionProvider().setAssetParser( new AssetDirectoryReader() )); extend(CompletionType.BASIC, TwigHelper.getAutocompletableAssetTag("stylesheets"), new AssetCompletionProvider().setIncludeCustom(true).setAssetParser( new AssetDirectoryReader().setFilterExtension(TwigHelper.CSS_FILES_EXTENSIONS).setIncludeBundleDir(true) )); extend(CompletionType.BASIC, TwigHelper.getAutocompletableAssetTag("javascripts"), new AssetCompletionProvider().setIncludeCustom(true).setAssetParser( new AssetDirectoryReader().setFilterExtension(TwigHelper.JS_FILES_EXTENSIONS).setIncludeBundleDir(true) )); // routing completion like path() function extend( CompletionType.BASIC, TwigHelper.getAutocompletableRoutePattern(), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } resultSet.addAllElements(RouteHelper.getRoutesLookupElements(parameters.getPosition().getProject())); } } ); // routing parameter completion extend( CompletionType.BASIC, TwigHelper.getPathAfterLeafPattern(), new PathParameterCompletionProvider() ); // simulated php completion var.<foo> extend( CompletionType.BASIC, TwigHelper.getTypeCompletionPattern(), new TypeCompletionProvider() ); // {% import 'detail/index.html.twig' as foobar %} // {{ foobar.<caret> }} extend( CompletionType.BASIC, TwigHelper.getTypeCompletionPattern(), new MyMacroImportAsCompletionProvider() ); // {# @var variable \Foo\ClassName #} // {# variable \Foo\ClassName #} extend( CompletionType.BASIC, TwigHelper.getTwigTypeDocBlock(), new PhpClassCompletionProvider(true).withTrimLeadBackslash(true) ); // {# @Container Foo:Bar #} extend( CompletionType.BASIC, TwigHelper.getTwigDocBlockMatchPattern(ControllerDocVariableCollector.DOC_PATTERN_COMPLETION), new ControllerCompletionProvider() ); // {% form_theme * %} extend( CompletionType.BASIC, TwigHelper.getFormThemeFileTag(), new FormThemeCompletionProvider() ); // {% <carpet> %} extend(CompletionType.BASIC, TwigHelper.getTagTokenParserPattern(), new TagTokenParserCompletionProvider() ); // {% if foo is defined %} extend( CompletionType.BASIC, TwigHelper.getAfterIsTokenPattern(), new TwigSimpleTestParametersCompletionProvider() ); // {% if foo.bar <carpet> %} extend( CompletionType.BASIC, TwigHelper.getAfterOperatorPattern(), new TwigOperatorCompletionProvider() ); // {% constant('FOO') %} extend( CompletionType.BASIC, TwigHelper.getPrintBlockOrTagFunctionPattern("constant"), new CompletionProvider<CompletionParameters>() { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { PsiElement position = parameters.getPosition(); if(!Symfony2ProjectComponent.isEnabled(position)) { return; } PhpIndex instance = PhpIndex.getInstance(position.getProject()); for(String constant : instance.getAllConstantNames(PrefixMatcher.ALWAYS_TRUE)) { resultSet.addElement(LookupElementBuilder.create(constant).withIcon(PhpIcons.CONSTANT)); } int foo = parameters.getOffset() - position.getTextRange().getStartOffset(); String before = position.getText().substring(0, foo); String[] parts = before.split("::"); if(parts.length >= 1) { PhpClass phpClass = PhpElementsUtil.getClassInterface(position.getProject(), parts[0].replace("\\\\", "\\")); if(phpClass != null) { phpClass.getFields().stream().filter(Field::isConstant).forEach(field -> resultSet.addElement(LookupElementBuilder.create(phpClass.getPresentableFQN().replace("\\", "\\\\") + "::" + field.getName()).withIcon(PhpIcons.CONSTANT)) ); } } } } ); } private static class FilterCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } // move this stuff to pattern fixed event stopping by phpstorm PsiElement currElement = parameters.getPosition().getOriginalElement(); PsiElement prevElement = currElement.getPrevSibling(); if ((prevElement != null) && ((prevElement instanceof PsiWhiteSpace))) prevElement = prevElement.getPrevSibling(); if ((prevElement != null) && (prevElement.getNode().getElementType() == TwigTokenTypes.FILTER)) { for(Map.Entry<String, TwigExtension> entry : new TwigExtensionParser(parameters.getPosition().getProject()).getFilters().entrySet()) { resultSet.addElement(new TwigExtensionLookupElement(currElement.getProject(), entry.getKey(), entry.getValue())); } } } } /** * Parse all classes that implements Twig_TokenParserInterface::getTag * and provide completion on string */ private static class TagTokenParserCompletionProvider extends CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } Collection<PhpClass> allSubclasses = PhpIndex.getInstance(parameters.getPosition().getProject()).getAllSubclasses("\\Twig_TokenParserInterface"); for (PhpClass allSubclass : allSubclasses) { // we dont want to see test extension like "ยง" if(allSubclass.getName().endsWith("Test") || allSubclass.getContainingFile().getVirtualFile().getNameWithoutExtension().endsWith("Test")) { continue; } Method getTag = allSubclass.findMethodByName("getTag"); if(getTag == null) { continue; } // get string return value PhpReturn childrenOfType = PsiTreeUtil.findChildOfType(getTag, PhpReturn.class); if(childrenOfType != null) { PhpPsiElement returnValue = childrenOfType.getFirstPsiChild(); if(returnValue instanceof StringLiteralExpression) { String contents = ((StringLiteralExpression) returnValue).getContents(); if(StringUtils.isNotBlank(contents)) { resultSet.addElement(LookupElementBuilder.create(contents).withIcon(Symfony2Icons.SYMFONY)); } } } } // add special tag ending, provide a static list. there no suitable safe way to extract them // search able via: "return $token->test(array('end" for (String s : new String[]{"endtranschoice", "endtrans"}) { resultSet.addElement(LookupElementBuilder.create(s).withIcon(Symfony2Icons.SYMFONY)); } } } private static class TranslationDomainCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } List<LookupElement> translationDomainLookupElements = TranslationUtil.getTranslationDomainLookupElements( parameters.getPosition().getProject() ); // decorate lookup elements to attach insert handle for quoted wrap resultSet.addAllElements( ContainerUtil.map(translationDomainLookupElements, QuotedInsertionLookupElement::new) ); } } private static class TwigSimpleTestParametersCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { PsiElement position = parameters.getPosition(); if(!Symfony2ProjectComponent.isEnabled(position)) { return; } Project project = position.getProject(); for (Map.Entry<String, TwigExtension> entry : new TwigExtensionParser(project).getSimpleTest().entrySet()) { resultSet.addElement(new TwigExtensionLookupElement(project, entry.getKey(), entry.getValue())); } } } private static class TwigOperatorCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { PsiElement position = parameters.getPosition(); if(!Symfony2ProjectComponent.isEnabled(position)) { return; } Project project = position.getProject(); for (Map.Entry<String, TwigExtension> entry : new TwigExtensionParser(project).getOperators().entrySet()) { resultSet.addElement(new TwigExtensionLookupElement(project, entry.getKey(), entry.getValue())); } } } private class FormThemeCompletionProvider extends CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { PsiElement psiElement = parameters.getOriginalPosition(); if(psiElement == null || !Symfony2ProjectComponent.isEnabled(psiElement)) { return; } resultSet.addAllElements(TwigHelper.getTwigLookupElements(parameters.getPosition().getProject())); } } private class TypeCompletionProvider extends CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext paramProcessingContext, @NotNull CompletionResultSet resultSet) { PsiElement psiElement = parameters.getOriginalPosition(); if(psiElement == null || !Symfony2ProjectComponent.isEnabled(psiElement)) { return; } String[] possibleTypes = TwigTypeResolveUtil.formatPsiTypeName(psiElement); // find core function for that for(TwigTypeContainer twigTypeContainer: TwigTypeResolveUtil.resolveTwigMethodName(psiElement, possibleTypes)) { if(twigTypeContainer.getPhpNamedElement() instanceof PhpClass) { for(Method method: ((PhpClass) twigTypeContainer.getPhpNamedElement()).getMethods()) { if(!(!method.getModifier().isPublic() || method.getName().startsWith("set") || method.getName().startsWith("__"))) { resultSet.addElement(new PhpTwigMethodLookupElement(method)); } } for(Field field: ((PhpClass) twigTypeContainer.getPhpNamedElement()).getFields()) { if(field.getModifier().isPublic()) { resultSet.addElement(new PhpTwigMethodLookupElement(field)); } } } if(twigTypeContainer.getStringElement() != null) { resultSet.addElement(LookupElementBuilder.create(twigTypeContainer.getStringElement())); } } } } private class PathParameterCompletionProvider extends CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext paramProcessingContext, @NotNull CompletionResultSet paramCompletionResultSet) { PsiElement psiElement = parameters.getOriginalPosition(); if(psiElement == null || !Symfony2ProjectComponent.isEnabled(psiElement)) { return; } String routeName = TwigHelper.getMatchingRouteNameOnParameter(parameters.getOriginalPosition()); if(routeName == null) { return; } paramCompletionResultSet.addAllElements(Arrays.asList( RouteHelper.getRouteParameterLookupElements(parameters.getPosition().getProject(), routeName)) ); } } private class TemplateCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { if(!Symfony2ProjectComponent.isEnabled(parameters.getPosition())) { return; } resultSet.addAllElements(TwigHelper.getTwigLookupElements(parameters.getPosition().getProject())); } } private class BlockCompletionProvider extends CompletionProvider<CompletionParameters> { public void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet resultSet) { PsiElement position = parameters.getPosition(); if(!Symfony2ProjectComponent.isEnabled(position)) { return; } // wtf: need to prefix the block tag itself. remove this behavior and strip for new Matcher // Find first Identifier "b" char or fallback to empty: // "{% block b", "{% block" String blockNamePrefix = resultSet.getPrefixMatcher().getPrefix(); int spacePos = blockNamePrefix.lastIndexOf(' '); blockNamePrefix = spacePos > 0 ? blockNamePrefix.substring(spacePos + 1) : ""; CompletionResultSet myResultSet = resultSet.withPrefixMatcher(blockNamePrefix); // collect blocks in all related files Pair<PsiFile[], Boolean> scopedContext = TwigHelper.findScopedFile(position); List<TwigBlock> blocks = new TwigBlockParser(TwigHelper.getTwigFilesByName(position.getProject())) .withSelfBlocks(scopedContext.getSecond()) .visit(scopedContext.getFirst()); Set<String> uniqueList = new HashSet<>(); for (TwigBlock block : blocks) { if(uniqueList.contains(block.getName())) { continue; } uniqueList.add(block.getName()); myResultSet.addElement(new TwigBlockLookupElement(block)); } } } /** * {% import 'detail/index.html.twig' as foobar %} * {{ foobar.<caret> }} */ private static class MyMacroImportAsCompletionProvider extends CompletionProvider<CompletionParameters> { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext processingContext, @NotNull CompletionResultSet resultSet) { PsiElement psiElement = parameters.getOriginalPosition(); if(psiElement == null || !Symfony2ProjectComponent.isEnabled(psiElement)) { return; } // "foobar".<caret> String[] possibleTypes = TwigTypeResolveUtil.formatPsiTypeName(psiElement); if(possibleTypes.length != 1) { return; } resultSet.addAllElements( TwigUtil.getImportedMacrosNamespaces(psiElement.getContainingFile()).stream() .filter(twigMacro -> twigMacro.getName().startsWith(possibleTypes[0] + ".") ) .map((Function<TwigMacro, LookupElement>) twigMacro -> LookupElementBuilder.create(twigMacro.getName().substring(possibleTypes[0].length() + 1)) .withTypeText(twigMacro.getTemplate(), true) .withTailText(twigMacro.getParameter(), true) .withIcon(TwigIcons.TwigFileIcon).withInsertHandler(FunctionInsertHandler.getInstance()) ).collect(Collectors.toList()) ); } } }