From fdf098d65cdd4311ccc4d934b121ef2db326c377 Mon Sep 17 00:00:00 2001 From: Vyacheslav Gerasimov Date: Wed, 24 May 2017 16:13:56 +0300 Subject: [PATCH] Add gutter icon with navigation to related files for Android components --- .../jetbrains/kotlin/android/AndroidUtil.kt | 25 +++- .../KotlinAndroidLineMarkerProvider.kt | 135 ++++++++++++++++++ .../kotlin/android/ParcelableUtil.kt | 2 +- .../org/jetbrains/kotlin/android/TypeUtil.kt | 26 +--- .../intention/AddActivityToManifest.kt | 2 +- .../AddBroadcastReceiverToManifest.kt | 2 +- .../android/intention/AddServiceToManifest.kt | 2 +- .../AndroidGutterIconTestGenerated.java | 6 + idea/src/META-INF/android.xml | 2 + .../android/gutterIcon/relatedFiles.kt | 9 ++ 10 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt create mode 100644 idea/testData/android/gutterIcon/relatedFiles.kt diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt index bf8369de4e9..2765d06dfcf 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/AndroidUtil.kt @@ -22,8 +22,11 @@ import com.android.SdkConstants.R_CLASS import com.android.resources.ResourceType import com.android.tools.idea.AndroidPsiUtils import com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType.* +import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import org.jetbrains.android.augment.AndroidPsiElementFinder +import org.jetbrains.android.dom.AndroidAttributeValue +import org.jetbrains.android.dom.manifest.Manifest import org.jetbrains.android.facet.AndroidFacet import org.jetbrains.android.util.AndroidResourceUtil import org.jetbrains.android.util.AndroidResourceUtil.isManifestJavaFile @@ -32,18 +35,38 @@ import org.jetbrains.android.util.AndroidUtils import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.load.java.descriptors.JavaClassDescriptor import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor +import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtExpression import org.jetbrains.kotlin.psi.KtQualifiedExpression import org.jetbrains.kotlin.psi.KtSimpleNameExpression import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.resolve.source.PsiSourceFile +internal fun KtClass.findComponentDeclarationInManifest(manifest: Manifest): AndroidAttributeValue? { + val application = manifest.application ?: return null + val type = (resolveToDescriptor(BodyResolveMode.PARTIAL) as? ClassDescriptor)?.defaultType ?: return null + + val component = when { + type.isSubclassOf(AndroidUtils.ACTIVITY_BASE_CLASS_NAME) -> + application.activities?.find { it.activityClass.value?.qualifiedName == fqName?.asString() }?.activityClass + type.isSubclassOf(AndroidUtils.SERVICE_CLASS_NAME) -> + application.services?.find { it.serviceClass.value?.qualifiedName == fqName?.asString() }?.serviceClass + type.isSubclassOf(AndroidUtils.RECEIVER_CLASS_NAME) -> + application.receivers?.find { it.receiverClass.value?.qualifiedName == fqName?.asString() }?.receiverClass + type.isSubclassOf(AndroidUtils.PROVIDER_CLASS_NAME) -> + application.providers?.find { it.providerClass.value?.qualifiedName == fqName?.asString() }?.providerClass + else -> null + } + + return component +} + internal fun PsiElement.getAndroidFacetForFile(): AndroidFacet? { val file = containingFile ?: return null return AndroidFacet.getInstance(file) diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt new file mode 100644 index 00000000000..dc6b259b950 --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.android + +import com.android.SdkConstants +import com.android.resources.ResourceType +import com.intellij.codeHighlighting.Pass +import com.intellij.codeInsight.daemon.GutterIconNavigationHandler +import com.intellij.codeInsight.daemon.LineMarkerInfo +import com.intellij.codeInsight.daemon.LineMarkerProvider +import com.intellij.codeInsight.navigation.NavigationUtil +import com.intellij.icons.AllIcons +import com.intellij.ide.highlighter.XmlFileType +import com.intellij.navigation.GotoRelatedItem +import com.intellij.openapi.editor.markup.GutterIconRenderer +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.xml.XmlAttributeValue +import com.intellij.ui.awt.RelativePoint +import com.intellij.util.Function +import org.jetbrains.android.dom.manifest.Manifest +import org.jetbrains.android.facet.AndroidFacet +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import java.awt.event.MouseEvent +import javax.swing.Icon + + +class KotlinAndroidLineMarkerProvider : LineMarkerProvider { + override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? = null + + override fun collectSlowLineMarkers(elements: List, result: MutableCollection>) { + elements.forEach { + (it as? KtClass)?.getLineMarkerInfo()?.let { marker -> result.add(marker) } + } + } + + private fun KtClass.getLineMarkerInfo(): LineMarkerInfo? { + val nameIdentifier = nameIdentifier ?: return null + val androidFacet = AndroidFacet.getInstance(this) ?: return null + val manifest = androidFacet.manifest ?: return null + val manifestItems = collectGoToRelatedManifestItems(manifest) + if (manifestItems.isEmpty() && !isClassWithLayoutXml()) { + return null + } + + return LineMarkerInfo( + nameIdentifier, + nameIdentifier.textRange, + AllIcons.FileTypes.Xml, + Pass.LINE_MARKERS, + Function { "Related XML file" }, + GutterIconNavigationHandler { e: MouseEvent, _: PsiElement -> + NavigationUtil + .getRelatedItemsPopup( + manifestItems + collectGoToRelatedLayoutItems(androidFacet), + "Go to Related Files") + .show(RelativePoint(e)) + }, + GutterIconRenderer.Alignment.RIGHT) + } + + private fun KtClass.collectGoToRelatedLayoutItems(androidFacet: AndroidFacet): List { + val resources = mutableSetOf() + accept(object: KtVisitorVoid() { + override fun visitKtElement(element: KtElement) { + element.acceptChildren(this) + } + + override fun visitReferenceExpression(expression: KtReferenceExpression) { + super.visitReferenceExpression(expression) + + val resClassName = ResourceType.LAYOUT.getName() + val info = (expression as? KtSimpleNameExpression)?.let { + getReferredResourceOrManifestField(androidFacet, it, resClassName, true) + } + + if (info == null || info.isFromManifest) { + return + } + + val files = androidFacet + .localResourceManager + .findResourcesByFieldName(resClassName, info.fieldName) + .filterIsInstance() + + resources.addAll(files) + } + }) + + return resources.map { GotoRelatedLayoutItem(it) } + } + + private fun KtClass.collectGoToRelatedManifestItems(manifest: Manifest): List = + findComponentDeclarationInManifest(manifest)?.xmlAttributeValue?.let { listOf(GotoManifestItem(it)) } ?: emptyList() + + private class GotoManifestItem(attributeValue: XmlAttributeValue) : GotoRelatedItem(attributeValue) { + override fun getCustomName(): String? = "AndroidManifest.xml" + override fun getCustomContainerName(): String? = "" + override fun getCustomIcon(): Icon? = XmlFileType.INSTANCE.icon + } + + private class GotoRelatedLayoutItem(private val file: PsiFile) : GotoRelatedItem(file, "Layout Files") { + override fun getCustomContainerName(): String? = "(${file.containingDirectory.name})" + } + + companion object { + private val CLASSES_WITH_LAYOUT_XML = arrayOf( + SdkConstants.CLASS_ACTIVITY, + SdkConstants.CLASS_FRAGMENT, + SdkConstants.CLASS_V4_FRAGMENT, + "android.widget.Adapter") + + private fun KtClass.isClassWithLayoutXml(): Boolean { + val type = (resolveToDescriptor(BodyResolveMode.PARTIAL) as? ClassDescriptor)?.defaultType ?: return false + return CLASSES_WITH_LAYOUT_XML.any { type.isSubclassOf(it, true) } + } + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/ParcelableUtil.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/ParcelableUtil.kt index b57327dca8b..b1426cfcadf 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/ParcelableUtil.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/ParcelableUtil.kt @@ -441,7 +441,7 @@ private fun KotlinType.getName() = constructor.declarationDescriptor?.name private fun KotlinType.fqNameEquals(fqName: String) = constructor.declarationDescriptor?.fqNameSafe?.asString() == fqName -private fun KotlinType.isSubclassOfParcelable(strict: Boolean = false): Boolean = isSubclassOf(FqName(CLASS_PARCELABLE), strict) +private fun KotlinType.isSubclassOfParcelable(strict: Boolean = false): Boolean = isSubclassOf(CLASS_PARCELABLE, strict) private fun KotlinType.isIBinder(): Boolean = fqNameEquals("android.os.IBinder") diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/TypeUtil.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/TypeUtil.kt index be5b58765d4..55916ebecd5 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/TypeUtil.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/TypeUtil.kt @@ -16,30 +16,14 @@ package org.jetbrains.kotlin.android -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe import org.jetbrains.kotlin.types.KotlinType -/* - * Copyright 2010-2017 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - internal fun KotlinType.fqNameSafe() = constructor.declarationDescriptor?.fqNameSafe -internal fun KotlinType.isSubclassOf(className: FqName, strict: Boolean = false): Boolean = - (!strict && fqNameSafe() == className) || constructor.supertypes.any { - className == it.fqNameSafe() || it.isSubclassOf(className, true) - } +internal fun KotlinType.isSubclassOf(className: String, strict: Boolean = false): Boolean { + return (!strict && fqNameSafe()?.asString() == className) || constructor.supertypes.any { + it.fqNameSafe()?.asString() == className || it.isSubclassOf(className, true) + } +} diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddActivityToManifest.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddActivityToManifest.kt index 9208e76bc37..7115bb951db 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddActivityToManifest.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddActivityToManifest.kt @@ -41,5 +41,5 @@ class AddActivityToManifest : AbstractRegisterComponentAction("Add activity to m } private fun KtClass.isSubclassOfActivity() = - (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_ACTIVITY), true) ?: false + (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(SdkConstants.CLASS_ACTIVITY, true) ?: false } \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddBroadcastReceiverToManifest.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddBroadcastReceiverToManifest.kt index abff7180a6e..0a59238be30 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddBroadcastReceiverToManifest.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddBroadcastReceiverToManifest.kt @@ -41,5 +41,5 @@ class AddBroadcastReceiverToManifest : AbstractRegisterComponentAction("Add broa } private fun KtClass.isSubclassOfBroadcastReceiver() = - (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_BROADCASTRECEIVER), true) ?: false + (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(SdkConstants.CLASS_BROADCASTRECEIVER, true) ?: false } \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddServiceToManifest.kt b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddServiceToManifest.kt index 08aaa1e6da1..c2089a66199 100644 --- a/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddServiceToManifest.kt +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/intention/AddServiceToManifest.kt @@ -41,5 +41,5 @@ class AddServiceToManifest : AbstractRegisterComponentAction("Add service to man } private fun KtClass.isSubclassOfService() = - (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(FqName(SdkConstants.CLASS_SERVICE), true) ?: false + (descriptor as? ClassDescriptor)?.defaultType?.isSubclassOf(SdkConstants.CLASS_SERVICE, true) ?: false } \ No newline at end of file diff --git a/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java index 0daa40e43fd..1bf625a7edd 100644 --- a/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java +++ b/idea/idea-android/tests/org/jetbrains/kotlin/android/annotator/AndroidGutterIconTestGenerated.java @@ -54,6 +54,12 @@ public class AndroidGutterIconTestGenerated extends AbstractAndroidGutterIconTes doTest(fileName); } + @TestMetadata("relatedFiles.kt") + public void testRelatedFiles() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/relatedFiles.kt"); + doTest(fileName); + } + @TestMetadata("systemColor.kt") public void testSystemColor() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/systemColor.kt"); diff --git a/idea/src/META-INF/android.xml b/idea/src/META-INF/android.xml index 0678f0f0953..dc7ef279011 100644 --- a/idea/src/META-INF/android.xml +++ b/idea/src/META-INF/android.xml @@ -18,6 +18,8 @@ + + org.jetbrains.kotlin.android.intention.KotlinAndroidAddStringResource Kotlin Android diff --git a/idea/testData/android/gutterIcon/relatedFiles.kt b/idea/testData/android/gutterIcon/relatedFiles.kt new file mode 100644 index 00000000000..438650e9fb9 --- /dev/null +++ b/idea/testData/android/gutterIcon/relatedFiles.kt @@ -0,0 +1,9 @@ +import android.app.Activity +import android.os.Bundle + +class KotlinActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_kotlin) + } +} \ No newline at end of file