package org.elixir_lang.reference; import com.google.common.collect.Sets; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.util.TextRange; import com.intellij.psi.*; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import org.apache.commons.lang.NotImplementedException; import org.elixir_lang.psi.*; import org.elixir_lang.psi.call.Call; import org.elixir_lang.psi.impl.ElixirPsiImplUtil; import org.elixir_lang.psi.scope.module_attribute.implemetation.For; import org.elixir_lang.psi.scope.module_attribute.implemetation.Protocol; import org.elixir_lang.structure_view.element.modular.Implementation; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.Set; import static org.elixir_lang.psi.scope.MultiResolve.HAS_VALID_RESULT_CONDITION; /** * Created by limhoff on 12/30/15. */ public class ModuleAttribute extends PsiPolyVariantReferenceBase<PsiElement> { /* * CONSTANTS */ private static final String BEHAVIOUR_NAME = "behaviour"; private static final Set<String> CALLBACK_NAME_SET = Sets.newHashSet("callback", "macrocallback"); private static final Set<String> DOCUMENTATION_NAME_SET = Sets.newHashSet("doc", "moduledoc", "typedoc"); private static final Set<String> NON_REFERENCING_NAME_SET; private static final String SPECIFICATION_NAME = "spec"; private static final Set<String> TYPE_NAME_SET = Sets.newHashSet("opaque", "type", "typep"); static { NON_REFERENCING_NAME_SET = new java.util.HashSet<String>(); NON_REFERENCING_NAME_SET.add(BEHAVIOUR_NAME); NON_REFERENCING_NAME_SET.addAll(CALLBACK_NAME_SET); NON_REFERENCING_NAME_SET.addAll(DOCUMENTATION_NAME_SET); NON_REFERENCING_NAME_SET.add(SPECIFICATION_NAME); NON_REFERENCING_NAME_SET.addAll(TYPE_NAME_SET); } /* * * Static Methods * */ /* * Public Static Methods */ /** * Whether the module attribute is used to declare function or macro callbacks for behaviours * * @return {@code true} if {@code "@callback"} or {@code "@macrocallback"}; otherwise, {@code false}. */ public static boolean isCallbackName(@NotNull String name) { return CALLBACK_NAME_SET.contains(name); } /** * Whether the module attribute is used to declare function, module, or type documentation * * @return {@code true} if {@code "doc"}, {@code "moduledoc"}, or {@code "typedoc"}; otherwise, {@code false}. */ public static boolean isDocumentationName(@NotNull String name) { return DOCUMENTATION_NAME_SET.contains(name); } /** * All the predefined module attributes that aren't used to define constants, but for defining behaviors, callback, * documents, or types. * * @param moduleAttribute the module attribute * @return {@code true} if the module attribute should have a {@code null} reference because it is used for * library control instead of constant definition; otherwise, {@code false}. */ public static boolean isNonReferencing(AtNonNumericOperation moduleAttribute) { String text = moduleAttribute.getText(); String name = text.substring(1); return isNonReferencingName(name); } /** * All the predefined module attributes that aren't used to define constants, but for defining behaviors, callback, * documents, or types. * * @param moduleAttribute the module attribute * @return {@code true} if the module attribute should have a {@code null} reference because it is used for * library control instead of constant definition; otherwise, {@code false}. */ public static boolean isNonReferencing(ElixirAtIdentifier moduleAttribute) { String text = moduleAttribute.getText(); String name = text.substring(1); return isNonReferencingName(name); } /** * Whether the module attribute is used to declare the specification for a function or macro. * * @return {@code true} if {@code "spec"}; otherwise, {@code false} */ public static boolean isSpecificationName(@NotNull String name) { return SPECIFICATION_NAME.equals(name); } /** * Whether the module attribute is used to declare opaque, transparent, or private types. * * @return {@code true} if {@code "opaque"}, {@code "type"}, or {@code "typep"; otherwise, {@code false}. */ public static boolean isTypeName(@NotNull String name) { return TYPE_NAME_SET.contains(name); } /* * Private Static Methods */ private static boolean isNonReferencingName(@NotNull String name) { return NON_REFERENCING_NAME_SET.contains(name); } /* * Constructors */ public ModuleAttribute(@NotNull final PsiElement psiElement) { super(psiElement, TextRange.create(0, psiElement.getTextLength())); } /* * Public Instance Methods */ /** * Returns the array of String, {@link PsiElement} and/or {@link LookupElement} * instances representing all identifiers that are visible at the location of the reference. The contents * of the returned array is used to build the lookup list for basic code completion. (The list * of visible identifiers may not be filtered by the completion prefix string - the * filtering is performed later by IDEA core.) * * @return the array of available identifiers. */ @NotNull @Override public Object[] getVariants() { List<LookupElement> lookupElementList = getVariantsUpFromElement(myElement); return lookupElementList.toArray(new Object[lookupElementList.size()]); } @Override public PsiElement handleElementRename(String newModuleAttributeName) throws IncorrectOperationException { PsiElement renamedElement = myElement; if (myElement instanceof AtNonNumericOperation) { PsiElement moduleAttributeUsage = ElementFactory.createModuleAttributeUsage( myElement.getProject(), newModuleAttributeName ); renamedElement = myElement.replace(moduleAttributeUsage); } else if (myElement instanceof ElixirAtIdentifier) { // do nothing; handled by setName on ElixirAtUnqualifiedNoParenthesesCall } else { throw new NotImplementedException( "Renaming module attribute reference on " + myElement.getClass().getCanonicalName() + " PsiElements is not implemented yet. Please open an issue " + "(https://github.com/KronicDeth/intellij-elixir/issues/new) with the class name and the " + "sample text:\n" + myElement.getText()); } return renamedElement; } /** * Returns the results of resolving the reference. * * @param incompleteCode if true, the code in the context of which the reference is * being resolved is considered incomplete, and the method may return additional * invalid results. * @return the array of results for resolving the reference. */ @NotNull @Override public ResolveResult[] multiResolve(boolean incompleteCode) { List<ResolveResult> resultList = new ArrayList<ResolveResult>(); boolean isNonReferencing = false; if (myElement instanceof AtNonNumericOperation) { isNonReferencing = isNonReferencing((AtNonNumericOperation) myElement); } else if (myElement instanceof ElixirAtIdentifier) { isNonReferencing = isNonReferencing((ElixirAtIdentifier) myElement); } if (!isNonReferencing) { Boolean validResult; validResult = validResult("@protocol", incompleteCode); if (validResult != null) { List<ResolveResult> resolveResultList = Protocol.resolveResultList(validResult, myElement); if (resolveResultList != null) { resultList.addAll(resolveResultList); } } if (incompleteCode || !ContainerUtil.exists(resultList, HAS_VALID_RESULT_CONDITION)) { validResult = validResult("@for", incompleteCode); if (validResult != null) { List<ResolveResult> resolveResultList = For.resolveResultList(validResult, myElement); if (resolveResultList != null) { resultList.addAll(resolveResultList); } } if (incompleteCode || !ContainerUtil.exists(resultList, HAS_VALID_RESULT_CONDITION)) { resultList.addAll(multiResolveUpFromElement(myElement, incompleteCode)); } } } return resultList.toArray(new ResolveResult[resultList.size()]); } /* * Private Instance Methods */ private List<ResolveResult> multiResolveSibling(PsiElement lastSibling, boolean incompleteCode) { List<ResolveResult> resultList = new ArrayList<ResolveResult>(); for (PsiElement sibling = lastSibling; sibling != null; sibling = sibling.getPrevSibling()) { if (sibling instanceof AtUnqualifiedNoParenthesesCall) { AtUnqualifiedNoParenthesesCall atUnqualifiedNoParenthesesCall = (AtUnqualifiedNoParenthesesCall) sibling; String moduleAttributeName = ElixirPsiImplUtil.moduleAttributeName(atUnqualifiedNoParenthesesCall); String value = getValue(); if (moduleAttributeName.equals(value)) { resultList.add(new PsiElementResolveResult(atUnqualifiedNoParenthesesCall)); } else if (incompleteCode && moduleAttributeName.startsWith(value)) { resultList.add(new PsiElementResolveResult(atUnqualifiedNoParenthesesCall, false)); } } } return resultList; } @NotNull private List<ResolveResult> multiResolveUpFromElement(@NotNull final PsiElement element, boolean incompleteCode) { List<ResolveResult> resultList = new ArrayList<ResolveResult>(); PsiElement lastSibling = element; while (lastSibling != null) { resultList.addAll(multiResolveSibling(lastSibling, incompleteCode)); lastSibling = lastSibling.getParent(); } return resultList; } private List<LookupElement> getVariantsSibling(PsiElement lastSibling) { List<LookupElement> lookupElementList = new ArrayList<LookupElement>(); for (PsiElement sibling = lastSibling; sibling != null; sibling = sibling.getPrevSibling()) { if (sibling instanceof AtUnqualifiedNoParenthesesCall) { AtUnqualifiedNoParenthesesCall atUnqualifiedNoParenthesesCall = (AtUnqualifiedNoParenthesesCall) sibling; lookupElementList.add( LookupElementBuilder.createWithSmartPointer( ElixirPsiImplUtil.moduleAttributeName(atUnqualifiedNoParenthesesCall), atUnqualifiedNoParenthesesCall ) ); } else if (sibling instanceof Call) { Call siblingCall = (Call) sibling; if (Implementation.is(siblingCall)) { PsiElement element = Implementation.protocolNameElement(siblingCall); if (element == null) { element = siblingCall; } lookupElementList.add( LookupElementBuilder.createWithSmartPointer( "@protocol", element ) ); } } } return lookupElementList; } private List<LookupElement> getVariantsUpFromElement(PsiElement element) { List<LookupElement> lookupElementList = new ArrayList<LookupElement>(); PsiElement lastSibling = element; while (lastSibling != null) { lookupElementList.addAll(getVariantsSibling(lastSibling)); lastSibling = lastSibling.getParent(); } return lookupElementList; } @Nullable private Boolean validResult(@NotNull String moduleAttributeName, boolean incompleteCode) { Boolean validResult = null; String value = getValue(); if (value.equals(moduleAttributeName)) { validResult = true; } else if (incompleteCode && moduleAttributeName.startsWith(value)) { validResult = false; } return validResult; } }