/*
* 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()));
}
}
}