IDEA-69782 IDEA-67009 converter for onClick xml attribute

This commit is contained in:
Eugene Kudelevsky
2011-06-24 16:59:11 +04:00
parent 3de32d3a7a
commit addd41e6ac
11 changed files with 298 additions and 6 deletions
@@ -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<String, String> SPECIAL_RESOURCE_TYPES = new HashMap<String, String>();
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;
}
@@ -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<String> implements CustomReferenceConverter<String> {
@NotNull
@Override
public PsiReference[] createReferences(GenericDomValue<String> 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<XmlAttributeValue> {
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<MyReference>() {
@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<ResolveResult> result = new ArrayList<ResolveResult>();
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<Object> result = new ArrayList<Object>();
final Set<String> methodNames = new HashSet<String>();
ClassInheritorsSearch.search(activityClass, scope, true).forEach(new Processor<PsiClass>() {
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;
}
}
@@ -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<AndroidDomE
@Override
protected boolean shouldCheckResolveProblems(GenericDomValue value) {
final Converter realConverter = WrappingConverter.getDeepestConverter(value.getConverter(), value);
return !(realConverter instanceof AndroidPackageConverter);
return !(realConverter instanceof AndroidPackageConverter) &&
!(realConverter instanceof OnClickConverter);
}
}
@@ -0,0 +1,27 @@
package p1.p2;
import android.app.Activity;
import android.view.View;
public class OnClick_Class1 extends Activity {
public void clickHandler1(View v) {
}
void clickHandler2(View v) {
}
public void clickHandler3(String s) {
}
public abstract void clickHandler4(View v) {
}
public static void clickHandler5(View v) {
}
public int clickHandler6(View v) {
}
public final void clickHandler7(View v) {
}
}
@@ -0,0 +1,9 @@
package p1.p2;
import android.app.Activity;
import android.view.View;
public class OnClick_Class2 extends Activity {
public final void clickHandler1(View v) {
}
}
@@ -0,0 +1,14 @@
package p1.p2;
import android.app.Activity;
import android.view.View;
public class OnClick_Class3 {
public void clickHandler1(View v) {
}
public void clickHandler123(View v) {
}
}
@@ -0,0 +1,11 @@
package p1.p2;
import android.app.Activity;
import android.view.View;
public class OnClick_Class4 {
void clickHandler1(View v) {
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:onClick="<caret>"/>
</LinearLayout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="clickHandler"/>
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="nonexistentClickHandler"/>
</LinearLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android">
<TextView android:onClick="clickH<caret>andler1"/>
</LinearLayout>
@@ -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");
}
}