Browse Source

[Do not merge] Migrate composable support from Android Studio as proof-of-concept

compose-tooling
Ilya Ryzhenkov 2 years ago
parent
commit
e229162423
  1. 11
      idea-plugin/build.gradle.kts
  2. 8
      idea-plugin/gradle.properties
  3. 3
      idea-plugin/lib/README
  4. BIN
      idea-plugin/lib/compiler-hosted-1.1.0-SNAPSHOT.jar
  5. 91
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableAnnotator.kt
  6. 520
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableCallChecker.kt
  7. 194
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableDeclarationChecker.kt
  8. 64
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableElementRefactoringElementListenerProvider.kt
  9. 69
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableFunctionExtractableAnalyser.kt
  10. 119
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeAutoDocumentation.kt
  11. 44
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeBundle.kt
  12. 45
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeCodeCompletionConfigurable.kt
  13. 78
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeColorSettingsPage.kt
  14. 73
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeDiagnosticSuppressor.kt
  15. 109
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrorMessages.kt
  16. 117
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrors.kt
  17. 63
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFoldingBuilder.kt
  18. 86
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFqNames.kt
  19. 97
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.kt
  20. 105
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginTypeResolutionInterceptorExtension.kt
  21. 45
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginUtils.kt
  22. 35
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSettings.kt
  23. 10
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSuppressor.kt
  24. 31
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeWritableSlices.kt
  25. 74
      idea-plugin/src/main/kotlin/com/android/tools/compose/PsiUtils.kt
  26. 380
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeCompletionContributor.kt
  27. 179
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeImplementationsCompletionContributor.kt
  28. 297
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeModifierCompletionContributor.kt
  29. 21
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/Constants.kt
  30. 155
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/ConstraintLayoutJsonCompletionContributor.kt
  31. 43
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormat.kt
  32. 91
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormatHandler.kt
  33. 29
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerClassesFilterProvider.kt
  34. 60
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettings.kt
  35. 26
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.form
  36. 48
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.java
  37. 145
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManager.kt
  38. 27
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManagerFactory.kt
  39. 258
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectClassRenderer.kt
  40. 77
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectRendererProviderBase.kt
  41. 69
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/KotlinMapEntryRenderer.kt
  42. 112
      idea-plugin/src/main/kotlin/com/android/tools/compose/formatting/ComposePostFormatProcessor.kt
  43. 112
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreateComposableFunction.kt
  44. 78
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreatePreviewAction.kt
  45. 123
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeDelegateStateImportFix.kt
  46. 147
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeSurroundWithWidgetAction.kt
  47. 74
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeUnwrapAction.kt
  48. 67
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeWrapModifiersAction.kt
  49. 31
      idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeCustomCodeStyleSettings.kt
  50. 79
      idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeFormattingCodeStyleSettingsProvider.kt
  51. 27
      idea-plugin/src/main/kotlin/com/android/tools/idea/AndroidTextUtils.kt
  52. 21
      idea-plugin/src/main/kotlin/com/android/tools/idea/flags/StudioFlags.kt
  53. 12
      idea-plugin/src/main/kotlin/com/android/tools/modules/Module.kt
  54. 25
      idea-plugin/src/main/kotlin/icons/StudioIcons.kt
  55. 102
      idea-plugin/src/main/resources/META-INF/plugin.xml
  56. 23
      idea-plugin/src/main/resources/colorschemes/IdeComposableAnnotatorColorSchemeDefault.xml
  57. 1
      idea-plugin/src/main/resources/icons/compose/composable-function.svg
  58. 7
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/after.kt.template
  59. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/before.kt.template
  60. 22
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/description.html
  61. 8
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template
  62. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template
  63. 5
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/description.html
  64. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/after.kt.template
  65. 8
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/before.kt.template
  66. 20
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/description.html
  67. 7
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/after.kt.template
  68. 4
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/before.kt.template
  69. 20
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/description.html
  70. 29
      idea-plugin/src/main/resources/messages/ComposeBundle.properties

11
idea-plugin/build.gradle.kts

@ -2,8 +2,8 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.5.10"
id("org.jetbrains.intellij") version "1.6.0"
id("org.jetbrains.kotlin.jvm") version "1.6.21"
id("org.jetbrains.intellij") version "1.7.0"
id("org.jetbrains.changelog") version "1.3.1"
}
@ -18,6 +18,7 @@ repositories {
dependencies {
implementation("org.jetbrains.compose:preview-rpc")
implementation(files("lib/compiler-hosted-1.1.0-SNAPSHOT.jar"))
}
intellij {
@ -47,7 +48,7 @@ tasks {
targetCompatibility = "1.8"
}
withType<KotlinJvmCompile> {
kotlinOptions.jvmTarget = "1.8"
kotlinOptions.jvmTarget = "11"
}
publishPlugin {
@ -60,6 +61,10 @@ tasks {
untilBuild.set(projectProperties.pluginUntilBuild)
}
runIde {
maxHeapSize = "2g"
}
runPluginVerifier {
ideVersions.set(projectProperties.pluginVerifierIdeVersions)
}

8
idea-plugin/gradle.properties

@ -5,11 +5,11 @@ kotlin.stdlib.default.dependency=false
deploy.version=0.1-SNAPSHOT
plugin.channels=snapshots
plugin.since.build=203
plugin.until.build=222.*
plugin.since.build=221
plugin.until.build=223.*
## See https://jb.gg/intellij-platform-builds-list for available build versions.
plugin.verifier.ide.versions=2020.3.2, 2021.1, 2022.1
plugin.verifier.ide.versions=2022.1
platform.type=IC
platform.version=2021.1
platform.version=2022.1
platform.download.sources=true

3
idea-plugin/lib/README

@ -0,0 +1,3 @@
compiler-hosted-1.1.0-SNAPSHOT.jar
-This file comes form https://androidx.dev/snapshots/builds/7722085/artifacts
-It is meant to be a temporary solution until we figure out how to bundle compose-compiler in Studio properly.

BIN
idea-plugin/lib/compiler-hosted-1.1.0-SNAPSHOT.jar

Binary file not shown.

91
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableAnnotator.kt

@ -0,0 +1,91 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.Annotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.analyzer.AnalysisResult
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithAllCompilerChecks
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
// Used to apply styles for calls to @Composable functions.
class ComposableAnnotator : Annotator {
companion object TextAttributeRegistry {
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY: TextAttributesKey
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME = "ComposableCallTextAttributes"
private val ANALYSIS_RESULT_KEY = Key<AnalysisResult>(
"ComposableAnnotator.DidAnnotateKey"
)
private val CAN_CONTAIN_COMPOSABLE_KEY = Key<Boolean>(
"ComposableAnnotator.CanContainComposable"
)
init {
COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY = TextAttributesKey.createTextAttributesKey(
COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME,
DefaultLanguageHighlighterColors.FUNCTION_CALL)
}
}
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
if (element !is KtCallExpression) return
// AnnotationHolder.currentAnnotationSession applies to a single file.
var canContainComposable = holder.currentAnnotationSession.getUserData(CAN_CONTAIN_COMPOSABLE_KEY)
if (canContainComposable == null) {
// isComposeEnabled doesn't work for library sources, we check all kt library sources files. File check only once on opening.
canContainComposable = isComposeEnabled(element) ||
(element.containingFile.virtualFile != null &&
ProjectFileIndex.getInstance(element.project).isInLibrarySource(element.containingFile.virtualFile))
holder.currentAnnotationSession.putUserData(CAN_CONTAIN_COMPOSABLE_KEY, canContainComposable)
}
if (!canContainComposable) return
// AnnotationHolder.currentAnnotationSession applies to a single file.
var analysisResult = holder.currentAnnotationSession.getUserData(
ANALYSIS_RESULT_KEY
)
if (analysisResult == null) {
val ktFile = element.containingFile as? KtFile ?: return
analysisResult = ktFile.analyzeWithAllCompilerChecks()
holder.currentAnnotationSession.putUserData(
ANALYSIS_RESULT_KEY, analysisResult
)
}
if (analysisResult.isError()) {
throw ProcessCanceledException(analysisResult.error)
}
if (!shouldStyleCall(analysisResult.bindingContext, element)) return
val elementToStyle = element.calleeExpression ?: return
holder.newSilentAnnotation(HighlightSeverity.INFORMATION).range(elementToStyle).textAttributes(COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY).create()
}
private fun shouldStyleCall(bindingContext: BindingContext, element: KtCallExpression): Boolean {
return element.getResolvedCall(bindingContext)?.isComposableInvocation() == true
}
}

520
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableCallChecker.kt

@ -0,0 +1,520 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.descriptors.PropertyGetterDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.psi.KtAnnotatedExpression
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtFunctionLiteral
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.psi.KtPsiUtil
import org.jetbrains.kotlin.psi.KtTryExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.checkers.AdditionalTypeChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallChecker
import org.jetbrains.kotlin.resolve.calls.checkers.CallCheckerContext
import org.jetbrains.kotlin.resolve.calls.context.ResolutionContext
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
import org.jetbrains.kotlin.resolve.calls.model.VariableAsFunctionResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getValueArgumentForExpression
import org.jetbrains.kotlin.resolve.inline.InlineUtil.canBeInlineArgument
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInline
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlineParameter
import org.jetbrains.kotlin.resolve.inline.InlineUtil.isInlinedArgument
import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.lowerIfFlexible
import org.jetbrains.kotlin.types.typeUtil.builtIns
import org.jetbrains.kotlin.types.upperIfFlexible
import org.jetbrains.kotlin.util.OperatorNameConventions
open class ComposableCallChecker : CallChecker, AdditionalTypeChecker,
StorageComponentContainerContributor {
override fun registerModuleComponents(
container: StorageComponentContainer,
platform: TargetPlatform,
moduleDescriptor: ModuleDescriptor
) {
//if (!platform.isJvm()) return
container.useInstance(this)
}
fun checkInlineLambdaCall(
resolvedCall: ResolvedCall<*>,
reportOn: PsiElement,
context: CallCheckerContext
) {
if (resolvedCall !is VariableAsFunctionResolvedCall) return
val descriptor = resolvedCall.variableCall.resultingDescriptor
if (descriptor !is ValueParameterDescriptor) return
if (descriptor.type.hasDisallowComposableCallsAnnotation()) return
val function = descriptor.containingDeclaration
if (
function is FunctionDescriptor &&
function.isInline &&
function.isMarkedAsComposable()
) {
val bindingContext = context.trace.bindingContext
var node: PsiElement? = reportOn
loop@while (node != null) {
when (node) {
is KtLambdaExpression -> {
val arg = getArgumentDescriptor(node.functionLiteral, bindingContext)
if (arg?.type?.hasDisallowComposableCallsAnnotation() == true) {
val parameterSrc = descriptor.findPsi()
if (parameterSrc != null) {
missingDisallowedComposableCallPropagation(
context,
parameterSrc,
descriptor,
arg
)
}
}
}
is KtFunction -> {
val fn = bindingContext[BindingContext.FUNCTION, node]
if (fn == function) {
return
}
}
}
node = node.parent as? KtElement
}
}
}
override fun check(
resolvedCall: ResolvedCall<*>,
reportOn: PsiElement,
context: CallCheckerContext
) {
if (!resolvedCall.isComposableInvocation()) {
checkInlineLambdaCall(resolvedCall, reportOn, context)
return
}
val bindingContext = context.trace.bindingContext
var node: PsiElement? = reportOn
loop@while (node != null) {
when (node) {
is KtFunctionLiteral -> {
// keep going, as this is a "KtFunction", but we actually want the
// KtLambdaExpression
}
is KtLambdaExpression -> {
val descriptor = bindingContext[BindingContext.FUNCTION, node.functionLiteral]
if (descriptor == null) {
illegalCall(context, reportOn)
return
}
val composable = descriptor.isComposableCallable(bindingContext)
if (composable) return
val arg = getArgumentDescriptor(node.functionLiteral, bindingContext)
if (arg?.type?.hasDisallowComposableCallsAnnotation() == true) {
context.trace.record(
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,
descriptor,
false
)
context.trace.report(
ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION.on(
reportOn,
arg,
arg.containingDeclaration
)
)
return
}
val argTypeDescriptor = arg
?.type
?.constructor
?.declarationDescriptor as? ClassDescriptor
if (argTypeDescriptor != null) {
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
if (sam != null && sam.hasComposableAnnotation()) {
return
}
}
// TODO(lmr): in future, we should check for CALLS_IN_PLACE contract
val inlined = arg != null &&
canBeInlineArgument(node.functionLiteral) &&
isInline(arg.containingDeclaration) &&
isInlineParameter(arg)
if (!inlined) {
illegalCall(context, reportOn)
return
} else {
// since the function is inlined, we continue going up the PSI tree
// until we find a composable context. We also mark this lambda
context.trace.record(
ComposeWritableSlices.LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE,
descriptor,
true
)
}
}
is KtTryExpression -> {
val tryKeyword = node.tryKeyword
if (
node.tryBlock.textRange.contains(reportOn.textRange) &&
tryKeyword != null
) {
context.trace.report(
ComposeErrors.ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE.on(tryKeyword)
)
}
}
is KtFunction -> {
val descriptor = bindingContext[BindingContext.FUNCTION, node]
if (descriptor == null) {
illegalCall(context, reportOn)
return
}
val composable = descriptor.isComposableCallable(bindingContext)
if (!composable) {
illegalCall(context, reportOn, node.nameIdentifier ?: node)
}
if (descriptor.hasReadonlyComposableAnnotation()) {
// enforce that the original call was readonly
if (!resolvedCall.isReadOnlyComposableInvocation()) {
illegalCallMustBeReadonly(
context,
reportOn
)
}
}
return
}
is KtProperty -> {
// NOTE: since we're explicitly going down a different branch for
// KtPropertyAccessor, the ONLY time we make it into this branch is when the
// call was done in the initializer of the property/variable.
val descriptor = bindingContext[BindingContext.DECLARATION_TO_DESCRIPTOR, node]
if (
descriptor !is LocalVariableDescriptor &&
node.annotationEntries.hasComposableAnnotation(bindingContext)
) {
// composables shouldn't have initializers in the first place
illegalCall(context, reportOn)
return
}
}
is KtPropertyAccessor -> {
val property = node.property
val isComposable = node.annotationEntries.hasComposableAnnotation(bindingContext)
if (!isComposable) {
illegalCall(context, reportOn, property.nameIdentifier ?: property)
}
val descriptor = bindingContext[BindingContext.PROPERTY_ACCESSOR, node]
?: return
if (descriptor.hasReadonlyComposableAnnotation()) {
// enforce that the original call was readonly
if (!resolvedCall.isReadOnlyComposableInvocation()) {
illegalCallMustBeReadonly(
context,
reportOn
)
}
}
return
}
is KtCallableReferenceExpression -> {
illegalComposableFunctionReference(context, node)
return
}
is KtFile -> {
// if we've made it this far, the call was made in a non-composable context.
illegalCall(context, reportOn)
return
}
is KtClass -> {
// composable calls are never allowed in the initializers of a class
illegalCall(context, reportOn)
return
}
}
node = node.parent as? KtElement
}
}
private fun missingDisallowedComposableCallPropagation(
context: CallCheckerContext,
unmarkedParamEl: PsiElement,
unmarkedParamDescriptor: ValueParameterDescriptor,
markedParamDescriptor: ValueParameterDescriptor
) {
context.trace.report(
ComposeErrors.MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION.on(
unmarkedParamEl,
unmarkedParamDescriptor,
markedParamDescriptor,
markedParamDescriptor.containingDeclaration
)
)
}
private fun illegalCall(
context: CallCheckerContext,
callEl: PsiElement,
functionEl: PsiElement? = null
) {
context.trace.report(ComposeErrors.COMPOSABLE_INVOCATION.on(callEl))
if (functionEl != null) {
context.trace.report(ComposeErrors.COMPOSABLE_EXPECTED.on(functionEl))
}
}
private fun illegalCallMustBeReadonly(
context: CallCheckerContext,
callEl: PsiElement
) {
context.trace.report(ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE.on(callEl))
}
private fun illegalComposableFunctionReference(
context: CallCheckerContext,
refExpr: KtCallableReferenceExpression
) {
context.trace.report(ComposeErrors.COMPOSABLE_FUNCTION_REFERENCE.on(refExpr))
}
override fun checkType(
expression: KtExpression,
expressionType: KotlinType,
expressionTypeWithSmartCast: KotlinType,
c: ResolutionContext<*>
) {
val bindingContext = c.trace.bindingContext
val expectedType = c.expectedType
if (expectedType === TypeUtils.NO_EXPECTED_TYPE) return
if (expectedType === TypeUtils.UNIT_EXPECTED_TYPE) return
val expectedComposable = expectedType.hasComposableAnnotation()
if (expression is KtLambdaExpression) {
val descriptor = bindingContext[BindingContext.FUNCTION, expression.functionLiteral]
?: return
val isComposable = descriptor.isComposableCallable(bindingContext)
if (expectedComposable != isComposable) {
val isInlineable = isInlinedArgument(
expression.functionLiteral,
c.trace.bindingContext,
true
)
if (isInlineable) return
if (!expectedComposable && isComposable) {
val inferred = c.trace.bindingContext[
ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR,
descriptor
] == true
if (inferred) {
return
}
}
val reportOn =
if (expression.parent is KtAnnotatedExpression)
expression.parent as KtExpression
else expression
c.trace.report(
ComposeErrors.TYPE_MISMATCH.on(
reportOn,
expectedType,
expressionTypeWithSmartCast
)
)
}
return
} else {
val nullableAnyType = expectedType.builtIns.nullableAnyType
val anyType = expectedType.builtIns.anyType
if (anyType == expectedType.lowerIfFlexible() &&
nullableAnyType == expectedType.upperIfFlexible()
) return
val nullableNothingType = expectedType.builtIns.nullableNothingType
// Handle assigning null to a nullable composable type
if (expectedType.isMarkedNullable &&
expressionTypeWithSmartCast == nullableNothingType
) return
val isComposable = expressionType.hasComposableAnnotation()
if (expectedComposable != isComposable) {
val reportOn =
if (expression.parent is KtAnnotatedExpression)
expression.parent as KtExpression
else expression
c.trace.report(
ComposeErrors.TYPE_MISMATCH.on(
reportOn,
expectedType,
expressionTypeWithSmartCast
)
)
}
return
}
}
}
fun ResolvedCall<*>.isReadOnlyComposableInvocation(): Boolean {
if (this is VariableAsFunctionResolvedCall) {
return false
}
return when (val candidateDescriptor = candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
is PropertyDescriptor -> {
val isGetter = valueArguments.isEmpty()
val getter = candidateDescriptor.getter
if (isGetter && getter != null) {
getter.hasReadonlyComposableAnnotation()
} else {
false
}
}
is PropertyGetterDescriptor -> candidateDescriptor.hasReadonlyComposableAnnotation()
else -> candidateDescriptor.hasReadonlyComposableAnnotation()
}
}
internal fun ResolvedCall<*>.isComposableInvocation(): Boolean {
if (this is VariableAsFunctionResolvedCall) {
if (variableCall.candidateDescriptor.type.hasComposableAnnotation())
return true
if (functionCall.resultingDescriptor.hasComposableAnnotation()) return true
return false
}
val candidateDescriptor = candidateDescriptor
if (candidateDescriptor is FunctionDescriptor) {
if (candidateDescriptor.isOperator &&
candidateDescriptor.name == OperatorNameConventions.INVOKE
) {
if (dispatchReceiver?.type?.hasComposableAnnotation() == true) {
return true
}
}
}
return when (candidateDescriptor) {
is ValueParameterDescriptor -> false
is LocalVariableDescriptor -> false
is PropertyDescriptor -> {
val isGetter = valueArguments.isEmpty()
val getter = candidateDescriptor.getter
if (isGetter && getter != null) {
getter.hasComposableAnnotation()
} else {
false
}
}
is PropertyGetterDescriptor ->
candidateDescriptor.correspondingProperty.hasComposableAnnotation()
else -> candidateDescriptor.hasComposableAnnotation()
}
}
internal fun CallableDescriptor.isMarkedAsComposable(): Boolean {
return when (this) {
is PropertyGetterDescriptor -> hasComposableAnnotation()
is ValueParameterDescriptor -> type.hasComposableAnnotation()
is LocalVariableDescriptor -> type.hasComposableAnnotation()
is PropertyDescriptor -> false
else -> hasComposableAnnotation()
}
}
// if you called this, it would need to be a composable call (composer, changed, etc.)
internal fun CallableDescriptor.isComposableCallable(bindingContext: BindingContext): Boolean {
// if it's marked as composable then we're done
if (isMarkedAsComposable()) return true
if (
this is FunctionDescriptor &&
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR, this] == true
) {
// even though it's not marked, it is inferred as so by the type system (by being passed
// into a parameter marked as composable or a variable typed as one. This isn't much
// different than being marked explicitly.
return true
}
val functionLiteral = findPsi() as? KtFunctionLiteral
// if it isn't a function literal then we are out of things to try.
?: return false
if (functionLiteral.annotationEntries.hasComposableAnnotation(bindingContext)) {
// in this case the function literal itself is being annotated as composable but the
// annotation isn't in the descriptor itself
return true
}
val lambdaExpr = functionLiteral.parent as? KtLambdaExpression
if (
lambdaExpr != null &&
bindingContext[ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, lambdaExpr] == true
) {
// this lambda was marked as inferred to be composable
return true
}
// TODO(lmr): i'm not sure that this is actually needed at this point, since this should have
// been covered by the TypeResolutionInterceptorExtension
val arg = getArgumentDescriptor(functionLiteral, bindingContext) ?: return false
return arg.type.hasComposableAnnotation()
}
internal fun getArgumentDescriptor(
argument: KtFunction,
bindingContext: BindingContext
): ValueParameterDescriptor? {
val call = KtPsiUtil.getParentCallIfPresent(argument) ?: return null
val resolvedCall = call.getResolvedCall(bindingContext) ?: return null
val valueArgument = resolvedCall.call.getValueArgumentForExpression(argument) ?: return null
val mapping = resolvedCall.getArgumentMapping(valueArgument) as? ArgumentMatch ?: return null
return mapping.valueParameter
}
internal fun List<KtAnnotationEntry>.hasComposableAnnotation(bindingContext: BindingContext): Boolean {
for (entry in this) {
val descriptor = bindingContext.get(BindingContext.ANNOTATION, entry) ?: continue
if (descriptor.isComposableAnnotation) return true
}
return false
}

194
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableDeclarationChecker.kt

@ -0,0 +1,194 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.android.tools.compose.ComposeErrors.COMPOSABLE_FUN_MAIN
import com.android.tools.compose.ComposeErrors.COMPOSABLE_PROPERTY_BACKING_FIELD
import com.android.tools.compose.ComposeErrors.COMPOSABLE_SUSPEND_FUN
import com.android.tools.compose.ComposeErrors.COMPOSABLE_VAR
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.isSuspendFunctionType
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.PropertyAccessorDescriptor
import org.jetbrains.kotlin.descriptors.PropertyDescriptor
import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
import org.jetbrains.kotlin.idea.MainFunctionDetector
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.jvm.isJvm
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPropertyAccessor
import org.jetbrains.kotlin.resolve.checkers.DeclarationChecker
import org.jetbrains.kotlin.resolve.checkers.DeclarationCheckerContext
import org.jetbrains.kotlin.types.KotlinType
class ComposableDeclarationChecker : DeclarationChecker, StorageComponentContainerContributor {
override fun registerModuleComponents(
container: StorageComponentContainer,
platform: TargetPlatform,
moduleDescriptor: ModuleDescriptor
) {
if (!platform.isJvm()) return
container.useInstance(this)
}
override fun check(
declaration: KtDeclaration,
descriptor: DeclarationDescriptor,
context: DeclarationCheckerContext
) {
when {
declaration is KtProperty &&
descriptor is PropertyDescriptor -> checkProperty(declaration, descriptor, context)
declaration is KtPropertyAccessor &&
descriptor is PropertyAccessorDescriptor -> checkPropertyAccessor(
declaration,
descriptor,
context
)
declaration is KtFunction &&
descriptor is FunctionDescriptor -> checkFunction(declaration, descriptor, context)
}
}
private fun checkFunction(
declaration: KtFunction,
descriptor: FunctionDescriptor,
context: DeclarationCheckerContext
) {
val hasComposableAnnotation = descriptor.hasComposableAnnotation()
if (descriptor.overriddenDescriptors.isNotEmpty()) {
val override = descriptor.overriddenDescriptors.first()
if (override.hasComposableAnnotation() != hasComposableAnnotation) {
context.trace.report(
ComposeErrors.CONFLICTING_OVERLOADS.on(
declaration,
listOf(descriptor, override)
)
)
}
}
if (descriptor.isSuspend && hasComposableAnnotation) {
context.trace.report(
COMPOSABLE_SUSPEND_FUN.on(declaration.nameIdentifier ?: declaration)
)
}
val params = descriptor.valueParameters
val ktparams = declaration.valueParameters
if (params.size == ktparams.size) {
for ((param, ktparam) in params.zip(ktparams)) {
val typeRef = ktparam.typeReference
if (typeRef != null) {
checkType(param.type, typeRef, context)
}
}
}
// NOTE: only use the MainFunctionDetector if the descriptor name is main, to avoid
// unnecessarily allocating this class
if (hasComposableAnnotation &&
descriptor.name.asString() == "main" &&
MainFunctionDetector(
context.trace.bindingContext,
context.languageVersionSettings
).isMain(descriptor)
) {
context.trace.report(
COMPOSABLE_FUN_MAIN.on(declaration.nameIdentifier ?: declaration)
)
}
}
private fun checkType(
type: KotlinType,
element: PsiElement,
context: DeclarationCheckerContext
) {
if (type.hasComposableAnnotation() && type.isSuspendFunctionType) {
context.trace.report(
COMPOSABLE_SUSPEND_FUN.on(element)
)
}
}
private fun checkProperty(
declaration: KtProperty,
descriptor: PropertyDescriptor,
context: DeclarationCheckerContext
) {
val hasComposableAnnotation = descriptor
.getter
?.hasComposableAnnotation() == true
if (descriptor.overriddenDescriptors.isNotEmpty()) {
val override = descriptor.overriddenDescriptors.first()
val overrideIsComposable = override.hasComposableAnnotation() ||
override.getter?.hasComposableAnnotation() == true
if (overrideIsComposable != hasComposableAnnotation) {
context.trace.report(
ComposeErrors.CONFLICTING_OVERLOADS.on(
declaration,
listOf(descriptor, override)
)
)
}
}
if (!hasComposableAnnotation) return
val initializer = declaration.initializer
val name = declaration.nameIdentifier
if (initializer != null && name != null) {
context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
}
if (descriptor.isVar && name != null) {
context.trace.report(COMPOSABLE_VAR.on(name))
}
}
private fun checkPropertyAccessor(
declaration: KtPropertyAccessor,
descriptor: PropertyAccessorDescriptor,
context: DeclarationCheckerContext
) {
val propertyDescriptor = descriptor.correspondingProperty
val propertyPsi = declaration.parent as? KtProperty ?: return
val name = propertyPsi.nameIdentifier
val initializer = propertyPsi.initializer
val hasComposableAnnotation = descriptor.hasComposableAnnotation()
if (descriptor.overriddenDescriptors.isNotEmpty()) {
val override = descriptor.overriddenDescriptors.first()
val overrideComposable = override.hasComposableAnnotation()
if (overrideComposable != hasComposableAnnotation) {
context.trace.report(
ComposeErrors.CONFLICTING_OVERLOADS.on(
declaration,
listOf(descriptor, override)
)
)
}
}
if (!hasComposableAnnotation) return
if (initializer != null && name != null) {
context.trace.report(COMPOSABLE_PROPERTY_BACKING_FIELD.on(name))
}
if (propertyDescriptor.isVar && name != null) {
context.trace.report(COMPOSABLE_VAR.on(name))
}
}
}

64
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableElementRefactoringElementListenerProvider.kt

@ -0,0 +1,64 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.android.tools.modules.*
import com.intellij.psi.PsiElement
import com.intellij.refactoring.rename.naming.AutomaticRenamer
import com.intellij.refactoring.rename.naming.AutomaticRenamerFactory
import com.intellij.usageView.UsageInfo
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
/**
* Renames KtFile if a @Composable function with the same name was renamed.
*/
class ComposableElementAutomaticRenamerFactory : AutomaticRenamerFactory {
override fun isApplicable(element: PsiElement): Boolean {
if (element.inComposeModule() != true ||
element !is KtNamedFunction ||
element.parent !is KtFile ||
!element.isComposableFunction()
) return false
val virtualFile = element.containingKtFile.virtualFile
return virtualFile?.nameWithoutExtension == element.name
}
override fun getOptionName() = null
override fun isEnabled() = true
override fun setEnabled(enabled: Boolean) {}
override fun createRenamer(element: PsiElement, newName: String?, usages: MutableCollection<UsageInfo>?): AutomaticRenamer {
return object : AutomaticRenamer() {
init {
val file = element.containingFile
myElements.add(file)
suggestAllNames(file.name, newName + "." + KotlinFileType.EXTENSION)
}
override fun getDialogTitle() = ComposeBundle.message("rename.file")
override fun getDialogDescription() = ComposeBundle.message("rename.files.with.following.names")
override fun entityName() = ComposeBundle.message("file.name")
}
}
}

69
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableFunctionExtractableAnalyser.kt

@ -0,0 +1,69 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.modules.*
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.AdditionalExtractableAnalyser
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.ExtractableCodeDescriptor
import org.jetbrains.kotlin.idea.util.findAnnotation
import org.jetbrains.kotlin.psi.KtAnnotated
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtLambdaArgument
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.model.ArgumentMatch
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
/**
* Adds [COMPOSABLE_FQ_NAME] annotation to a function when it's extracted from a function annotated with [COMPOSABLE_FQ_NAME]
* or Composable context.
*/
class ComposableFunctionExtractableAnalyser : AdditionalExtractableAnalyser {
/**
* Returns @Composable annotation of type of given KtLambdaArgument if there is any otherwise returns null.
*
* Example: fun myFunction(context: @Composable () -> Unit)
* If given [KtLambdaArgument] corresponds to context parameter function returns [AnnotationDescriptor] for @Composable.
*/
private fun KtLambdaArgument.getComposableAnnotation(bindingContext: BindingContext): AnnotationDescriptor? {
val callExpression = parent as KtCallExpression
val resolvedCall = callExpression.getResolvedCall(bindingContext)
val argument = (resolvedCall?.getArgumentMapping(this) as? ArgumentMatch)?.valueParameter ?: return null
return argument.type.annotations.findAnnotation(ComposeFqNames.Composable)
}
override fun amendDescriptor(descriptor: ExtractableCodeDescriptor): ExtractableCodeDescriptor {
if (!StudioFlags.COMPOSE_FUNCTION_EXTRACTION.get() ||
descriptor.extractionData.commonParent.inComposeModule() != true) {
return descriptor
}
val bindingContext = descriptor.extractionData.bindingContext ?: return descriptor
val sourceFunction = descriptor.extractionData.targetSibling
if (sourceFunction is KtAnnotated) {
val composableAnnotation = sourceFunction.findAnnotation(ComposeFqNames.Composable)?.resolveToDescriptorIfAny()
if (composableAnnotation != null) {
return descriptor.copy(annotations = descriptor.annotations + composableAnnotation)
}
}
val outsideLambda = descriptor.extractionData.commonParent.parentOfType<KtLambdaArgument>(true)
val composableAnnotation = outsideLambda?.getComposableAnnotation(bindingContext) ?: return descriptor
return descriptor.copy(annotations = descriptor.annotations + composableAnnotation)
}
}

119
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeAutoDocumentation.kt

@ -0,0 +1,119 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.android.tools.idea.flags.StudioFlags.COMPOSE_AUTO_DOCUMENTATION
import com.android.tools.idea.flags.StudioFlags.COMPOSE_EDITOR_SUPPORT
import com.android.tools.modules.*
import com.intellij.codeInsight.CodeInsightSettings
import com.intellij.codeInsight.completion.CompletionService
import com.intellij.codeInsight.documentation.DocumentationManager
import com.intellij.codeInsight.lookup.Lookup
import com.intellij.codeInsight.lookup.LookupEvent
import com.intellij.codeInsight.lookup.LookupListener
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.intellij.util.Alarm
import java.beans.PropertyChangeListener
/**
* Automatically shows quick documentation for Compose functions during code completion
*/
class ComposeAutoDocumentation(private val project: Project) {
private var documentationOpenedByCompose = false
private val lookupListener = PropertyChangeListener { evt ->
if (COMPOSE_EDITOR_SUPPORT.get() &&
COMPOSE_AUTO_DOCUMENTATION.get () &&
LookupManager.PROP_ACTIVE_LOOKUP == evt.propertyName &&
evt.newValue is Lookup) {
val lookup = evt.newValue as Lookup
val moduleSystem = FileDocumentManager.getInstance().getFile(lookup.editor.document)
?.let { ModuleUtilCore.findModuleForFile(it, lookup.project) }
if (moduleSystem?.isComposeModule() == true) {
lookup.addLookupListener(object : LookupListener {
override fun currentItemChanged(event: LookupEvent) {
showJavaDoc(lookup)
}
})
}
}
}
fun onProjectOpened() {
if (COMPOSE_EDITOR_SUPPORT.get() && COMPOSE_AUTO_DOCUMENTATION.get() && !ApplicationManager.getApplication().isUnitTestMode) {
LookupManager.getInstance(project).addPropertyChangeListener(lookupListener)
}
}
class MyStartupActivity : StartupActivity {
override fun runActivity(project: Project) = getInstance(project).onProjectOpened()
}
companion object {
@JvmStatic
fun getInstance(project: Project): ComposeAutoDocumentation = project.getService(ComposeAutoDocumentation::class.java)
}
private fun showJavaDoc(lookup: Lookup) {
if (LookupManager.getInstance(project).activeLookup !== lookup) {
return
}
// If we open doc when lookup is not visible, doc will have wrong parent window (editor window instead of lookup).
if ((lookup as? LookupImpl)?.isVisible != true) {
Alarm().addRequest({ showJavaDoc(lookup) }, CodeInsightSettings.getInstance().JAVADOC_INFO_DELAY)
return
}
val psiElement = lookup.currentItem?.psiElement ?: return
val docManager = DocumentationManager.getInstance(project)
if (!psiElement.isComposableFunction()) {
// Close documentation for not composable function if it was opened by [AndroidComposeAutoDocumentation].
// Case docManager.docInfoHint?.isFocused == true: user clicked on doc window and after that clicked on lookup and selected another
// element. Due to bug docManager.docInfoHint?.isFocused == true even after clicking on lookup element, in that case if we close
// docManager.docInfoHint, lookup will be closed as well.
if (documentationOpenedByCompose && docManager.docInfoHint?.isFocused == false) {
docManager.docInfoHint?.cancel()
documentationOpenedByCompose = false
}
return
}
// It's composable function and documentation already opened
if (docManager.docInfoHint != null) return // will auto-update
val currentItem = lookup.currentItem
if (currentItem != null && currentItem.isValid && CompletionService.getCompletionService().currentCompletion != null) {
try {
docManager.showJavaDocInfo(lookup.editor, lookup.psiFile, false) {
documentationOpenedByCompose = false
}
documentationOpenedByCompose = true
}
catch (ignored: IndexNotReadyException) {
}
}
}
}

44
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeBundle.kt

@ -0,0 +1,44 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.AbstractBundle
import org.jetbrains.annotations.PropertyKey
import java.lang.ref.Reference
import java.lang.ref.SoftReference
import java.util.ResourceBundle
private const val BUNDLE_NAME = "messages.ComposeBundle"
class ComposeBundle private constructor() {
companion object {
private var ourBundle: Reference<ResourceBundle?>? = null
private fun getBundle(): ResourceBundle {
var bundle: ResourceBundle? = com.intellij.reference.SoftReference.dereference(ourBundle)
if (bundle == null) {
bundle = ResourceBundle.getBundle(BUNDLE_NAME)
ourBundle = SoftReference(bundle)
}
return bundle!!
}
@JvmStatic
fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any?): String {
return AbstractBundle.message(getBundle(), key, *params)
}
}
}

