diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt.as32 b/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt.as32 new file mode 100644 index 00000000000..8fd57d7706a --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/KotlinAndroidLineMarkerProvider.kt.as32 @@ -0,0 +1,138 @@ +/* + * 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.android.resourceManagers.ModuleResourceManagers +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor +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 = ModuleResourceManagers + .getInstance(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.oldName(), + SdkConstants.CLASS_V4_FRAGMENT.newName(), + "android.widget.Adapter") + + private fun KtClass.isClassWithLayoutXml(): Boolean { + val type = (unsafeResolveToDescriptor(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/quickfix/AddTargetApiQuickFix.kt.as32 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetApiQuickFix.kt.as32 new file mode 100644 index 00000000000..2e3c95ff25d --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/AddTargetApiQuickFix.kt.as32 @@ -0,0 +1,95 @@ +/* + * 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.quickfix + +import com.android.SdkConstants +import com.android.tools.lint.checks.ApiDetector.REQUIRES_API_ANNOTATION +import com.intellij.codeInsight.FileModificationService +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidQuickfixContexts +import org.jetbrains.android.util.AndroidBundle +import org.jetbrains.kotlin.android.hasBackingField +import org.jetbrains.kotlin.idea.util.addAnnotation +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.* + + +class AddTargetApiQuickFix( + val api: Int, + val useRequiresApi: Boolean +) : AndroidLintQuickFix { + + private companion object { + val FQNAME_TARGET_API = FqName(SdkConstants.FQCN_TARGET_API) + val FQNAME_REQUIRES_API = FqName(REQUIRES_API_ANNOTATION.defaultName()) + } + + override fun isApplicable(startElement: PsiElement, endElement: PsiElement, contextType: AndroidQuickfixContexts.ContextType): Boolean = + getAnnotationContainer(startElement, useRequiresApi) != null + + override fun getName(): String = getAnnotationValue(false).let { + if (useRequiresApi) { + // Not Available in Android plugin 2.0 + // AndroidBundle.message("android.lint.fix.add.requires.api", it) + "Add @RequiresApi($it) Annotation" + } else { + AndroidBundle.message("android.lint.fix.add.target.api", it) + } + } + + override fun apply(startElement: PsiElement, endElement: PsiElement, context: AndroidQuickfixContexts.Context) { + val annotationContainer = getAnnotationContainer(startElement, useRequiresApi) ?: return + if (!FileModificationService.getInstance().preparePsiElementForWrite(annotationContainer)) { + return + } + + if (annotationContainer is KtModifierListOwner) { + annotationContainer.addAnnotation( + if (useRequiresApi) FQNAME_REQUIRES_API else FQNAME_TARGET_API, + getAnnotationValue(true), + whiteSpaceText = if (annotationContainer.isNewLineNeededForAnnotation()) "\n" else " ") + } + } + + private fun KtElement.isNewLineNeededForAnnotation() = !(this is KtParameter || this is KtTypeParameter || this is KtPropertyAccessor) + + private fun getAnnotationValue(fullyQualified: Boolean) = getVersionField(api, fullyQualified) + + private fun getAnnotationContainer(element: PsiElement, useRequiresApi: Boolean): PsiElement? { + return PsiTreeUtil.findFirstParent(element) { + if (useRequiresApi) + it.isRequiresApiAnnotationValidTarget() + else + it.isTargetApiAnnotationValidTarget() + } + } + + private fun PsiElement.isRequiresApiAnnotationValidTarget(): Boolean { + return this is KtClassOrObject || + (this is KtFunction && this !is KtFunctionLiteral) || + (this is KtProperty && !isLocal && hasBackingField()) || + this is KtPropertyAccessor + } + + private fun PsiElement.isTargetApiAnnotationValidTarget(): Boolean { + return this is KtClassOrObject || + (this is KtFunction && this !is KtFunctionLiteral) || + this is KtPropertyAccessor + } +} \ No newline at end of file diff --git a/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.as32 b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.as32 new file mode 100644 index 00000000000..c96480c222c --- /dev/null +++ b/idea/idea-android/src/org/jetbrains/kotlin/android/quickfix/KotlinAndroidQuickFixProvider.kt.as32 @@ -0,0 +1,80 @@ +/* + * 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.quickfix + +import com.android.SdkConstants.SUPPORT_ANNOTATIONS_PREFIX +import com.android.tools.lint.checks.ApiDetector +import com.android.tools.lint.checks.CommentDetector +import com.android.tools.lint.checks.ParcelDetector +import com.android.tools.lint.detector.api.Issue +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import org.jetbrains.android.inspections.lint.AndroidLintQuickFix +import org.jetbrains.android.inspections.lint.AndroidLintQuickFixProvider +import java.util.regex.Pattern + + +class KotlinAndroidQuickFixProvider : AndroidLintQuickFixProvider { + override fun getQuickFixes( + issue: Issue, + startElement: PsiElement, + endElement: PsiElement, + message: String, + data: Any? + ): Array { + val fixes: Array = when (issue) { + ApiDetector.UNSUPPORTED, ApiDetector.INLINED -> getApiQuickFixes(issue, startElement, message) + ParcelDetector.ISSUE -> arrayOf(ParcelableQuickFix()) + else -> emptyArray() + } + + if (issue != CommentDetector.STOP_SHIP) { + return fixes + SuppressLintQuickFix(issue.id) + } + + return fixes + } + + fun getApiQuickFixes(issue: Issue, element: PsiElement, message: String): Array { + val api = getRequiredVersion(message) + if (api == -1) { + return AndroidLintQuickFix.EMPTY_ARRAY + } + + val project = element.project + if (JavaPsiFacade.getInstance(project).findClass(REQUIRES_API_ANNOTATION, GlobalSearchScope.allScope(project)) != null) { + return arrayOf(AddTargetApiQuickFix(api, true), AddTargetApiQuickFix(api, false), AddTargetVersionCheckQuickFix(api)) + } + + return arrayOf(AddTargetApiQuickFix(api, false), AddTargetVersionCheckQuickFix(api)) + } + + private fun getRequiredVersion(errorMessage: String): Int { + val pattern = Pattern.compile("\\s(\\d+)\\s") + val matcher = pattern.matcher(errorMessage) + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)) + } + + return -1 + } + + companion object { + val REQUIRES_API_ANNOTATION = SUPPORT_ANNOTATIONS_PREFIX.defaultName() + "RequiresApi" + } +} \ No newline at end of file