package com.haskforce.codeInsight;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.haskforce.HaskellIcons;
import com.haskforce.HaskellLanguage;
import com.haskforce.codeInsight.HaskellCompletionCacheLoader.Cache;
import com.haskforce.codeInsight.HaskellCompletionCacheLoader.LookupElementWrapper;
import com.haskforce.highlighting.annotation.external.GhcMod;
import com.haskforce.highlighting.annotation.external.GhcModi;
import com.haskforce.psi.*;
import com.haskforce.utils.ExecUtil;
import com.haskforce.utils.HaskellUtil;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.Future;
/**
* Fills the list of completions available on ctrl-space.
*/
public class HaskellCompletionContributor extends CompletionContributor {
@SuppressWarnings("UnusedDeclaration")
private static final Logger LOG = Logger.getInstance(HaskellCompletionContributor.class);
private static String[] PRAGMA_TYPES = new String[]{
"LANGUAGE ", "OPTIONS_GHC ", "WARNING ", "DEPRECATED ", "INLINE ", "NOINLINE ", "INLINABLE ", "CONLIKE ",
"RULES ", "ANN ", "LINE ", "SPECIALIZE ", "UNPACK ", "SOURCE "};
public static String[] getPragmaTypes() {
return PRAGMA_TYPES.clone();
}
public HaskellCompletionContributor() {
extend(CompletionType.BASIC,
PlatformPatterns.psiElement().withLanguage(HaskellLanguage.INSTANCE),
new CompletionProvider<CompletionParameters>() {
public void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
PsiElement position = parameters.getPosition();
PsiFile file = parameters.getOriginalFile();
List<HaskellPsiUtil.Import> imports = HaskellPsiUtil.parseImports(file);
Cache cache = getCache(file);
// Completion methods should return either void or boolean. If boolean, then it should indicate
// whether or not we were in the appropriate context. This is useful to determine if following
// completions should be added.
completeKeywordImport(position, result);
completeKeywordQualified(position, result);
if (completePragma(position, cache, result)) return;
if (completeModuleImport(position, cache, result)) return;
if (completeQualifiedNames(position, imports, cache, result)) return;
if (completeNameImport(position, cache, result)) return;
completeExpressionKeywords(position, result);
completeLocalNames(position, imports, cache, result);
completeFunctionLocalNames(position,result);
}
}
);
}
private static Cache getCache(PsiFile file) {
return HaskellCompletionCacheLoader.get(file.getProject()).cache();
}
public static void completeKeywordImport(@NotNull final PsiElement position, @NotNull final CompletionResultSet result) {
if (PsiTreeUtil.getParentOfType(position, HaskellImpdecl.class) != null) return;
HaskellBody body = PsiTreeUtil.getParentOfType(position, HaskellBody.class);
PsiElement root = body == null ? position.getContainingFile() : body;
PsiElement topLevel = PsiTreeUtil.findPrevParent(root, position);
// If we have spaces, then we are into an expression, definition, etc. and shouldn't provide completion.
if (topLevel.getText().contains(" ")) return;
for (PsiElement child : root.getChildren()) {
if (PsiTreeUtil.instanceOf(child,
HaskellPpragma.class, HaskellImpdecl.class, PsiWhiteSpace.class, PsiComment.class)) continue;
// If something else other than the allowed elements appear before our element, don't provide completion.
if (!child.equals(topLevel)) return;
}
result.addElement(LookupElementUtil.fromString("import "));
}
public static void completeKeywordQualified(@NotNull final PsiElement position, @NotNull final CompletionResultSet result) {
final PsiElement prevLeaf = PsiTreeUtil.prevVisibleLeaf(position);
if (prevLeaf != null && prevLeaf.getText().equals("import")) {
result.addElement(LookupElementUtil.fromString("qualified "));
}
}
private static String[] EXPRESSION_KEYWORDS = {"do", "if", "then", "else"};
public static void completeExpressionKeywords(@NotNull final PsiElement position, @NotNull final CompletionResultSet result) {
if (PsiTreeUtil.getParentOfType(position, HaskellExp.class) == null) return;
for (String keyword : EXPRESSION_KEYWORDS) {
result.addElement(LookupElementUtil.fromString(keyword));
}
}
public static boolean completePragma(@NotNull final PsiElement position,
@NotNull final Cache cache,
@NotNull final CompletionResultSet result) {
final PsiElement prevSibling = getPrevSiblingWhere(new Function<PsiElement, Boolean>() {
@Override
public Boolean fun(PsiElement psiElement) {
return !(psiElement instanceof PsiWhiteSpace);
}
}, position);
// Pragma types.
if (prevSibling != null && "{-#".equals(prevSibling.getText())) {
addAllElements(result, LookupElementUtil.fromStrings(PRAGMA_TYPES));
}
final PsiElement openPragma = getPrevSiblingWhere(new Function<PsiElement, Boolean>() {
@Override
public Boolean fun(PsiElement psiElement) {
return psiElement.getText().equals("{-#");
}
}, position);
final PsiElement pragmaTypeElement = getNextSiblingWhere(new Function<PsiElement, Boolean>() {
@Override
public Boolean fun(PsiElement psiElement) {
return !(psiElement instanceof PsiWhiteSpace);
}
}, openPragma);
if (pragmaTypeElement == null) {
return false;
}
final String pragmaType = pragmaTypeElement.getText();
if ("LANGUAGE".equals(pragmaType)) {
addAllElements(result, cache.languageExtensions());
} else if ("OPTIONS_GHC".equals(pragmaType)) {
// TODO: Workaround since completion autocompletes after the "-", so without this
// we may end up completing -foo with --foo (inserting a "-").
final Set<String> flags = cache.ghcFlags();
if (flags != null) {
if (position.getText().startsWith("-")) {
addAllElements(result, ContainerUtil.map(flags, new Function<String, LookupElement>() {
@Override
public LookupElement fun(String s) {
return LookupElementUtil.fromString(s.startsWith("-") ? s.substring(1) : s);
}
}));
} else {
addAllElements(result, LookupElementUtil.fromStrings(flags));
}
}
}
return true;
}
public static boolean completeModuleImport(@NotNull final PsiElement position,
@NotNull final Cache cache,
@NotNull final CompletionResultSet result) {
// TODO: Refactor this implementation.
PsiElement el = position.getParent();
if (!(el instanceof HaskellConid)) {
return false;
}
el = el.getParent();
if (!(el instanceof HaskellQconid)) {
return false;
}
el = el.getParent();
if (!(el instanceof HaskellImpdecl)) {
return false;
}
// Regardless of whether we actually have cache data to work with, we still want to return true
// after this point since we've already identified that we are in the appropriate context.
final Set<String> list = cache.visibleModules();
if (list != null) {
StringBuilder builder = new StringBuilder(0);
el = position.getParent();
while (el != null) {
el = el.getPrevSibling();
if (el != null) {
builder.insert(0, el.getText());
}
}
final String partialModule = builder.toString();
Set<String> newLines = new HashSet<String>(0);
for (String line : list) {
if (line.startsWith(partialModule)) {
String newLine = line.replace(partialModule, "");
final int firstDotPos = newLine.indexOf('.');
if (firstDotPos != -1) {
newLine = newLine.substring(0, firstDotPos);
}
newLines.add(newLine);
}
}
addAllElements(result, LookupElementUtil.fromStrings(newLines));
}
return true;
}
public static boolean completeNameImport(@NotNull final PsiElement position,
@NotNull final Cache cache,
@NotNull final CompletionResultSet result) {
// Ensure we are in an import name element.
if (PsiTreeUtil.getParentOfType(position, HaskellImportt.class) == null) return false;
HaskellImpdecl impdecl = PsiTreeUtil.getParentOfType(position, HaskellImpdecl.class);
if (impdecl == null) return true;
HaskellQconid qconid = PsiTreeUtil.findChildOfType(impdecl, HaskellQconid.class);
if (qconid == null) return true;
final String module = qconid.getText();
final Set<LookupElementWrapper> cachedNames = cache.moduleSymbols().get(module);
addAllElements(result, cachedNames);
return true;
}
public static boolean completeFunctionLocalNames(@NotNull final PsiElement position,
@NotNull final CompletionResultSet result){
List<PsiElement> allDefinitionsInScope = HaskellUtil.getAllDefinitionsInScope(position);
for (PsiElement psiElement : allDefinitionsInScope) {
result.addElement(LookupElementBuilder.create((PsiNamedElement)psiElement));
}
List<PsiElement> allDefinitionsInWhereClausesInScope = HaskellUtil.getAllDefinitionsInWhereClausesInScope(position);
for (PsiElement psiElement : allDefinitionsInWhereClausesInScope) {
result.addElement(LookupElementBuilder.create((PsiNamedElement)psiElement));
}
return true;
}
public static boolean completeQualifiedNames(@NotNull final PsiElement position,
@NotNull final List<HaskellPsiUtil.Import> imports,
@NotNull final Cache cacheHolder,
@NotNull final CompletionResultSet result) {
PsiElement el = position.getParent();
if (el == null) {
return false;
}
el = el.getParent();
if (!(el instanceof HaskellQconid || el instanceof HaskellQvarid)) {
return false;
}
final String qName = el.getText();
final int lastDotPos = qName.lastIndexOf('.');
if (lastDotPos == -1) {
return false;
}
final String alias = qName.substring(0, lastDotPos);
// Pull user-qualified names from cache.
final Map<String, Set<LookupElementWrapper>> browseCache = cacheHolder.moduleSymbols();
if (browseCache != null) {
final Iterable<HaskellPsiUtil.Import> filteredImports = Iterables.filter(imports, new Predicate<HaskellPsiUtil.Import>() {
@Override
public boolean apply(HaskellPsiUtil.Import anImport) {
return anImport != null && alias.equals(anImport.alias);
}
});
final HaskellPsiUtil.Import anImport = Iterables.getFirst(filteredImports, null);
if (anImport != null) {
addAllElements(result, browseCache.get(anImport.module));
}
}
return true;
}
public static boolean completeLocalNames(@NotNull final PsiElement position,
@NotNull final List<HaskellPsiUtil.Import> imports,
@NotNull final Cache holder,
@NotNull final CompletionResultSet result) {
if (PsiTreeUtil.getParentOfType(position, HaskellExp.class) == null) {
return false;
}
final Map<String, Set<LookupElementWrapper>> cachedNames = holder.moduleSymbols();
if (cachedNames == null) {
return false;
}
for (HaskellPsiUtil.Import anImport : imports) {
Set<LookupElementWrapper> names = cachedNames.get(anImport.module);
if (names == null) continue;
String[] importedNames = anImport.getImportedNames();
String[] hidingNames = anImport.getHidingNames();
for (LookupElementWrapper cachedName : names) {
String lookupString = cachedName.get().getLookupString();
boolean noExplicitNames = importedNames == null;
boolean isImportedName = importedNames != null && ArrayUtil.contains(lookupString, importedNames);
boolean isHidingName = hidingNames != null && ArrayUtil.contains(lookupString, hidingNames);
if ((noExplicitNames || isImportedName) && !isHidingName) {
result.addElement(cachedName.get());
}
}
}
return true;
}
/**
* Helper to prevent having to do a null check before adding elements to the completion result.
*/
public static void addAllElements(CompletionResultSet result, List<LookupElement> elements) {
if (elements != null) {
result.addAllElements(elements);
}
}
public static void addAllElements(CompletionResultSet result, Set<LookupElementWrapper> elements) {
if (elements == null) return;
for (LookupElementWrapper el : elements) {
result.addElement(el.get());
}
}
@Nullable
public static PsiElement getFirstElementWhere(Function<PsiElement, PsiElement> modify,
Function<PsiElement, Boolean> where,
PsiElement initialElement) {
if (initialElement == null) {
return null;
}
PsiElement result = modify.fun(initialElement);
while (result != null) {
if (where.fun(result)) {
return result;
}
result = modify.fun(result);
}
return null;
}
@Nullable
public static PsiElement getPrevSiblingWhere(Function<PsiElement, Boolean> f, PsiElement e) {
return getFirstElementWhere(new Function<PsiElement, PsiElement>() {
@Override
public PsiElement fun(PsiElement psiElement) {
return psiElement.getPrevSibling();
}
}, f, e);
}
@Nullable
public static PsiElement getNextSiblingWhere(Function<PsiElement, Boolean> f, PsiElement e) {
return getFirstElementWhere(new Function<PsiElement, PsiElement>() {
@Override
public PsiElement fun(PsiElement psiElement) {
return psiElement.getNextSibling();
}
}, f, e);
}
/**
* Adjust the error message when no lookup is found.
*/
@Nullable
@Override
public String handleEmptyLookup(@NotNull CompletionParameters parameters, final Editor editor) {
return "HaskForce: no completion found.";
}
}