45
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeCodeCompletionConfigurable.kt

@ -0,0 +1,45 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.application.options.editor.CheckboxDescriptor
import com.intellij.application.options.editor.checkBox
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.ui.layout.*
/**
* Provides additional options in Settings | Editor | Code Completion section.
*
* Contains a checkbox that allows enable/disable [ComposeInsertHandler].
*/
class ComposeCodeCompletionConfigurable : BoundConfigurable("Compose") {
private val settings = ComposeSettings.getInstance()
private val checkboxDescriptor = CheckboxDescriptor(
ComposeBundle.message("compose.enable.insertion.handler"),
settings.state::isComposeInsertHandlerEnabled)
override fun createPanel(): DialogPanel {
return panel {
row {
titledRow("Compose") {
row { checkBox(checkboxDescriptor) }
}
}
}
}
}

78
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeColorSettingsPage.kt

@ -0,0 +1,78 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.openapi.fileTypes.SyntaxHighlighter
import com.intellij.openapi.options.colors.AttributesDescriptor
import com.intellij.openapi.options.colors.ColorDescriptor
import com.intellij.openapi.options.colors.ColorSettingsPage
import org.jetbrains.kotlin.idea.highlighter.KotlinColorSettingsPage
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingColors
import javax.swing.Icon
// This class is used by AndroidStudio to allow the user to change the style of Compose attributes.
class ComposeColorSettingsPage : ColorSettingsPage {
override fun getHighlighter(): SyntaxHighlighter {
return KotlinColorSettingsPage().highlighter
}
override fun getAdditionalHighlightingTagToDescriptorMap(): MutableMap<String,
TextAttributesKey> {
val attributes = HashMap<String, TextAttributesKey>()
attributes[ComposableAnnotator.COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME] =
ComposableAnnotator.COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY
attributes["ANNOTATION"] = KotlinHighlightingColors.ANNOTATION
attributes["KEYWORD"] = KotlinHighlightingColors.KEYWORD
attributes["FUNCTION_DECLARATION"] = KotlinHighlightingColors.FUNCTION_DECLARATION
attributes["FUNCTION_PARAMETER"] = KotlinHighlightingColors.PARAMETER
return attributes
}
override fun getIcon(): Icon? {
return null
}
override fun getAttributeDescriptors(): Array<AttributesDescriptor> {
// TODO: this needs to be localized.
return arrayOf(AttributesDescriptor("Calls to @Compose functions",
ComposableAnnotator.COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY))
}
override fun getColorDescriptors(): Array<ColorDescriptor> {
return emptyArray()
}
override fun getDisplayName(): String {
// TODO: this needs to be localized.
return "Compose"
}
override fun getDemoText(): String {
return "<ANNOTATION>@Composable</ANNOTATION>\n" +
"<KEYWORD>fun</KEYWORD> <FUNCTION_DECLARATION>Text</FUNCTION_DECLARATION>(" +
"<FUNCTION_PARAMETER>text</FUNCTION_PARAMETER>: <FUNCTION_PARAMETER>String" +
"</FUNCTION_PARAMETER>)\n" +
"}\n" +
"\n" +
"<ANNOTATION>@Composable</ANNOTATION>\n" +
"<KEYWORD>fun</KEYWORD> <FUNCTION_DECLARATION>Greeting</FUNCTION_DECLARATION>() {\n" +
" <ComposableCallTextAttributes>Text</ComposableCallTextAttributes>(" +
"<FUNCTION_PARAMETER>\"Hello\"</FUNCTION_PARAMETER>)\n" +
"}"
}
}

73
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeDiagnosticSuppressor.kt

@ -0,0 +1,73 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.openapi.extensions.Extensions
import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.psi.KtAnnotatedExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getCall
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.diagnostics.DiagnosticSuppressor
class ComposeDiagnosticSuppressor : DiagnosticSuppressor {
companion object {
fun registerExtension(
@Suppress("UNUSED_PARAMETER") project: Project,
extension: DiagnosticSuppressor
) {
@Suppress("DEPRECATION")
Extensions.getRootArea().getExtensionPoint(DiagnosticSuppressor.EP_NAME)
.registerExtension(extension)
}
}
override fun isSuppressed(diagnostic: Diagnostic): Boolean {
return isSuppressed(diagnostic, null)
}
override fun isSuppressed(diagnostic: Diagnostic, bindingContext: BindingContext?): Boolean {
if (!isComposeEnabled(diagnostic.psiElement)) return false
if (diagnostic.factory == Errors.NON_SOURCE_ANNOTATION_ON_INLINED_LAMBDA_EXPRESSION) {
for (entry in (
diagnostic.psiElement.parent as KtAnnotatedExpression
).annotationEntries) {
if (bindingContext != null) {
val annotation = bindingContext.get(BindingContext.ANNOTATION, entry)
if (annotation != null && annotation.isComposableAnnotation) return true
}
// Best effort, maybe jetbrains can get rid of nullability.
else if (entry.shortName?.identifier == "Composable") return true
}
}
if (diagnostic.factory == Errors.NAMED_ARGUMENTS_NOT_ALLOWED) {
if (bindingContext != null) {
val call = (diagnostic.psiElement.parent.parent.parent.parent as KtCallExpression)
.getCall(bindingContext).getResolvedCall(bindingContext)
if (call != null) {
return call.isComposableInvocation()
}
}
}
return false
}
}

