package com.haskforce.psi.references; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.haskforce.codeInsight.HaskellCompletionContributor; import com.haskforce.codeInsight.LookupElementUtil; import com.haskforce.index.HaskellModuleIndex; import com.haskforce.psi.*; import com.haskforce.psi.impl.HaskellPsiImplUtil; import com.haskforce.utils.HaskellUtil; import com.haskforce.utils.HaskellUtil.FoundDefinition; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.IncorrectOperationException; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; /** * Resolves references to elements. Will be used in Go To Symbol as well as its inverse, Find Usages. * Currently, the implementation of the multiresolve is very 'patchy', as it took quite a few tries to get * most (hopefully all) cases covered. The class has been developed totally test first, so refactoring to a better * structured implementation should be feasible. * It draws heavily on functions that are implemented in the HaskellUtil class. Those functions are not * tested in the HaskellUtil class but are also tested through the HaskellGoToSymbolTest class (as is this class) * * There are also some tests present in the HaskellRenameTest. Not all test cases could be fitted into that approach. * My hypothesis is that the test framework doesn't like the 'ref' and 'resolve' tags in the same file, as it always * only seemed to want to find one of both. */ public class HaskellReference extends PsiReferenceBase<PsiNamedElement> implements PsiPolyVariantReference { private String name; public HaskellReference(@NotNull PsiNamedElement element, TextRange textRange) { super(element, textRange); name = element.getName(); } public static final ResolveResult[] EMPTY_RESOLVE_RESULT = new ResolveResult[0]; /** * Resolves references to a set of results. */ @NotNull @Override public ResolveResult[] multiResolve(boolean incompleteCode) { // We should only be resolving varids or conids. if (!(myElement instanceof HaskellVarid || myElement instanceof HaskellConid)) { return EMPTY_RESOLVE_RESULT; } Project project = myElement.getProject(); final List<FoundDefinition> namedElements = HaskellUtil.findDefinitionNode(project, name, myElement); // Make sure that we only complete the last conid in a qualified expression. if (myElement instanceof HaskellConid) { // Don't resolve a module import to a constructor. HaskellQconid qconid = PsiTreeUtil.getParentOfType(myElement, HaskellQconid.class); if (qconid == null) { /** * Are we in a qualified variable call? */ HaskellQvarid haskellQvarid = PsiTreeUtil.getParentOfType(myElement, HaskellQvarid.class); if(haskellQvarid != null){ String fullQualifierAndFunctionName = haskellQvarid.getText(); int i1 = StringUtils.lastIndexOf(fullQualifierAndFunctionName, '.'); String fullQualifierName = StringUtils.substring(fullQualifierAndFunctionName, 0, i1); String functionName = StringUtils.substring(fullQualifierAndFunctionName,i1+1); List<HaskellConid> conidList = haskellQvarid.getConidList(); int i = 0; for (; i < conidList.size(); i++) { if (conidList.get(i).equals(myElement)){ break; } } /** * TODO still take care of the situation where you import * A.B as AB as well as A.C as AB. Only point to one of both */ HaskellFile containingFile = (HaskellFile)myElement.getContainingFile(); final HaskellBody body = containingFile.getBody(); if (body == null) return EMPTY_RESOLVE_RESULT; List<HaskellImpdecl> importDeclarations = body.getImpdeclList(); for (HaskellImpdecl importDeclaration : importDeclarations) { List<HaskellQconid> qconidList = importDeclaration.getQconidList(); if (importDeclaration.getQualified() != null){ HaskellQconid moduleName = Iterables.getFirst(qconidList, null); if (moduleName == null) continue; HaskellQconid last = Iterables.getLast(qconidList); if (fullQualifierName.equals(last.getText())){ List<HaskellFile> filesByModuleName = HaskellModuleIndex.getFilesByModuleName(myElement.getProject(), moduleName.getText(), GlobalSearchScope.projectScope(myElement.getProject())); List<PsiNamedElement> definitionNodes; try { definitionNodes = HaskellUtil.findDefinitionNodes(filesByModuleName.get(0), functionName); } catch (IndexOutOfBoundsException e) { continue; } for (PsiNamedElement definitionNode : definitionNodes) { String definitionNodeName = definitionNode.getName(); if (definitionNodeName == null) continue; if (definitionNodeName.equals(functionName)){ HaskellConid haskellConid; try { haskellConid = last.getConidList().get(i); } catch (IndexOutOfBoundsException e) { continue; } return new ResolveResult[]{new PsiElementResolveResult(haskellConid)}; } } } } } } else { if (myElement.getParent() instanceof HaskellTycon){ List<ResolveResult> results = new ArrayList<ResolveResult>(20); for (FoundDefinition namedElement : namedElements){ if (namedElement.element.getParent() instanceof HaskellTycon){ results.add(new PsiElementResolveResult(namedElement.element)); } } return results.toArray(new ResolveResult[results.size()]); } return EMPTY_RESOLVE_RESULT; } } } /** * Do not resolve module to constructor. In fact, do not resolve module as it's a declaration itself. */ if(PsiTreeUtil.getParentOfType(myElement, HaskellModuledecl.class) != null){ return EMPTY_RESOLVE_RESULT; } HaskellImpdecl haskellImpdecl = PsiTreeUtil.getParentOfType(myElement, HaskellImpdecl.class); if (haskellImpdecl != null){ PsiElement parent = myElement.getParent(); if (parent instanceof HaskellQconid) { HaskellQconid haskellQconid = (HaskellQconid) parent; if (haskellImpdecl.getQconidList().get(0).equals(haskellQconid)) { List<HaskellConid> conidList = haskellQconid.getConidList(); for (int i = 0; i < conidList.size(); i++) { if(myElement.equals(conidList.get(i))){ List<PsiElementResolveResult> results = handleImportReferences(haskellImpdecl, Iterables.getLast(conidList), i); return results.toArray(new ResolveResult[results.size()]); } } } return EMPTY_RESOLVE_RESULT; } } /** * TODO * Would like to use this StubIndex, but using it here creates problems when performing go to symbol, * ends up in a neverending recursive call and a nice stackoverflow error. Don't know why. * * https://devnet.jetbrains.com/thread/459789 */ // GlobalSearchScope scope = GlobalSearchScope.allScope(project); // Collection<HaskellNamedElement> namedElements = StubIndex.getElements(HaskellAllNameIndex.KEY, name, project, scope, HaskellNamedElement.class); // Guess 20 variants tops most of the time in any real code base. List<ResolveResult> results = new ArrayList<ResolveResult>(20); String qualifiedCallName = HaskellUtil.getQualifiedPrefix(myElement); if (qualifiedCallName == null){ /** * This is a set because sometimes there seems to be overlap between findDefintionNode which * should return all 'left-most' variables and the local variables. Also called * a stop gap. */ Set<PsiElement> resultSet = Sets.newHashSet(); resultSet.addAll(HaskellUtil.matchGlobalNamesUnqualified(namedElements)); List<PsiElement> localVariables = HaskellUtil.matchLocalDefinitionsInScope(myElement, name); for (PsiElement psiElement : localVariables) { resultSet.add(psiElement); } /** * TODO find out if we can or can not check the where clauses in case we found something higher up (a * let clause or so). Not yet sure about the correct precedence. */ List<PsiElement> localWhereDefinitions = HaskellUtil.matchWhereClausesInScope(myElement, name); for (PsiElement element : localWhereDefinitions) { resultSet.add(element); } Iterator<PsiElement> iterator = resultSet.iterator(); while(iterator.hasNext()){ results.add(new PsiElementResolveResult(iterator.next())); } return results.toArray(new ResolveResult[results.size()]); } else { results.addAll(HaskellUtil.matchGlobalNamesQualified(namedElements, qualifiedCallName)); return results.toArray(new ResolveResult[results.size()]); } } private @NotNull List<PsiElementResolveResult> handleImportReferences(@NotNull HaskellImpdecl haskellImpdecl, @NotNull PsiNamedElement myElement, int i) { /** * Don't use the named element yet to determine which element we're * talking about, not necessary yet */ List<PsiElementResolveResult> modulesFound = new ArrayList<PsiElementResolveResult>(); List<HaskellQconid> qconidList = haskellImpdecl.getQconidList(); if (qconidList.size() > 0){ String moduleName = qconidList.get(0).getText(); GlobalSearchScope globalSearchScope = GlobalSearchScope.projectScope(myElement.getProject()); List<HaskellFile> filesByModuleName = HaskellModuleIndex.getFilesByModuleName(myElement.getProject(), moduleName, globalSearchScope); for (HaskellFile haskellFile : filesByModuleName) { HaskellModuledecl[] moduleDecls = PsiTreeUtil.getChildrenOfType(haskellFile, HaskellModuledecl.class); if (moduleDecls != null && moduleDecls.length > 0){ HaskellQconid qconid = moduleDecls[0].getQconid(); if (qconid != null) { List<HaskellConid> conidList = moduleDecls[0].getQconid().getConidList(); modulesFound.add(new PsiElementResolveResult(conidList.get(i))); } } } } return modulesFound; } /** * Resolves references to a single result, or fails. */ @Nullable @Override public PsiElement resolve() { ResolveResult[] resolveResults = multiResolve(false); return resolveResults.length == 1 ? resolveResults[0].getElement() : null; } /** * Controls what names that get added to the autocompletion popup available * on ctrl-space. */ @NotNull @Override public Object[] getVariants() { // If we are not in an expression, don't provide reference completion. if (PsiTreeUtil.getParentOfType(myElement, HaskellExp.class) == null) { return new Object[]{}; } // If we are in a qualified name, don't provide reference completion. final PsiElement qId = HaskellUtil.getParentOfType(myElement, HaskellQconid.class, HaskellQvarid.class); if (qId != null && qId.textContains('.')) { return new Object[]{}; } final PsiFile containingFile = myElement.getContainingFile(); if (!(containingFile instanceof HaskellFile)) { return new Object[]{}; } List<PsiNamedElement> namedNodes = HaskellUtil.findDefinitionNodes((HaskellFile)containingFile); List<LookupElement> variants = new ArrayList<LookupElement>(20); for (final PsiNamedElement namedElement : namedNodes) { final PsiElement genDecl = PsiTreeUtil.getParentOfType(namedElement, HaskellGendecl.class); final PsiFile psiFile = namedElement.getContainingFile(); if (!(psiFile instanceof HaskellFile)) { continue; } final String module = ((HaskellFile) psiFile).getModuleOrFileName(); final String name = namedElement.getName(); if (name == null) { continue; } final String type; if (genDecl != null) { final PsiElement cType = PsiTreeUtil.getChildOfType(genDecl, HaskellCtype.class); type = cType == null ? "" : cType.getText(); } else { type = ""; } variants.add(LookupElementUtil.create(name, module, type)); } return variants.toArray(); } @Override public TextRange getRangeInElement() { return new TextRange(0, this.getElement().getNode().getTextLength()); } /** * Called when renaming refactoring tries to rename the Psi tree. */ @Override public PsiElement handleElementRename(final String newName) throws IncorrectOperationException { PsiElement element; if (myElement instanceof HaskellVarid) { element = HaskellPsiImplUtil.setName((HaskellVarid) myElement, newName); if (element != null) return element; throw new IncorrectOperationException("Cannot rename " + name + " to " + newName); } else if (myElement instanceof HaskellConid) { element = HaskellPsiImplUtil.setName((HaskellConid) myElement, newName); if (element != null) return element; throw new IncorrectOperationException("Cannot rename " + name + " to " + newName); } return super.handleElementRename(newName); } }