package org.jetbrains.plugins.clojure.psi.impl.symbols; import com.intellij.lang.ASTNode; import com.intellij.navigation.ItemPresentation; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Iconable; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; import com.intellij.psi.impl.source.resolve.ResolveCache; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.stubs.StubIndex; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.MethodSignature; import com.intellij.util.Function; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.plugins.clojure.ClojureIcons; import org.jetbrains.plugins.clojure.lexer.ClojureTokenTypes; import org.jetbrains.plugins.clojure.lexer.TokenSets; import org.jetbrains.plugins.clojure.psi.ClojurePsiElementImpl; import org.jetbrains.plugins.clojure.psi.api.*; import org.jetbrains.plugins.clojure.psi.api.ns.ClNs; import org.jetbrains.plugins.clojure.psi.api.symbols.ClSymbol; import org.jetbrains.plugins.clojure.psi.impl.ImportOwner; import org.jetbrains.plugins.clojure.psi.impl.list.ListDeclarations; import org.jetbrains.plugins.clojure.psi.impl.ns.ClSyntheticNamespace; import org.jetbrains.plugins.clojure.psi.impl.ns.NamespaceUtil; import org.jetbrains.plugins.clojure.psi.resolve.ClojureResolveResult; import org.jetbrains.plugins.clojure.psi.resolve.ClojureResolveResultImpl; import org.jetbrains.plugins.clojure.psi.resolve.ResolveUtil; import org.jetbrains.plugins.clojure.psi.resolve.completion.CompleteSymbol; import org.jetbrains.plugins.clojure.psi.resolve.completion.CompletionProcessor; import org.jetbrains.plugins.clojure.psi.resolve.processors.ResolveKind; import org.jetbrains.plugins.clojure.psi.resolve.processors.ResolveProcessor; import org.jetbrains.plugins.clojure.psi.resolve.processors.SymbolResolveProcessor; import org.jetbrains.plugins.clojure.psi.stubs.index.ClojureNsNameIndex; import org.jetbrains.plugins.clojure.psi.util.ClojureKeywords; import org.jetbrains.plugins.clojure.psi.util.ClojurePsiFactory; import javax.swing.*; import java.util.Collection; import java.util.List; /** * @author ilyas */ public class ClSymbolImpl extends ClojurePsiElementImpl implements ClSymbol { public ClSymbolImpl(ASTNode node) { super(node); } @NotNull @Override public PsiReference[] getReferences() { PsiReference fakeClassReference = new MyFakeClassPsiReference(); PsiReference[] refs = {this, fakeClassReference}; return refs; } @Override public PsiReference getReference() { return this; } @Override public String toString() { return "ClSymbol"; } public PsiElement getElement() { return this; } public TextRange getRangeInElement() { final PsiElement refNameElement = getReferenceNameElement(); if (refNameElement != null) { final int offsetInParent = refNameElement.getStartOffsetInParent(); return new TextRange(offsetInParent, offsetInParent + refNameElement.getTextLength()); } return new TextRange(0, getTextLength()); } @Nullable public PsiElement getReferenceNameElement() { final ASTNode lastChild = getNode().getLastChildNode(); if (lastChild == null) return null; for (IElementType elementType : TokenSets.REFERENCE_NAMES.getTypes()) { if (lastChild.getElementType() == elementType) return lastChild.getPsi(); } return null; } @Nullable public String getReferenceName() { PsiElement nameElement = getReferenceNameElement(); if (nameElement != null) { if (nameElement.getNode().getElementType() == ClojureTokenTypes.symATOM) return nameElement.getText(); } return null; } @NotNull public ResolveResult[] multiResolve(boolean incomplete) { final ResolveCache resolveCache = ResolveCache.getInstance(getProject()); return resolveCache.resolveWithCaching(this, RESOLVER, true, incomplete); } public PsiElement setName(@NotNull @NonNls String newName) throws IncorrectOperationException { final ASTNode newNode = ClojurePsiFactory.getInstance(getProject()).createSymbolNodeFromText(newName); getParent().getNode().replaceChild(getNode(), newNode); return newNode.getPsi(); } @Override public Icon getIcon(int flags) { return ClojureIcons.SYMBOL; } @Override public ItemPresentation getPresentation() { return new ItemPresentation() { public String getPresentableText() { final String name = getName(); return name == null ? "<undefined>" : name; } @Nullable public String getLocationString() { String name = getContainingFile().getName(); //todo show namespace return "(in " + name + ")"; } @Nullable public Icon getIcon(boolean open) { return ClSymbolImpl.this.getIcon(Iconable.ICON_FLAG_VISIBILITY | Iconable.ICON_FLAG_READ_STATUS); } @Nullable public TextAttributesKey getTextAttributesKey() { return null; } }; } public ResolveKind[] getKinds() { return ResolveKind.allKinds(); } public static class MyResolver implements ResolveCache.PolyVariantResolver<ClSymbol> { public ResolveResult[] resolve(ClSymbol symbol, boolean incompleteCode) { final String name = symbol.getReferenceName(); if (name == null) return null; // Resolve Java methods invocations ClSymbol qualifier = symbol.getQualifierSymbol(); final String nameString = symbol.getNameString(); if (qualifier == null && nameString.startsWith(".")) { return resolveJavaMethodReference(symbol, null, false); } ResolveKind[] kinds = symbol.getKinds(); if (nameString.endsWith(".")) { kinds = ResolveKind.javaClassesKinds(); } ResolveProcessor processor = new SymbolResolveProcessor(StringUtil.trimEnd(name, "."), symbol, incompleteCode, kinds); resolveImpl(symbol, processor); if (nameString.contains(".")) { ResolveProcessor nsProcessor = new SymbolResolveProcessor(nameString, symbol, incompleteCode, ResolveKind.namesSpaceKinds()); resolveNamespace(symbol, nsProcessor); } ClojureResolveResult[] candidates = processor.getCandidates(); if (candidates.length > 0) return candidates; return ClojureResolveResult.EMPTY_ARRAY; } public static ResolveResult[] resolveJavaMethodReference(final ClSymbol symbol, @Nullable PsiElement start, final boolean forCompletion) { final CompletionProcessor processor = new CompletionProcessor(symbol, symbol.getKinds()); if (start == null) start = symbol; ResolveUtil.treeWalkUp(start, processor); final String name = symbol.getReferenceName(); assert name != null; final String originalName = StringUtil.trimStart(name, "."); final PsiElement[] elements = ResolveUtil.mapToElements(processor.getCandidates()); final HashMap<MethodSignature, HashSet<PsiMethod>> sig2Method = CompleteSymbol.collectAvailableMethods(elements); final List<MethodSignature> goodSignatures = ContainerUtil.findAll(sig2Method.keySet(), new Condition<MethodSignature>() { public boolean value(MethodSignature methodSignature) { return forCompletion || originalName.equals(methodSignature.getName()); } }); final HashSet<ClojureResolveResult> results = new HashSet<ClojureResolveResult>(); for (MethodSignature signature : goodSignatures) { final HashSet<PsiMethod> methodSet = sig2Method.get(signature); for (PsiMethod method : methodSet) { results.add(new ClojureResolveResultImpl(method, true)); } } return results.toArray(new ClojureResolveResult[results.size()]); } private void resolveImpl(ClSymbol symbol, ResolveProcessor processor) { final ClSymbol qualifier = symbol.getQualifierSymbol(); //process other places if (qualifier == null) { ResolveUtil.treeWalkUp(symbol, processor); } else { for (ResolveResult result : qualifier.multiResolve(false)) { final PsiElement element = result.getElement(); if (element != null) { final PsiElement sep = symbol.getSeparatorToken(); if (sep != null && "/".equals(sep.getText())) { //get class elements if (element instanceof PsiClass) { element.processDeclarations(processor, ResolveState.initial(), null, symbol); } //get namespace declarations if (element instanceof ClSyntheticNamespace) { final String fqn = ((ClSyntheticNamespace) element).getQualifiedName(); // namespace declarations for (PsiNamedElement named : NamespaceUtil.getDeclaredElements(fqn, element.getProject())) { if (!ResolveUtil.processElement(processor, named)) return; } } } else if (sep == null || ".".equals(sep.getText())) { element.processDeclarations(processor, ResolveState.initial(), null, symbol); } } } } } private void resolveNamespace(ClSymbol symbol, ResolveProcessor processor) { // process namespaces final Project project = symbol.getProject(); final GlobalSearchScope scope = GlobalSearchScope.allScope(project); final Collection<ClNs> nses = StubIndex.getInstance().get(ClojureNsNameIndex.KEY, symbol.getNameString(), project, scope); for (ClNs ns : nses) { ResolveUtil.processElement(processor, ns); } } } @Nullable public ClSymbol getRawQualifierSymbol() { return findChildByClass(ClSymbol.class); } /** * For references, which hasn't prefix * (import '(prefix symbol)) * (require '(prefix symbol)) * (require '[prefix symbol]) * (require '(prefix [symbol :as id])) * @return qualifier symbol */ @Nullable public ClSymbol getQualifierSymbol() { final ClSymbol rawQualifierSymbol = getRawQualifierSymbol(); if (rawQualifierSymbol != null) return rawQualifierSymbol; final PsiElement parent = getParent(); return getQualifiedNameInner(parent, false); } private ClSymbol getQualifiedNameInner(PsiElement parent, boolean onlyRequireOrUse) { if (parent instanceof ClList) { return getListQualifier(onlyRequireOrUse, (ClList) parent, parent.getParent(), false); } else if (parent instanceof ClVector && ImportOwner.isSpecialVector((ClVector) parent)) { final ClSymbol[] symbols = ((ClVector) parent).getAllSymbols(); return symbols[0] == this ? getQualifiedNameInner(parent.getParent(), true) : null; } else if (parent instanceof ClVector) { return getVectorQualifier(onlyRequireOrUse, (ClVector) parent, parent.getParent(), false); } return null; } private ClSymbol getListQualifier(boolean onlyRequireOrUse, ClList list, PsiElement listParent, boolean isQuoted) { if (listParent instanceof ClQuotedForm) { return getListQualifier(onlyRequireOrUse, list, listParent.getParent(), true); } else if (listParent instanceof ClList) { final PsiElement listParentFirstSymbol = ((ClList) listParent).getFirstNonLeafElement(); if (listParentFirstSymbol instanceof ClSymbol || listParentFirstSymbol instanceof ClKeyword) { final String name; if (listParentFirstSymbol instanceof ClSymbol) { name = ((ClSymbol) listParentFirstSymbol).getNameString(); } else { name = ((ClKeyword) listParentFirstSymbol).getName(); } boolean isOk = false; if ((name.equals(ClojureKeywords.IMPORT) || name.equals(ListDeclarations.IMPORT)) && !onlyRequireOrUse) isOk = true; else if ((name.equals(ClojureKeywords.REQUIRE) || name.equals(ClojureKeywords.USE)) && !isQuoted) isOk = true; else if ((name.equals(ListDeclarations.REQUIRE) || name.equals(ListDeclarations.USE)) && isQuoted) isOk = true; final ClSymbol firstSymbol = list.getFirstSymbol(); if (isOk && firstSymbol != this && firstSymbol instanceof ClSymbol) { return firstSymbol; } } } return null; } private ClSymbol getVectorQualifier(boolean onlyRequireOrUse, ClVector vector, PsiElement list, boolean isQuoted) { if (list instanceof ClList) { final PsiElement firstSymbol = ((ClList) list).getFirstNonLeafElement(); if (firstSymbol instanceof ClSymbol || firstSymbol instanceof ClKeyword) { final String name; if (firstSymbol instanceof ClSymbol) { name = ((ClSymbol) firstSymbol).getNameString(); } else { name = ((ClKeyword) firstSymbol).getName(); } boolean isOk = false; if ((name.equals(ClojureKeywords.IMPORT) || name.equals(ListDeclarations.IMPORT)) && !onlyRequireOrUse) isOk = true; else if ((name.equals(ClojureKeywords.REQUIRE) || name.equals(ClojureKeywords.USE)) && !isQuoted) isOk = true; else if ((name.equals(ListDeclarations.REQUIRE) || name.equals(ListDeclarations.USE)) && isQuoted) isOk = true; if (isOk) { final PsiElement firstNonLeafElement = vector.getFirstNonLeafElement(); if (firstNonLeafElement != null && firstNonLeafElement != this && firstNonLeafElement instanceof ClSymbol) { return (ClSymbol) firstNonLeafElement; } } } } else if (list instanceof ClQuotedForm) { return getVectorQualifier(onlyRequireOrUse, vector, list.getParent(), true); } return null; } public boolean isQualified() { return getQualifierSymbol() != null; } @Override public String getName() { return getNameString(); } @Nullable public PsiElement getSeparatorToken() { return findChildByType(TokenSets.DOTS); } private static final MyResolver RESOLVER = new MyResolver(); public PsiElement resolve() { final ResolveCache resolveCache = ResolveCache.getInstance(getProject()); ResolveResult[] results = resolveCache.resolveWithCaching(this, RESOLVER, false, false); return results.length == 1 ? results[0].getElement() : null; } @NotNull public String getCanonicalText() { return getText(); } private List<PsiElement> multipleResolveResults() { final ResolveCache resolveCache = ResolveCache.getInstance(getProject()); final ResolveResult[] results = resolveCache.resolveWithCaching(this, RESOLVER, false, false); return ContainerUtil.map(results, new Function<ResolveResult, PsiElement>() { public PsiElement fun(ResolveResult resolveResult) { return resolveResult.getElement(); } }); } public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException { PsiElement nameElement = getReferenceNameElement(); if (nameElement != null) { ASTNode node = nameElement.getNode(); ASTNode newNameNode = ClojurePsiFactory.getInstance(getProject()).createSymbolNodeFromText(newElementName); assert newNameNode != null && node != null; node.getTreeParent().replaceChild(node, newNameNode); } return this; } public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { if (isReferenceTo(element)) return this; final PsiFile file = getContainingFile(); if (element instanceof PsiClass && (file instanceof ClojureFile)) { // todo test me!! final PsiClass clazz = (PsiClass) element; final Application application = ApplicationManager.getApplication(); application.runWriteAction(new Runnable() { public void run() { final ClNs ns = ((ClojureFile) file).findOrCreateNamespaceElement(); ns.addImportForClass(ClSymbolImpl.this, clazz); } }); return this; } return this; } public boolean isReferenceTo(PsiElement element) { return multipleResolveResults().contains(element); } @NotNull public Object[] getVariants() { return CompleteSymbol.getVariants(this); } public boolean isSoft() { return false; } @NotNull public String getNameString() { return getText(); } private class MyFakeClassPsiReference implements PsiReference { public PsiElement getElement() { return ClSymbolImpl.this; } public TextRange getRangeInElement() { return new TextRange(0, 0); } public PsiElement resolve() { for (PsiElement element : multipleResolveResults()) { if (element instanceof PsiClass) { return element; } } return null; } @NotNull public String getCanonicalText() { return ClSymbolImpl.this.getCanonicalText(); } public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException { return null; } public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { return null; } public boolean isReferenceTo(PsiElement element) { return ClSymbolImpl.this.isReferenceTo(element); } @NotNull public Object[] getVariants() { return new Object[0]; } public boolean isSoft() { return false; } } }