109
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrorMessages.kt

@ -0,0 +1,109 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import org.jetbrains.kotlin.diagnostics.rendering.CommonRenderers
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages
import org.jetbrains.kotlin.diagnostics.rendering.DiagnosticFactoryToRendererMap
import org.jetbrains.kotlin.diagnostics.rendering.Renderers
import org.jetbrains.kotlin.diagnostics.rendering.Renderers.RENDER_TYPE_WITH_ANNOTATIONS
object ComposeErrorMessages : DefaultErrorMessages.Extension {
private val MAP = DiagnosticFactoryToRendererMap("Compose")
override fun getMap() = MAP
init {
MAP.put(
ComposeErrors.COMPOSABLE_INVOCATION,
ComposeBundle.message("errors.composable_invocation")
)
MAP.put(
ComposeErrors.COMPOSABLE_EXPECTED,
ComposeBundle.message("errors.composable_expected")
)
MAP.put(
ComposeErrors.CAPTURED_COMPOSABLE_INVOCATION,
@Suppress("InvalidBundleOrProperty")
ComposeBundle.message("errors.captured_composable_invocation"),
Renderers.NAME,
Renderers.COMPACT
)
MAP.put(
ComposeErrors.COMPOSABLE_PROPERTY_BACKING_FIELD,
ComposeBundle.message("errors.composable_property_backing_field")
)
MAP.put(
ComposeErrors.COMPOSABLE_VAR,
ComposeBundle.message("errors.composable_var")
)
MAP.put(
ComposeErrors.COMPOSABLE_SUSPEND_FUN,
ComposeBundle.message("errors.composable_suspend_fun")
)
MAP.put(
ComposeErrors.ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE,
ComposeBundle.message("errors.illegal_try_catch_around_composable")
)
MAP.put(
ComposeErrors.COMPOSABLE_FUNCTION_REFERENCE,
ComposeBundle.message("errors.composable_function_reference")
)
MAP.put(
ComposeErrors.CONFLICTING_OVERLOADS,
@Suppress("InvalidBundleOrProperty")
ComposeBundle.message("errors.conflicting_overloads"),
CommonRenderers.commaSeparated(
Renderers.FQ_NAMES_IN_TYPES_WITH_ANNOTATIONS
)
)
MAP.put(
ComposeErrors.TYPE_MISMATCH,
@Suppress("InvalidBundleOrProperty")
ComposeBundle.message("errors.type_mismatch"),
RENDER_TYPE_WITH_ANNOTATIONS,
RENDER_TYPE_WITH_ANNOTATIONS
)
MAP.put(
ComposeErrors.MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION,
@Suppress("InvalidBundleOrProperty")
ComposeBundle.message("errors.missing_disallow_composable_calls_annotation"),
Renderers.NAME,
Renderers.NAME,
Renderers.NAME
)
MAP.put(
ComposeErrors.NONREADONLY_CALL_IN_READONLY_COMPOSABLE,
ComposeBundle.message("errors.nonreadonly_call_in_readonly_composable")
)
MAP.put(
ComposeErrors.COMPOSABLE_FUN_MAIN,
ComposeBundle.message("errors.composable_fun_main")
)
}
}

117
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrors.kt

@ -0,0 +1,117 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory0
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory1
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory2
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory3
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.diagnostics.PositioningStrategies.DECLARATION_SIGNATURE_OR_DEFAULT
import org.jetbrains.kotlin.diagnostics.Severity
import org.jetbrains.kotlin.psi.KtCallableReferenceExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.types.KotlinType
object ComposeErrors {
// error goes on the composable call in a non-composable function
@JvmField
val COMPOSABLE_INVOCATION =
DiagnosticFactory0.create<PsiElement>(Severity.ERROR)
// error goes on the non-composable function with composable calls
@JvmField
val COMPOSABLE_EXPECTED =
DiagnosticFactory0.create<PsiElement>(Severity.ERROR)
@JvmField
val COMPOSABLE_PROPERTY_BACKING_FIELD =
DiagnosticFactory0.create<PsiElement>(
Severity.ERROR
)
@JvmField
val COMPOSABLE_VAR =
DiagnosticFactory0.create<PsiElement>(
Severity.ERROR
)
@JvmField
val COMPOSABLE_SUSPEND_FUN =
DiagnosticFactory0.create<PsiElement>(
Severity.ERROR
)
@JvmField
val CAPTURED_COMPOSABLE_INVOCATION =
DiagnosticFactory2.create<PsiElement, DeclarationDescriptor, DeclarationDescriptor>(
Severity.ERROR
)
@JvmField
var ILLEGAL_TRY_CATCH_AROUND_COMPOSABLE =
DiagnosticFactory0.create<PsiElement>(Severity.ERROR)
@JvmField
val COMPOSABLE_FUNCTION_REFERENCE =
DiagnosticFactory0.create<KtCallableReferenceExpression>(
Severity.ERROR
)
@JvmField
val COMPOSABLE_FUN_MAIN =
DiagnosticFactory0.create<PsiElement>(Severity.ERROR)
@JvmField
val MISSING_DISALLOW_COMPOSABLE_CALLS_ANNOTATION =
DiagnosticFactory3.create<
PsiElement,
ValueParameterDescriptor, // unmarked
ValueParameterDescriptor, // marked
CallableDescriptor
>(
Severity.ERROR
)
@JvmField
val NONREADONLY_CALL_IN_READONLY_COMPOSABLE = DiagnosticFactory0.create<PsiElement>(
Severity.ERROR
)
// This error matches Kotlin's CONFLICTING_OVERLOADS error, except that it renders the
// annotations with the descriptor. This is important to use for errors where the
// only difference is whether or not it is annotated with @Composable or not.
@JvmField
var CONFLICTING_OVERLOADS: DiagnosticFactory1<PsiElement, Collection<DeclarationDescriptor>> =
DiagnosticFactory1.create(
Severity.ERROR,
DECLARATION_SIGNATURE_OR_DEFAULT
)
// This error matches Kotlin's TYPE_MISMATCH error, except that it renders the annotations
// with the types. This is important to use for type mismatch errors where the only
// difference is whether or not it is annotated with @Composable or not.
@JvmField
val TYPE_MISMATCH =
DiagnosticFactory2.create<KtExpression, KotlinType, KotlinType>(Severity.ERROR)
init {
Errors.Initializer.initializeFactoryNamesAndDefaultErrorMessages(ComposeErrors::class.java, ComposeErrorMessages)
}
}

63
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFoldingBuilder.kt

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.lang.ASTNode
import com.intellij.lang.folding.CustomFoldingBuilder
import com.intellij.lang.folding.FoldingDescriptor
import com.intellij.openapi.editor.Document
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
/**
* Adds a folding region for a Modifier chain longer than two.
*/
class ComposeFoldingBuilder : CustomFoldingBuilder() {
override fun buildLanguageFoldRegions(descriptors: MutableList<FoldingDescriptor>, root: PsiElement, document: Document, quick: Boolean) {
if (root !is KtFile || DumbService.isDumb(root.project) || !isComposeEnabled(root)) {
return
}
val composableFunctions = root.getChildrenOfType<KtNamedFunction>().filter { it.isComposableFunction() }
for (function in composableFunctions) {
val modifiersChains = PsiTreeUtil.findChildrenOfType(function, KtDotQualifiedExpression::class.java).filter {
it.parent !is KtDotQualifiedExpression &&
isModifierChainLongerThanTwo(it)
}
for (modifierChain in modifiersChains) {
descriptors.add(FoldingDescriptor(modifierChain.node, modifierChain.node.textRange))
}
}
}
/**
* For Modifier.adjust().adjust() -> Modifier.(...)
*/
override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String {
return node.text.substringBefore(".").trim() + ".(...)"
}
override fun isRegionCollapsedByDefault(node: ASTNode): Boolean = false
}

86
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFqNames.kt

@ -0,0 +1,86 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.SourceElement
import org.jetbrains.kotlin.descriptors.annotations.Annotated
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
import org.jetbrains.kotlin.descriptors.annotations.Annotations
import org.jetbrains.kotlin.descriptors.findClassAcrossModuleDependencies
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.resolve.constants.ConstantValue
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils.NO_EXPECTED_TYPE
import org.jetbrains.kotlin.types.TypeUtils.UNIT_EXPECTED_TYPE
import org.jetbrains.kotlin.types.typeUtil.replaceAnnotations
object ComposeFqNames {
const val root = "androidx.compose.runtime"
object old {
private const val root = "androidx.compose"
fun fqNameFor(cname: String) = FqName("$root.$cname")
val Composable = fqNameFor("Composable")
}
val Composable = fqNameFor("Composable")
val DisallowComposableCalls = fqNameFor("DisallowComposableCalls")
val ReadOnlyComposable = fqNameFor("ReadOnlyComposable")
fun fqNameFor(cname: String) = FqName("$root.$cname")
fun makeComposableAnnotation(module: ModuleDescriptor): AnnotationDescriptor =
object : AnnotationDescriptor {
override val type: KotlinType
get() {
val clazz = module.findClassAcrossModuleDependencies(ClassId.topLevel(Composable)) ?:
module.findClassAcrossModuleDependencies(ClassId.topLevel(old.Composable))
return clazz!!.defaultType
}
override val allValueArguments: Map<Name, ConstantValue<*>> get() = emptyMap()
override val source: SourceElement get() = SourceElement.NO_SOURCE
override fun toString() = "[@Composable]"
}
}
fun KotlinType.makeComposable(module: ModuleDescriptor): KotlinType {
if (hasComposableAnnotation()) return this
val annotation = ComposeFqNames.makeComposableAnnotation(module)
return replaceAnnotations(Annotations.create(annotations + annotation))
}
fun KotlinType.hasComposableAnnotation(): Boolean =
!isSpecialType && (
annotations.findAnnotation(ComposeFqNames.Composable) != null ||
annotations.findAnnotation(ComposeFqNames.old.Composable) != null
)
fun Annotated.hasComposableAnnotation(): Boolean =
annotations.findAnnotation(ComposeFqNames.Composable) != null ||
annotations.findAnnotation(ComposeFqNames.old.Composable) != null
fun Annotated.hasReadonlyComposableAnnotation(): Boolean =
annotations.findAnnotation(ComposeFqNames.ReadOnlyComposable) != null
fun Annotated.hasDisallowComposableCallsAnnotation(): Boolean =
annotations.findAnnotation(ComposeFqNames.DisallowComposableCalls) != null
internal val KotlinType.isSpecialType: Boolean get() =
this === NO_EXPECTED_TYPE || this === UNIT_EXPECTED_TYPE
val AnnotationDescriptor.isComposableAnnotation: Boolean
get() = fqName == ComposeFqNames.Composable ||
fqName == ComposeFqNames.old.Composable

97
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.kt

@ -0,0 +1,97 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import org.jetbrains.kotlin.name.*
private const val UI_PACKAGE = "androidx.ui"
private const val COMPOSE_PACKAGE = "androidx.compose.ui"
/** Preview element name */
const val COMPOSE_PREVIEW_ANNOTATION_NAME = "Preview"
const val COMPOSABLE_ANNOTATION_NAME = "Composable"
const val COMPOSE_ALIGNMENT = "${COMPOSE_PACKAGE}.Alignment"
const val COMPOSE_ALIGNMENT_HORIZONTAL = "${COMPOSE_ALIGNMENT}.Horizontal"
const val COMPOSE_ALIGNMENT_VERTICAL = "${COMPOSE_ALIGNMENT}.Vertical"
const val COMPOSE_ARRANGEMENT = "androidx.compose.foundation.layout.Arrangement"
const val COMPOSE_ARRANGEMENT_HORIZONTAL = "${COMPOSE_ARRANGEMENT}.Horizontal"
const val COMPOSE_ARRANGEMENT_VERTICAL = "${COMPOSE_ARRANGEMENT}.Vertical"
val COMPOSABLE_FQ_NAMES = setOf(
"androidx.compose.$COMPOSABLE_ANNOTATION_NAME",
"androidx.compose.runtime.$COMPOSABLE_ANNOTATION_NAME"
)
/**
* Represents the Jetpack Compose library package name. The compose libraries will move from
* `androidx.ui` to `androidx.compose` and this enum encapsulates the naming for the uses in tools.
*/
enum class ComposeLibraryNamespace(
val packageName: String,
/** Package containing the API preview definitions. Elements here will be referenced by the user. */
val apiPreviewPackage: String = "$packageName.tooling.preview",
/** Package containing the preview implementation. Elements in this package are for use of tooling only. */
val implementationPreviewPackage: String = apiPreviewPackage
) {
ANDROIDX_COMPOSE(COMPOSE_PACKAGE),
/** New namespace where the API and implementation are split in two separate packages */
ANDROIDX_COMPOSE_WITH_API(COMPOSE_PACKAGE, implementationPreviewPackage = "${COMPOSE_PACKAGE}.tooling");
/**
* Name of the `ComposeViewAdapter` object that is used by the preview surface to hold
* the previewed `@Composable`s.
*/
val composableAdapterName: String = "$implementationPreviewPackage.ComposeViewAdapter"
val composeModifierClassName: String = "$packageName.Modifier"
/** Only composables with this annotations will be rendered to the surface. */
val previewAnnotationName = "$apiPreviewPackage.$COMPOSE_PREVIEW_ANNOTATION_NAME"
/** Same as [previewAnnotationName] but in [FqName] form. */
val previewAnnotationNameFqName = FqName(previewAnnotationName)
/** Annotation FQN for `Preview` annotated parameters. */
val previewParameterAnnotationName = "$apiPreviewPackage.PreviewParameter"
/** FqName of @Composable function that loads a string resource. **/
val stringResourceFunctionFqName = "$packageName.res.stringResource"
/** FqName of the Devices class for its corresponding `@Preview` parameter. */
val composeDevicesClassName = "$apiPreviewPackage.Devices"
val previewActivityName = "$implementationPreviewPackage.PreviewActivity"
}
/** Only composables with this annotations will be rendered to the surface. */
@JvmField
val COMPOSE_VIEW_ADAPTER_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.composableAdapterName,
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.composableAdapterName)
/** FQNs for the `@Preview` annotation. Only composables with this annotations will be rendered to the surface. */
@JvmField
val PREVIEW_ANNOTATION_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewAnnotationName,
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.previewAnnotationName)
/** Annotations FQNs for `Preview` annotated parameters. */
@JvmField
val PREVIEW_PARAMETER_FQNS = setOf(ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewParameterAnnotationName,
ComposeLibraryNamespace.ANDROIDX_COMPOSE_WITH_API.previewParameterAnnotationName)

105
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginTypeResolutionInterceptorExtension.kt

@ -0,0 +1,105 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import androidx.compose.compiler.plugins.kotlin.ComposeTypeResolutionInterceptorExtension
import com.android.tools.compose.ComposeWritableSlices.INFERRED_COMPOSABLE_DESCRIPTOR
import com.android.tools.idea.flags.StudioFlags
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.psiUtil.getAnnotationEntries
import org.jetbrains.kotlin.resolve.descriptorUtil.module
import org.jetbrains.kotlin.resolve.sam.getSingleAbstractMethodOrNull
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeUtils
import org.jetbrains.kotlin.types.expressions.ExpressionTypingContext
@Suppress("INVISIBLE_REFERENCE", "EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(org.jetbrains.kotlin.extensions.internal.InternalNonStableExtensionPoints::class)
class ComposePluginTypeResolutionInterceptorExtension : ComposeTypeResolutionInterceptorExtension() {
override fun interceptFunctionLiteralDescriptor(
expression: KtLambdaExpression,
context: ExpressionTypingContext,
descriptor: AnonymousFunctionDescriptor
): AnonymousFunctionDescriptor {
if (StudioFlags.COMPOSE_DEPLOY_LIVE_EDIT_USE_EMBEDDED_COMPILER.get()) {
return super.interceptFunctionLiteralDescriptor(expression, context, descriptor)
}
if (descriptor.isSuspend) return descriptor
if (
context.expectedType.hasComposableAnnotation() &&
!descriptor.hasComposableAnnotation()
) {
// If the expected type has an @Composable annotation then the literal function
// expression should infer a an @Composable annotation
context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true)
}
val arg = getArgumentDescriptor(expression.functionLiteral, context.trace.bindingContext)
val argTypeDescriptor = arg
?.type
?.constructor
?.declarationDescriptor as? ClassDescriptor
if (argTypeDescriptor != null) {
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
if (sam != null && sam.hasComposableAnnotation()) {
context.trace.record(INFERRED_COMPOSABLE_DESCRIPTOR, descriptor, true)
}
}
return descriptor
}
override fun interceptType(
element: KtElement,
context: ExpressionTypingContext,
resultType: KotlinType
): KotlinType {
if (resultType === TypeUtils.NO_EXPECTED_TYPE) return resultType
if (element !is KtLambdaExpression) return resultType
val arg = getArgumentDescriptor(element.functionLiteral, context.trace.bindingContext)
val argTypeDescriptor = arg
?.type
?.constructor
?.declarationDescriptor as? ClassDescriptor
if (argTypeDescriptor != null) {
val sam = getSingleAbstractMethodOrNull(argTypeDescriptor)
if (sam != null && sam.hasComposableAnnotation()) {
context.trace.record(
ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL,
element,
true
)
return resultType.makeComposable(context.scope.ownerDescriptor.module)
}
}
if (
element.getAnnotationEntries().hasComposableAnnotation(context.trace.bindingContext) ||
context.expectedType.hasComposableAnnotation()
) {
context.trace.record(ComposeWritableSlices.INFERRED_COMPOSABLE_LITERAL, element, true)
return resultType.makeComposable(context.scope.ownerDescriptor.module)
}
return resultType
}
}

45
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginUtils.kt

