package org.elixir_lang.annonator; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.Annotator; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.util.containers.ContainerUtil; import org.elixir_lang.ElixirSyntaxHighlighter; import org.elixir_lang.errorreport.Logger; import org.elixir_lang.psi.AtNonNumericOperation; import org.elixir_lang.psi.AtUnqualifiedNoParenthesesCall; import org.elixir_lang.psi.call.Call; import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.Collections; import java.util.List; import static org.elixir_lang.reference.Callable.BIT_STRING_TYPES; import static org.elixir_lang.reference.Callable.isBitStreamSegmentOption; /** * Annotates callables. */ public class Callable implements Annotator, DumbAware { /* * Public Instance Methods */ /** * Annotates the specified PSI element. * It is guaranteed to be executed in non-reentrant fashion. * I.e there will be no call of this method for this instance before previous call get completed. * Multiple instances of the annotator might exist simultaneously, though. * * @param element to annotate. * @param holder the container which receives annotations created by the plugin. */ @Override public void annotate(@NotNull final PsiElement element, @NotNull final AnnotationHolder holder) { element.accept( new PsiRecursiveElementVisitor() { /* * Public Instance Methods */ @Override public void visitElement(@NotNull final PsiElement element) { if (element instanceof Call) { visitCall((Call) element); } } /* * Private Instance Methods */ private void visitCall(@NotNull final Call call) { if (!(call instanceof AtNonNumericOperation || call instanceof AtUnqualifiedNoParenthesesCall)) { visitNonModuleAttributeCall(call); } } private void visitCallDefinitionClause(@NotNull final Call call) { PsiElement head = org.elixir_lang.structure_view.element.CallDefinitionClause.head(call); if (head != null) { visitCallDefinitionHead(head, call); } } private void visitCallDefinitionHead(@NotNull final PsiElement head, @NotNull final Call clause) { PsiElement stripped = org.elixir_lang.structure_view.element.CallDefinitionHead.strip(head); if (stripped instanceof Call) { visitStrippedCallDefinitionHead((Call) stripped, clause); } } private void visitNonModuleAttributeCall(@NotNull final Call call) { if (org.elixir_lang.structure_view.element.CallDefinitionClause.is(call)) { visitCallDefinitionClause(call); } else { PsiReference reference = call.getReference(); if (reference != null) { Collection<PsiElement> resolvedCollection = null; if (reference instanceof PsiPolyVariantReference) { PsiPolyVariantReference polyVariantReference = (PsiPolyVariantReference) reference; ResolveResult[] resolveResults; try { resolveResults = polyVariantReference.multiResolve(false); } catch (StackOverflowError stackOverflowError) { Logger.error(Callable.class, "StackOverflowError when annotating Call", call); resolveResults = new ResolveResult[0]; } List<ResolveResult> validResolveResults = ContainerUtil.filter( resolveResults, new Condition<ResolveResult>() { @Override public boolean value(ResolveResult resolveResult) { return resolveResult.isValidResult(); } } ); resolvedCollection = ContainerUtil.map( validResolveResults, new com.intellij.util.Function<ResolveResult, PsiElement>() { @Override public PsiElement fun(ResolveResult resolveResult) { return resolveResult.getElement(); } } ); } else { PsiElement resolved = reference.resolve(); if (resolved != null) { resolvedCollection = Collections.singleton(resolved); } } if (resolvedCollection != null && resolvedCollection.size() > 0) { for (PsiElement resolved : resolvedCollection) { highlight(call, reference.getRangeInElement(), resolved, holder); } } else if (call.hasDoBlockOrKeyword()) { /* Even though it can't be resolved, it is called like a macro, so highlight like one */ PsiElement functionNameElement = call.functionNameElement(); if (functionNameElement != null) { highlight( functionNameElement.getTextRange(), holder, ElixirSyntaxHighlighter.MACRO_CALL ); } } } else if (isBitStreamSegmentOption(call)) { String name = call.getName(); if (name != null && BIT_STRING_TYPES.contains(name)) { highlight(call, holder, ElixirSyntaxHighlighter.TYPE); } } } } private void visitStrippedCallDefinitionHead(@NotNull final Call stripped, @NotNull final Call clause) { PsiElement functionNameElement = stripped.functionNameElement(); if (functionNameElement != null) { TextAttributesKey textAttributeKey = null; if (org.elixir_lang.structure_view.element.CallDefinitionClause.isFunction(clause)) { textAttributeKey = ElixirSyntaxHighlighter.FUNCTION_DECLARATION; } else if (org.elixir_lang.structure_view.element.CallDefinitionClause.isMacro(clause)) { textAttributeKey = ElixirSyntaxHighlighter.MACRO_DECLARATION; } if (textAttributeKey != null) { highlight(functionNameElement, holder, textAttributeKey); } } } } ); } /* * Private Instance Methods */ private void highlight(@NotNull Call referrer, @NotNull TextRange rangeInReferrer, @NotNull PsiElement resolved, @NotNull AnnotationHolder annotationHolder) { TextAttributesKey referrerTextAttributesKey = null; TextAttributesKey resolvedTextAttributesKey = null; if (org.elixir_lang.reference.Callable.isIgnored(resolved)) { referrerTextAttributesKey = ElixirSyntaxHighlighter.IGNORED_VARIABLE; resolvedTextAttributesKey = ElixirSyntaxHighlighter.IGNORED_VARIABLE; } else { Parameter parameter = new Parameter(resolved); Parameter.Type parameterType = Parameter.putParameterized(parameter).type; if (parameterType != null) { switch (parameterType) { case FUNCTION_NAME: referrerTextAttributesKey = ElixirSyntaxHighlighter.FUNCTION_CALL; // will be handled visitCallDefinitionClause resolvedTextAttributesKey = null; break; case MACRO_NAME: referrerTextAttributesKey = ElixirSyntaxHighlighter.MACRO_CALL; // will be handled visitCallDefinitionClause resolvedTextAttributesKey = null; break; case VARIABLE: referrerTextAttributesKey = ElixirSyntaxHighlighter.PARAMETER; resolvedTextAttributesKey = ElixirSyntaxHighlighter.PARAMETER; } } else if (org.elixir_lang.reference.Callable.isParameterWithDefault(resolved)) { referrerTextAttributesKey = ElixirSyntaxHighlighter.PARAMETER; resolvedTextAttributesKey = ElixirSyntaxHighlighter.PARAMETER; } else if (org.elixir_lang.reference.Callable.isVariable(resolved)) { referrerTextAttributesKey = ElixirSyntaxHighlighter.VARIABLE; resolvedTextAttributesKey = ElixirSyntaxHighlighter.VARIABLE; } } if (referrerTextAttributesKey != null) { highlight(referrer, rangeInReferrer, annotationHolder, referrerTextAttributesKey); } /* Annotations can only be applied to the single, active file, which belongs to the referrer. The resolved may be outside the file if it is a cross-file function or macro usage */ if (resolvedTextAttributesKey != null && sameFile(referrer, resolved)) { highlight(resolved, annotationHolder, resolvedTextAttributesKey); } } private void highlight(@NotNull final PsiElement element, @NotNull AnnotationHolder annotationHolder, @NotNull final TextAttributesKey textAttributesKey) { highlight(element.getTextRange(), annotationHolder, textAttributesKey); } private void highlight(@NotNull final PsiElement element, @NotNull TextRange rangeInElement, @NotNull AnnotationHolder annotationHolder, @NotNull final TextAttributesKey textAttributesKey) { TextRange elementRangeInDocument = element.getTextRange(); int startOffset = elementRangeInDocument.getStartOffset(); TextRange rangeInElementInDocument = new TextRange( startOffset + rangeInElement.getStartOffset(), startOffset + rangeInElement.getEndOffset() ); highlight(rangeInElementInDocument, annotationHolder, textAttributesKey); } /** * Highlights `textRange` with the given `textAttributesKey`. * * @param textRange textRange in the document to highlight * @param annotationHolder the container which receives annotations created by the plugin. * @param textAttributesKey text attributes to apply to the `node`. */ private void highlight(@NotNull final TextRange textRange, @NotNull AnnotationHolder annotationHolder, @NotNull final TextAttributesKey textAttributesKey) { annotationHolder.createInfoAnnotation(textRange, null) .setEnforcedTextAttributes(TextAttributes.ERASE_MARKER); annotationHolder.createInfoAnnotation(textRange, null) .setEnforcedTextAttributes( EditorColorsManager.getInstance().getGlobalScheme().getAttributes(textAttributesKey) ); } private boolean sameFile(@NotNull PsiElement referrer, @NotNull PsiElement resolved) { @NotNull PsiFile referrerPsiFile = referrer.getContainingFile(); VirtualFile referrerVirtualFile = referrerPsiFile.getVirtualFile(); @NotNull PsiFile resolvedPsiFile = resolved.getContainingFile(); VirtualFile resolvedVirtualFile = resolvedPsiFile.getVirtualFile(); return Comparing.equal(referrerVirtualFile, resolvedVirtualFile); } }