Add gutter icon with navigation to related files for Android components

This commit is contained in:
Vyacheslav Gerasimov
2017-05-24 16:13:56 +03:00
parent 3f03b05bd3
commit fdf098d65c
10 changed files with 185 additions and 26 deletions
@@ -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<PsiClass>? {
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)
@@ -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<PsiElement>, result: MutableCollection<LineMarkerInfo<PsiElement>>) {
elements.forEach {
(it as? KtClass)?.getLineMarkerInfo()?.let { marker -> result.add(marker) }
}
}
private fun KtClass.getLineMarkerInfo(): LineMarkerInfo<PsiElement>? {
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<GotoRelatedItem> {
val resources = mutableSetOf<PsiFile>()
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<PsiFile>()
resources.addAll(files)
}
})
return resources.map { GotoRelatedLayoutItem(it) }
}
private fun KtClass.collectGoToRelatedManifestItems(manifest: Manifest): List<GotoRelatedItem> =
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) }
}
}
}
@@ -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")
@@ -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)
}
}
@@ -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
}
@@ -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
}
@@ -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
}
@@ -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");
+2
View File
@@ -18,6 +18,8 @@
<editorNotificationProvider implementation="org.jetbrains.kotlin.android.actions.KotlinNewActivityNotification"/>
<codeInsight.lineMarkerProvider language="kotlin" implementationClass="org.jetbrains.kotlin.android.KotlinAndroidLineMarkerProvider"/>
<intentionAction>
<className>org.jetbrains.kotlin.android.intention.KotlinAndroidAddStringResource</className>
<category>Kotlin Android</category>
+9
View File
@@ -0,0 +1,9 @@
import android.app.Activity
import android.os.Bundle
class <caret>KotlinActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_kotlin)
}
}