@ -0,0 +1,45 @@
/*
* Copyright 2019 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.android.tools.modules.*
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.typeUtil.supertypes
fun isComposeEnabled(element: PsiElement): Boolean = element.inComposeModule() ?: false
fun isModifierChainLongerThanTwo(element: KtElement): Boolean {
if (element.getChildrenOfType<KtDotQualifiedExpression>().isNotEmpty()) {
val fqName = element.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName?.asString()
if (fqName == ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName) {
return true
}
}
return false
}
internal fun KotlinType.isClassOrExtendsClass(classFqName:String): Boolean {
return fqName?.asString() == classFqName || supertypes().any { it.fqName?.asString() == classFqName }
}

35
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSettings.kt

@ -0,0 +1,35 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.BaseState
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.SimplePersistentStateComponent
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
@Service
@State(name = "ComposeSettings", storages = [Storage("composeSettings.xml")])
class ComposeSettings : SimplePersistentStateComponent<ComposeSettingsState>(ComposeSettingsState()) {
companion object {
fun getInstance(): ComposeSettings = ApplicationManager.getApplication().getService(ComposeSettings::class.java)
}
}
class ComposeSettingsState : BaseState() {
var isComposeInsertHandlerEnabled by property(true)
}

10
idea-plugin/src/main/kotlin/org/jetbrains/compose/inspections/ComposeSuppressor.kt → idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeSuppressor.kt

@ -13,25 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.compose.inspections
package com.android.tools.compose
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInspection.InspectionSuppressor
import com.intellij.codeInspection.SuppressQuickFix
import com.intellij.psi.PsiElement
import org.jetbrains.compose.desktop.ide.preview.isComposableFunction
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtNamedFunction
/**
* Suppress inspection that require composable function names to start with a lower case letter.
*/
class ComposeSuppressor : InspectionSuppressor {
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
return toolId == "FunctionName" &&
return StudioFlags.COMPOSE_EDITOR_SUPPORT.get() &&
toolId == "FunctionName" &&
element.language == KotlinLanguage.INSTANCE &&
element.node.elementType == KtTokens.IDENTIFIER &&
element.parent.let { it is KtNamedFunction && it.isComposableFunction() }
element.parent.isComposableFunction()
}
override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> {

31
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeWritableSlices.kt

@ -0,0 +1,31 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.util.slicedMap.BasicWritableSlice
import org.jetbrains.kotlin.util.slicedMap.RewritePolicy
import org.jetbrains.kotlin.util.slicedMap.WritableSlice
object ComposeWritableSlices {
val INFERRED_COMPOSABLE_DESCRIPTOR: WritableSlice<FunctionDescriptor, Boolean> =
BasicWritableSlice(RewritePolicy.DO_NOTHING)
val LAMBDA_CAPABLE_OF_COMPOSER_CAPTURE: WritableSlice<FunctionDescriptor, Boolean> =
BasicWritableSlice(RewritePolicy.DO_NOTHING)
val INFERRED_COMPOSABLE_LITERAL: WritableSlice<KtLambdaExpression, Boolean> =
BasicWritableSlice(RewritePolicy.DO_NOTHING)
}

74
idea-plugin/src/main/kotlin/com/android/tools/compose/PsiUtils.kt

@ -0,0 +1,74 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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.
*/
@file:JvmName("AndroidComposablePsiUtils")
package com.android.tools.compose
import com.intellij.openapi.roots.*
import com.intellij.psi.*
import com.intellij.psi.util.*
import org.jetbrains.kotlin.idea.*
import org.jetbrains.kotlin.idea.caches.resolve.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.resolve.*
import org.jetbrains.kotlin.resolve.lazy.*
fun KtAnnotationEntry.fqNameMatches(fqName: String): Boolean {
// For inspiration, see IDELightClassGenerationSupport.KtUltraLightSupportImpl.findAnnotation in the Kotlin plugin.
val shortName = shortName?.asString() ?: return false
return fqName.endsWith(shortName) && fqName == getQualifiedName()
}
fun KtAnnotationEntry.getQualifiedName(): String? {
return analyze(BodyResolveMode.PARTIAL).get(BindingContext.ANNOTATION, this)?.fqName?.asString()
}
fun KtAnnotationEntry.fqNameMatches(fqName: Set<String>): Boolean {
val qualifiedName by lazy { getQualifiedName() }
val shortName = shortName?.asString() ?: return false
return fqName.filter { it.endsWith(shortName) }.any { it == qualifiedName }
}
fun PsiElement.isComposableFunction(): Boolean {
if (this !is KtNamedFunction) return false
return CachedValuesManager.getCachedValue(this) {
val hasComposableAnnotation = annotationEntries.any {
// fqNameMatches is expensive, so we first verify that the short name of the annotation matches.
it.shortName?.identifier == COMPOSABLE_ANNOTATION_NAME &&
it.fqNameMatches(COMPOSABLE_FQ_NAMES)
}
val containingKtFile = this.containingKtFile
CachedValueProvider.Result.create(
// TODO: see if we can handle alias imports without ruining performance.
hasComposableAnnotation,
containingKtFile,
ProjectRootModificationTracker.getInstance(project)
)
}
}
fun PsiElement.isComposableAnnotation():Boolean {
if (this !is KtAnnotationEntry) return false
val fqName = this.getQualifiedName() ?: return false
return COMPOSABLE_FQ_NAMES.any { it == fqName }
}
fun PsiElement.isInsideComposableCode(): Boolean {
// TODO: also handle composable lambdas.
return language == KotlinLanguage.INSTANCE && parentOfType<KtNamedFunction>()?.isComposableFunction() == true
}

380
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeCompletionContributor.kt

@ -0,0 +1,380 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion
import com.android.tools.compose.COMPOSABLE_FQ_NAMES
import com.android.tools.compose.ComposeSettings
import com.android.tools.compose.isComposableFunction
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.idea.flags.StudioFlags.COMPOSE_COMPLETION_INSERT_HANDLER
import com.android.tools.idea.flags.StudioFlags.COMPOSE_COMPLETION_PRESENTATION
import com.android.tools.modules.*
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionLocation
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionWeigher
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.ConstantNode
import com.intellij.openapi.application.runWriteAction
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.impl.source.tree.LeafPsiElement
import com.intellij.psi.util.parentOfType
import com.intellij.util.castSafelyTo
import icons.StudioIcons
import org.jetbrains.kotlin.builtins.isBuiltinFunctionalType
import org.jetbrains.kotlin.builtins.isFunctionType
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.idea.KotlinLanguage
import org.jetbrains.kotlin.idea.completion.BasicLookupElementFactory
import org.jetbrains.kotlin.idea.completion.LambdaSignatureTemplates
import org.jetbrains.kotlin.idea.completion.LookupElementFactory
import org.jetbrains.kotlin.idea.completion.handlers.KotlinCallableInsertHandler
import org.jetbrains.kotlin.idea.core.completion.DeclarationLookupObject
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.CallType
import org.jetbrains.kotlin.idea.util.CallTypeAndReceiver
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace
import org.jetbrains.kotlin.renderer.DescriptorRenderer
import org.jetbrains.kotlin.resolve.calls.components.hasDefaultValue
import org.jetbrains.kotlin.resolve.calls.results.argumentValueType
import org.jetbrains.kotlin.types.typeUtil.isUnit
private val COMPOSABLE_FUNCTION_ICON = StudioIcons.Compose.Editor.COMPOSABLE_FUNCTION
/**
* Checks if this completion is for a statement (where Compose views usually called) and not part of another expression.
*/
private fun CompletionParameters.isForStatement(): Boolean {
return position is LeafPsiElement &&
position.node.elementType == KtTokens.IDENTIFIER &&
position.parent?.parent is KtBlockExpression
}
private fun LookupElement.getFunctionDescriptor(): FunctionDescriptor? {
return this.`object`
.castSafelyTo<DeclarationLookupObject>()
?.descriptor
?.castSafelyTo<FunctionDescriptor>()
}
private val List<ValueParameterDescriptor>.hasComposableChildren: Boolean
get() {
val lastArgType = lastOrNull()?.type ?: return false
return lastArgType.isBuiltinFunctionalType
&& COMPOSABLE_FQ_NAMES.any { lastArgType.annotations.hasAnnotation(FqName(it)) }
}
private val ValueParameterDescriptor.isLambdaWithNoParameters: Boolean
get() = type.isFunctionType
// The only type in the list is the return type (can be Unit).
&& argumentValueType.arguments.size == 1
/**
* true if the last parameter is required, and a lambda type with no parameters.
*/
private val List<ValueParameterDescriptor>.isLastRequiredLambdaWithNoParameters: Boolean
get() {
val lastParameter = lastOrNull() ?: return false
return !lastParameter.hasDefaultValue() && lastParameter.isLambdaWithNoParameters
}
/**
* Find the [CallType] from the [InsertionContext]. The [CallType] can be used to detect if the completion is being done in a regular
* statement, an import or some other expression and decide if we want to apply the [ComposeInsertHandler].
*/
private fun InsertionContext.inferCallType(): CallType<*> {
// Look for an existing KtSimpleNameExpression to pass to CallTypeAndReceiver.detect so we can infer the call type.
val namedExpression = (file.findElementAt(startOffset)?.parent as? KtSimpleNameExpression)?.mainReference?.expression
?: return CallType.DEFAULT
return CallTypeAndReceiver.detect(namedExpression).callType
}
/**
* Modifies [LookupElement]s for composable functions, to improve Compose editing UX.
*/
class ComposeCompletionContributor : CompletionContributor() {
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) {
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() ||
parameters.position.inComposeModule() != true ||
parameters.position.language != KotlinLanguage.INSTANCE) {
return
}
resultSet.runRemainingContributors(parameters) { completionResult ->
val lookupElement = completionResult.lookupElement
val psi = lookupElement.psiElement
val newResult = when {
psi == null || !psi.isComposableFunction() -> completionResult
lookupElement.isForSpecialLambdaLookupElement() -> null
else -> completionResult.withLookupElement(ComposeLookupElement(lookupElement))
}
newResult?.let(resultSet::passResult)
}
}
/**
* Checks if the [LookupElement] is an additional, "special" lookup element created for functions that can be invoked using the lambda
* syntax. These are created by [LookupElementFactory.addSpecialFunctionCallElements] and can be confusing for Compose APIs that often
* use overloaded function names.
*/
private fun LookupElement.isForSpecialLambdaLookupElement(): Boolean {
val presentation = LookupElementPresentation()
renderElement(presentation)
return presentation.tailText?.startsWith(" {...} (..., ") ?: false
}
}
/**
* Wraps original Kotlin [LookupElement]s for composable functions to make them stand out more.
*/
private class ComposeLookupElement(original: LookupElement) : LookupElementDecorator<LookupElement>(original) {
/**
* Set of [CallType]s that should be handled by the [ComposeInsertHandler].
*/
private val validCallTypes = setOf(CallType.DEFAULT, CallType.DOT)
init {
require(original.psiElement?.isComposableFunction() == true)
}
override fun getPsiElement(): KtNamedFunction = super.getPsiElement() as KtNamedFunction
override fun renderElement(presentation: LookupElementPresentation) {
super.renderElement(presentation)
if (COMPOSE_COMPLETION_PRESENTATION.get()) {
val descriptor = getFunctionDescriptor() ?: return
presentation.icon = COMPOSABLE_FUNCTION_ICON
presentation.setTypeText(if (descriptor.returnType?.isUnit() == true) null else presentation.typeText, null)
rewriteSignature(descriptor, presentation)
}
}
override fun handleInsert(context: InsertionContext) {
val descriptor = getFunctionDescriptor()
val callType by lazy { context.inferCallType() }
return when {
!COMPOSE_COMPLETION_INSERT_HANDLER.get() -> super.handleInsert(context)
!ComposeSettings.getInstance().state.isComposeInsertHandlerEnabled -> super.handleInsert(context)
descriptor == null -> super.handleInsert(context)
!validCallTypes.contains(callType) -> super.handleInsert(context)
else -> ComposeInsertHandler(descriptor, callType).handleInsert(context, this)
}
}
private fun rewriteSignature(descriptor: FunctionDescriptor, presentation: LookupElementPresentation) {
val allParameters = descriptor.valueParameters
val requiredParameters = allParameters.filter { !it.declaresDefaultValue() }
val inParens = if (requiredParameters.hasComposableChildren) requiredParameters.dropLast(1) else requiredParameters
val renderer = when {
requiredParameters.size < allParameters.size -> SHORT_NAMES_WITH_DOTS
inParens.isEmpty() && requiredParameters.hasComposableChildren -> {
// Don't render an empty pair of parenthesis if we're rendering a lambda afterwards.
null
}
else -> BasicLookupElementFactory.SHORT_NAMES_RENDERER
}
presentation.clearTail()
renderer
?.renderValueParameters(inParens, false)
?.let { presentation.appendTailTextItalic(it, false) }
if (requiredParameters.hasComposableChildren) {
presentation.appendTailText(" " + LambdaSignatureTemplates.DEFAULT_LAMBDA_PRESENTATION, true)
}
}
}
/**
* A version of [BasicLookupElementFactory.SHORT_NAMES_RENDERER] that adds `, ...)` at the end of the parameters list.
*/
private val SHORT_NAMES_WITH_DOTS = BasicLookupElementFactory.SHORT_NAMES_RENDERER.withOptions {
val delegate = DescriptorRenderer.ValueParametersHandler.DEFAULT
valueParametersHandler = object : DescriptorRenderer.ValueParametersHandler {
override fun appendAfterValueParameter(
parameter: ValueParameterDescriptor,
parameterIndex: Int,
parameterCount: Int,
builder: StringBuilder
) {
delegate.appendAfterValueParameter(parameter, parameterIndex, parameterCount, builder)
}
override fun appendBeforeValueParameter(
parameter: ValueParameterDescriptor,
parameterIndex: Int,
parameterCount: Int,
builder: StringBuilder
) {
delegate.appendBeforeValueParameter(parameter, parameterIndex, parameterCount, builder)
}
override fun appendBeforeValueParameters(parameterCount: Int, builder: StringBuilder) {
delegate.appendBeforeValueParameters(parameterCount, builder)
}
override fun appendAfterValueParameters(parameterCount: Int, builder: StringBuilder) {
builder.append(if (parameterCount == 0) "...)" else ", ...)")
}
}
}
/**
* Set of Composable FQNs that have a conflicting name with a non-composable and where we want to promote the
* non-composable instead.
*/
private val COMPOSABLE_CONFLICTING_NAMES = setOf(
"androidx.compose.material.MaterialTheme"
)
/**
* Custom [CompletionWeigher] which moves composable functions up the completion list.
*
* It doesn't give composable functions "absolute" priority, some weighers are hardcoded to run first: specifically one that puts prefix
* matches above [LookupElement]s where the match is in the middle of the name. Overriding this behavior would require an extension point in
* [org.jetbrains.kotlin.idea.completion.CompletionSession.createSorter].
*
* See [com.intellij.codeInsight.completion.PrioritizedLookupElement] for more information on how ordering of lookup elements works and how
* to debug it.
*/
class ComposeCompletionWeigher : CompletionWeigher() {
override fun weigh(element: LookupElement, location: CompletionLocation): Int = when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> 0
!StudioFlags.COMPOSE_COMPLETION_WEIGHER.get() -> 0
location.completionParameters.position.language != KotlinLanguage.INSTANCE -> 0
location.completionParameters.position.inComposeModule() != true -> 0
element.isForNamedArgument() -> 3
location.completionParameters.isForStatement() -> {
val isConflictingName = COMPOSABLE_CONFLICTING_NAMES.contains((element.psiElement as? KtNamedDeclaration)?.fqName?.asString() ?: "")
val isComposableFunction = element.psiElement?.isComposableFunction() ?: false
// This method ensures that the order of completion ends up as:
//
// Composables with non-conflicting names (like Button {}) +2
// Non Composables with conflicting names (like the MaterialTheme object) +2
// Composable with conflicting names (like MaterialTheme {}) +1
// Anything else 0
when {
isComposableFunction && !isConflictingName -> 2
!isComposableFunction && isConflictingName -> 2
isComposableFunction && isConflictingName -> 1
else -> 0
}
}
else -> 0
}
private fun LookupElement.isForNamedArgument() = lookupString.endsWith(" =")
}
private fun InsertionContext.getNextElementIgnoringWhitespace(): PsiElement? {
val elementAtCaret = file.findElementAt(editor.caretModel.offset) ?: return null
return elementAtCaret.getNextSiblingIgnoringWhitespace(true)
}
private fun InsertionContext.isNextElementOpenCurlyBrace() = getNextElementIgnoringWhitespace()?.text?.startsWith("{") == true
private fun InsertionContext.isNextElementOpenParenthesis() = getNextElementIgnoringWhitespace()?.text?.startsWith("(") == true
private class ComposeInsertHandler(
private val descriptor: FunctionDescriptor,
callType: CallType<*>) : KotlinCallableInsertHandler(callType) {
override fun handleInsert(context: InsertionContext, item: LookupElement) = with(context) {
super.handleInsert(context, item)
if (isNextElementOpenParenthesis()) return
// All Kotlin insertion handlers do this, possibly to post-process adding a new import in the call to super above.
val psiDocumentManager = PsiDocumentManager.getInstance(project)
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(document)
val templateManager = TemplateManager.getInstance(project)
val allParameters = descriptor.valueParameters
val requiredParameters = allParameters.filter { !it.declaresDefaultValue() }
val insertLambda = requiredParameters.hasComposableChildren
|| allParameters.isLastRequiredLambdaWithNoParameters
val inParens = if (insertLambda) requiredParameters.dropLast(1) else requiredParameters
val template = templateManager.createTemplate("", "").apply {
isToReformat = true
setToIndent(true)
when {
inParens.isNotEmpty() -> {
addTextSegment("(")
inParens.forEachIndexed { index, parameter ->
if (index > 0) {
addTextSegment(", ")
}
addTextSegment(parameter.name.asString() + " = ")
if (parameter.isLambdaWithNoParameters) {
addVariable(ConstantNode("{ /*TODO*/ }"), true)
}
else {
addVariable(EmptyExpression(), true)
}
}
addTextSegment(")")
}
!insertLambda -> addTextSegment("()")
requiredParameters.size < allParameters.size -> {
addTextSegment("(")
addVariable(EmptyExpression(), true)
addTextSegment(")")
}
}
if (insertLambda && !isNextElementOpenCurlyBrace()) {
addTextSegment(" {\n")
addEndVariable()
addTextSegment("\n}")
}
}
templateManager.startTemplate(editor, template, object : TemplateEditingAdapter() {
override fun templateFinished(template: Template, brokenOff: Boolean) {
if (!brokenOff) {
val callExpression = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtCallExpression>() ?: return
val valueArgumentList = callExpression.valueArgumentList ?: return
if (valueArgumentList.arguments.isEmpty() && callExpression.lambdaArguments.isNotEmpty()) {
runWriteAction { valueArgumentList.delete() }
}
}
}
})
}
}

179
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeImplementationsCompletionContributor.kt

@ -0,0 +1,179 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion
import com.android.tools.compose.COMPOSE_ALIGNMENT
import com.android.tools.compose.COMPOSE_ALIGNMENT_HORIZONTAL
import com.android.tools.compose.COMPOSE_ALIGNMENT_VERTICAL
import com.android.tools.compose.COMPOSE_ARRANGEMENT
import com.android.tools.compose.COMPOSE_ARRANGEMENT_HORIZONTAL
import com.android.tools.compose.COMPOSE_ARRANGEMENT_VERTICAL
import com.android.tools.compose.isClassOrExtendsClass
import com.android.tools.compose.isComposeEnabled
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.DefaultLookupItemRenderer
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.contextOfType
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.search.*
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.nj2k.postProcessing.type
import org.jetbrains.kotlin.psi.KtCallElement
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtDeclaration
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLambdaArgument
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Suggests specific implementations of frequently used Compose interfaces in a parameter or a property position.
*/
class ComposeImplementationsCompletionContributor : CompletionContributor() {
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
val elementToComplete = parameters.position
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !isComposeEnabled(elementToComplete) || parameters.originalFile !is KtFile) {
return
}
val elementToCompleteTypeFqName = elementToComplete.argumentTypeFqName ?: elementToComplete.propertyTypeFqName
val project = elementToComplete.project
val (elementsToSuggest, classForImport) = when (elementToCompleteTypeFqName) {
COMPOSE_ALIGNMENT_HORIZONTAL -> Pair(getAlignments(project, COMPOSE_ALIGNMENT_HORIZONTAL), COMPOSE_ALIGNMENT)
COMPOSE_ALIGNMENT_VERTICAL -> Pair(getAlignments(project, COMPOSE_ALIGNMENT_VERTICAL), COMPOSE_ALIGNMENT)
COMPOSE_ARRANGEMENT_HORIZONTAL -> Pair(getArrangements(project, COMPOSE_ARRANGEMENT_HORIZONTAL), COMPOSE_ARRANGEMENT)
COMPOSE_ARRANGEMENT_VERTICAL -> Pair(getArrangements(project, COMPOSE_ARRANGEMENT_VERTICAL), COMPOSE_ARRANGEMENT)
else -> return
}
val isNewElement = elementToComplete.parentOfType<KtDotQualifiedExpression>() == null
val lookupElements = elementsToSuggest.map { getStaticPropertyLookupElement(it, classForImport, isNewElement) }
result.addAllElements(lookupElements)
if (!isNewElement) {
val addedElementsNames = elementsToSuggest.mapNotNull { it.name }
result.runRemainingContributors(parameters) { completionResult ->
val skipResult = completionResult.lookupElement.psiElement.safeAs<KtProperty>()?.name?.let { addedElementsNames.contains(it) }
if (skipResult != true) {
result.passResult(completionResult)
}
}
}
}
private fun getKotlinClass(project: Project, classFqName: String): KtClassOrObject? {
return KotlinFullClassNameIndex
.getInstance().get(classFqName, project, project.allScope())
.firstOrNull()
.safeAs<KtClassOrObject>()
}
private fun getAlignments(project: Project, alignmentFqName: String): Collection<KtDeclaration> {
val alignmentClass = getKotlinClass(project, alignmentFqName) ?: return emptyList()
return CachedValuesManager.getManager(project).getCachedValue(alignmentClass) {
val alignmentTopLevelClass = getKotlinClass(project, COMPOSE_ALIGNMENT)!!
val companionObject = alignmentTopLevelClass.companionObjects.firstOrNull()
val alignments = companionObject?.declarations?.filter {
it is KtProperty && it.type()?.isClassOrExtendsClass(alignmentFqName) == true
}
CachedValueProvider.Result.create(alignments, ProjectRootModificationTracker.getInstance(project))
}
}
private fun getArrangements(project: Project, arrangementFqName: String): Collection<KtDeclaration> {
val arrangementClass = getKotlinClass(project, arrangementFqName) ?: return emptyList()
return CachedValuesManager.getManager(project).getCachedValue(arrangementClass) {
val arrangementTopLevelClass = getKotlinClass(project, COMPOSE_ARRANGEMENT)!!
val arrangements = arrangementTopLevelClass.declarations.filter {
it is KtProperty && it.type()?.isClassOrExtendsClass(arrangementFqName) == true
}
CachedValueProvider.Result.create(arrangements, ProjectRootModificationTracker.getInstance(project))
}
}
private fun getStaticPropertyLookupElement(psiElement: KtDeclaration, ktClassName: String, isNewElement: Boolean): LookupElement {
val fqName = FqName(ktClassName)
val mainLookupString = if (isNewElement) "${fqName.shortName()}.${psiElement.name}" else psiElement.name!!
val builder = LookupElementBuilder
.create(psiElement, mainLookupString)
.withLookupString(psiElement.name!!)
.bold()
.withTailText(" (${ktClassName.substringBeforeLast('.')})", true)
.withInsertHandler lambda@{ context, item ->
//Add import.
val psiDocumentManager = PsiDocumentManager.getInstance(context.project)
val ktFile = context.file as KtFile
val modifierDescriptor = ktFile.resolveImportReference(fqName).singleOrNull() ?: return@lambda
ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, modifierDescriptor)
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)
}
return object : LookupElementDecorator<LookupElement>(builder) {
override fun renderElement(presentation: LookupElementPresentation) {
super.renderElement(presentation)
presentation.icon = DefaultLookupItemRenderer.getRawIcon(builder)
}
}
}
private val PsiElement.propertyTypeFqName: String?
get() {
val property = contextOfType<KtProperty>() ?: return null
return property.type()?.fqName?.asString()
}
private val PsiElement.argumentTypeFqName: String?
get() {
val argument = contextOfType<KtValueArgument>().takeIf { it !is KtLambdaArgument } ?: return null
val callExpression = argument.parentOfType<KtCallElement>() ?: return null
val callee = callExpression.calleeExpression?.mainReference?.resolve().safeAs<KtNamedFunction>() ?: return null
val argumentTypeFqName = if (argument.isNamed()) {
val argumentName = argument.getArgumentName()!!.asName.asString()
callee.valueParameters.find { it.name == argumentName }?.type()?.fqName
}
else {
val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument)
callee.valueParameters.getOrNull(argumentIndex)?.type()?.fqName
}
return argumentTypeFqName?.asString()
}
}

