package org.jetbrains.android; import com.android.SdkConstants; import com.android.resources.ResourceType; import com.android.tools.idea.AndroidPsiUtils; import com.intellij.ide.highlighter.XmlFileType; import com.intellij.navigation.GotoRelatedItem; import com.intellij.navigation.GotoRelatedProvider; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.FileIndexFacade; import com.intellij.openapi.util.Computable; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.searches.ReferencesSearch; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.psi.xml.XmlAttributeValue; import com.intellij.psi.xml.XmlFile; import com.intellij.util.Processor; import com.intellij.util.containers.HashSet; import org.jetbrains.android.dom.AndroidAttributeValue; import org.jetbrains.android.dom.AndroidDomUtil; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.util.AndroidCommonUtils; import org.jetbrains.android.util.AndroidResourceUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; /** * @author Eugene.Kudelevsky */ public class AndroidGotoRelatedProvider extends GotoRelatedProvider { public static boolean ourAddDeclarationToManifest = false; private static final String[] CONTEXT_CLASSES = { SdkConstants.CLASS_ACTIVITY, SdkConstants.CLASS_FRAGMENT, SdkConstants.CLASS_V4_FRAGMENT, "android.widget.Adapter" }; @NotNull @Override public List<? extends GotoRelatedItem> getItems(@NotNull PsiElement element) { final Computable<List<GotoRelatedItem>> items = getLazyItemsComputable(element); return items != null ? items.compute() : Collections.<GotoRelatedItem>emptyList(); } @Nullable private static Computable<List<GotoRelatedItem>> getLazyItemsComputable(@NotNull PsiElement element) { final PsiFile file = element.getContainingFile(); if (!(file instanceof XmlFile) && !(file instanceof PsiJavaFile)) { return null; } final VirtualFile vFile = file.getVirtualFile(); if (vFile == null) { return null; } final Project project = element.getProject(); if (!FileIndexFacade.getInstance(project).isInContent(vFile)) { return null; } final Module module = ModuleUtilCore.findModuleForFile(vFile, project); if (module == null) { return null; } final AndroidFacet facet = AndroidFacet.getInstance(module); if (facet == null) { return null; } if (file instanceof PsiJavaFile) { PsiClass aClass = PsiTreeUtil.getParentOfType(element, PsiClass.class, false); if (aClass == null) { final PsiClass[] rootClasses = ((PsiJavaFile)file).getClasses(); if (rootClasses.length == 1) { aClass = rootClasses[0]; } } if (aClass != null) { return getLazyItemsForClass(aClass, facet, ourAddDeclarationToManifest); } } else { return getLazyItemsForXmlFile((XmlFile)file, facet); } return null; } @Nullable public static Computable<List<GotoRelatedItem>> getLazyItemsForXmlFile(@NotNull XmlFile file, @NotNull AndroidFacet facet) { final String resourceType = facet.getLocalResourceManager().getFileResourceType(file); // TODO: Handle menus as well! if (ResourceType.LAYOUT.getName().equals(resourceType)) { return collectRelatedJavaFiles(file, facet); } return null; } @Nullable static Computable<List<GotoRelatedItem>> getLazyItemsForClass(@NotNull PsiClass aClass, @NotNull AndroidFacet facet, boolean addDeclarationInManifest) { final GotoRelatedItem item = findDeclarationInManifest(aClass); final boolean isContextClass = isInheritorOfContextClass(aClass, facet.getModule()); if (!isContextClass && item == null) { return null; } final List<GotoRelatedItem> items; if (isContextClass) { items = new ArrayList<GotoRelatedItem>(collectRelatedLayoutFiles(facet, aClass)); if (addDeclarationInManifest) { if (item != null) { items.add(item); } } if (items.isEmpty()) { return null; } } else { items = Collections.singletonList(item); } return new Computable<List<GotoRelatedItem>>() { @Override public List<GotoRelatedItem> compute() { return items; } }; } @Nullable private static GotoRelatedItem findDeclarationInManifest(@NotNull PsiClass psiClass) { final AndroidAttributeValue<PsiClass> domAttrValue = AndroidDomUtil.findComponentDeclarationInManifest(psiClass); if (domAttrValue == null) { return null; } final XmlAttributeValue attrValue = domAttrValue.getXmlAttributeValue(); return attrValue != null ? new MyGotoManifestItem(attrValue) : null; } private static boolean isInheritorOfContextClass(@NotNull PsiClass psiClass, @NotNull Module module) { final JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject()); for (String contextClassName : CONTEXT_CLASSES) { final PsiClass contextClass = facade.findClass( contextClassName, module.getModuleWithDependenciesAndLibrariesScope(false)); if (contextClass != null && psiClass.isInheritor(contextClass, true)) { return true; } } return false; } @Nullable private static Computable<List<GotoRelatedItem>> collectRelatedJavaFiles(@NotNull final XmlFile file, @NotNull final AndroidFacet facet) { final String resType = ResourceType.LAYOUT.getName(); final String resourceName = AndroidCommonUtils.getResourceName(resType, file.getName()); final PsiField[] fields = AndroidResourceUtil.findResourceFields(facet, resType, resourceName, true); if (fields.length == 0 || fields.length > 1) { return null; } final PsiField field = fields[0]; final Module module = facet.getModule(); final GlobalSearchScope scope = module.getModuleScope(false); return new Computable<List<GotoRelatedItem>>() { @Override public List<GotoRelatedItem> compute() { final JavaPsiFacade facade = JavaPsiFacade.getInstance(module.getProject()); final List<PsiClass> psiContextClasses = new ArrayList<PsiClass>(); // Explicitly chosen in the layout/menu file with a tools:context attribute? PsiClass declared = AndroidPsiUtils.getContextClass(module, file); if (declared != null) { return Collections.singletonList(new GotoRelatedItem(declared, "JAVA")); } for (String contextClassName : CONTEXT_CLASSES) { final PsiClass contextClass = facade.findClass( contextClassName, module.getModuleWithDependenciesAndLibrariesScope(false)); if (contextClass != null) { psiContextClasses.add(contextClass); } } if (psiContextClasses.isEmpty()) { return Collections.emptyList(); } final List<GotoRelatedItem> result = new ArrayList<GotoRelatedItem>(); ReferencesSearch.search(field, scope).forEach(new Processor<PsiReference>() { @Override public boolean process(PsiReference reference) { PsiElement element = reference.getElement(); if (!(element instanceof PsiReferenceExpression)) { return true; } element = element.getParent(); if (!(element instanceof PsiExpressionList)) { return true; } element = element.getParent(); if (!(element instanceof PsiMethodCallExpression)) { return true; } final String methodName = ((PsiMethodCallExpression)element). getMethodExpression().getReferenceName(); if ("setContentView".equals(methodName) || "inflate".equals(methodName)) { final PsiClass relatedClass = PsiTreeUtil.getParentOfType(element, PsiClass.class); if (relatedClass != null && isInheritorOfOne(relatedClass, psiContextClasses)) { result.add(new GotoRelatedItem(relatedClass, "JAVA")); } } return true; } }); return result; } }; } private static boolean isInheritorOfOne(@NotNull PsiClass psiClass, @NotNull Collection<PsiClass> possibleBaseClasses) { for (PsiClass baseClass : possibleBaseClasses) { if (psiClass.isInheritor(baseClass, true)) { return true; } } return false; } @NotNull private static List<GotoRelatedItem> collectRelatedLayoutFiles(@NotNull final AndroidFacet facet, @NotNull PsiClass context) { final Set<PsiFile> files = new HashSet<PsiFile>(); context.accept(new JavaRecursiveElementWalkingVisitor() { @Override public void visitReferenceExpression(PsiReferenceExpression expression) { super.visitReferenceExpression(expression); final String resClassName = ResourceType.LAYOUT.getName(); final AndroidResourceUtil.MyReferredResourceFieldInfo info = AndroidResourceUtil.getReferredResourceOrManifestField(facet, expression, resClassName, true); if (info == null || info.isFromManifest()) { return; } final String resFieldName = info.getFieldName(); final List<PsiElement> resources = facet.getLocalResourceManager().findResourcesByFieldName(resClassName, resFieldName); for (PsiElement resource : resources) { if (resource instanceof PsiFile) { files.add((PsiFile)resource); } } } }); if (files.isEmpty()) { return Collections.emptyList(); } final List<GotoRelatedItem> result = new ArrayList<GotoRelatedItem>(files.size()); for (PsiFile file : files) { result.add(new MyGotoRelatedLayoutItem(file)); } return result; } private static class MyGotoRelatedLayoutItem extends GotoRelatedItem { private final PsiFile myFile; public MyGotoRelatedLayoutItem(@NotNull PsiFile file) { super(file, "Layout Files"); myFile = file; } @Nullable @Override public String getCustomContainerName() { final PsiDirectory directory = myFile.getContainingDirectory(); return directory != null ? "(" + directory.getName() + ")" : null; } } private static class MyGotoManifestItem extends GotoRelatedItem { public MyGotoManifestItem(@NotNull XmlAttributeValue attributeValue) { super(attributeValue); } @Nullable @Override public String getCustomName() { return "AndroidManifest.xml"; } @Nullable @Override public String getCustomContainerName() { return ""; } @Nullable @Override public Icon getCustomIcon() { return XmlFileType.INSTANCE.getIcon(); } } }