org.intellij.erlang.completion.ErlangCompletionContributor.java Source code

Java tutorial

Introduction

Here is the source code for org.intellij.erlang.completion.ErlangCompletionContributor.java

Source

/*
 * Copyright 2012-2015 Sergey Ignatov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.intellij.erlang.completion;

import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
import com.intellij.psi.formatter.FormatterUtil;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.Function;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import org.intellij.erlang.ErlangFileType;
import org.intellij.erlang.ErlangTypes;
import org.intellij.erlang.formatter.settings.ErlangCodeStyleSettings;
import org.intellij.erlang.icons.ErlangIcons;
import org.intellij.erlang.index.ErlangApplicationIndex;
import org.intellij.erlang.index.ErlangAtomIndex;
import org.intellij.erlang.index.ErlangModuleIndex;
import org.intellij.erlang.parser.ErlangParserUtil;
import org.intellij.erlang.psi.*;
import org.intellij.erlang.psi.impl.ErlangPsiImplUtil;
import org.intellij.erlang.rebar.util.RebarConfigUtil;
import org.intellij.erlang.roots.ErlangIncludeDirectoryUtil;
import org.intellij.erlang.sdk.ErlangSystemUtil;
import org.intellij.erlang.stubs.index.ErlangBehaviourModuleIndex;
import org.intellij.erlang.types.ErlangExpressionType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.*;

import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.patterns.StandardPatterns.instanceOf;
import static org.intellij.erlang.psi.impl.ErlangPsiImplUtil.*;

public class ErlangCompletionContributor extends CompletionContributor {
    private static final int ATOM_PRIORITY = 50;
    public static final int TYPE_PRIORITY = 10;
    public static final int MODULE_FUNCTIONS_PRIORITY = -4;
    public static final int BIF_PRIORITY = -5;
    public static final int EXTERNAL_FUNCTIONS_PRIORITY = -7;
    public static final int KEYWORD_PRIORITY = -10;
    private static final int MODULE_PRIORITY = -15;

    @Override
    public void beforeCompletion(@NotNull CompletionInitializationContext context) {
        PsiFile file = context.getFile();
        if (ErlangParserUtil.isApplicationConfigFileType(file))
            return;
        int startOffset = context.getStartOffset();
        PsiElement elementAt = file.findElementAt(startOffset);
        PsiElement parent = elementAt == null ? null : elementAt.getParent();
        ErlangExport export = PsiTreeUtil.getPrevSiblingOfType(parent, ErlangExport.class);
        ErlangExportTypeAttribute exportType = PsiTreeUtil.getParentOfType(elementAt,
                ErlangExportTypeAttribute.class);
        ErlangRecordTuple recordTuple = PsiTreeUtil.getPrevSiblingOfType(parent, ErlangRecordTuple.class);
        PsiElement previousByOffset = elementAt != null ? PsiTreeUtil.prevVisibleLeaf(elementAt)
                : startOffset > 0 ? file.findElementAt(startOffset - 1) : null;
        ErlangBehaviour behaviour = PsiTreeUtil.getParentOfType(elementAt, ErlangBehaviour.class);
        //noinspection unchecked
        ErlangCompositeElement typeParent = PsiTreeUtil.getParentOfType(elementAt, ErlangTypeSig.class,
                ErlangTypedRecordFields.class, ErlangTypeDefinition.class);
        if (parent instanceof ErlangExport
                || PsiTreeUtil.getParentOfType(parent, ErlangExportFunctions.class, false) != null
                || parent instanceof ErlangImportDirective || parent instanceof ErlangImportFunctions
                || exportType != null || export != null || prevIsRadix(elementAt)
                || is(previousByOffset, ErlangTypes.ERL_RADIX)
                || (previousByOffset != null && previousByOffset.getParent() instanceof ErlangRecordField
                        || parent instanceof ErlangRecordTuple || recordTuple != null
                        || parent instanceof ErlangRecordField) && !is(previousByOffset, ErlangTypes.ERL_OP_EQ)
                || typeParent != null || isRecordFunctionCallCompletion(previousByOffset) || behaviour != null) {
            context.setDummyIdentifier("a");
        }
    }

    private static boolean isRecordFunctionCallCompletion(@Nullable PsiElement previousByOffset) {
        PsiElement prevSibling = previousByOffset != null ? previousByOffset.getPrevSibling() : null;
        if (prevSibling == null)
            return false;
        return is(previousByOffset, ErlangTypes.ERL_COMMA)
                && inIsRecord(0).accepts(prevSibling, new ProcessingContext());
    }

    public ErlangCompletionContributor() {
        extend(CompletionType.BASIC, psiElement().inFile(instanceOf(ErlangFile.class)),
                new CompletionProvider<CompletionParameters>() {
                    @Override
                    protected void addCompletions(@NotNull CompletionParameters parameters,
                            ProcessingContext context, @NotNull CompletionResultSet result) {
                        PsiElement position = parameters.getPosition();
                        PsiFile file = position.getContainingFile();
                        if (ErlangParserUtil.isApplicationConfigFileType(file))
                            return;

                        boolean inConsole = ErlangParserUtil.isConsole(file);
                        PsiElement parent = position.getParent();
                        if (parent instanceof ErlangAtom) {
                            // This assignment makes the code below work the same way as it did before ErlangAtom stopped being a leaf element.
                            parent = parent.getParent();
                        }
                        PsiElement grandPa = parent.getParent();
                        PsiElement originalPosition = parameters.getOriginalPosition();
                        PsiElement originalParent = originalPosition != null ? originalPosition.getParent() : null;

                        if (originalParent instanceof ErlangIncludeString
                                && originalPosition instanceof LeafPsiElement
                                && ErlangTypes.ERL_STRING == ((LeafPsiElement) originalPosition).getElementType()) {
                            TextRange range = new TextRange(
                                    ((LeafPsiElement) originalPosition).getStartOffset() + 1,
                                    parameters.getOffset());
                            String includeText = range.getLength() >= 0 ? range.substring(file.getText()) : "";
                            if (grandPa instanceof ErlangInclude) {
                                result.addAllElements(getModulePathLookupElements(file, includeText));
                            } else if (grandPa instanceof ErlangIncludeLib) {
                                result.addAllElements(getLibPathLookupElements(file, includeText));
                            }
                        }

                        if (originalParent instanceof ErlangStringLiteral || originalPosition instanceof PsiComment)
                            return;

                        if (grandPa instanceof ErlangType) {
                            result.addAllElements(getTypeLookupElements(file, true, false));
                        }

                        if (originalParent instanceof ErlangRecordExpression || prevIsRadix(originalPosition)
                                || prevIsRadix(grandPa)) {
                            result.addAllElements(getRecordLookupElements(file));
                        } else if (grandPa instanceof ErlangExportFunction && file instanceof ErlangFile) {
                            result.addAllElements(
                                    createFunctionLookupElements(((ErlangFile) file).getFunctions(), true));
                        } else {
                            ErlangColonQualifiedExpression colonQualified = PsiTreeUtil.getParentOfType(position,
                                    ErlangColonQualifiedExpression.class);
                            if (colonQualified != null
                                    && (PsiTreeUtil.getParentOfType(position, ErlangClauseBody.class) != null
                                            || inConsole)) {
                                ErlangQAtom moduleAtom = getQAtom(colonQualified);

                                result.addAllElements(getFunctionLookupElements(file, false, moduleAtom));

                                // when completing at my_module:<caret>, ... the cursor is actually located at comma and not at the colon qualified expression
                                ErlangColonQualifiedExpression originalColonQExpr = PsiTreeUtil
                                        .getParentOfType(originalPosition, ErlangColonQualifiedExpression.class);
                                String moduleName = moduleAtom != null ? getName(moduleAtom) : null;
                                String prefix = originalColonQExpr != null
                                        ? StringUtil.first(originalColonQExpr.getText(),
                                                parameters.getOffset() - originalColonQExpr.getTextOffset(), false)
                                        : moduleName != null ? moduleName + ":" : null;
                                (StringUtil.isEmpty(prefix) ? result
                                        : result.withPrefixMatcher(
                                                result.getPrefixMatcher().cloneWithPrefix(prefix))).addAllElements(
                                                        getAllExportedFunctionsWithModuleLookupElements(
                                                                file.getProject(), false, moduleName));
                            } else if (grandPa instanceof ErlangRecordField
                                    || grandPa instanceof ErlangRecordTuple) {
                                Pair<List<ErlangTypedExpr>, List<ErlangQAtom>> recordFields = getRecordFields(
                                        grandPa);
                                final boolean withoutEq = is(grandPa.getFirstChild(), ErlangTypes.ERL_DOT);
                                result.addAllElements(ContainerUtil.map(recordFields.first,
                                        e -> createFieldLookupElement(e.getProject(), e.getName(), withoutEq)));
                                result.addAllElements(ContainerUtil.map(recordFields.second,
                                        a -> createFieldLookupElement(a.getProject(), a.getText(), withoutEq)));
                                return;
                            } else if (grandPa instanceof ErlangMacros) {
                                return;
                            } else if (PsiTreeUtil.getParentOfType(position, ErlangExport.class) == null) {
                                //noinspection unchecked
                                boolean inside = PsiTreeUtil.getParentOfType(position, ErlangClauseBody.class,
                                        ErlangFunTypeSigs.class, ErlangTypeRef.class) != null;
                                //noinspection unchecked
                                boolean insideImport = PsiTreeUtil.getParentOfType(position,
                                        ErlangImportDirective.class,
                                        ErlangImportFunctions.class) instanceof ErlangImportDirective;
                                boolean insideBehaviour = PsiTreeUtil.getParentOfType(position,
                                        ErlangBehaviour.class) != null;
                                if (inside || inConsole && !isDot(position) || insideImport) {
                                    boolean withColon = !insideImport && null == PsiTreeUtil
                                            .getParentOfType(position, ErlangFunctionCallExpression.class, false);
                                    suggestModules(result, position, withColon);
                                } else if (insideBehaviour) {
                                    suggestBehaviours(result, position);
                                }
                            }
                            if (colonQualified == null && grandPa instanceof ErlangExpression
                                    && (inFunction(position) || inConsole || PsiTreeUtil.getParentOfType(position,
                                            ErlangTypedRecordFields.class) != null)) {
                                result.addAllElements(getFunctionLookupElements(file, false, null));
                                result.addAllElements(getAllExportedFunctionsWithModuleLookupElements(
                                        file.getProject(), false, null));
                            }

                            int invocationCount = parameters.getInvocationCount();
                            boolean moduleScope = invocationCount > 0 && invocationCount % 2 == 0;
                            boolean moduleWithDeps = invocationCount > 0 && invocationCount % 3 == 0;

                            if (moduleScope || moduleWithDeps) {
                                Project project = file.getProject();

                                Module module = ModuleUtilCore.findModuleForPsiElement(position);
                                GlobalSearchScope scope = module != null && moduleScope
                                        ? GlobalSearchScope.moduleScope(module)
                                        : ProjectScope.getProjectScope(project);

                                Collection<String> names = ErlangAtomIndex.getNames(project, scope);
                                for (String name : names) {
                                    result.addElement(PrioritizedLookupElement.withPriority(
                                            LookupElementBuilder.create(name).withLookupString(name.toLowerCase())
                                                    .withIcon(ErlangIcons.ATOM),
                                            ATOM_PRIORITY));
                                }
                            }

                            String shortcut = getActionShortcut(IdeActions.ACTION_CODE_COMPLETION);
                            if (invocationCount == 1 && new Random().nextBoolean()) {
                                result.addLookupAdvertisement("Press " + shortcut
                                        + " to activate atom completion from application scope");
                            }
                            if (moduleScope) {
                                result.addLookupAdvertisement(
                                        "Press " + shortcut + " to activate atom completion from project scope");
                            }
                        }

                        // fun foo/n
                        if (!(parent instanceof ErlangRecordExpression)
                                && grandPa instanceof ErlangFunctionWithArityVariables) {
                            result.addAllElements(ErlangPsiImplUtil.getFunctionLookupElements(file, true, null));
                        }
                    }

                    private boolean isDot(@NotNull PsiElement position) {
                        PsiElement dot = PsiTreeUtil.prevVisibleLeaf(position);
                        return dot != null && dot.getNode().getElementType() == ErlangTypes.ERL_DOT;
                    }
                });
        extend(CompletionType.SMART, psiElement().inside(true, psiElement(ErlangArgumentList.class)),
                new CompletionProvider<CompletionParameters>() {
                    @Override
                    protected void addCompletions(@NotNull CompletionParameters parameters,
                            ProcessingContext context, @NotNull CompletionResultSet result) {
                        PsiElement position = parameters.getPosition();
                        Set<ErlangExpressionType> expectedTypes = ErlangCompletionUtil
                                .expectedArgumentTypes(position);
                        if (expectedTypes.isEmpty())
                            return;

                        List<LookupElement> functionLookupElements = getFunctionLookupElements(
                                position.getContainingFile(), false, null);
                        for (LookupElement lookupElement : functionLookupElements) {
                            ErlangFunction function = ObjectUtils.tryCast(lookupElement.getPsiElement(),
                                    ErlangFunction.class);
                            ErlangExpressionType type = function != null
                                    ? ErlangExpressionType.calculateFunctionType(function)
                                    : null;
                            if (type != null && ErlangCompletionUtil.containsType(expectedTypes, type)) {
                                result.addElement(lookupElement);
                            }
                        }
                    }
                });
    }

    private static LookupElement createFieldLookupElement(@NotNull Project project, @NotNull String text,
            boolean withoutEq) {
        ErlangCodeStyleSettings customSettings = CodeStyleSettingsManager.getSettings(project)
                .getCustomSettings(ErlangCodeStyleSettings.class);
        boolean surroundWithSpaces = customSettings.SPACE_AROUND_EQ_IN_RECORDS;
        return LookupElementBuilder.create(text).withIcon(ErlangIcons.FIELD)
                .withInsertHandler(withoutEq ? null : new SingleCharInsertHandler('=', surroundWithSpaces));
    }

    @NotNull
    private static List<LookupElement> getLibPathLookupElements(@NotNull PsiFile file,
            @NotNull final String includeText) {
        if (FileUtil.isAbsolute(includeText))
            return Collections.emptyList();

        final VirtualFile virtualFile = file.getOriginalFile().getVirtualFile();
        List<LookupElement> result = new ArrayList<>();
        List<String> split = StringUtil.split(includeText, "/");
        if (split.isEmpty()) {
            split = Collections.singletonList("");
        }

        String appName = split.get(0);
        String pathSeparator = includeText.endsWith("/") ? "/" : "";
        String libRelativePath = split.size() > 1
                ? StringUtil.join(split.subList(1, split.size()), "/") + pathSeparator
                : "";
        boolean completingAppName = split.size() == 1 && !includeText.endsWith("/");
        List<VirtualFile> appDirs = getApplicationDirectories(file.getProject(), appName, !completingAppName);
        List<VirtualFile> matchingFiles = new ArrayList<>();

        for (VirtualFile appRoot : appDirs) {
            final String appFullName = appRoot != null ? appRoot.getName() : null;
            String appShortName = appFullName != null ? getAppShortName(appFullName) : null;
            if (appRoot == null)
                continue;
            if (completingAppName) {
                result.add(getDefaultPathLookupElementBuilder(includeText, appRoot, appShortName)
                        .withPresentableText(appShortName + "/").withTypeText("in " + appFullName, true));
                continue;
            }
            addMatchingFiles(appRoot, libRelativePath, matchingFiles);
            result.addAll(ContainerUtil.mapNotNull(matchingFiles,
                    (Function<VirtualFile, LookupElement>) f -> f.equals(virtualFile) ? null
                            : getDefaultPathLookupElementBuilder(includeText, f, null)
                                    .withTypeText("in " + appFullName, true)));
            matchingFiles.clear();
        }
        result.addAll(getModulePathLookupElements(file, includeText));
        return result;
    }

    @NotNull
    private static List<VirtualFile> getApplicationDirectories(@NotNull Project project,
            @NotNull final String appName, boolean nameIsComplete) {
        GlobalSearchScope searchScope = GlobalSearchScope.allScope(project);
        if (nameIsComplete) {
            return ContainerUtil.createMaybeSingletonList(
                    ErlangApplicationIndex.getApplicationDirectoryByName(appName, searchScope));
        }
        return ContainerUtil.filter(ErlangApplicationIndex.getAllApplicationDirectories(project, searchScope),
                virtualFile -> virtualFile != null && virtualFile.getName().startsWith(appName));
    }

    @NotNull
    private static String getAppShortName(@NotNull String appFullName) {
        int dashIdx = appFullName.indexOf('-');
        return dashIdx != -1 ? appFullName.substring(0, dashIdx) : appFullName;
    }

    @NotNull
    private static List<LookupElement> getModulePathLookupElements(@NotNull PsiFile file,
            @NotNull String includeText) {
        VirtualFile includeOwner = file.getOriginalFile().getVirtualFile();
        VirtualFile parentFile = includeOwner != null ? includeOwner.getParent() : null;
        List<LookupElement> result = new ArrayList<>();
        if (FileUtil.isAbsolute(includeText))
            return result;
        //search in this module's directory
        result.addAll(getModulePathLookupElements(parentFile, includeOwner, includeText));
        //search in include directories
        Module module = ModuleUtilCore.findModuleForPsiElement(file);
        for (VirtualFile includeDir : ErlangIncludeDirectoryUtil.getIncludeDirectories(module)) {
            result.addAll(getModulePathLookupElements(includeDir, includeOwner, includeText));
        }
        if (ErlangSystemUtil.isSmallIde()) {
            VirtualFile otpAppRoot = getContainingOtpAppRoot(file.getProject(), includeOwner);
            VirtualFile otpIncludeDirectory = otpAppRoot != null ? otpAppRoot.findChild("include") : null;
            result.addAll(getModulePathLookupElements(otpIncludeDirectory, includeOwner, includeText));
            ErlangFile rebarConfigPsi = RebarConfigUtil.getRebarConfig(file.getProject(), otpAppRoot);
            if (rebarConfigPsi != null && otpAppRoot != null) {
                for (String relativeIncludePath : ContainerUtil
                        .reverse(RebarConfigUtil.getIncludePaths(rebarConfigPsi))) {
                    VirtualFile includePath = VfsUtilCore.findRelativeFile(relativeIncludePath, otpAppRoot);
                    result.addAll(getModulePathLookupElements(includePath, includeOwner, includeText));
                }
            }
        }
        return result;
    }

    @NotNull
    private static List<LookupElement> getModulePathLookupElements(@Nullable VirtualFile includeDir,
            @Nullable final VirtualFile includeOwner, @NotNull final String includeText) {
        if (includeDir == null || !includeDir.isDirectory())
            return ContainerUtil.emptyList();
        List<VirtualFile> matchingFiles = new ArrayList<>();
        addMatchingFiles(includeDir, includeText, matchingFiles);
        return ContainerUtil.mapNotNull(matchingFiles,
                f -> f.equals(includeOwner) ? null : getDefaultPathLookupElementBuilder(includeText, f, null));
    }

    private static LookupElementBuilder getDefaultPathLookupElementBuilder(@NotNull String includeText,
            @NotNull VirtualFile lookedUpFile, @Nullable String appName) {
        String slash = lookedUpFile.isDirectory() ? "/" : "";
        Icon icon = lookedUpFile.isDirectory() ? ErlangIcons.MODULE : lookedUpFile.getFileType().getIcon();
        return LookupElementBuilder.create(getCompletedString(includeText, lookedUpFile, appName))
                .withPresentableText(lookedUpFile.getName() + slash).withIcon(icon)
                .withInsertHandler(new RunCompletionInsertHandler());
    }

    @NotNull
    private static String getCompletedString(@NotNull String beforeCompletion, @NotNull VirtualFile lookedUpFile,
            @Nullable String appName) {
        String prefixPath = beforeCompletion.substring(0, beforeCompletion.lastIndexOf('/') + 1);
        String completion = appName == null ? lookedUpFile.getName() : appName;
        String pathSeparator = appName != null || lookedUpFile.isDirectory() ? "/" : "";
        return prefixPath + completion + pathSeparator;
    }

    private static void addMatchingFiles(VirtualFile searchRoot, @NotNull String includeText,
            @NotNull List<VirtualFile> result) {
        String[] split = includeText.split("/");
        if (split.length == 0)
            return;

        int joinEndIndex = includeText.endsWith("/") ? split.length : split.length - 1;
        final String childPrefix = joinEndIndex == split.length ? "" : split[split.length - 1];
        VirtualFile directory = VfsUtilCore.findRelativeFile(StringUtil.join(split, 0, joinEndIndex, "/"),
                searchRoot);
        VirtualFile[] children = directory != null ? directory.getChildren() : null;
        if (children == null)
            return;

        JBIterable.of(children).filter(new Condition<VirtualFile>() {
            @Override
            public boolean value(VirtualFile child) {
                return child.getName().startsWith(childPrefix) && (child.isDirectory() || canBeIncluded(child));
            }

            private boolean canBeIncluded(@NotNull VirtualFile file) {
                FileType type = file.getFileType();
                return type == ErlangFileType.HEADER || type == ErlangFileType.MODULE;
            }
        }).addAllTo(result);
    }

    private static boolean prevIsRadix(@Nullable PsiElement element) {
        if (element == null)
            return false;
        ASTNode prev = FormatterUtil.getPreviousNonWhitespaceSibling(element.getNode());
        return prev != null && prev.getElementType() == ErlangTypes.ERL_RADIX;
    }

    private static void suggestModules(@NotNull CompletionResultSet result, @NotNull PsiElement position,
            boolean withColon) {
        Project project = position.getProject();

        Collection<String> names = ErlangModuleIndex.getNames(project);
        for (String name : names) {
            result.addElement(PrioritizedLookupElement
                    .withPriority(LookupElementBuilder.create(name).withIcon(ErlangIcons.MODULE).withInsertHandler(
                            new QuoteInsertHandler.ModuleInsertHandler(name, withColon)), MODULE_PRIORITY));
        }
    }

    private static void suggestBehaviours(@NotNull CompletionResultSet result, @NotNull PsiElement position) {
        Project project = position.getProject();
        Collection<ErlangModule> modules = ErlangBehaviourModuleIndex.getModules(project,
                GlobalSearchScope.allScope(project));
        for (ErlangModule module : modules) {
            QuoteInsertHandler.ModuleInsertHandler handler = new QuoteInsertHandler.ModuleInsertHandler(
                    module.getName(), false);
            result.addElement(PrioritizedLookupElement.withPriority(
                    LookupElementBuilder.create(module).withIcon(ErlangIcons.MODULE).withInsertHandler(handler),
                    MODULE_PRIORITY));
        }
    }

    private static class RunCompletionInsertHandler implements InsertHandler<LookupElement> {
        @Override
        public void handleInsert(@NotNull final InsertionContext context, @NotNull LookupElement item) {
            if (item.getLookupString().endsWith("/"))
                context.setLaterRunnable(() -> new CodeCompletionHandlerBase(CompletionType.BASIC)
                        .invokeCompletion(context.getProject(), context.getEditor()));
        }
    }
}