297
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeModifierCompletionContributor.kt

@ -0,0 +1,297 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion
import com.android.tools.compose.ComposeLibraryNamespace
import com.android.tools.compose.isComposeEnabled
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionInitializationContext
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementDecorator
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.contextOfType
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.descriptors.CallableDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptorWithVisibility
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference
import org.jetbrains.kotlin.idea.caches.resolve.resolveToCall
import org.jetbrains.kotlin.idea.caches.resolve.util.getResolveScope
import org.jetbrains.kotlin.idea.completion.BasicLookupElementFactory
import org.jetbrains.kotlin.idea.completion.CollectRequiredTypesContextVariablesProvider
import org.jetbrains.kotlin.idea.completion.CompletionSession
import org.jetbrains.kotlin.idea.completion.InsertHandlerProvider
import org.jetbrains.kotlin.idea.completion.LookupElementFactory
import org.jetbrains.kotlin.idea.core.KotlinIndicesHelper
import org.jetbrains.kotlin.idea.core.isVisible
import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.CallType
import org.jetbrains.kotlin.idea.util.CallTypeAndReceiver
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.idea.util.receiverTypesWithIndex
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.nj2k.postProcessing.resolve
import org.jetbrains.kotlin.nj2k.postProcessing.type
import org.jetbrains.kotlin.psi.KtCallElement
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtLambdaArgument
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtProperty
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.KtValueArgument
import org.jetbrains.kotlin.psi.KtValueArgumentList
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.psi.psiUtil.getReceiverExpression
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Enhances code completion for Modifier (androidx.compose.ui.Modifier)
*
* Adds Modifier extension functions to code completion in places where modifier is expected
* e.g. parameter of type Modifier, variable of type Modifier as it was called on Modifier.<caret>
*
* Moves extension functions for method called on modifier [isMethodCalledOnModifier] up in the completion list.
*
* @see ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName
*/
class ComposeModifierCompletionContributor : CompletionContributor() {
companion object {
private val modifierFqName = ComposeLibraryNamespace.ANDROIDX_COMPOSE.composeModifierClassName
}
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) {
val element = parameters.position
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !isComposeEnabled(element) || parameters.originalFile !is KtFile) {
return
}
// It says "on imported" because only in that case we are able resolve that it called on Modifier.
val isMethodCalledOnImportedModifier = element.isMethodCalledOnModifier()
ProgressManager.checkCanceled()
val isModifierType = isMethodCalledOnImportedModifier || element.isModifierArgument || element.isModifierProperty
if (!isModifierType) return
ProgressManager.checkCanceled()
val nameExpression = createNameExpression(element)
val extensionFunctions = getExtensionFunctionsForModifier(nameExpression, element)
ProgressManager.checkCanceled()
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == modifierFqName }
val lookupElementFactory = createLookupElementFactory(nameExpression, parameters)
val isNewModifier = !isMethodCalledOnImportedModifier && element.parentOfType<KtDotQualifiedExpression>() == null
//Prioritise functions that return Modifier over other extension function.
resultSet.addAllElements(returnsModifier.toLookupElements(lookupElementFactory, 2.0, insertModifier = isNewModifier))
//If user didn't type Modifier don't suggest extensions that doesn't return Modifier.
if (isMethodCalledOnImportedModifier) {
resultSet.addAllElements(others.toLookupElements(lookupElementFactory, 0.0, insertModifier = isNewModifier))
}
ProgressManager.checkCanceled()
//If method is called on modifier [KotlinCompletionContributor] will add extensions function one more time, we need to filter them out.
if (isMethodCalledOnImportedModifier) {
val extensionFunctionsNames = extensionFunctions.map { it.name.asString() }.toSet()
resultSet.runRemainingContributors(parameters) { completionResult ->
val skipResult = completionResult.lookupElement.psiElement.safeAs<KtFunction>()?.name?.let { extensionFunctionsNames.contains(it) }
if (skipResult != true) {
resultSet.passResult(completionResult)
}
}
}
}
private fun List<CallableDescriptor>.toLookupElements(
lookupElementFactory: LookupElementFactory,
weight: Double,
insertModifier: Boolean
) = flatMap { descriptor ->
lookupElementFactory.createStandardLookupElementsForDescriptor(descriptor, useReceiverTypes = true).map {
PrioritizedLookupElement.withPriority(ModifierLookupElement(it, insertModifier), weight)
}
}
/**
* Creates LookupElementFactory that is similar to the one kotlin-plugin uses during completion session.
* Code partially copied from [CompletionSession].
*/
private fun createLookupElementFactory(nameExpression: KtSimpleNameExpression, parameters: CompletionParameters): LookupElementFactory {
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val file = parameters.originalFile.safeAs<KtFile>()!!
val resolutionFacade = file.getResolutionFacade()
val moduleDescriptor = resolutionFacade.moduleDescriptor
val callTypeAndReceiver = CallTypeAndReceiver.detect(nameExpression)
val receiverTypes = callTypeAndReceiver.receiverTypesWithIndex(
bindingContext, nameExpression, moduleDescriptor, resolutionFacade,
stableSmartCastsOnly = true, /* we don't include smart cast receiver types for "unstable" receiver value to mark members grayed */
withImplicitReceiversWhenExplicitPresent = true
)
val inDescriptor = nameExpression.getResolutionScope(bindingContext, resolutionFacade).ownerDescriptor
val insertHandler = InsertHandlerProvider(CallType.DOT, parameters.editor, ::emptyList)
val basicLookupElementFactory = BasicLookupElementFactory(nameExpression.project, insertHandler)
return LookupElementFactory(
basicLookupElementFactory, receiverTypes,
callTypeAndReceiver.callType, inDescriptor, CollectRequiredTypesContextVariablesProvider()
)
}
/**
* Creates "Modifier.call" expression as it would be if user typed "Modifier.<caret>" themselves.
*/
private fun createNameExpression(originalElement: PsiElement): KtSimpleNameExpression {
val originalFile = originalElement.containingFile.safeAs<KtFile>()!!
val file = KtPsiFactory(originalFile.project).createAnalyzableFile("temp.kt", "val x = $modifierFqName.call", originalFile)
return file.getChildOfType<KtProperty>()!!.getChildOfType<KtDotQualifiedExpression>()!!.lastChild as KtSimpleNameExpression
}
private fun getExtensionFunctionsForModifier(nameExpression: KtSimpleNameExpression,
originalPosition: PsiElement): Collection<CallableDescriptor> {
val file = nameExpression.containingFile as KtFile
val searchScope = getResolveScope(file)
val resolutionFacade = file.getResolutionFacade()
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_WITH_DIAGNOSTICS)
val callTypeAndReceiver = CallTypeAndReceiver.detect(nameExpression)
fun isVisible(descriptor: DeclarationDescriptor): Boolean {
if (descriptor is DeclarationDescriptorWithVisibility) {
return descriptor.isVisible(originalPosition, callTypeAndReceiver.receiver as? KtExpression, bindingContext, resolutionFacade)
}
return true
}
val indicesHelper = KotlinIndicesHelper(resolutionFacade, searchScope, ::isVisible, file = file)
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null) { true }
}
private val PsiElement.isModifierProperty: Boolean
get() {
val property = contextOfType<KtProperty>() ?: return false
return property.type()?.fqName?.asString() == modifierFqName
}
private val PsiElement.isModifierArgument: Boolean
get() {
val argument = contextOfType<KtValueArgument>().takeIf { it !is KtLambdaArgument } ?: return false
val callExpression = argument.parentOfType<KtCallElement>() ?: return false
val callee = callExpression.calleeExpression?.mainReference?.resolve().safeAs<KtNamedFunction>() ?: return false
val argumentTypeFqName = if (argument.isNamed()) {
val argumentName = argument.getArgumentName()!!.asName.asString()
callee.valueParameters.find { it.name == argumentName }?.type()?.fqName
}
else {
val argumentIndex = (argument.parent as KtValueArgumentList).arguments.indexOf(argument)
callee.valueParameters.getOrNull(argumentIndex)?.type()?.fqName
}
return argumentTypeFqName?.asString() == modifierFqName
}
/**
* Returns true if psiElement is method called on object that has Modifier type.
*
* Returns true for Modifier.align().%this%, myModifier.%this%, Modifier.%this%.
*/
private fun PsiElement.isMethodCalledOnModifier(): Boolean {
val elementOnWhichMethodCalled: KtExpression = parent.safeAs<KtNameReferenceExpression>()?.getReceiverExpression() ?: return false
// Case Modifier.align().%this%, modifier.%this%
val fqName = elementOnWhichMethodCalled.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName ?:
// Case Modifier.%this%
elementOnWhichMethodCalled.safeAs<KtNameReferenceExpression>()?.resolve().safeAs<KtClass>()?.fqName
return fqName?.asString() == modifierFqName
}
/**
* Inserts "Modifier." before [delegate] and imports [ComposeModifierCompletionContributor.modifierFqName] if it's not imported.
*/
private class ModifierLookupElement(
delegate: LookupElement,
val insertModifier: Boolean
) : LookupElementDecorator<LookupElement>(delegate) {
companion object {
private const val callOnModifierObject = "Modifier."
}
override fun renderElement(presentation: LookupElementPresentation) {
super.renderElement(presentation)
presentation.itemText = lookupString
}
override fun getAllLookupStrings(): MutableSet<String> {
if (insertModifier) {
val lookupStrings = super.getAllLookupStrings().toMutableSet()
lookupStrings.add(callOnModifierObject + super.getLookupString())
return lookupStrings
}
return super.getAllLookupStrings()
}
override fun getLookupString(): String {
if (insertModifier) {
return callOnModifierObject + super.getLookupString()
}
return super.getLookupString()
}
override fun handleInsert(context: InsertionContext) {
val psiDocumentManager = PsiDocumentManager.getInstance(context.project)
if (insertModifier) {
context.document.insertString(context.startOffset, callOnModifierObject)
context.offsetMap.addOffset(CompletionInitializationContext.START_OFFSET, context.startOffset + callOnModifierObject.length)
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)
}
val ktFile = context.file as KtFile
val modifierDescriptor = ktFile.resolveImportReference(FqName(modifierFqName)).singleOrNull()
modifierDescriptor?.let { ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, it) }
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)
super.handleInsert(context)
}
}
}

21
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/Constants.kt

@ -0,0 +1,21 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion.constraintlayout
internal object KeyWords {
const val ConstraintSets = "ConstraintSets"
const val Extends = "Extends"
}

155
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/ConstraintLayoutJsonCompletionContributor.kt

@ -0,0 +1,155 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion.constraintlayout
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.json.JsonElementTypes
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonReferenceExpression
import com.intellij.openapi.util.Key
import com.intellij.patterns.PlatformPatterns
import com.intellij.patterns.PsiElementPattern
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.util.ProcessingContext
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildrenOfType
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.psi.psiUtil.getTopmostParentOfType
/**
* [CompletionContributor] for the JSON5 format supported in ConstraintLayout-Compose (and MotionLayout).
*
* See the official wiki in [GitHub](https://github.com/androidx/constraintlayout/wiki/ConstraintSet-JSON5-syntax) to learn more about the
* supported JSON5 syntax.
*/
class ConstraintLayoutJsonCompletionContributor : CompletionContributor() {
init {
extend(
CompletionType.BASIC,
jsonPropertyName().withConstraintSetsParentAtLevel(6),
ConstraintIdsProvider
)
}
}
/**
* [SmartPsiElementPointer] to the [JsonProperty] corresponding to the ConstraintSets property.
*/
private typealias ConstraintSetsPropertyPointer = SmartPsiElementPointer<JsonProperty>
private val constraintSetsPropertyKey =
Key.create<ConstraintSetsPropertyPointer>("compose.json.autocomplete.constraint.sets.property")
/**
* Completion provider that looks for the 'ConstraintSets' declaration and caches it, provides useful functions for inheritors that want to
* provide completions based con the contents of the 'ConstraintSets' [JsonProperty].
*/
private abstract class ConstraintSetCompletionProvider : CompletionProvider<CompletionParameters>() {
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val setsProperty = if (context[constraintSetsPropertyKey] != null) {
context[constraintSetsPropertyKey]!!
}
else {
parameters.position.getTopmostParentOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.firstOrNull {
it.name == KeyWords.ConstraintSets
}?.let {
val pointer = SmartPointerManager.createPointer(it)
context.put(constraintSetsPropertyKey, pointer)
return@let pointer
}
}
addCompletions(setsProperty, parameters, result)
}
/**
* Inheritors should implement this function that may pass a reference to the ConstraintSets property.
*/
abstract fun addCompletions(
constraintSetsProperty: ConstraintSetsPropertyPointer?,
parameters: CompletionParameters,
result: CompletionResultSet
)
/**
* Returns the available constraint IDs for the given [constraintSetName], this is done by reading all IDs in all ConstraintSets and
* subtracting the IDs already present in [constraintSetName].
*/
protected fun ConstraintSetsPropertyPointer.findConstraintIdsForSet(constraintSetName: String): List<String> {
val availableNames = mutableSetOf(KeyWords.Extends)
val usedNames = mutableSetOf<String>()
this.element?.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { cSetProperty ->
cSetProperty.getChildOfType<JsonObject>()?.getChildrenOfType<JsonProperty>()?.forEach { constraintNameProperty ->
if (cSetProperty.name == constraintSetName) {
usedNames.add(constraintNameProperty.name)
}
else {
availableNames.add(constraintNameProperty.name)
}
}
}
availableNames.removeAll(usedNames)
return availableNames.toList()
}
}
/**
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other
* constraint sets.
*/
private object ConstraintIdsProvider : ConstraintSetCompletionProvider() {
override fun addCompletions(constraintSetsProperty: SmartPsiElementPointer<JsonProperty>?,
parameters: CompletionParameters,
result: CompletionResultSet) {
val parentName = parameters.position.getParentOfType<JsonProperty>(true)?.getParentOfType<JsonProperty>(true)?.name
if (constraintSetsProperty != null && parentName != null) {
constraintSetsProperty.findConstraintIdsForSet(parentName).forEach {
val template = if (it == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate
result.addLookupElement(name = it, tailText = null, template)
}
}
}
}
private fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER)
private inline fun <reified T : PsiElement> psiElement() = PlatformPatterns.psiElement(T::class.java)
private fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) =
this.withSuperParent(level, psiElement<JsonProperty>().withChild(psiElement<JsonReferenceExpression>().withText(name)))
private fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, "ConstraintSets")
private fun CompletionResultSet.addLookupElement(name: String, tailText: String? = null, format: InsertionFormat? = null) {
var lookupBuilder = if (format == null) {
LookupElementBuilder.create(name)
}
else {
LookupElementBuilder.create(format, name).withInsertHandler(InsertionFormatHandler)
}
lookupBuilder = lookupBuilder.withCaseSensitivity(false)
if (tailText != null) {
lookupBuilder = lookupBuilder.withTailText(tailText, true)
}
addElement(lookupBuilder)
}

43
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormat.kt

@ -0,0 +1,43 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion.constraintlayout
/**
* Describes a string that may be automatically inserted when selecting an autocomplete option.
*/
internal sealed class InsertionFormat(
val insertableString: String
)
/**
* Inserts the string after the auto-completed value.
*
* The caret will be moved to the position marked by the '|' character.
*/
internal class LiteralWithCaretFormat(literalFormat: String) : InsertionFormat(literalFormat)
/**
* Inserts the string after the auto-complete value.
*
* It will insert a new line as if it was done by an ENTER keystroke, marked by the '\n' character.
*
* Note that it will only apply the new line on the first '\n' character.
*/
internal class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat)
internal val JsonStringValueTemplate = LiteralWithCaretFormat(": '|',")
internal val JsonNewObjectTemplate = LiteralNewLineFormat(": {\n}")

91
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormatHandler.kt

@ -0,0 +1,91 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.code.completion.constraintlayout
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.IdeActions
import com.intellij.openapi.editor.EditorModificationUtil
import com.intellij.openapi.editor.actionSystem.CaretSpecificDataContext
import com.intellij.openapi.editor.actionSystem.EditorActionManager
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.psi.PsiDocumentManager
/**
* An [InsertHandler] to handle [InsertionFormat].
*
* The [InsertionFormat] object needs to be present in [LookupElement.getObject] to be handled here.
*/
internal object InsertionFormatHandler : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val format = item.`object` as? InsertionFormat ?: return
when (format) {
is LiteralWithCaretFormat -> handleCaretInsertion(context, format)
is LiteralNewLineFormat -> handleNewLineInsertion(context, format)
}
}
/**
* Handles insertions of [LiteralWithCaretFormat], moving the caret at the position specified by the '|' character.
*/
private fun handleCaretInsertion(context: InsertionContext, format: LiteralWithCaretFormat) {
with(context) {
val isMoveCaret = format.insertableString.contains('|')
val stringToInsert = format.insertableString.replace("|", "")
// Insert the string without the reserved character: |
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true)
PsiDocumentManager.getInstance(project).commitDocument(document)
// Move caret to the position indicated by '|'
EditorActionUtil.moveCaretToLineEnd(editor, false, true)
if (isMoveCaret && stringToInsert.isNotEmpty()) {
val caretPosition = format.insertableString.indexOf('|').coerceAtLeast(0)
EditorModificationUtil.moveCaretRelatively(editor, caretPosition - stringToInsert.length)
}
}
}
/**
* Handles insertions of [LiteralNewLineFormat], applying the new line with the [IdeActions.ACTION_EDITOR_ENTER] and moving the caret at
* the end of the new line.
*/
private fun handleNewLineInsertion(context: InsertionContext, format: LiteralNewLineFormat) {
val literal = format.insertableString
with(context) {
val newLineOffset = literal.indexOf('\n')
val stringToInsert = if (newLineOffset >= 0) {
StringBuilder(literal).deleteCharAt(newLineOffset).toString()
}
else {
literal
}
val moveBy = newLineOffset - stringToInsert.length
EditorModificationUtil.insertStringAtCaret(editor, stringToInsert, false, true)
PsiDocumentManager.getInstance(project).commitDocument(document)
EditorActionUtil.moveCaretToLineEnd(editor, false, true)
EditorModificationUtil.moveCaretRelatively(editor, moveBy)
val caret = editor.caretModel.currentCaret
EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_ENTER).execute(
editor,
caret,
CaretSpecificDataContext(DataManager.getInstance().getDataContext(editor.contentComponent), caret)
)
}
}
}

