diff --git a/plugins/android/src/org/jetbrains/android/dom/AndroidDomUtil.java b/plugins/android/src/org/jetbrains/android/dom/AndroidDomUtil.java index f3c062599a22..bdb68532e840 100644 --- a/plugins/android/src/org/jetbrains/android/dom/AndroidDomUtil.java +++ b/plugins/android/src/org/jetbrains/android/dom/AndroidDomUtil.java @@ -23,6 +23,7 @@ import com.intellij.psi.xml.XmlAttribute; import com.intellij.psi.xml.XmlTag; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.HashMap; +import com.intellij.util.xml.Converter; import com.intellij.util.xml.DomElement; import com.intellij.util.xml.ResolvingConverter; import com.intellij.util.xml.XmlName; @@ -59,6 +60,7 @@ public class AndroidDomUtil { public static final StaticEnumConverter BOOLEAN_CONVERTER = new StaticEnumConverter("true", "false"); public static final Map SPECIAL_RESOURCE_TYPES = new HashMap(); private static final PackageClassConverter ACTIVITY_CONVERTER = new PackageClassConverter(AndroidUtils.ACTIVITY_BASE_CLASS_NAME); + private static final OnClickConverter ON_CLICK_CONVERTER = new OnClickConverter(); static { addSpecialResourceType("string", "label", "description", "title"); @@ -146,7 +148,7 @@ public class AndroidDomUtil { } @Nullable - public static ResolvingConverter getSpecificConverter(@NotNull XmlName attrName, DomElement context) { + public static Converter getSpecificConverter(@NotNull XmlName attrName, DomElement context) { if (context == null) { return null; } @@ -168,6 +170,11 @@ public class AndroidDomUtil { return ACTIVITY_CONVERTER; } } + else if (context instanceof LayoutViewElement) { + if ("onClick".equals(localName)) { + return ON_CLICK_CONVERTER; + } + } return null; } diff --git a/plugins/android/src/org/jetbrains/android/dom/converters/OnClickConverter.java b/plugins/android/src/org/jetbrains/android/dom/converters/OnClickConverter.java new file mode 100644 index 000000000000..be9d864c9916 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/dom/converters/OnClickConverter.java @@ -0,0 +1,165 @@ +package org.jetbrains.android.dom.converters; + +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementBuilder; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Iconable; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.*; +import com.intellij.psi.impl.PsiManagerEx; +import com.intellij.psi.impl.source.resolve.ResolveCache; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.psi.search.ProjectScope; +import com.intellij.psi.search.PsiShortNamesCache; +import com.intellij.psi.search.searches.ClassInheritorsSearch; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.util.ArrayUtil; +import com.intellij.util.Processor; +import com.intellij.util.containers.HashSet; +import com.intellij.util.xml.ConvertContext; +import com.intellij.util.xml.Converter; +import com.intellij.util.xml.CustomReferenceConverter; +import com.intellij.util.xml.GenericDomValue; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * @author Eugene.Kudelevsky + */ +public class OnClickConverter extends Converter implements CustomReferenceConverter { + @NotNull + @Override + public PsiReference[] createReferences(GenericDomValue value, PsiElement element, ConvertContext context) { + final int length = element.getTextLength(); + if (length > 1) { + return new PsiReference[]{new MyReference((XmlAttributeValue)element, new TextRange(1, length - 1))}; + } + return PsiReference.EMPTY_ARRAY; + } + + @Override + public String fromString(@Nullable @NonNls String s, ConvertContext context) { + return s; + } + + @Override + public String toString(@Nullable String s, ConvertContext context) { + return s; + } + + private static class MyReference extends PsiPolyVariantReferenceBase { + + public MyReference(XmlAttributeValue value, TextRange range) { + super(value, range, true); + } + + @NotNull + @Override + public ResolveResult[] multiResolve(boolean incompleteCode) { + return ((PsiManagerEx)getElement().getManager()).getResolveCache() + .resolveWithCaching(this, new ResolveCache.PolyVariantResolver() { + @Override + public ResolveResult[] resolve(MyReference myReference, boolean incompleteCode) { + return resolveInner(); + } + }, false, incompleteCode); + } + + private ResolveResult[] resolveInner() { + final String methodName = myElement.getValue(); + if (methodName == null) { + return ResolveResult.EMPTY_ARRAY; + } + + final Project project = myElement.getProject(); + final JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project); + final PsiShortNamesCache cache = psiFacade.getShortNamesCache(); + + final PsiMethod[] methods = cache.getMethodsByName(methodName, GlobalSearchScope.projectScope(project)); + if (methods.length == 0) { + return ResolveResult.EMPTY_ARRAY; + } + + final List result = new ArrayList(); + for (PsiMethod method : methods) { + if (checkSignature(method)) { + result.add(new PsiElementResolveResult(method)); + } + } + return result.toArray(new ResolveResult[result.size()]); + } + + private static boolean checkSignature(PsiMethod method) { + if (method.getReturnType() != PsiType.VOID) { + return false; + } + + if (method.hasModifierProperty(PsiModifier.STATIC) || + method.hasModifierProperty(PsiModifier.ABSTRACT) || + !method.hasModifierProperty(PsiModifier.PUBLIC)) { + return false; + } + + final PsiClass aClass = method.getContainingClass(); + if (aClass == null || aClass.isInterface()) { + return false; + } + + final PsiParameter[] parameters = method.getParameterList().getParameters(); + if (parameters.length != 1) { + return false; + } + + final PsiType paramType = parameters[0].getType(); + if (!(paramType instanceof PsiClassType)) { + return false; + } + + final PsiClass paramClass = ((PsiClassType)paramType).resolve(); + return paramClass != null && AndroidUtils.VIEW_CLASS_NAME.equals(paramClass.getQualifiedName()); + } + + @NotNull + @Override + public Object[] getVariants() { + final Project project = myElement.getProject(); + final GlobalSearchScope scope = ProjectScope.getAllScope(project); + + final PsiClass activityClass = JavaPsiFacade.getInstance(project).findClass(AndroidUtils.ACTIVITY_BASE_CLASS_NAME, scope); + if (activityClass == null) { + return EMPTY_ARRAY; + } + + final List result = new ArrayList(); + final Set methodNames = new HashSet(); + + ClassInheritorsSearch.search(activityClass, scope, true).forEach(new Processor() { + public boolean process(PsiClass c) { + for (PsiMethod method : c.getMethods()) { + if (checkSignature(method) && methodNames.add(method.getName())) { + result.add(createLookupElement(method)); + } + } + return true; + } + }); + return ArrayUtil.toObjectArray(result); + } + } + + private static LookupElement createLookupElement(PsiMethod method) { + final LookupElementBuilder builder = LookupElementBuilder.create(method, method.getName()) + .setIcon(method.getIcon(Iconable.ICON_FLAG_VISIBILITY)) + .setPresentableText(method.getName()); + final PsiClass containingClass = method.getContainingClass(); + return containingClass != null + ? builder.setTailText(" (" + containingClass.getQualifiedName() + ')') + : builder; + } +} diff --git a/plugins/android/src/org/jetbrains/android/inspections/AndroidDomInspection.java b/plugins/android/src/org/jetbrains/android/inspections/AndroidDomInspection.java index 20afa22bd832..95d71a6ba263 100644 --- a/plugins/android/src/org/jetbrains/android/inspections/AndroidDomInspection.java +++ b/plugins/android/src/org/jetbrains/android/inspections/AndroidDomInspection.java @@ -21,6 +21,7 @@ import com.intellij.util.xml.WrappingConverter; import com.intellij.util.xml.highlighting.BasicDomElementsInspection; import org.jetbrains.android.dom.AndroidDomElement; import org.jetbrains.android.dom.converters.AndroidPackageConverter; +import org.jetbrains.android.dom.converters.OnClickConverter; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.NonNls; @@ -55,6 +56,7 @@ public class AndroidDomInspection extends BasicDomElementsInspection + + + + diff --git a/plugins/android/testData/dom/layout/onClickHighlighting.xml b/plugins/android/testData/dom/layout/onClickHighlighting.xml new file mode 100644 index 000000000000..37709a9f8d51 --- /dev/null +++ b/plugins/android/testData/dom/layout/onClickHighlighting.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/plugins/android/testData/dom/layout/onClickNavigation.xml b/plugins/android/testData/dom/layout/onClickNavigation.xml new file mode 100644 index 000000000000..40d0f1fa29b9 --- /dev/null +++ b/plugins/android/testData/dom/layout/onClickNavigation.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/android/testSrc/org/jetbrains/android/dom/AndroidLayoutDomTest.java b/plugins/android/testSrc/org/jetbrains/android/dom/AndroidLayoutDomTest.java index 2dc52e01bf13..28a771d99517 100644 --- a/plugins/android/testSrc/org/jetbrains/android/dom/AndroidLayoutDomTest.java +++ b/plugins/android/testSrc/org/jetbrains/android/dom/AndroidLayoutDomTest.java @@ -1,13 +1,12 @@ package org.jetbrains.android.dom; import com.android.sdklib.SdkConstants; +import com.intellij.codeInsight.TargetElementUtilBase; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiReference; +import com.intellij.psi.*; import com.intellij.util.ArrayUtil; +import java.io.IOException; import java.util.List; /** @@ -282,5 +281,37 @@ public class AndroidLayoutDomTest extends AndroidDomTest { PsiElement rootViewClass = rootReference.resolve(); assertTrue("Must be PsiClass reference", rootViewClass instanceof PsiClass); } + + public void testOnClickCompletion() throws Throwable { + copyOnClickClasses(); + doTestCompletionVariants(getTestName(true) + ".xml", "clickHandler1", "clickHandler7"); + } + + public void testOnClickHighlighting() throws Throwable { + copyOnClickClasses(); + doTestHighlighting(); + } + + public void testOnClickNavigation() throws Throwable { + copyOnClickClasses(); + final VirtualFile file = copyFileToProject(getTestName(true) + ".xml"); + myFixture.configureFromExistingVirtualFile(file); + + final PsiReference reference = TargetElementUtilBase.findReference(myFixture.getEditor(), myFixture.getCaretOffset()); + assertNotNull(reference); + assertInstanceOf(reference, PsiPolyVariantReference.class); + final ResolveResult[] results = ((PsiPolyVariantReference)reference).multiResolve(false); + assertEquals(3, results.length); + for (ResolveResult result : results) { + assertInstanceOf(result.getElement(), PsiMethod.class); + } + } + + private void copyOnClickClasses() throws IOException { + copyFileToProject("OnClick_Class1.java", "src/p1/p2/OnClick_Class1.java"); + copyFileToProject("OnClick_Class2.java", "src/p1/p2/OnClick_Class2.java"); + copyFileToProject("OnClick_Class3.java", "src/p1/p2/OnClick_Class3.java"); + copyFileToProject("OnClick_Class4.java", "src/p1/p2/OnClick_Class4.java"); + } }