Add gutter icon with navigation to related files for Android components
This commit is contained in:
@@ -22,8 +22,11 @@ import com.android.SdkConstants.R_CLASS
|
|||||||
import com.android.resources.ResourceType
|
import com.android.resources.ResourceType
|
||||||
import com.android.tools.idea.AndroidPsiUtils
|
import com.android.tools.idea.AndroidPsiUtils
|
||||||
import com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType.*
|
import com.android.tools.idea.AndroidPsiUtils.ResourceReferenceType.*
|
||||||
|
import com.intellij.psi.PsiClass
|
||||||
import com.intellij.psi.PsiElement
|
import com.intellij.psi.PsiElement
|
||||||
import org.jetbrains.android.augment.AndroidPsiElementFinder
|
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.facet.AndroidFacet
|
||||||
import org.jetbrains.android.util.AndroidResourceUtil
|
import org.jetbrains.android.util.AndroidResourceUtil
|
||||||
import org.jetbrains.android.util.AndroidResourceUtil.isManifestJavaFile
|
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.ClassDescriptor
|
||||||
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
|
import org.jetbrains.kotlin.descriptors.PackageFragmentDescriptor
|
||||||
import org.jetbrains.kotlin.idea.caches.resolve.analyze
|
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.JavaClassDescriptor
|
||||||
import org.jetbrains.kotlin.load.java.descriptors.JavaPropertyDescriptor
|
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.KtExpression
|
||||||
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
import org.jetbrains.kotlin.psi.KtQualifiedExpression
|
||||||
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
|
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
|
||||||
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
|
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelector
|
||||||
import org.jetbrains.kotlin.resolve.BindingContext
|
import org.jetbrains.kotlin.resolve.BindingContext
|
||||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
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.lazy.BodyResolveMode
|
||||||
import org.jetbrains.kotlin.resolve.source.PsiSourceFile
|
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? {
|
internal fun PsiElement.getAndroidFacetForFile(): AndroidFacet? {
|
||||||
val file = containingFile ?: return null
|
val file = containingFile ?: return null
|
||||||
return AndroidFacet.getInstance(file)
|
return AndroidFacet.getInstance(file)
|
||||||
|
|||||||
+135
@@ -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.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")
|
private fun KotlinType.isIBinder(): Boolean = fqNameEquals("android.os.IBinder")
|
||||||
|
|
||||||
|
|||||||
@@ -16,30 +16,14 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.android
|
package org.jetbrains.kotlin.android
|
||||||
|
|
||||||
import org.jetbrains.kotlin.name.FqName
|
|
||||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
|
||||||
import org.jetbrains.kotlin.types.KotlinType
|
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.fqNameSafe() = constructor.declarationDescriptor?.fqNameSafe
|
||||||
|
|
||||||
internal fun KotlinType.isSubclassOf(className: FqName, strict: Boolean = false): Boolean =
|
internal fun KotlinType.isSubclassOf(className: String, strict: Boolean = false): Boolean {
|
||||||
(!strict && fqNameSafe() == className) || constructor.supertypes.any {
|
return (!strict && fqNameSafe()?.asString() == className) || constructor.supertypes.any {
|
||||||
className == it.fqNameSafe() || it.isSubclassOf(className, true)
|
it.fqNameSafe()?.asString() == className || it.isSubclassOf(className, true)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -41,5 +41,5 @@ class AddActivityToManifest : AbstractRegisterComponentAction("Add activity to m
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun KtClass.isSubclassOfActivity() =
|
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
|
||||||
}
|
}
|
||||||
+1
-1
@@ -41,5 +41,5 @@ class AddBroadcastReceiverToManifest : AbstractRegisterComponentAction("Add broa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun KtClass.isSubclassOfBroadcastReceiver() =
|
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
|
||||||
}
|
}
|
||||||
+1
-1
@@ -41,5 +41,5 @@ class AddServiceToManifest : AbstractRegisterComponentAction("Add service to man
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun KtClass.isSubclassOfService() =
|
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
|
||||||
}
|
}
|
||||||
+6
@@ -54,6 +54,12 @@ public class AndroidGutterIconTestGenerated extends AbstractAndroidGutterIconTes
|
|||||||
doTest(fileName);
|
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")
|
@TestMetadata("systemColor.kt")
|
||||||
public void testSystemColor() throws Exception {
|
public void testSystemColor() throws Exception {
|
||||||
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/systemColor.kt");
|
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/android/gutterIcon/systemColor.kt");
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
<editorNotificationProvider implementation="org.jetbrains.kotlin.android.actions.KotlinNewActivityNotification"/>
|
<editorNotificationProvider implementation="org.jetbrains.kotlin.android.actions.KotlinNewActivityNotification"/>
|
||||||
|
|
||||||
|
<codeInsight.lineMarkerProvider language="kotlin" implementationClass="org.jetbrains.kotlin.android.KotlinAndroidLineMarkerProvider"/>
|
||||||
|
|
||||||
<intentionAction>
|
<intentionAction>
|
||||||
<className>org.jetbrains.kotlin.android.intention.KotlinAndroidAddStringResource</className>
|
<className>org.jetbrains.kotlin.android.intention.KotlinAndroidAddStringResource</className>
|
||||||
<category>Kotlin Android</category>
|
<category>Kotlin Android</category>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user