29
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerClassesFilterProvider.kt

@ -0,0 +1,29 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug
import com.intellij.ui.classFilter.ClassFilter
import com.intellij.ui.classFilter.DebuggerClassFilterProvider
class ComposeDebuggerClassesFilterProvider : DebuggerClassFilterProvider {
private companion object {
private val FILTERS = listOf(ClassFilter("androidx.compose.runtime*"))
}
override fun getFilters(): List<ClassFilter> {
return if (ComposeDebuggerSettings.getInstance().filterComposeRuntimeClasses) FILTERS else listOf()
}
}

60
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettings.kt

@ -0,0 +1,60 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug
import com.android.tools.compose.ComposeBundle
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.SimpleConfigurable
import com.intellij.openapi.util.Getter
import com.intellij.util.xmlb.XmlSerializerUtil
import com.intellij.xdebugger.XDebuggerUtil
import com.intellij.xdebugger.settings.DebuggerSettingsCategory
import com.intellij.xdebugger.settings.XDebuggerSettings
@State(
name = "ComposeDebuggerSettings",
storages = [Storage("compose.debug.xml")]
)
class ComposeDebuggerSettings : XDebuggerSettings<ComposeDebuggerSettings>("compose_debugger"), Getter<ComposeDebuggerSettings> {
var filterComposeRuntimeClasses: Boolean = true
companion object {
fun getInstance(): ComposeDebuggerSettings {
return XDebuggerUtil.getInstance()?.getDebuggerSettings(ComposeDebuggerSettings::class.java)!!
}
}
override fun createConfigurables(category: DebuggerSettingsCategory): Collection<Configurable> =
if (category == DebuggerSettingsCategory.STEPPING) {
listOf(
SimpleConfigurable.create(
"reference.idesettings.debugger.compose",
ComposeBundle.message("compose"),
ComposeDebuggerSettingsUi::class.java,
this
)
)
} else listOf()
override fun get() = this
override fun getState() = this
override fun loadState(state: ComposeDebuggerSettings) {
XmlSerializerUtil.copyBean(state, this)
}
}

26
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.form

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.android.tools.compose.debug.ComposeDebuggerSettingsUi">
<grid id="27dc6" binding="myPanel" layout-manager="GridLayoutManager" row-count="2" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="3cc22" class="javax.swing.JCheckBox" binding="filterComposeInternalClasses">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text resource-bundle="messages/ComposeBundle" key="filter.ignore.compose.runtime.classes"/>
</properties>
</component>
<vspacer id="821e6">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>

48
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.java

@ -0,0 +1,48 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug;
import com.intellij.openapi.options.ConfigurableUi;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;
public class ComposeDebuggerSettingsUi implements ConfigurableUi<ComposeDebuggerSettings> {
private JPanel myPanel;
private JCheckBox filterComposeInternalClasses;
@Override
public void reset(@NotNull ComposeDebuggerSettings settings) {
filterComposeInternalClasses.setSelected(settings.getFilterComposeRuntimeClasses());
}
@Override
public boolean isModified(@NotNull ComposeDebuggerSettings settings) {
return filterComposeInternalClasses.isSelected() != settings.getFilterComposeRuntimeClasses();
}
@Override
public void apply(@NotNull ComposeDebuggerSettings settings) {
settings.setFilterComposeRuntimeClasses(filterComposeInternalClasses.isSelected());
}
@Override
public @NotNull JComponent getComponent() {
return myPanel;
}
}

145
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManager.kt

@ -0,0 +1,145 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug
import com.intellij.debugger.*
import com.intellij.debugger.engine.*
import com.intellij.debugger.engine.evaluation.*
import com.intellij.debugger.jdi.*
import com.intellij.debugger.requests.*
import com.intellij.openapi.application.*
import com.intellij.openapi.fileTypes.*
import com.intellij.util.*
import com.intellij.xdebugger.frame.*
import com.sun.jdi.*
import com.sun.jdi.request.*
import org.jetbrains.kotlin.fileClasses.*
import org.jetbrains.kotlin.idea.*
import org.jetbrains.kotlin.idea.debugger.*
import org.jetbrains.kotlin.load.kotlin.*
import org.jetbrains.kotlin.psi.*
/**
* A PositionManager capable of setting breakpoints inside of ComposableSingleton lambdas.
*
* This class essentially resolves breakpoints for lambdas generated by the compose compiler
* optimization that was introduced in I8c967b14c5d9bf67e5646e60f630f2e29e006366
* The default [KotlinPositionManager] only locates source positions in enclosing and nested
* classes, while composable singleton lambdas are cached in a separate top-level class.
*
* See https://issuetracker.google.com/190373291 for more information.
*/
class ComposePositionManager(
private val debugProcess: DebugProcess,
private val kotlinPositionManager: KotlinPositionManager
) : MultiRequestPositionManager by kotlinPositionManager, PositionManagerWithMultipleStackFrames {
override fun getAcceptedFileTypes(): Set<FileType> = setOf(KotlinFileType.INSTANCE)
override fun createStackFrames(frame: StackFrameProxyImpl, debugProcess: DebugProcessImpl, location: Location): List<XStackFrame> =
kotlinPositionManager.createStackFrames(frame, debugProcess, location)
override fun evaluateCondition(context: EvaluationContext,
frame: StackFrameProxyImpl,
location: Location,
expression: String): ThreeState =
kotlinPositionManager.evaluateCondition(context, frame, location, expression)
/**
* Returns all prepared classes which could contain the given classPosition.
*
* This handles the case where a user sets a breakpoint in a ComposableSingleton
* lambda after the debug process has already initialized the corresponding
* `lambda-n` class.
*/
override fun getAllClasses(classPosition: SourcePosition): List<ReferenceType> {
val file = classPosition.file
// Unlike [KotlinPositionManager] we don't handle compiled code, since the
// Kotlin decompiler does not undo any Compose specific optimizations.
if (file !is KtFile) {
throw NoDataException.INSTANCE
}
val vm = debugProcess.virtualMachineProxy
val singletonClasses = vm.classesByName(computeComposableSingletonsClassName(file)).flatMap { referenceType ->
if (referenceType.isPrepared) vm.nestedTypes(referenceType) else listOf()
}
if (singletonClasses.isEmpty()) {
throw NoDataException.INSTANCE
}
// Since [CompoundPositionManager] returns the first successful result from [getAllClasses],
// we need to query [KotlinPositionManager] here in order to locate breakpoints
// in ordinary Kotlin code.
val kotlinReferences = kotlinPositionManager.getAllClasses(classPosition)
return kotlinReferences + singletonClasses
}
/**
* Registers search patterns in the form of [ClassPrepareRequest]s for classes which may contain
* the given `position`, but may not be loaded yet. The `requestor` will be called for any newly
* prepared class which matches any of the created search patterns.
*/
override fun createPrepareRequests(requestor: ClassPrepareRequestor, position: SourcePosition): List<ClassPrepareRequest> {
val file = position.file
if (file !is KtFile) {
throw NoDataException.INSTANCE
}
// Similar to getAllClasses above, [CompoundPositionManager] uses the first successful
// position manager, so we need to include the prepare requests from [KotlinPositionManager]
// in order to locate breakpoints in ordinary Kotlin code.
val kotlinRequests = kotlinPositionManager.createPrepareRequests(requestor, position)
val singletonRequest = debugProcess.requestsManager.createClassPrepareRequest(
requestor,
"${computeComposableSingletonsClassName(file)}\$*"
)
return if (singletonRequest == null) kotlinRequests else kotlinRequests + singletonRequest
}
/**
* A method from [PositionManager] which was superseded by [createPrepareRequests] in [MultiRequestPositionManager].
* Intellij code should never call this method for subclasses of [MultiRequestPositionManager].
*/
override fun createPrepareRequest(requestor: ClassPrepareRequestor, position: SourcePosition): ClassPrepareRequest? {
return createPrepareRequests(requestor, position).firstOrNull()
}
/**
* Compute the name of the ComposableSingletons class for the given file.
*
* The Compose compiler plugin creates per-file ComposableSingletons classes to cache
* composable lambdas without captured variables. We need to locate these classes in order
* to search them for breakpoint locations.
*
* NOTE: The pattern for ComposableSingletons classes needs to be kept in sync with the
* code in `ComposerLambdaMemoization.getOrCreateComposableSingletonsClass`.
* The optimization was introduced in I8c967b14c5d9bf67e5646e60f630f2e29e006366
*/
private fun computeComposableSingletonsClassName(file: KtFile): String {
// The code in `ComposerLambdaMemoization` always uses the file short name and
// ignores `JvmName` annotations, but (implicitly) respects `JvmPackageName`
// annotations.
val filePath = file.virtualFile?.path ?: file.name
val fileName = filePath.split('/').last()
val shortName = PackagePartClassUtils.getFilePartShortName(fileName)
val fileClassFqName = runReadAction { JvmFileClassUtil.getFileClassInfoNoResolve(file) }.facadeClassFqName
return "${fileClassFqName.parent().asString()}.ComposableSingletons\$$shortName"
}
}

27
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManagerFactory.kt

@ -0,0 +1,27 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug
import com.intellij.debugger.PositionManager
import com.intellij.debugger.PositionManagerFactory
import com.intellij.debugger.engine.DebugProcess
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager
class ComposePositionManagerFactory : PositionManagerFactory() {
override fun createPositionManager(process: DebugProcess): PositionManager {
return ComposePositionManager(process, KotlinPositionManager(process))
}
}

258
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectClassRenderer.kt

@ -0,0 +1,258 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug.render
import com.android.tools.compose.debug.render.ComposeStateObjectClassRenderer.Companion.DEBUGGER_DISPLAY_VALUE_METHOD_NAME
import com.intellij.debugger.DebuggerContext
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.evaluation.CodeFragmentKind
import com.intellij.debugger.engine.evaluation.EvaluateException
import com.intellij.debugger.engine.evaluation.EvaluateExceptionUtil
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.engine.evaluation.EvaluationContextImpl
import com.intellij.debugger.engine.evaluation.TextWithImportsImpl
import com.intellij.debugger.impl.DebuggerUtilsAsync
import com.intellij.debugger.impl.DebuggerUtilsImpl
import com.intellij.debugger.settings.NodeRendererSettings
import com.intellij.debugger.ui.impl.watch.ValueDescriptorImpl
import com.intellij.debugger.ui.tree.DebuggerTreeNode
import com.intellij.debugger.ui.tree.NodeDescriptor
import com.intellij.debugger.ui.tree.ValueDescriptor
import com.intellij.debugger.ui.tree.render.CachedEvaluator
import com.intellij.debugger.ui.tree.render.ChildrenBuilder
import com.intellij.debugger.ui.tree.render.ClassRenderer
import com.intellij.debugger.ui.tree.render.DescriptorLabelListener
import com.intellij.debugger.ui.tree.render.NodeRenderer
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiExpression
import com.intellij.xdebugger.impl.ui.XDebuggerUIConstants
import com.sun.jdi.ClassType
import com.sun.jdi.Type
import com.sun.jdi.Value
import org.jetbrains.kotlin.idea.debugger.KotlinClassRenderer
import org.jetbrains.kotlin.idea.debugger.isInKotlinSources
import java.util.concurrent.CompletableFuture
/**
* Renderer for a given compose `StateObject` type object.
*
* Basically, for a given compose state object, its underlying value (by invoking [DEBUGGER_DISPLAY_VALUE_METHOD_NAME])
* determines how it's rendered in the "Variables" pane. This is to provide an auto-unboxing experience while debugging,
* that users can identify the data by a glance at this more readable data view.
*
* E.g.
* 1) if the underlying value is an integer `1`, the label is rendered `1`.
* 2) if the underlying value is a list, then the given object is rendered by a `List` renderer instead of the
* original `Kotlin class` renderer. That is, "size = xx" is the label, and the `ArrayRenderer` is the children renderer
* in this case.
* 3) if the underlying value is a map, then the given object is rendered by a `Map` renderer instead of the original
* `Kotlin class` renderer. That is, "size = xx" is the label, and the `ArrayRenderer` is the children renderer in
* this case. When expanding, each of the entry is rendered by the `Map.Entry` renderer.
*
* @param fqcn the fully qualified class name of the Compose State Object to apply this custom renderer to.
*/
class ComposeStateObjectClassRenderer(private val fqcn: String) : ClassRenderer() {
// We fallback to [KotlinClassRenderer] when the following exception is thrown:
// Unable to evaluate the expression No such instance method: 'getDebuggerDisplayValue',
private val fallbackRenderer by lazy {
KotlinClassRenderer()
}
private val prioritizedCollectionRenderers by lazy {
NodeRendererSettings.getInstance()
.alternateCollectionRenderers
.filter { it.name == "Map" || it.name == "List" }
.filter { it.isEnabled }
.toList()
}
private val debuggerDisplayValueEvaluator = DebuggerDisplayValueEvaluator(fqcn)
init {
setIsApplicableChecker { type: Type? ->
if (type !is ClassType || !type.isInKotlinSources()) return@setIsApplicableChecker CompletableFuture.completedFuture(false)
DebuggerUtilsAsync.instanceOf(type, fqcn)
}
}
companion object {
private val NODE_RENDERER_KEY = Key.create<NodeRenderer>(this::class.java.simpleName)
// The name of the method we expect the Compose State Object to implement. We invoke it to retrieve the underlying
// Compose State Object value.
private const val DEBUGGER_DISPLAY_VALUE_METHOD_NAME = "getDebuggerDisplayValue"
}
override fun buildChildren(value: Value, builder: ChildrenBuilder, evaluationContext: EvaluationContext) {
val debuggerDisplayValueDescriptor = try {
getDebuggerDisplayValueDescriptor(value, evaluationContext, null)
}
catch (evaluateException: EvaluateException) {
if (evaluateException.localizedMessage.startsWith("No such instance method:")) {
return fallbackRenderer.buildChildren(value, builder, evaluationContext)
}
throw evaluateException
}
getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor)
.thenAccept { renderer ->
builder.parentDescriptor.putUserData(NODE_RENDERER_KEY, renderer)
renderer.buildChildren(debuggerDisplayValueDescriptor.value, builder, evaluationContext)
}
}
override fun getChildValueExpression(node: DebuggerTreeNode, context: DebuggerContext): PsiElement? {
return node.parent.descriptor.getUserData(NODE_RENDERER_KEY)?.getChildValueExpression(node, context)
}
override fun isExpandableAsync(
value: Value,
evaluationContext: EvaluationContext,
parentDescriptor: NodeDescriptor
): CompletableFuture<Boolean> {
val debuggerDisplayValueDescriptor = try {
getDebuggerDisplayValueDescriptor(value, evaluationContext, null)
}
catch (evaluateException: EvaluateException) {
if (evaluateException.localizedMessage.startsWith("No such instance method:")) {
return fallbackRenderer.isExpandableAsync(value, evaluationContext, parentDescriptor)
}
return CompletableFuture.failedFuture(evaluateException)
}
return getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor)
.thenCompose { renderer ->
renderer.isExpandableAsync(debuggerDisplayValueDescriptor.value, evaluationContext, debuggerDisplayValueDescriptor)
}
}
/**
* Returns a [ValueDescriptor] for the underlying "debugger display value", which is evaluated by invoking the
* [DEBUGGER_DISPLAY_VALUE_METHOD_NAME] method of the Compose `StateObject` type object: [value].
*/
private fun getDebuggerDisplayValueDescriptor(
value: Value,
evaluationContext: EvaluationContext,
originalDescriptor: ValueDescriptor?
): ValueDescriptor {
val debugProcess = evaluationContext.debugProcess
if (!debugProcess.isAttached) throw EvaluateExceptionUtil.PROCESS_EXITED
val thisEvaluationContext = evaluationContext.createEvaluationContext(value)
val debuggerDisplayValue = debuggerDisplayValueEvaluator.evaluate(debugProcess.project, thisEvaluationContext)
return object : ValueDescriptorImpl(evaluationContext.project, debuggerDisplayValue) {
override fun getDescriptorEvaluation(context: DebuggerContext): PsiExpression? = null
override fun calcValue(evaluationContext: EvaluationContextImpl?): Value = debuggerDisplayValue
override fun calcValueName(): String = "value"
override fun setValueLabel(label: String) {
originalDescriptor?.setValueLabel(label)
}
}
}
/**
* Return an ID of this renderer class, used by the IntelliJ platform to identify our renderer among all active
* renderers in the system.
*/
override fun getUniqueId(): String {
return fqcn
}
override fun calcLabel(descriptor: ValueDescriptor, evaluationContext: EvaluationContext, listener: DescriptorLabelListener): String {
val debuggerDisplayValueDescriptor: ValueDescriptor = try {
getDebuggerDisplayValueDescriptor(descriptor.value, evaluationContext, descriptor)
}
catch (evaluateException: EvaluateException) {
if (evaluateException.localizedMessage.startsWith("No such instance method:")) {
return fallbackRenderer.calcLabel(descriptor, evaluationContext, listener)
}
throw evaluateException
}
val renderer = getDelegatedRendererAsync(evaluationContext.debugProcess, debuggerDisplayValueDescriptor)
return calcLabelAsync(renderer, debuggerDisplayValueDescriptor, evaluationContext, listener)
.getNow(XDebuggerUIConstants.getCollectingDataMessage())
}
private fun calcLabelAsync(
renderer: CompletableFuture<NodeRenderer>,
descriptor: ValueDescriptor,
evaluationContext: EvaluationContext?,
listener: DescriptorLabelListener
): CompletableFuture<String> {
return renderer.thenApply { r: NodeRenderer ->
try {
val label = r.calcLabel(descriptor, evaluationContext, listener)
descriptor.setValueLabel(label)
listener.labelChanged()
return@thenApply label
}
catch (evaluateException: EvaluateException) {
descriptor.setValueLabelFailed(evaluateException)
listener.labelChanged()
return@thenApply ""
}
}
}
/**
* Returns a [CompletableFuture] of the first applicable renderer for the given [valueDescriptor].
*/
private fun getDelegatedRendererAsync(debugProcess: DebugProcess, valueDescriptor: ValueDescriptor): CompletableFuture<NodeRenderer> {
val type = valueDescriptor.type
return DebuggerUtilsImpl.getApplicableRenderers(prioritizedCollectionRenderers, type)
.thenCompose { renderers ->
// Return any applicable renderer of [prioritizedCollectionRenderers]. This is to de-prioritize `Kotlin class` renderer.
// Or fallback to the default renderer.
val found = renderers.firstOrNull() ?: return@thenCompose (debugProcess as DebugProcessImpl).getAutoRendererAsync(type)
CompletableFuture.completedFuture(found)
}
}
/**
* [CachedEvaluator] used to invoke the [DEBUGGER_DISPLAY_VALUE_METHOD_NAME] method.
*/
private class DebuggerDisplayValueEvaluator(private val fqcn: String) : CachedEvaluator() {
init {
referenceExpression = TextWithImportsImpl(
CodeFragmentKind.EXPRESSION,
"this.$DEBUGGER_DISPLAY_VALUE_METHOD_NAME()",
"",
JavaFileType.INSTANCE
)
}
override fun getClassName(): String {
return fqcn
}
fun evaluate(project: Project, context: EvaluationContext): Value {
return getEvaluator(project).evaluate(context)
}
}
}

77
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectRendererProviderBase.kt

@ -0,0 +1,77 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug.render
import com.android.tools.idea.flags.StudioFlags
import com.intellij.debugger.ui.tree.render.ChildrenRenderer
import com.intellij.debugger.ui.tree.render.CompoundRendererProvider
import com.intellij.debugger.ui.tree.render.ValueLabelRenderer
import com.sun.jdi.Type
import java.util.concurrent.CompletableFuture
import java.util.function.Function
/**
* Base custom renderer provider for rendering a given compose `StateObject` type object.
*
* [stateObjectClassRenderer] is the actual underlying renderer for the label and the children nodes. Users can select
* the provided renderer by [rendererName] if applicable.
*
* @param fqcn the fully qualified class name of the Compose State Object to apply the underlying custom renderer to.
*/
sealed class ComposeStateObjectRendererProviderBase(private val fqcn: String) : CompoundRendererProvider() {
private val rendererName = "Compose State Object"
private val stateObjectClassRenderer by lazy {
ComposeStateObjectClassRenderer(fqcn)
}
override fun getName(): String {
return rendererName
}
override fun isEnabled(): Boolean {
return StudioFlags.COMPOSE_STATE_OBJECT_CUSTOM_RENDERER.get()
}
override fun getIsApplicableChecker(): Function<Type?, CompletableFuture<Boolean>> {
return Function { type: Type? ->
stateObjectClassRenderer.isApplicableAsync(type)
}
}
override fun getValueLabelRenderer(): ValueLabelRenderer {
return stateObjectClassRenderer
}
override fun getChildrenRenderer(): ChildrenRenderer {
return stateObjectClassRenderer
}
}
class SnapshotMutableStateImplRendererProvider : ComposeStateObjectRendererProviderBase(
"androidx.compose.runtime.SnapshotMutableStateImpl"
)
class DerivedSnapshotStateRendererProvider : ComposeStateObjectRendererProviderBase(
"androidx.compose.runtime.DerivedSnapshotState"
)
class ComposeStateObjectListRendererProvider : ComposeStateObjectRendererProviderBase(
"androidx.compose.runtime.snapshots.SnapshotStateList"
)
class ComposeStateObjectMapRendererProvider : ComposeStateObjectRendererProviderBase(
"androidx.compose.runtime.snapshots.SnapshotStateMap"
)

69
idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/KotlinMapEntryRenderer.kt

@ -0,0 +1,69 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.debug.render
import com.android.tools.idea.flags.StudioFlags
import com.intellij.debugger.impl.DebuggerUtilsAsync
import com.intellij.debugger.settings.NodeRendererSettings
import com.intellij.debugger.ui.tree.render.ChildrenRenderer
import com.intellij.debugger.ui.tree.render.CompoundReferenceRenderer
import com.intellij.debugger.ui.tree.render.CompoundRendererProvider
import com.intellij.debugger.ui.tree.render.ValueLabelRenderer
import com.sun.jdi.ClassType
import com.sun.jdi.Type
import org.jetbrains.kotlin.idea.debugger.isInKotlinSources
import java.util.concurrent.CompletableFuture
import java.util.function.Function
/**
* Custom renderer for "MapEntry" type objects.
*
* This is to precede the `Kotlin class` renderer, as [KotlinMapEntryRenderer] provides a more readable data view,
* that the underlying `Map.Entry` renderer does the real work.
*/
class KotlinMapEntryRenderer : CompoundRendererProvider() {
private val MAP_ENTRY_FQCN = "java.util.Map\$Entry"
private val mapEntryLabelRender = NodeRendererSettings.getInstance().alternateCollectionRenderers.find {
it.name == "Map.Entry"
}
override fun isEnabled(): Boolean {
if (StudioFlags.COMPOSE_STATE_OBJECT_CUSTOM_RENDERER.get()) return true
return false
}
override fun getName(): String {
return "Kotlin MapEntry"
}
override fun getIsApplicableChecker(): Function<Type?, CompletableFuture<Boolean>> {
return Function { type: Type? ->
if (type !is ClassType || !type.isInKotlinSources()) return@Function CompletableFuture.completedFuture(false)
DebuggerUtilsAsync.instanceOf(type, MAP_ENTRY_FQCN)
}
}
override fun getValueLabelRenderer(): ValueLabelRenderer {
return (mapEntryLabelRender as CompoundReferenceRenderer).labelRenderer
}
override fun getChildrenRenderer(): ChildrenRenderer {
return NodeRendererSettings.createEnumerationChildrenRenderer(arrayOf(arrayOf("key", "getKey()"), arrayOf("value", "getValue()")))
}
}

112
idea-plugin/src/main/kotlin/com/android/tools/compose/formatting/ComposePostFormatProcessor.kt

@ -0,0 +1,112 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.formatting
import com.android.tools.compose.isComposeEnabled
import com.android.tools.compose.isModifierChainLongerThanTwo
import com.android.tools.compose.settings.ComposeCustomCodeStyleSettings
import com.intellij.application.options.CodeStyle
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CommonCodeStyleSettings
import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessor
import com.intellij.psi.impl.source.codeStyle.PostFormatProcessorHelper
import org.jetbrains.kotlin.idea.formatter.kotlinCommonSettings
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtTreeVisitorVoid
/**
* Runs after explicit code formatting invocation and for Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer,
* splits it in one modifier per line.
*/
class ComposePostFormatProcessor : PostFormatProcessor {
private fun isAvailable(psiElement: PsiElement, settings: CodeStyleSettings): Boolean {
return psiElement.containingFile is KtFile &&
isComposeEnabled(psiElement) &&
!DumbService.isDumb(psiElement.project) &&
settings.getCustomSettings(ComposeCustomCodeStyleSettings::class.java).USE_CUSTOM_FORMATTING_FOR_MODIFIERS
}
override fun processElement(source: PsiElement, settings: CodeStyleSettings): PsiElement {
return if (isAvailable(source, settings)) ComposeModifierProcessor(settings).process(source) else source
}
override fun processText(source: PsiFile, rangeToReformat: TextRange, settings: CodeStyleSettings): TextRange {
return if (isAvailable(source, settings)) ComposeModifierProcessor(settings).processText(source, rangeToReformat) else rangeToReformat
}
}
class ComposeModifierProcessor(private val settings: CodeStyleSettings) : KtTreeVisitorVoid() {
private val myPostProcessor = PostFormatProcessorHelper(settings.kotlinCommonSettings)
private fun updateResultRange(oldTextLength: Int, newTextLength: Int) {
myPostProcessor.updateResultRange(oldTextLength, newTextLength)
}
override fun visitKtElement(element: KtElement) {
super.visitElement(element)
if (element.isPhysical && isModifierChainThatNeedToBeWrapped(element)) {
val oldTextLength: Int = element.textLength
wrapModifierChain(element as KtDotQualifiedExpression, settings)
updateResultRange(oldTextLength, element.textLength)
}
}
fun process(formatted: PsiElement): PsiElement {
formatted.accept(this)
return formatted
}
fun processText(
source: PsiFile,
rangeToReformat: TextRange
): TextRange {
myPostProcessor.resultTextRange = rangeToReformat
source.accept(this)
return myPostProcessor.resultTextRange
}
}
/**
* Returns true if it's Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer.
*/
private fun isModifierChainThatNeedToBeWrapped(element: KtElement): Boolean {
// Take very top KtDotQualifiedExpression, e.g for `Modifier.adjust1().adjust2()` take whole expression, not only `Modifier.adjust1()`.
return element is KtDotQualifiedExpression &&
element.parent !is KtDotQualifiedExpression &&
isModifierChainLongerThanTwo(element)
}
/**
* Splits KtDotQualifiedExpression it one call per line.
*/
internal fun wrapModifierChain(element: KtDotQualifiedExpression, settings: CodeStyleSettings) {
CodeStyle.doWithTemporarySettings(
element.project,
settings
) { tempSettings: CodeStyleSettings ->
tempSettings.kotlinCommonSettings.METHOD_CALL_CHAIN_WRAP = CommonCodeStyleSettings.WRAP_ALWAYS
tempSettings.kotlinCommonSettings.WRAP_FIRST_METHOD_IN_CALL_CHAIN = true
CodeFormatterFacade(tempSettings, element.language).processElement(element.node)
}
}

112
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreateComposableFunction.kt

@ -0,0 +1,112 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.COMPOSABLE_ANNOTATION_NAME
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.isComposableFunction
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.intention.IntentionAction
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.caches.resolve.analyzeAndGetResult
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor
import org.jetbrains.kotlin.idea.quickfix.QuickFixes
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.CallableInfo
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.FunctionInfo
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.TypeInfo
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getParameterInfos
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.getTypeInfoForTypeArguments
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.guessTypes
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createCallable.CreateCallableFromUsageFix
import org.jetbrains.kotlin.idea.refactoring.getExtractionContainers
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedExpressionForSelectorOrThis
import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType
import org.jetbrains.kotlin.types.Variance
class ComposeUnresolvedFunctionFixContributor : QuickFixContributor {
override fun registerQuickFixes(quickFixes: QuickFixes) {
if (StudioFlags.COMPOSE_EDITOR_SUPPORT.get()) {
quickFixes.register(Errors.UNRESOLVED_REFERENCE, ComposeUnresolvedFunctionFixFactory())
}
}
}
/**
* Creates quick fix(IntentionAction) for an unresolved reference inside a Composable function.
*
* Created action creates new function with @Composable annotation.
*
* Example:
* For
*
* @Composable
* fun myComposable() {
* <caret>newFunction()
* }
*
* creates
*
* @Composable
* fun newFunction() {
* TODO("Not yet implemented")
* }
*
*/
private class ComposeUnresolvedFunctionFixFactory : KotlinSingleIntentionActionFactory() {
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val unresolvedCall = diagnostic.psiElement.parent as? KtCallExpression ?: return null
val parentFunction = unresolvedCall.getStrictParentOfType<KtNamedFunction>() ?: return null
if (!parentFunction.isComposableFunction()) return null
val name = (unresolvedCall.calleeExpression as? KtSimpleNameExpression)?.getReferencedName() ?: return null
// Composable function usually starts with uppercase first letter.
if (name.isBlank() || !name[0].isUpperCase()) return null
val ktCreateCallableFromUsageFix = CreateCallableFromUsageFix(unresolvedCall) { listOfNotNull(createNewComposeFunctionInfo(name, unresolvedCall)) }
// Since CreateCallableFromUsageFix is no longer an 'open' class, we instead use delegation to customize the text.
return object : IntentionAction by ktCreateCallableFromUsageFix {
override fun getText(): String = ComposeBundle.message("create.composable.function") + " '$name'"
}
}
private val composableAnnotation = "@$COMPOSABLE_ANNOTATION_NAME"
// n.b. Do not cache this CallableInfo anywhere, otherwise it is easy to leak Kotlin descriptors.
// (see https://github.com/JetBrains/intellij-community/commit/608589428c).
private fun createNewComposeFunctionInfo(name: String, element: KtCallExpression): CallableInfo? {
val analysisResult = element.analyzeAndGetResult()
val fullCallExpression = element.getQualifiedExpressionForSelectorOrThis()
val expectedType = fullCallExpression.guessTypes(analysisResult.bindingContext, analysisResult.moduleDescriptor).singleOrNull()
if (expectedType != null && KotlinBuiltIns.isUnit(expectedType)) {
val parameters = element.getParameterInfos()
val typeParameters = element.getTypeInfoForTypeArguments()
val returnType = TypeInfo(expectedType, Variance.OUT_VARIANCE)
val modifierList = KtPsiFactory(element).createModifierList(composableAnnotation)
val containers = element.getQualifiedExpressionForSelectorOrThis().getExtractionContainers()
return FunctionInfo(name, TypeInfo.Empty, returnType, containers, parameters, typeParameters, modifierList = modifierList)
}
return null
}
}

78
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreatePreviewAction.kt

@ -0,0 +1,78 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.*
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.idea.core.ShortenReferences
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtFunction
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.psiUtil.getNextSiblingIgnoringWhitespace
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Adds a @Preview annotation when a full @Composable is selected or cursor at @Composable annotation.
*/
class ComposeCreatePreviewAction : IntentionAction {
override fun startInWriteAction() = true
override fun getText() = ComposeBundle.message("create.preview")
override fun getFamilyName() = ComposeBundle.message("create.preview")
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
return when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false
file == null || editor == null -> false
!file.isWritable || file !is KtFile -> false
else -> getComposableAnnotationEntry(editor, file) != null
}
}
private fun getComposableAnnotationEntry(editor: Editor, file: PsiFile): KtAnnotationEntry? {
if (editor.selectionModel.hasSelection()) {
val elementAtCaret = file.findElementAt(editor.selectionModel.selectionStart)?.parentOfType<KtAnnotationEntry>()
if (elementAtCaret?.isComposableAnnotation() == true) {
return elementAtCaret
}
else {
// Case when user selected few extra blank lines before @Composable annotation.
val elementAtCaretAfterSpace = file.findElementAt(editor.selectionModel.selectionStart)?.getNextSiblingIgnoringWhitespace()
return elementAtCaretAfterSpace.safeAs<KtFunction>()?.annotationEntries?.find { it.fqNameMatches(COMPOSABLE_FQ_NAMES) }
}
}
else {
return file.findElementAt(editor.caretModel.offset)?.parentOfType<KtAnnotationEntry>()?.takeIf { it.isComposableAnnotation() }
}
}
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) return
val composableAnnotationEntry = getComposableAnnotationEntry(editor, file) ?: return
val composableFunction = composableAnnotationEntry.parentOfType<KtFunction>() ?: return
val previewAnnotationEntry = KtPsiFactory(project).createAnnotationEntry(
"@" + ComposeLibraryNamespace.ANDROIDX_COMPOSE.previewAnnotationName)
ShortenReferences.DEFAULT.process(composableFunction.addAnnotationEntry(previewAnnotationEntry))
}
}

123
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeDelegateStateImportFix.kt

@ -0,0 +1,123 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.isClassOrExtendsClass
import com.android.tools.idea.AndroidTextUtils
import com.intellij.codeInsight.daemon.QuickFixBundle
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.diagnostics.Diagnostic
import org.jetbrains.kotlin.diagnostics.Errors
import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveImportReference
import org.jetbrains.kotlin.idea.debugger.sequence.psi.callName
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
import org.jetbrains.kotlin.idea.quickfix.QuickFixContributor
import org.jetbrains.kotlin.idea.quickfix.QuickFixes
import org.jetbrains.kotlin.idea.util.ImportInsertHelper
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.js.translate.callTranslator.getReturnType
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Registers ComposeDelegateStateImportFixFactory for DELEGATE_SPECIAL_FUNCTION_MISSING error.
*
* DELEGATE_SPECIAL_FUNCTION_MISSING is an error when there is no getValue or setValue function for an object after "by" keyword.
*
* TODO(b/157543181):Delete code after JB bug is fixed.
*/
class ComposeDelegateStateImportFixContributor : QuickFixContributor {
override fun registerQuickFixes(quickFixes: QuickFixes) {
quickFixes.register(Errors.DELEGATE_SPECIAL_FUNCTION_MISSING, ComposeDelegateStateImportFixFactory())
}
}
/**
* Creates an IntentionAction that allow to add [androidx.compose.runtime.getValue] import for [androidx.compose.runtime.MutableState] in delegate position.
*/
private class ComposeDelegateStateImportFixFactory : KotlinSingleIntentionActionFactory() {
private val stateMethodNames = mapOf(
"mutableStateOf" to "androidx.compose.runtime.mutableStateOf",
"state" to "androidx.compose.runtime.state")
/**
* Returns true, if the given [callExpression] contains a call to `mutableStateOf` or `state`. This allows suggesting the automatic
* import of `getValue` even when `mutableStateOf` or `state` have not been imported yet.
*/
private fun resolveUndefinedStateCall(callExpression: KtCallExpression): String? =
stateMethodNames[PsiTreeUtil.findChildOfType(callExpression, KtCallExpression::class.java, true)?.callName()]
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
val callExpression = diagnostic.psiElement.safeAs<KtCallExpression>()
?: diagnostic.psiElement.getChildOfType()
?: return null
val delegateType = callExpression.getResolvedCall(callExpression.analyze(BodyResolveMode.FULL))?.getReturnType() ?: return null
return when {
delegateType.isClassOrExtendsClass("androidx.compose.runtime.State") -> ComposeDelegateStateImportFixAction()
// Handle the case where the state is embedded within a remember {} call but we can not infer the type.
delegateType.isClassOrExtendsClass("androidx.compose.runtime.remember.T") && resolveUndefinedStateCall(callExpression) != null ->
ComposeDelegateStateImportFixAction(listOfNotNull(resolveUndefinedStateCall(callExpression)))
else -> null
}
}
}
private class ComposeDelegateStateImportFixAction(additionalImports: List<String> = emptyList()) : IntentionAction {
/**
* List of all the imports that this fix will add when invoked.
*/
private val importList = additionalImports + "androidx.compose.runtime.getValue"
/**
* List of the short names for the [importList].
*/
private val importShortNames = importList.map { it.substringAfterLast(".") }.sorted()
override fun startInWriteAction() = false
override fun getFamilyName() = text
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?) = true
override fun getText() = ComposeBundle.message("import.compose.state",
AndroidTextUtils.generateCommaSeparatedList(importShortNames, "and"))
// Inspired by KotlinAddImportAction#addImport
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file !is KtFile) return
val psiDocumentManager = PsiDocumentManager.getInstance(project)
psiDocumentManager.commitAllDocuments()
project.executeWriteCommand(QuickFixBundle.message("add.import")) {
importList.forEach { importFqName ->
val descriptor = file.resolveImportReference(FqName(importFqName)).firstOrNull() ?: return@executeWriteCommand
ImportInsertHelper.getInstance(project).importDescriptor(file, descriptor)
}
}
}
}

147
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeSurroundWithWidgetAction.kt

@ -0,0 +1,147 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.isInsideComposableCode
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.intention.HighPriorityAction
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInsight.intention.impl.IntentionActionGroup
import com.intellij.codeInsight.template.impl.InvokeTemplateAction
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateSettings
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.ListPopup
import com.intellij.openapi.ui.popup.PopupStep
import com.intellij.openapi.ui.popup.util.BaseListPopupStep
import com.intellij.psi.PsiFile
import com.intellij.ui.popup.list.ListPopupImpl
import org.jetbrains.kotlin.idea.codeInsight.surroundWith.statement.KotlinStatementSurroundDescriptor
import org.jetbrains.kotlin.psi.KtFile
/**
* Intention action that includes [ComposeSurroundWithBoxAction], [ComposeSurroundWithRowAction], [ComposeSurroundWithColumnAction].
*
* After this action is selected, a new pop-up appears, in which user can choose between actions listed above.
*
* @see intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template
* intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template
*/
class ComposeSurroundWithWidgetActionGroup :
IntentionActionGroup<ComposeSurroundWithWidgetAction>(
listOf(ComposeSurroundWithBoxAction(), ComposeSurroundWithRowAction(), ComposeSurroundWithColumnAction())
) {
override fun getGroupText(actions: List<ComposeSurroundWithWidgetAction>) =
ComposeBundle.message("surround.with.widget.intention.text")
override fun chooseAction(project: Project,
editor: Editor,
file: PsiFile,
actions: List<ComposeSurroundWithWidgetAction>,
invokeAction: (ComposeSurroundWithWidgetAction) -> Unit) {
createPopup(project, actions, invokeAction).showInBestPositionFor(editor)
}
private fun createPopup(project: Project,
actions: List<ComposeSurroundWithWidgetAction>,
invokeAction: (ComposeSurroundWithWidgetAction) -> Unit): ListPopup {
val step = object : BaseListPopupStep<ComposeSurroundWithWidgetAction>(null, actions) {
override fun getTextFor(action: ComposeSurroundWithWidgetAction) = action.text
override fun onChosen(selectedValue: ComposeSurroundWithWidgetAction, finalChoice: Boolean): PopupStep<*>? {
invokeAction(selectedValue)
return FINAL_CHOICE
}
}
return ListPopupImpl(project, step)
}
override fun getFamilyName() = ComposeBundle.message("surround.with.widget.intention.text")
}
/**
* Surrounds selected statements inside a @Composable function with a widget.
*
* @see intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template
* intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template
*/
abstract class ComposeSurroundWithWidgetAction : IntentionAction, HighPriorityAction {
override fun getFamilyName() = "Compose Surround With Action"
override fun startInWriteAction(): Boolean = true
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> return false
file == null || editor == null -> return false
!file.isWritable || file !is KtFile || !editor.selectionModel.hasSelection() -> return false
else -> {
val element = file.findElementAt(editor.caretModel.offset) ?: return false
if (!element.isInsideComposableCode()) return false
val statements = KotlinStatementSurroundDescriptor()
.getElementsToSurround(file, editor.selectionModel.selectionStart, editor.selectionModel.selectionEnd)
return statements.isNotEmpty()
}
}
}
protected abstract fun getTemplate(): TemplateImpl?
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
InvokeTemplateAction(getTemplate(), editor, project, HashSet()).perform()
}
}
/**
* Surrounds selected statements inside a @Composable function with Box widget.
*/
class ComposeSurroundWithBoxAction : ComposeSurroundWithWidgetAction() {
override fun getText(): String = ComposeBundle.message("surround.with.box.intention.text")
override fun getTemplate(): TemplateImpl? {
return TemplateSettings.getInstance().getTemplate("W", "AndroidCompose")
}
}
/**
* Surrounds selected statements inside a @Composable function with Row widget.
*/
class ComposeSurroundWithRowAction : ComposeSurroundWithWidgetAction() {
override fun getText(): String = ComposeBundle.message("surround.with.row.intention.text")
override fun getTemplate(): TemplateImpl? {
return TemplateSettings.getInstance().getTemplate("WR", "AndroidCompose")
}
}
/**
* Surrounds selected statements inside a @Composable function with Column widget.
*/
class ComposeSurroundWithColumnAction : ComposeSurroundWithWidgetAction() {
override fun getText(): String = ComposeBundle.message("surround.with.column.intention.text")
override fun getTemplate(): TemplateImpl? {
return TemplateSettings.getInstance().getTemplate("WC", "AndroidCompose")
}
}

74
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeUnwrapAction.kt

@ -0,0 +1,74 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.ComposeBundle
import com.android.tools.idea.flags.StudioFlags
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.nj2k.postProcessing.resolve
import org.jetbrains.kotlin.psi.KtBlockExpression
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Removes wrappers like Row, Column and Box around widgets.
*/
class ComposeUnwrapAction : IntentionAction {
private val WRAPPERS_FQ_NAMES = setOf(
"androidx.compose.foundation.layout.Box",
"androidx.compose.foundation.layout.Row",
"androidx.compose.foundation.layout.Column"
)
override fun startInWriteAction() = true
override fun getText() = ComposeBundle.message("remove.wrapper")
override fun getFamilyName() = ComposeBundle.message("remove.wrapper")
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
return when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false
file == null || editor == null -> false
!file.isWritable || file !is KtFile -> false
else -> isCaretAtWrapper(editor, file)
}
}
private fun isCaretAtWrapper(editor: Editor, file: PsiFile): Boolean {
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtNameReferenceExpression>() ?: return false
val name = elementAtCaret.resolve().safeAs<KtNamedFunction>()?.fqName?.asString() ?: return false
return WRAPPERS_FQ_NAMES.contains(name)
}
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (file == null || editor == null) return
val wrapper = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtNameReferenceExpression>() ?: return
val outerBlock = wrapper.parent.safeAs<KtCallExpression>() ?: return
val lambdaBlock = PsiTreeUtil.findChildOfType(outerBlock, KtBlockExpression::class.java, true) ?: return
outerBlock.replace(lambdaBlock)
}
}

67
idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeWrapModifiersAction.kt

@ -0,0 +1,67 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* 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 com.android.tools.compose.intentions
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.formatting.wrapModifierChain
import com.android.tools.compose.isModifierChainLongerThanTwo
import com.android.tools.idea.flags.StudioFlags
import com.intellij.application.options.CodeStyle
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.psi.KtDotQualifiedExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.psiUtil.getLastParentOfTypeInRowWithSelf
/**
* Wraps Modifier(androidx.compose.ui.Modifier) chain that is two modifiers or longer, in one modifier per line.
*/
class ComposeWrapModifiersAction : IntentionAction {
private companion object {
val NO_NEW_LINE_BEFORE_DOT = Regex("[^.\\n\\s]\\.")
}
override fun startInWriteAction() = true
override fun getText() = ComposeBundle.message("wrap.modifiers")
override fun getFamilyName() = ComposeBundle.message("wrap.modifiers")
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean {
return when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false
file == null || editor == null -> false
!file.isWritable || file !is KtFile -> false
else -> {
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtDotQualifiedExpression>()
val topLevelExpression = elementAtCaret?.getLastParentOfTypeInRowWithSelf<KtDotQualifiedExpression>() ?: return false
isModifierChainLongerThanTwo(topLevelExpression) &&
NO_NEW_LINE_BEFORE_DOT.containsMatchIn(topLevelExpression.text)
}
}
}
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (file == null || editor == null) return
val elementAtCaret = file.findElementAt(editor.caretModel.offset)?.parentOfType<KtDotQualifiedExpression>()
val topLevelExpression = elementAtCaret?.getLastParentOfTypeInRowWithSelf<KtDotQualifiedExpression>() ?: return
wrapModifierChain(topLevelExpression, CodeStyle.getSettings(file))
}
}

31
idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeCustomCodeStyleSettings.kt

@ -0,0 +1,31 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.settings
import com.intellij.configurationStore.*
import com.intellij.psi.codeStyle.*
class ComposeCustomCodeStyleSettings(settings: CodeStyleSettings) : CustomCodeStyleSettings("ComposeCustomCodeStyleSettings", settings) {
@Property(externalName = "use_custom_formatting_for_modifiers")
@JvmField
var USE_CUSTOM_FORMATTING_FOR_MODIFIERS = true
companion object {
fun getInstance(settings: CodeStyleSettings): ComposeCustomCodeStyleSettings {
return settings.getCustomSettings(ComposeCustomCodeStyleSettings::class.java)
}
}
}

79
idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeFormattingCodeStyleSettingsProvider.kt

@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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 com.android.tools.compose.settings
import com.android.tools.compose.ComposeBundle
import com.intellij.openapi.ui.DialogPanel
import com.intellij.psi.codeStyle.CodeStyleConfigurable
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsProvider
import com.intellij.psi.codeStyle.CustomCodeStyleSettings
import com.intellij.ui.layout.panel
import org.jetbrains.kotlin.idea.KotlinLanguage
import javax.swing.JCheckBox
/**
* Allows to turn on and off [ComposePostFormatProcessor] in Code Style settings.
*/
class ComposeFormattingCodeStyleSettingsProvider : CodeStyleSettingsProvider() {
override fun hasSettingsPage() = false
override fun createCustomSettings(settings: CodeStyleSettings): CustomCodeStyleSettings {
return ComposeCustomCodeStyleSettings(settings)
}
override fun getConfigurableDisplayName(): String = ComposeBundle.message("compose")
override fun getLanguage() = KotlinLanguage.INSTANCE
override fun createConfigurable(originalSettings: CodeStyleSettings, modelSettings: CodeStyleSettings): CodeStyleConfigurable {
return object : CodeStyleConfigurable {
private lateinit var checkBox: JCheckBox
override fun createComponent(): DialogPanel {
return panel {
row {
titledRow("Compose formatting") {
row {
checkBox = checkBox(
ComposeBundle.message("compose.enable.formatting.for.modifiers"),
ComposeCustomCodeStyleSettings.getInstance(originalSettings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS).component
}
}
}
}
}
override fun isModified() = ComposeCustomCodeStyleSettings.getInstance(
originalSettings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS != checkBox.isSelected
override fun apply(settings: CodeStyleSettings) {
ComposeCustomCodeStyleSettings.getInstance(settings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS = checkBox.isSelected
}
override fun apply() = apply(originalSettings)
override fun reset(settings: CodeStyleSettings) {
checkBox.isSelected = ComposeCustomCodeStyleSettings.getInstance(settings).USE_CUSTOM_FORMATTING_FOR_MODIFIERS
}
override fun getDisplayName() = ComposeBundle.message("compose")
}
}
}

27
idea-plugin/src/main/kotlin/com/android/tools/idea/AndroidTextUtils.kt

@ -0,0 +1,27 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package com.android.tools.idea
object AndroidTextUtils {
fun generateCommaSeparatedList(items: Collection<String?>, lastSeparator: String): String {
val n = items.size
if (n == 0) {
return ""
}
var i = 0
val result = StringBuilder()
for (word in items) {
result.append(word)
if (i < n - 2) {
result.append(", ")
} else if (i == n - 2) {
result.append(" ").append(lastSeparator).append(" ")
}
i++
}
return result.toString()
}
}

21
idea-plugin/src/main/kotlin/com/android/tools/idea/flags/StudioFlags.kt

@ -0,0 +1,21 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package com.android.tools.idea.flags
object StudioFlags {
val COMPOSE_EDITOR_SUPPORT = Flag(true)
val COMPOSE_AUTO_DOCUMENTATION = Flag(true)
val COMPOSE_FUNCTION_EXTRACTION = Flag(true)
val COMPOSE_DEPLOY_LIVE_EDIT_USE_EMBEDDED_COMPILER = Flag(true)
val COMPOSE_COMPLETION_INSERT_HANDLER = Flag(true)
val COMPOSE_COMPLETION_PRESENTATION = Flag(true)
val COMPOSE_COMPLETION_WEIGHER = Flag(true)
val COMPOSE_STATE_OBJECT_CUSTOM_RENDERER = Flag(true)
}
class Flag<T>(val value: T) {
fun get(): T = value
}

12
idea-plugin/src/main/kotlin/com/android/tools/modules/Module.kt

@ -0,0 +1,12 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package com.android.tools.modules
import com.intellij.openapi.module.Module
import com.intellij.psi.*
fun PsiElement.inComposeModule() = true
fun Module.isComposeModule() = true

25
idea-plugin/src/main/kotlin/icons/StudioIcons.kt

@ -0,0 +1,25 @@
/*
* Copyright 2020-2022 JetBrains s.r.o. and respective authors and developers.
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file.
*/
package icons
import com.intellij.ui.*
import javax.swing.*
object StudioIcons {
private fun load(path: String, cacheKey: Int, flags: Int): Icon {
return IconManager.getInstance().loadRasterizedIcon(
path,
StudioIcons::class.java.classLoader, cacheKey, flags
)
}
class Compose {
object Editor {
/** 16x16 */
val COMPOSABLE_FUNCTION = load("icons/compose/composable-function.svg", -238070477, 2)
}
}
}

102
idea-plugin/src/main/resources/META-INF/plugin.xml

@ -7,8 +7,6 @@
IDE support for developing
<a href="https://www.jetbrains.com/lp/compose/">Compose for Desktop</a>
applications.
The main feature at the moment is IDE preview of composable functions
marked by @Preview annotation.
]]>
</description>
@ -20,7 +18,105 @@
<depends>com.intellij.gradle</depends>
<depends>org.jetbrains.kotlin</depends>
<extensions defaultExtensionNs="org.jetbrains.kotlin">
<storageComponentContainerContributor implementation="com.android.tools.compose.ComposableCallChecker"/>
<storageComponentContainerContributor implementation="com.android.tools.compose.ComposableDeclarationChecker"/>
<diagnosticSuppressor implementation="com.android.tools.compose.ComposeDiagnosticSuppressor"/>
<quickFixContributor implementation="com.android.tools.compose.intentions.ComposeDelegateStateImportFixContributor"/>
<quickFixContributor implementation="com.android.tools.compose.intentions.ComposeUnresolvedFunctionFixContributor"/>
<additionalExtractableAnalyser implementation="com.android.tools.compose.ComposableFunctionExtractableAnalyser"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin.extensions.internal">
<typeResolutionInterceptorExtension implementation="com.android.tools.compose.ComposePluginTypeResolutionInterceptorExtension"/>
</extensions>
<extensions defaultExtensionNs="com.intellij">
<dependencySupport coordinate="androidx.compose.runtime:runtime" kind="java" displayName="Jetpack Compose"/>
<annotator
language="kotlin"
implementationClass="com.android.tools.compose.ComposableAnnotator"/>
<additionalTextAttributes scheme="Default" file="colorschemes/IdeComposableAnnotatorColorSchemeDefault.xml"/>
<colorSettingsPage implementation="com.android.tools.compose.ComposeColorSettingsPage"/>
<intentionAction>
<className>com.android.tools.compose.intentions.ComposeSurroundWithWidgetActionGroup</className>
<category>Compose Desktop</category>
</intentionAction>
<intentionAction>
<className>com.android.tools.compose.intentions.ComposeCreatePreviewAction</className>
<category>Compose Desktop</category>
</intentionAction>
<intentionAction>
<className>com.android.tools.compose.intentions.ComposeUnwrapAction</className>
<category>Compose Desktop</category>
</intentionAction>
<intentionAction>
<className>com.android.tools.compose.intentions.ComposeWrapModifiersAction</className>
<category>Compose Desktop</category>
</intentionAction>
<lang.foldingBuilder language="kotlin" implementationClass="com.android.tools.compose.ComposeFoldingBuilder"/>
<codeCompletionConfigurable instance="com.android.tools.compose.ComposeCodeCompletionConfigurable"/>
<codeStyleSettingsProvider implementation="com.android.tools.compose.settings.ComposeFormattingCodeStyleSettingsProvider"/>
<completion.contributor language="kotlin"
id="ComposeCompletionContributor"
implementationClass="com.android.tools.compose.code.completion.ComposeCompletionContributor"
order="first, before KotlinCompletionContributor"/>
<completion.contributor language="kotlin"
id="ComposeAlignmentCompletionContributor"
implementationClass="com.android.tools.compose.code.completion.ComposeImplementationsCompletionContributor"
order="first, before KotlinCompletionContributor"/>
<completion.contributor language="kotlin"
implementationClass="com.android.tools.compose.code.completion.ComposeModifierCompletionContributor"
order="first, before ComposeCompletionContributor"/>
<completion.contributor language="JSON"
id="MotionSceneCompletionContributor"
implementationClass="com.android.tools.compose.code.completion.constraintlayout.ConstraintLayoutJsonCompletionContributor"
order="first, before JsonCompletionContributor"/>
<weigher key="completion"
implementationClass="com.android.tools.compose.code.completion.ComposeCompletionWeigher"
id="android.compose"
order="first"/>
<lang.inspectionSuppressor language="kotlin" implementationClass="com.android.tools.compose.ComposeSuppressor"/>
<postFormatProcessor implementation="com.android.tools.compose.formatting.ComposePostFormatProcessor"/>
<automaticRenamerFactory implementation="com.android.tools.compose.ComposableElementAutomaticRenamerFactory"/>
<debugger.positionManagerFactory implementation="com.android.tools.compose.debug.ComposePositionManagerFactory"/>
<debuggerClassFilterProvider implementation="com.android.tools.compose.debug.ComposeDebuggerClassesFilterProvider"/>
<xdebugger.settings implementation="com.android.tools.compose.debug.ComposeDebuggerSettings"/>
<debugger.compoundRendererProvider id="SnapshotMutableStateImplRenderer"
implementation="com.android.tools.compose.debug.render.SnapshotMutableStateImplRendererProvider"
order="first"/>
<debugger.compoundRendererProvider id="DerivedSnapshotStateRenderer"
implementation="com.android.tools.compose.debug.render.DerivedSnapshotStateRendererProvider"
order="first"/>
<debugger.compoundRendererProvider id="ComposeStateObjectListRenderer"
implementation="com.android.tools.compose.debug.render.ComposeStateObjectListRendererProvider"
order="first"/>
<debugger.compoundRendererProvider id="ComposeStateObjectMapRenderer"
implementation="com.android.tools.compose.debug.render.ComposeStateObjectMapRendererProvider"
order="first"/>
<debugger.compoundRendererProvider id="KotlinMapEntryRenderer"
implementation="com.android.tools.compose.debug.render.KotlinMapEntryRenderer"
order="first"/>
<runLineMarkerContributor
language="kotlin"
implementationClass="org.jetbrains.compose.desktop.ide.preview.PreviewRunLineMarkerContributor"/>
@ -37,8 +133,6 @@
id="Desktop Preview" doNotActivateOnStart="true"
anchor="right" />
<lang.inspectionSuppressor language="kotlin" implementationClass="org.jetbrains.compose.inspections.ComposeSuppressor"/>
<notificationGroup id="Compose MPP Notifications" displayType="BALLOON"/>
<editorFloatingToolbarProvider implementation="org.jetbrains.compose.desktop.ide.preview.PreviewFloatingToolbarProvider"/>

23
idea-plugin/src/main/resources/colorschemes/IdeComposableAnnotatorColorSchemeDefault.xml

@ -0,0 +1,23 @@
<!--
Copyright 2019 The Android Open Source Project
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.
-->
<list>
<option name="ComposableCallTextAttributes">
<value>
<option name="FOREGROUND" value="009900"/>
</value>
</option>
</list>

1
idea-plugin/src/main/resources/icons/compose/composable-function.svg

@ -0,0 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 1h8a3 3 0 0 1 3 3v5.848l-1.236-.714-1.102 1.91a5.05 5.05 0 0 0-1.327 0l-1.103-1.91-2.598 1.5.976 1.69A4.993 4.993 0 0 0 7.1 15H4a3 3 0 0 1-3-3V4a3 3 0 0 1 3-3zm10.582 10.717a4.975 4.975 0 0 0-.907-.43l.455-.787.866.5-.414.717zM9.866 10.5l.455.789a4.979 4.979 0 0 0-.906.43L9 11l.866-.5z" fill="#3DDC84" fill-opacity=".6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7 12h1V8h1.5V7H8l-.005-1c-.008-.497.138-.927.437-1.028.488-.164 1.137.05 1.258.092l.295-.838C9.91 4.199 9.455 4 8.811 4c-.644 0-.994.061-1.33.389-.316.31-.465.831-.48 1.611L7 7c0 .018-.333.018-1 0v.972h1V12z" fill="#231F20" fill-opacity=".7"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8.031 16.5h7.938a4 4 0 1 0-7.938 0z" fill="#6E6E6E"/><path fill="#6E6E6E" d="M9 11l.866-.5 1.5 2.598-.866.5zM14.996 11l-.866-.5-1.5 2.598.866.5z"/></g><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

7
idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/after.kt.template

@ -0,0 +1,7 @@
@Preview
@Composable
fun NewsStory() {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}

6
idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/before.kt.template

@ -0,0 +1,6 @@
@Composable
fun NewsStory() {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}

22
idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/description.html

@ -0,0 +1,22 @@
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ 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.
-->
<html>
<body>
<p>
Add @Preview annotation to @Composable function.
</p>
</body>
</html>

8
idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template

@ -0,0 +1,8 @@
@Composable
fun NewsStory() {
Container {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}
}

6
idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template

@ -0,0 +1,6 @@
@Composable
fun NewsStory() {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}

5
idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/description.html

@ -0,0 +1,5 @@
<html>
<body>
This intention surrounds selected compose code with a widget.
</body>
</html>

6
idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/after.kt.template

@ -0,0 +1,6 @@
@Composable
fun NewsStory() {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}

8
idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/before.kt.template

@ -0,0 +1,8 @@
@Composable
fun NewsStory() {
Row {
Text("A day in Shark Fin Cove")
Text("Davenport, California")
Text("December 2018")
}
}

20
idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/description.html

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ 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.
-->
<html>
<body>
This intention removes Composable wrapper like Row or Column
</body>
</html>

7
idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/after.kt.template

@ -0,0 +1,7 @@
@Composable
fun NewsStory() {
Modifier
.padding(padding)
.clickable(onClick = onClick)
.fillMaxWidth()
}

4
idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/before.kt.template

@ -0,0 +1,4 @@
@Composable
fun NewsStory() {
Modifier.padding(padding).clickable(onClick = onClick).fillMaxWidth()
}

20
idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/description.html

@ -0,0 +1,20 @@
<!--
~ Copyright (C) 2020 The Android Open Source Project
~
~ 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.
-->
<html>
<body>
Wraps Modifier chain that is two modifiers or longer, in one modifier per line.
</body>
</html>

29
idea-plugin/src/main/resources/messages/ComposeBundle.properties

@ -0,0 +1,29 @@
create.preview=Create Preview
surround.with.widget.intention.text=Surround with widget
surround.with.box.intention.text=Surround with Container
surround.with.row.intention.text=Surround with Row
surround.with.column.intention.text=Surround with Column
import.compose.state=Import {0}
compose.enable.insertion.handler=Enable enhanced auto-completion when using Jetpack Compose
compose.enable.formatting.for.modifiers=Enable Compose formatting for Modifiers
create.composable.function=Create @Composable function
remove.wrapper=Remove wrapper
wrap.modifiers=Wrap modifiers
compose=Compose
rename.file=Rename File
file.name=File name
rename.files.with.following.names=Rename files with the following names to:
errors.composable_invocation=@Composable invocations can only happen from the context of a @Composable function
errors.composable_expected=Functions which invoke @Composable functions must be marked with the @Composable annotation
errors.captured_composable_invocation=Composable calls are not allowed inside the {0} parameter of {1}
errors.composable_property_backing_field=Composable properties are not able to have backing fields
errors.composable_var=Composable properties are not able to have backing fields
errors.composable_suspend_fun=Suspend functions cannot be made Composable
errors.illegal_try_catch_around_composable=Try catch is not supported around composable function invocations.
errors.composable_function_reference=Function References of @Composable functions are not currently supported
errors.conflicting_overloads=Conflicting overloads: {0}
errors.type_mismatch=Type inference failed. Expected type mismatch: inferred type is {1} but {0} was expected
errors.missing_disallow_composable_calls_annotation=Parameter {0} cannot be inlined inside of lambda argument {1} of {2} without also being annotated with @DisallowComposableCalls
errors.nonreadonly_call_in_readonly_composable=Composables marked with @ReadOnlyComposable can only call other @ReadOnlyComposable composables
errors.composable_fun_main=Composable main functions are not currently supported
filter.ignore.compose.runtime.classes=Do not step into Compose internal classes
Loading…
Cancel
Save