Browse Source

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

compose-tooling-223
Ilya Ryzhenkov 2 years ago
parent
commit
5c65cc0d0f
  1. 33
      idea-plugin/build.gradle.kts
  2. 12
      idea-plugin/gradle.properties
  3. 7
      idea-plugin/settings.gradle.kts
  4. 47
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableAnnotator.kt
  5. 520
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableCallChecker.kt
  6. 194
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableDeclarationChecker.kt
  7. 64
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableElementRefactoringElementListenerProvider.kt
  8. 69
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposableFunctionExtractableAnalyser.kt
  9. 44
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeBundle.kt
  10. 47
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeCodeCompletionConfigurable.kt
  11. 78
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeColorSettingsPage.kt
  12. 74
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeDiagnosticSuppressor.kt
  13. 109
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrorMessages.kt
  14. 117
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeErrors.kt
  15. 64
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFoldingBuilder.kt
  16. 86
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeFqNames.kt
  17. 68
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeKDocLinkResolutionService.kt
  18. 45
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.kt
  19. 44
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginIrGenerationExtension.kt
  20. 105
      idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginTypeResolutionInterceptorExtension.kt
  21. 43
      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. 24
      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. 68
      idea-plugin/src/main/kotlin/com/android/tools/compose/PsiUtils.kt
  26. 381
      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. 300
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/ComposeModifierCompletionContributor.kt
  29. 212
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/Constants.kt
  30. 218
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/ConstraintLayoutJsonCompletionContributor.kt
  31. 47
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/InsertionFormat.kt
  32. 27
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/JsonPsiUtil.kt
  33. 101
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/PatternUtils.kt
  34. 433
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/CompletionProviders.kt
  35. 101
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/BaseJsonElementModel.kt
  36. 70
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetModel.kt
  37. 66
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetsPropertyModel.kt
  38. 29
      idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintsModel.kt
  39. 53
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/CompletionResultSetUtils.kt
  40. 46
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithCaretInsertHandler.kt
  41. 86
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithLiveTemplateInsertHandler.kt
  42. 58
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithNewLineInsertHandler.kt
  43. 50
      idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/InsertionFormat.kt
  44. 29
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerClassesFilterProvider.kt
  45. 60
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettings.kt
  46. 26
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.form
  47. 48
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposeDebuggerSettingsUi.java
  48. 150
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManager.kt
  49. 27
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/ComposePositionManagerFactory.kt
  50. 258
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectClassRenderer.kt
  51. 77
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/ComposeStateObjectRendererProviderBase.kt
  52. 69
      idea-plugin/src/main/kotlin/com/android/tools/compose/debug/render/KotlinMapEntryRenderer.kt
  53. 112
      idea-plugin/src/main/kotlin/com/android/tools/compose/formatting/ComposePostFormatProcessor.kt
  54. 113
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreateComposableFunction.kt
  55. 81
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeCreatePreviewAction.kt
  56. 123
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeDelegateStateImportFix.kt
  57. 215
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeSurroundWithWidgetAction.kt
  58. 74
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeUnwrapAction.kt
  59. 67
      idea-plugin/src/main/kotlin/com/android/tools/compose/intentions/ComposeWrapModifiersAction.kt
  60. 32
      idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeCustomCodeStyleSettings.java
  61. 79
      idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeFormattingCodeStyleSettingsProvider.kt
  62. 27
      idea-plugin/src/main/kotlin/com/android/tools/idea/AndroidTextUtils.kt
  63. 23
      idea-plugin/src/main/kotlin/com/android/tools/idea/flags/StudioFlags.kt
  64. 28
      idea-plugin/src/main/kotlin/com/android/tools/modules/Module.kt
  65. 25
      idea-plugin/src/main/kotlin/icons/StudioIcons.kt
  66. 4
      idea-plugin/src/main/kotlin/org/jetbrains/compose/web/ide/run/psiUtils.kt
  67. 110
      idea-plugin/src/main/resources/META-INF/plugin.xml
  68. 23
      idea-plugin/src/main/resources/colorschemes/IdeComposableAnnotatorColorSchemeDefault.xml
  69. 1
      idea-plugin/src/main/resources/icons/compose/composable-function.svg
  70. 7
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/after.kt.template
  71. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/before.kt.template
  72. 22
      idea-plugin/src/main/resources/intentionDescriptions/ComposeCreatePreviewAction/description.html
  73. 8
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/after.kt.template
  74. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/before.kt.template
  75. 5
      idea-plugin/src/main/resources/intentionDescriptions/ComposeSurroundWithWidgetActionGroup/description.html
  76. 6
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/after.kt.template
  77. 8
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/before.kt.template
  78. 20
      idea-plugin/src/main/resources/intentionDescriptions/ComposeUnwrapAction/description.html
  79. 7
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/after.kt.template
  80. 4
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/before.kt.template
  81. 20
      idea-plugin/src/main/resources/intentionDescriptions/ComposeWrapModifiersAction/description.html
  82. 29
      idea-plugin/src/main/resources/messages/ComposeBundle.properties

33
idea-plugin/build.gradle.kts

@ -2,8 +2,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.5.10"
id("org.jetbrains.intellij") version "1.12.0"
id("org.jetbrains.kotlin.jvm") version "1.8.10"
id("org.jetbrains.intellij") version "1.13.3"
id("org.jetbrains.changelog") version "1.3.1"
}
@ -12,18 +12,30 @@ val projectProperties = ProjectProperties(project)
group = "org.jetbrains.compose.desktop.ide"
version = projectProperties.deployVersion
sourceSets {
main {
java.srcDir("src/main/kotlin")
}
}
repositories {
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
implementation("org.jetbrains.compose:preview-rpc")
implementation("org.jetbrains.compose.compiler:compiler-hosted:1.4.0")
}
intellij {
pluginName.set("Compose Multiplatform IDE Support")
type.set(projectProperties.platformType)
version.set(projectProperties.platformVersion)
if (projectProperties.platformVersion == "local") {
localPath.set(projectProperties.platformPath)
} else {
version.set(projectProperties.platformVersion)
}
downloadSources.set(projectProperties.platformDownloadSources)
updateSinceUntilBuild.set(false)
@ -42,19 +54,23 @@ tasks.buildSearchableOptions {
}
tasks {
// Set the compatibility versions to 1.8
withType<JavaCompile> {
sourceCompatibility = "11"
targetCompatibility = "11"
sourceCompatibility = "17"
targetCompatibility = "17"
}
withType<KotlinJvmCompile> {
kotlinOptions.jvmTarget = "11"
kotlinOptions.jvmTarget = "17"
kotlinOptions.freeCompilerArgs += listOf("-Xopt-in=org.jetbrains.kotlin.utils.addToStdlib.UnsafeCastFunction")
}
publishPlugin {
token.set(System.getenv("IDE_PLUGIN_PUBLISH_TOKEN"))
channels.set(projectProperties.pluginChannels)
}
runIde {
maxHeapSize = "2g"
}
runPluginVerifier {
ideVersions.set(projectProperties.pluginVerifierIdeVersions)
@ -65,12 +81,13 @@ class ProjectProperties(private val project: Project) {
val deployVersion get() = stringProperty("deploy.version")
val platformType get() = stringProperty("platform.type")
val platformVersion get() = stringProperty("platform.version")
val platformPath get() = stringProperty("platform.path")
val platformDownloadSources get() = stringProperty("platform.download.sources").toBoolean()
val pluginChannels get() = listProperty("plugin.channels")
val pluginVerifierIdeVersions get() = listProperty("plugin.verifier.ide.versions")
private fun stringProperty(key: String): String =
project.findProperty(key)!!.toString()
project.findProperty(key)?.toString() ?: error("Cannot find property $key")
private fun listProperty(key: String): List<String> =
stringProperty(key).split(",").map { it.trim() }
}

12
idea-plugin/gradle.properties

@ -2,13 +2,19 @@
# See https://kotlinlang.org/docs/reference/using-gradle.html#dependency-on-the-standard-library for details.
kotlin.stdlib.default.dependency=false
deploy.version=0.1-SNAPSHOT
deploy.version=2.0-SNAPSHOT
plugin.channels=snapshots
# Intellij since-build should be updated directly in src/main/resources/META-INF/plugin.xml
# See https://jb.gg/intellij-platform-builds-list for available build versions.
plugin.verifier.ide.versions=2022.1, 2022.2, 2022.3
plugin.verifier.ide.versions=2023.2
platform.type=IC
platform.version=2022.1.1
#platform.version=232-EAP-SNAPSHOT
platform.version=local
## For nightly builds IDEA SDK is not published, so use local installation:
## Comment out platform.version above, uncomment two lines below, fix the path to your setup.
#platform.version=local
platform.path=/Users/orangy/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-1/232.5096/IntelliJ IDEA 2023.2 EAP.app/Contents
platform.download.sources=true

7
idea-plugin/settings.gradle.kts

@ -1,3 +1,10 @@
pluginManagement {
repositories {
maven("https://oss.sonatype.org/content/repositories/snapshots/")
gradlePluginPortal()
}
}
includeBuild("../gradle-plugins") {
name = "compose-gradle-components"
}

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

@ -0,0 +1,47 @@
/*
* 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.DefaultLanguageHighlighterColors
import com.intellij.openapi.editor.colors.TextAttributesKey
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.highlighter.KotlinHighlightingVisitorExtension
import org.jetbrains.kotlin.resolve.calls.model.ResolvedCall
// Used to apply styles for calls to @Composable functions.
class ComposableHighlighter : KotlinHighlightingVisitorExtension() {
override fun highlightDeclaration(elementToHighlight: PsiElement, descriptor: DeclarationDescriptor): TextAttributesKey? {
return null
}
override fun highlightCall(elementToHighlight: PsiElement, resolvedCall: ResolvedCall<*>): TextAttributesKey? {
return if (resolvedCall.isComposableInvocation()) COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY else null
}
companion object TextAttributeRegistry {
val COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY: TextAttributesKey
const val COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME = "ComposableCallTextAttributes"
init {
COMPOSABLE_CALL_TEXT_ATTRIBUTES_KEY = TextAttributesKey.createTextAttributesKey(
COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME,
DefaultLanguageHighlighterColors.FUNCTION_CALL)
}
}
}

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)
}
private 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)
}
}

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)
}
}
}

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

@ -0,0 +1,47 @@
/*
* 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.PropertyBinding
import com.intellij.ui.layout.panel
/**
* 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"),
PropertyBinding({ settings.state.isComposeInsertHandlerEnabled }, { settings.state.isComposeInsertHandlerEnabled = it })
)
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[ComposableHighlighter.COMPOSABLE_CALL_TEXT_ATTRIBUTES_NAME] =
ComposableHighlighter.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",
ComposableHighlighter.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" +
"}"
}
}

74
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeDiagnosticSuppressor.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
import com.android.tools.modules.inComposeModule
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 (!diagnostic.psiElement.inComposeModule()) 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)
}
}

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

@ -0,0 +1,64 @@
/*
* 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.modules.*
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) || !root.inComposeModule()) {
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"
private fun fqNameFor(cname: String) = FqName("$root.$cname")
val Composable = fqNameFor("Composable")
}
val Composable = fqNameFor("Composable")
val DisallowComposableCalls = fqNameFor("DisallowComposableCalls")
val ReadOnlyComposable = fqNameFor("ReadOnlyComposable")
private 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

68
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeKDocLinkResolutionService.kt

@ -0,0 +1,68 @@
/*
* 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.intellij.psi.search.GlobalSearchScope
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor
import org.jetbrains.kotlin.idea.kdoc.IdeKDocLinkResolutionService
import org.jetbrains.kotlin.idea.kdoc.KDocLinkResolutionService
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade
import org.jetbrains.kotlin.idea.stubindex.KotlinClassShortNameIndex
import org.jetbrains.kotlin.idea.stubindex.KotlinFunctionShortNameIndex
import org.jetbrains.kotlin.idea.base.projectStructure.scope.KotlinSourceFilterScope
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode
/**
* Resolves links to functions and classes inside KDoc that are not included to the project (as byte code).
*
* It's a copy of [org.jetbrains.kotlin.idea.kdoc.IdeKDocLinkResolutionService], but with a larger search scope:
* GlobalSearchScope.everythingScope(project) instead of GlobalSearchScope.projectScope(project).
* Source code is already in the index, it attached in [AndroidModuleDependenciesSetup#setUpLibraryDependency]
*/
class ComposeKDocLinkResolutionService : KDocLinkResolutionService {
override fun resolveKDocLink(
context: BindingContext,
fromDescriptor: DeclarationDescriptor,
resolutionFacade: ResolutionFacade,
qualifiedName: List<String>
): Collection<DeclarationDescriptor> {
val project = resolutionFacade.project
val descriptors = IdeKDocLinkResolutionService(project).resolveKDocLink(context, fromDescriptor, resolutionFacade, qualifiedName)
if (!StudioFlags.SAMPLES_SUPPORT_ENABLED.get()) return descriptors
val scope = KotlinSourceFilterScope.librarySources(GlobalSearchScope.everythingScope(project), project)
val shortName = qualifiedName.lastOrNull() ?: return emptyList()
val targetFqName = FqName.fromSegments(qualifiedName)
val functions = KotlinFunctionShortNameIndex.get(shortName, project, scope).asSequence()
val classes = KotlinClassShortNameIndex.get(shortName, project, scope).asSequence()
val additionalDescriptors = (functions + classes)
.filter { it.fqName == targetFqName }
.map { it.unsafeResolveToDescriptor(BodyResolveMode.PARTIAL) } // TODO Filter out not visible due dependencies config descriptors
.toList()
if (additionalDescriptors.isNotEmpty())
return additionalDescriptors + descriptors
return descriptors
}
}

45
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposeLibraryNamespace.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
const val COMPOSE_UI_PACKAGE = "androidx.compose.ui"
const val COMPOSE_UI_TOOLING_PACKAGE = "$COMPOSE_UI_PACKAGE.tooling"
const val COMPOSE_UI_TOOLING_PREVIEW_PACKAGE = "$COMPOSE_UI_TOOLING_PACKAGE.preview"
/** Preview element name */
const val COMPOSE_PREVIEW_ANNOTATION_NAME = "Preview"
const val COMPOSE_PREVIEW_ANNOTATION_FQN = "$COMPOSE_UI_TOOLING_PREVIEW_PACKAGE.$COMPOSE_PREVIEW_ANNOTATION_NAME"
const val COMPOSE_PREVIEW_PARAMETER_ANNOTATION_FQN = "$COMPOSE_UI_TOOLING_PREVIEW_PACKAGE.PreviewParameter"
const val COMPOSE_PREVIEW_ACTIVITY_FQN = "$COMPOSE_UI_TOOLING_PACKAGE.PreviewActivity"
const val COMPOSE_VIEW_ADAPTER_FQN = "$COMPOSE_UI_TOOLING_PACKAGE.ComposeViewAdapter"
const val COMPOSABLE_ANNOTATION_NAME = "Composable"
const val COMPOSE_ALIGNMENT = "${COMPOSE_UI_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"
const val COMPOSE_MODIFIER_FQN = "$COMPOSE_UI_PACKAGE.Modifier"
const val COMPOSE_STRING_RESOURCE_FQN = "$COMPOSE_UI_PACKAGE.res.stringResource"
val COMPOSABLE_FQ_NAMES = setOf(
"androidx.compose.$COMPOSABLE_ANNOTATION_NAME",
"androidx.compose.runtime.$COMPOSABLE_ANNOTATION_NAME"
)

44
idea-plugin/src/main/kotlin/com/android/tools/compose/ComposePluginIrGenerationExtension.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 androidx.compose.compiler.plugins.kotlin.ComposeIrGenerationExtension
import androidx.compose.compiler.plugins.kotlin.IncompatibleComposeRuntimeVersionException
import com.intellij.openapi.progress.ProcessCanceledException
import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@Suppress("INVISIBLE_REFERENCE", "EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(org.jetbrains.kotlin.extensions.internal.InternalNonStableExtensionPoints::class)
class ComposePluginIrGenerationExtension : IrGenerationExtension {
override fun generate(moduleFragment: IrModuleFragment, pluginContext: IrPluginContext) {
try {
ComposeIrGenerationExtension(reportsDestination = null,
metricsDestination = null,
generateFunctionKeyMetaClasses = true,
intrinsicRememberEnabled = false).generate(moduleFragment, pluginContext);
} catch (e : ProcessCanceledException) {
// From ProcessCanceledException javadoc: "Usually, this exception should not be caught, swallowed, logged, or handled in any way.
// Instead, it should be rethrown so that the infrastructure can handle it correctly."
throw e;
} catch (t : Throwable) {
t.printStackTrace()
}
}
}

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
}
}

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

@ -0,0 +1,43 @@
/*
* 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 isModifierChainLongerThanTwo(element: KtElement): Boolean {
if (element.getChildrenOfType<KtDotQualifiedExpression>().isNotEmpty()) {
val fqName = element.resolveToCall(BodyResolveMode.PARTIAL)?.getReturnType()?.fqName?.asString()
if (fqName == COMPOSE_MODIFIER_FQN) {
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() = ApplicationManager.getApplication().getService(ComposeSettings::class.java)
}
}
class ComposeSettingsState : BaseState() {
var isComposeInsertHandlerEnabled by property(true)
}

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

@ -13,29 +13,29 @@
* 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" &&
element.language == KotlinLanguage.INSTANCE &&
element.node.elementType == KtTokens.IDENTIFIER &&
element.parent.let { it is KtNamedFunction && it.isComposableFunction() }
}
override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean {
return StudioFlags.COMPOSE_EDITOR_SUPPORT.get() &&
toolId == "FunctionName" &&
element.language == KotlinLanguage.INSTANCE &&
element.node.elementType == KtTokens.IDENTIFIER &&
element.parent.isComposableFunction()
}
override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> {
return SuppressQuickFix.EMPTY_ARRAY
}
override fun getSuppressActions(element: PsiElement?, toolId: String): Array<SuppressQuickFix> {
return SuppressQuickFix.EMPTY_ARRAY
}
}

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)
}

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

@ -0,0 +1,68 @@
/*
* 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.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 =
when (this) {
is KtAnnotationEntry -> this.fqNameMatches(COMPOSABLE_FQ_NAMES)
else -> false
}
fun PsiElement.isInsideComposableCode(): Boolean {
// TODO: also handle composable lambdas.
return language == KotlinLanguage.INSTANCE && parentOfType<KtNamedFunction>()?.isComposableFunction() == true
}

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

@ -0,0 +1,381 @@
/*
* 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.asSafely
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.core.completion.DescriptorBasedDeclarationLookupObject
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`
.asSafely<DescriptorBasedDeclarationLookupObject>()
?.descriptor
?.asSafely<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) ?: return null
}
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.idea.flags.StudioFlags
import com.android.tools.modules.*
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.base.util.allScope
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.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() || !elementToComplete.inComposeModule() || 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
.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()
}
}

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

@ -0,0 +1,300 @@
/*
* 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.COMPOSE_MODIFIER_FQN
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.modules.*
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.PrefixMatcher
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 COMPOSE_MODIFIER_FQN
*/
class ComposeModifierCompletionContributor : CompletionContributor() {
override fun fillCompletionVariants(parameters: CompletionParameters, resultSet: CompletionResultSet) {
val element = parameters.position
if (!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() || !element.inComposeModule() || 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, resultSet.prefixMatcher)
ProgressManager.checkCanceled()
val (returnsModifier, others) = extensionFunctions.partition { it.returnType?.fqName?.asString() == COMPOSE_MODIFIER_FQN }
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, parameters.editor, 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 = $COMPOSE_MODIFIER_FQN.call", originalFile)
return file.getChildOfType<KtProperty>()!!.getChildOfType<KtDotQualifiedExpression>()!!.lastChild as KtSimpleNameExpression
}
private fun getExtensionFunctionsForModifier(
nameExpression: KtSimpleNameExpression,
originalPosition: PsiElement,
prefixMatcher: PrefixMatcher
): Collection<CallableDescriptor> {
val file = nameExpression.containingFile as KtFile
val searchScope = getResolveScope(file)
val resolutionFacade = file.getResolutionFacade()
val bindingContext = nameExpression.analyze(BodyResolveMode.PARTIAL_FOR_COMPLETION)
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)
val nameFilter = { name: String -> prefixMatcher.prefixMatches(name) }
return indicesHelper.getCallableTopLevelExtensions(callTypeAndReceiver, nameExpression, bindingContext, null, nameFilter)
}
private val PsiElement.isModifierProperty: Boolean
get() {
// Case val myModifier:Modifier = <caret>
val property = parent?.parent?.safeAs<KtProperty>() ?: return false
return property.type()?.fqName?.asString() == COMPOSE_MODIFIER_FQN
}
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() == COMPOSE_MODIFIER_FQN
}
/**
* 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() == COMPOSE_MODIFIER_FQN
}
/**
* 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)
// Compose plugin inserts Modifier if completion character is '\n', doesn't happened with '\t'. Looks like a bug.
if (insertModifier && context.completionChar != '\n') {
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(COMPOSE_MODIFIER_FQN)).singleOrNull()
modifierDescriptor?.let { ImportInsertHelper.getInstance(context.project).importDescriptor(ktFile, it) }
psiDocumentManager.commitAllDocuments()
psiDocumentManager.doPostponedOperationsAndUnblockDocument(context.document)
super.handleInsert(context)
}
}
}

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

@ -0,0 +1,212 @@
/*
* 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 {
/**
* Name of the property within a MotionScene that contains several ConstraintSet declarations.
*/
const val ConstraintSets = "ConstraintSets"
/**
* Name of the property within a MotionScene that contains several Transition declarations.
*/
const val Transitions = "Transitions"
/**
* Name of the property used to indicate that the containing ConstraintSet inherits its constraints from the ConstraintSet given by the
* `Extends` property value.
*/
const val Extends = "Extends"
/**
* Reserved ID for the containing layout. Typically referenced in constraint blocks.
*/
const val ParentId = "parent"
/**
* Name of the Visibility property in a constraint block.
*/
const val Visibility = "visibility"
/**
* Name of the Clear property in a constraint block.
*
* Populated by an array of options to clear inherited parameters from [Extends].
*
* @see ClearOption
*/
const val Clear = "clear"
}
/**
* Common interface to simplify handling multiple enum Classes.
*
* [keyWord] is the case-sensitive string used in the syntax.
*/
internal interface ConstraintLayoutKeyWord {
val keyWord: String
}
//region Constrain KeyWords
/**
* The classic anchors used to constrain a widget.
*/
internal enum class StandardAnchor(override val keyWord: String) : ConstraintLayoutKeyWord {
Start("start"),
Left("left"),
End("end"),
Right("right"),
Top("top"),
Bottom("bottom"),
Baseline("baseline");
companion object {
fun isVertical(keyWord: String) = verticalAnchors.any { it.keyWord == keyWord }
fun isHorizontal(keyWord: String) = horizontalAnchors.any { it.keyWord == keyWord }
val horizontalAnchors: List<StandardAnchor> = listOf(Start, End, Left, Right)
val verticalAnchors: List<StandardAnchor> = listOf(Top, Bottom, Baseline)
}
}
/**
* Non-typical anchors.
*
* These implicitly apply multiple [StandardAnchor]s.
*/
internal enum class SpecialAnchor(override val keyWord: String) : ConstraintLayoutKeyWord {
Center("center"),
CenterH("centerHorizontally"),
CenterV("centerVertically")
}
/**
* Supported keywords to define the dimension of a widget.
*/
internal enum class Dimension(override val keyWord: String) : ConstraintLayoutKeyWord {
Width("width"),
Height("height")
}
/**
* Keywords to apply rendering time transformations to a widget.
*/
internal enum class RenderTransform(override val keyWord: String) : ConstraintLayoutKeyWord {
Alpha("alpha"),
ScaleX("scaleX"),
ScaleY("scaleY"),
RotationX("rotationX"),
RotationY("rotationY"),
RotationZ("rotationZ"),
TranslationX("translationX"),
TranslationY("translationY"),
TranslationZ("translationZ"),
}
//endregion
internal enum class DimBehavior(override val keyWord: String) : ConstraintLayoutKeyWord {
Spread("spread"),
Wrap("wrap"),
PreferWrap("preferWrap"),
MatchParent("parent")
}
internal enum class VisibilityMode(override val keyWord: String): ConstraintLayoutKeyWord {
Visible("visible"),
Invisible("invisible"),
Gone("gone")
}
internal enum class ClearOption(override val keyWord: String): ConstraintLayoutKeyWord {
Constraints("constraints"),
Dimensions("dimensions"),
Transforms("transforms")
}
internal enum class TransitionField(override val keyWord: String): ConstraintLayoutKeyWord {
From("from"),
To("to"),
PathArc("pathMotionArc"),
KeyFrames("KeyFrames"),
OnSwipe("onSwipe")
}
internal enum class OnSwipeField(override val keyWord: String): ConstraintLayoutKeyWord {
AnchorId("anchor"),
Direction("direction"),
Side("side"),
Mode("mode")
}
internal enum class OnSwipeSide(override val keyWord: String): ConstraintLayoutKeyWord {
Top("top"),
Left("left"),
Right("right"),
Bottom("bottom"),
Middle("middle"),
Start("start"),
End("end")
}
internal enum class OnSwipeDirection(override val keyWord: String): ConstraintLayoutKeyWord {
Up("up"),
Down("down"),
Left("left"),
Right("right"),
Start("start"),
End("end"),
Clockwise("clockwise"),
AntiClockwise("anticlockwise")
}
internal enum class OnSwipeMode(override val keyWord: String): ConstraintLayoutKeyWord {
Velocity("velocity"),
Spring("spring")
}
internal enum class KeyFrameField(override val keyWord: String): ConstraintLayoutKeyWord {
Positions("KeyPositions"),
Attributes("KeyAttributes"),
Cycles("KeyCycles")
}
/**
* Common fields used by any of [KeyFrameField].
*/
internal enum class KeyFrameChildCommonField(override val keyWord: String): ConstraintLayoutKeyWord {
TargetId("target"),
Frames("frames"),
Easing("transitionEasing"),
Fit("curveFit"),
}
internal enum class KeyPositionField(override val keyWord: String): ConstraintLayoutKeyWord {
PercentX("percentX"),
PercentY("percentY"),
PercentWidth("percentWidth"),
PercentHeight("percentHeight"),
PathArc("pathMotionArc"),
Type("type")
}
internal enum class KeyCycleField(override val keyWord: String): ConstraintLayoutKeyWord {
Period("period"),
Offset("offset"),
Phase("phase")
}

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

@ -0,0 +1,218 @@
/*
* 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.android.tools.compose.code.completion.constraintlayout.provider.AnchorablesProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ClearOptionsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintIdsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintSetFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintSetNamesProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.ConstraintsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.EnumValuesCompletionProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.KeyFrameChildFieldsCompletionProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.KeyFramesFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.OnSwipeFieldsProvider
import com.android.tools.compose.code.completion.constraintlayout.provider.TransitionFieldsProvider
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.modules.inComposeModule
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.json.JsonLanguage
import com.intellij.json.psi.JsonStringLiteral
internal const val BASE_DEPTH_FOR_LITERAL_IN_PROPERTY = 2
internal const val BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT = BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of the list of ConstraintSets. With respect to the ConstraintSets root element. */
private const val CONSTRAINT_SET_LIST_PROPERTY_DEPTH = BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a ConstraintSet. With respect to the ConstraintSets root element. */
private const val CONSTRAINT_SET_PROPERTY_DEPTH = CONSTRAINT_SET_LIST_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a Transition. With respect to the Transitions root element. */
private const val TRANSITION_PROPERTY_DEPTH = CONSTRAINT_SET_PROPERTY_DEPTH
/** Depth for a literal of a property of a Constraints block. With respect to the ConstraintSets root element. */
internal const val CONSTRAINT_BLOCK_PROPERTY_DEPTH = CONSTRAINT_SET_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of an OnSwipe block. With respect to the Transitions root element. */
internal const val ONSWIPE_PROPERTY_DEPTH = TRANSITION_PROPERTY_DEPTH + BASE_DEPTH_FOR_LITERAL_IN_PROPERTY
/** Depth for a literal of a property of a KeyFrames block. With respect to the Transitions root element. */
internal const val KEYFRAMES_PROPERTY_DEPTH = ONSWIPE_PROPERTY_DEPTH
/**
* [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 {
// region ConstraintSets
extend(
CompletionType.BASIC,
// Complete field names in ConstraintSets
jsonPropertyName().withConstraintSetsParentAtLevel(CONSTRAINT_SET_PROPERTY_DEPTH),
ConstraintSetFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete constraints field names (width, height, start, end, etc.)
jsonPropertyName().withConstraintSetsParentAtLevel(CONSTRAINT_BLOCK_PROPERTY_DEPTH),
ConstraintsProvider
)
extend(
CompletionType.BASIC,
// Complete ConstraintSet names in Extends keyword
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, KeyWords.Extends),
ConstraintSetNamesProvider
)
extend(
CompletionType.BASIC,
// Complete IDs on special anchors, they take a single string value
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, SpecialAnchor.values().map { it.keyWord }),
ConstraintIdsProvider
)
extend(
CompletionType.BASIC,
// Complete IDs in the constraint array (first position)
jsonStringValue()
// First element in the array, ie: there is no PsiElement preceding the desired one at this level
.withParent(psiElement<JsonStringLiteral>().atIndexOfJsonArray(0))
.insideConstraintArray(),
ConstraintIdsProvider
)
extend(
CompletionType.BASIC,
// Complete anchors in the constraint array (second position)
jsonStringValue()
// Second element in the array, ie: there is one PsiElement preceding the desired one at this level
.withParent(psiElement<JsonStringLiteral>().atIndexOfJsonArray(1))
.insideConstraintArray(),
AnchorablesProvider
)
extend(
CompletionType.BASIC,
// Complete a clear option within the 'clear' array
jsonStringValue()
.insideClearArray(),
ClearOptionsProvider
)
extend(
CompletionType.BASIC,
// Complete non-numeric dimension values for width & height
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, Dimension.values().map { it.keyWord }),
EnumValuesCompletionProvider(DimBehavior::class)
)
extend(
CompletionType.BASIC,
// Complete Visibility mode values
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, KeyWords.Visibility),
EnumValuesCompletionProvider(VisibilityMode::class)
)
//endregion
//region Transitions
extend(
CompletionType.BASIC,
// Complete fields of a Transition block
jsonPropertyName()
.withTransitionsParentAtLevel(TRANSITION_PROPERTY_DEPTH),
TransitionFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete existing ConstraintSet names for `from` and `to` Transition properties
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, listOf(TransitionField.From.keyWord, TransitionField.To.keyWord))
.withTransitionsParentAtLevel(TRANSITION_PROPERTY_DEPTH),
// TODO(b/207030860): Guarantee that provided names for 'from' or 'to' are distinct from each other,
// ie: both shouldn't reference the same ConstraintSet
ConstraintSetNamesProvider
)
extend(
CompletionType.BASIC,
// Complete fields of a KeyFrames block
jsonPropertyName()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT, TransitionField.KeyFrames.keyWord)
.withTransitionsParentAtLevel(KEYFRAMES_PROPERTY_DEPTH),
KeyFramesFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete fields of an OnSwipe block
jsonPropertyName()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT, TransitionField.OnSwipe.keyWord)
.withTransitionsParentAtLevel(ONSWIPE_PROPERTY_DEPTH),
OnSwipeFieldsProvider
)
extend(
CompletionType.BASIC,
// Complete the possible IDs for the OnSwipe `anchor` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.AnchorId.keyWord),
ConstraintIdsProvider
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `side` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Side.keyWord),
EnumValuesCompletionProvider(OnSwipeSide::class)
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `direction` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Direction.keyWord),
EnumValuesCompletionProvider(OnSwipeDirection::class)
)
extend(
CompletionType.BASIC,
// Complete the known values for the OnSwipe `mode` property
jsonStringValue()
.withPropertyParentAtLevel(BASE_DEPTH_FOR_LITERAL_IN_PROPERTY, OnSwipeField.Mode.keyWord),
EnumValuesCompletionProvider(OnSwipeMode::class)
)
extend(
CompletionType.BASIC,
// Complete the fields for any of the possible KeyFrames children
jsonPropertyName()
// A level deeper considering the array surrounding the object
.withPropertyParentAtLevel(BASE_DEPTH_FOR_NAME_IN_PROPERTY_OBJECT + 1, KeyFrameField.values().map { it.keyWord }),
KeyFrameChildFieldsCompletionProvider
)
//endregion
}
override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) {
if (!StudioFlags.COMPOSE_CONSTRAINTLAYOUT_COMPLETION.get() ||
parameters.position.inComposeModule() != true ||
parameters.position.language != JsonLanguage.INSTANCE) {
// TODO(b/207030860): Allow in other contexts once the syntax is supported outside Compose
return
}
super.fillCompletionVariants(parameters, result)
}
}

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

@ -0,0 +1,47 @@
/*
* 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.android.tools.compose.completion.inserthandler.LiteralNewLineFormat
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat
internal val JsonStringValueTemplate = LiteralWithCaretFormat(": '|',")
internal val JsonNumericValueTemplate = LiteralWithCaretFormat(": |,")
internal val JsonNewObjectTemplate = LiteralNewLineFormat(": {\n}")
internal val JsonStringArrayTemplate = LiteralWithCaretFormat(": ['|'],")
internal val JsonObjectArrayTemplate = LiteralNewLineFormat(": [{\n}],")
internal val ConstrainAnchorTemplate = LiveTemplateFormat(": ['<>', '<>', <0>],")
internal val ClearAllTemplate = LiteralWithCaretFormat(
literalFormat = ": ['${ClearOption.Constraints}', '${ClearOption.Dimensions}', '${ClearOption.Transforms}'],"
)
/**
* Returns a [LiveTemplateFormat] that contains a template for a Json array with numeric type, where the size of the array is given by
* [count] and the user may edit each of the values in the array using Live Templates.
*
* E.g.: For [count] = 3, returns the template: `": [0, 0, 0],"`, where every value may be changed by the user.
*/
internal fun buildJsonNumberArrayTemplate(count: Int): LiveTemplateFormat {
val times = count.coerceAtLeast(1)
return LiveTemplateFormat(": [" + "<0>, ".repeat(times).removeSuffix(", ") + "],")
}

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

@ -0,0 +1,27 @@
/*
* Copyright (C) 2022 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.CompletionParameters
import com.intellij.json.psi.JsonProperty
import com.intellij.psi.util.parentOfType
/**
* From the element being invoked, returns the [JsonProperty] parent that also includes the [JsonProperty] from which completion is
* triggered.
*/
internal fun getJsonPropertyParent(parameters: CompletionParameters): JsonProperty? =
parameters.position.parentOfType<JsonProperty>(withSelf = true)?.parentOfType<JsonProperty>(withSelf = false)

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

@ -0,0 +1,101 @@
/*
* Copyright (C) 2022 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.json.JsonElementTypes
import com.intellij.json.psi.JsonArray
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonReferenceExpression
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.json.psi.JsonValue
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.patterns.PsiElementPattern
import com.intellij.patterns.StandardPatterns
import com.intellij.patterns.StringPattern
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
// region ConstraintLayout Pattern Helpers
internal fun jsonPropertyName() = PlatformPatterns.psiElement(JsonElementTypes.IDENTIFIER)
internal fun jsonStringValue() =
PlatformPatterns.psiElement(JsonElementTypes.SINGLE_QUOTED_STRING).withParent<JsonStringLiteral>()
internal fun PsiElementPattern<*, *>.withConstraintSetsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.ConstraintSets)
internal fun PsiElementPattern<*, *>.withTransitionsParentAtLevel(level: Int) = withPropertyParentAtLevel(level, KeyWords.Transitions)
internal fun PsiElementPattern<*, *>.insideClearArray() = inArrayWithinConstraintBlockProperty {
// For the 'clear' constraint block property
matches(KeyWords.Clear)
}
internal fun PsiElementPattern<*, *>.insideConstraintArray() = inArrayWithinConstraintBlockProperty {
// The parent property name may only be a StandardAnchor
oneOf(StandardAnchor.values().map { it.keyWord })
}
/**
* [PsiElementPattern] that matches an element in a [JsonArray] within a Constraint block. Where the property the array is assigned to, has
* a name that is matched by [matchPropertyName].
*/
internal fun PsiElementPattern<*, *>.inArrayWithinConstraintBlockProperty(matchPropertyName: StringPattern.() -> StringPattern) =
withSuperParent(2, psiElement<JsonArray>())
.withSuperParent(
BASE_DEPTH_FOR_LITERAL_IN_PROPERTY + 1, // JsonArray adds one level
psiElement<JsonProperty>().withChild(
// The first expression in a JsonProperty corresponds to the name of the property
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().matchPropertyName())
)
)
.withConstraintSetsParentAtLevel(CONSTRAINT_BLOCK_PROPERTY_DEPTH + 1) // JsonArray adds one level
// endregion
// region Kotlin Syntax Helpers
internal inline fun <reified T : PsiElement> psiElement(): PsiElementPattern<T, PsiElementPattern.Capture<T>> =
PlatformPatterns.psiElement(T::class.java)
internal inline fun <reified T : PsiElement> PsiElementPattern<*, *>.withParent() = this.withParent(T::class.java)
/**
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. And its name matches
* the given [name].
*/
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, name: String) =
withPropertyParentAtLevel(level, listOf(name))
/**
* Pattern such that when traversing up the tree from the current element, the element at [level] is a [JsonProperty]. Which name matches
* one of the given [names].
*/
internal fun PsiElementPattern<*, *>.withPropertyParentAtLevel(level: Int, names: Collection<String>) =
this.withSuperParent(level, psiElement<JsonProperty>().withChild(
psiElement<JsonReferenceExpression>().withText(StandardPatterns.string().oneOf(names)))
)
/**
* Verifies that the current element is at the given [index] of the elements contained by its [JsonArray] parent.
*/
internal fun <T : JsonValue> PsiElementPattern<T, PsiElementPattern.Capture<T>>.atIndexOfJsonArray(index: Int) =
with(object : PatternCondition<T>("atIndexOfJsonArray") {
override fun accepts(element: T, context: ProcessingContext?): Boolean {
val parent = element.context as? JsonArray ?: return false
val children = parent.valueList
val indexOfSelf = children.indexOf(element)
return index == indexOfSelf
}
})
// endregion

433
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/CompletionProviders.kt

@ -0,0 +1,433 @@
/*
* 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.provider
import com.android.tools.compose.code.completion.constraintlayout.ClearAllTemplate
import com.android.tools.compose.code.completion.constraintlayout.ClearOption
import com.android.tools.compose.code.completion.constraintlayout.ConstrainAnchorTemplate
import com.android.tools.compose.code.completion.constraintlayout.ConstraintLayoutKeyWord
import com.android.tools.compose.code.completion.constraintlayout.Dimension
import com.android.tools.compose.code.completion.constraintlayout.JsonNewObjectTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonNumericValueTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonObjectArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonStringArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.JsonStringValueTemplate
import com.android.tools.compose.code.completion.constraintlayout.KeyCycleField
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameChildCommonField
import com.android.tools.compose.code.completion.constraintlayout.KeyFrameField
import com.android.tools.compose.code.completion.constraintlayout.KeyPositionField
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.android.tools.compose.code.completion.constraintlayout.OnSwipeField
import com.android.tools.compose.code.completion.constraintlayout.RenderTransform
import com.android.tools.compose.code.completion.constraintlayout.SpecialAnchor
import com.android.tools.compose.code.completion.constraintlayout.StandardAnchor
import com.android.tools.compose.code.completion.constraintlayout.TransitionField
import com.android.tools.compose.code.completion.constraintlayout.buildJsonNumberArrayTemplate
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetModel
import com.android.tools.compose.code.completion.constraintlayout.provider.model.ConstraintSetsPropertyModel
import com.android.tools.compose.code.completion.constraintlayout.provider.model.JsonPropertyModel
import com.android.tools.compose.completion.addLookupElement
import com.android.tools.compose.completion.inserthandler.InsertionFormat
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.json.psi.JsonArray
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.PsiElement
import com.intellij.psi.util.parentOfType
import com.intellij.util.ProcessingContext
import kotlin.reflect.KClass
/**
* Completion provider that looks for the 'ConstraintSets' declaration and passes a model that provides useful functions for inheritors that
* want to provide completions based on the contents of the 'ConstraintSets' [JsonProperty].
*/
internal abstract class BaseConstraintSetsCompletionProvider : CompletionProvider<CompletionParameters>() {
final override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val constraintSetsModel = createConstraintSetsModel(initialElement = parameters.position)
if (constraintSetsModel != null) {
ProgressManager.checkCanceled()
addCompletions(constraintSetsModel, parameters, result)
}
}
/**
* Inheritors should implement this function that may pass a reference to the ConstraintSets property.
*/
abstract fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
)
/**
* Finds the [JsonProperty] for the 'ConstraintSets' declaration and returns its model.
*
* The `ConstraintSets` property is expected to be a property of the root [JsonObject].
*/
private fun createConstraintSetsModel(initialElement: PsiElement): ConstraintSetsPropertyModel? {
// Start with the closest JsonObject towards the root
var currentJsonObject: JsonObject? = initialElement.parentOfType<JsonObject>(withSelf = true) ?: return null
lateinit var topLevelJsonObject: JsonObject
// Then find the top most JsonObject while checking for cancellation
while (currentJsonObject != null) {
topLevelJsonObject = currentJsonObject
currentJsonObject = currentJsonObject.parentOfType<JsonObject>(withSelf = false)
ProgressManager.checkCanceled()
}
// The last non-null JsonObject is the topmost, the ConstraintSets property is expected within this element
val constraintSetsProperty = topLevelJsonObject.findProperty(KeyWords.ConstraintSets) ?: return null
// TODO(b/207030860): Consider creating the model even if there's no property that is explicitly called 'ConstraintSets'
// ie: imply that the root JsonObject is the ConstraintSets object, with the downside that figuring out the correct context would
// be much more difficult
return ConstraintSetsPropertyModel(constraintSetsProperty)
}
}
/**
* Provides options to autocomplete constraint IDs for constraint set declarations, based on the IDs already defined by the user in other
* constraint sets.
*/
internal object ConstraintSetFieldsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters) ?: return
val currentSetName = currentConstraintSet.name ?: return
constraintSetsPropertyModel.getRemainingFieldsForConstraintSet(currentSetName).forEach { fieldName ->
val template = if (fieldName == KeyWords.Extends) JsonStringValueTemplate else JsonNewObjectTemplate
result.addLookupElement(lookupString = fieldName, tailText = null, template)
}
}
}
/**
* Autocomplete options with the names of all available ConstraintSets, except from the one the autocomplete was invoked from.
*/
internal object ConstraintSetNamesProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentConstraintSet = ConstraintSetModel.getModelForCompletionOnConstraintSetProperty(parameters)
val currentSetName = currentConstraintSet?.name
val names = constraintSetsPropertyModel.getConstraintSetNames().toMutableSet()
if (currentSetName != null) {
names.remove(currentSetName)
}
names.forEach(result::addLookupElement)
}
}
/**
* Autocomplete options used to define the constraints of a widget (defined by the ID) within a ConstraintSet
*/
internal object ConstraintsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
val existingFieldsSet = parentPropertyModel.declaredFieldNamesSet
StandardAnchor.values().forEach {
if (!existingFieldsSet.contains(it.keyWord)) {
result.addLookupElement(lookupString = it.keyWord, tailText = " [...]", format = ConstrainAnchorTemplate)
}
}
if (!existingFieldsSet.contains(KeyWords.Visibility)) {
result.addLookupElement(lookupString = KeyWords.Visibility, format = JsonStringValueTemplate)
}
result.addEnumKeyWordsWithStringValueTemplate<SpecialAnchor>(existingFieldsSet)
result.addEnumKeyWordsWithNumericValueTemplate<Dimension>(existingFieldsSet)
result.addEnumKeyWordsWithNumericValueTemplate<RenderTransform>(existingFieldsSet)
// Complete 'clear' if the containing ConstraintSet has `extendsFrom`
val containingConstraintSetModel = parentPropertyModel.getParentProperty()?.let {
ConstraintSetModel(it)
}
if (containingConstraintSetModel?.extendsFrom != null) {
// Add an option with an empty string array and another one with all clear options
result.addLookupElement(lookupString = KeyWords.Clear, format = JsonStringArrayTemplate)
result.addLookupElement(lookupString = KeyWords.Clear, format = ClearAllTemplate, tailText = " [<all>]")
}
}
}
/**
* Provides IDs when autocompleting a constraint array.
*
* The ID may be either 'parent' or any of the declared IDs in all ConstraintSets, except the ID of the constraints block from which this
* provider was invoked.
*/
internal object ConstraintIdsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val possibleIds = constraintSetsPropertyModel.constraintSets.flatMap { it.declaredIds }.toCollection(HashSet())
// Parent ID should always be present
possibleIds.add(KeyWords.ParentId)
// Remove the current ID
getJsonPropertyParent(parameters)?.name?.let(possibleIds::remove)
possibleIds.forEach { id ->
result.addLookupElement(lookupString = id)
}
}
}
/**
* Provides the appropriate anchors when completing a constraint array.
*
* [StandardAnchor.verticalAnchors] can only be constrained to other vertical anchors. Same logic for [StandardAnchor.horizontalAnchors].
*/
internal object AnchorablesProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val currentAnchorKeyWord = parameters.position.parentOfType<JsonProperty>(withSelf = true)?.name ?: return
val possibleAnchors = when {
StandardAnchor.isVertical(currentAnchorKeyWord) -> StandardAnchor.verticalAnchors
StandardAnchor.isHorizontal(currentAnchorKeyWord) -> StandardAnchor.horizontalAnchors
else -> emptyList()
}
possibleAnchors.forEach { result.addLookupElement(lookupString = it.keyWord) }
}
}
/**
* Provides the appropriate options when completing string literals within a `clear` array.
*
* @see ClearOption
*/
internal object ClearOptionsProvider : BaseConstraintSetsCompletionProvider() {
override fun addCompletions(
constraintSetsPropertyModel: ConstraintSetsPropertyModel,
parameters: CompletionParameters,
result: CompletionResultSet
) {
val existing = parameters.position.parentOfType<JsonArray>(withSelf = false)?.valueList
?.filterIsInstance<JsonStringLiteral>()
?.map { it.value }
?.toSet() ?: emptySet()
addEnumKeywords<ClearOption>(result, existing)
}
}
/**
* Provides completion for the fields of a `Transition`.
*
* @see TransitionField
*/
internal object TransitionFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
TransitionField.values().forEach {
if (parentPropertyModel.containsPropertyOfName(it.keyWord)) {
// skip
return@forEach
}
when (it) {
TransitionField.OnSwipe,
TransitionField.KeyFrames -> {
result.addLookupElement(lookupString = it.keyWord, format = JsonNewObjectTemplate)
}
else -> {
result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate)
}
}
}
}
}
/**
* Provides completion for the fields of an `OnSwipe` block.
*
* @see OnSwipeField
*/
internal object OnSwipeFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
result.addEnumKeyWordsWithStringValueTemplate<OnSwipeField>(parentPropertyModel.declaredFieldNamesSet)
}
}
/**
* Provides completion for the fields of a `KeyFrames` block.
*
* @see KeyFrameField
*/
internal object KeyFramesFieldsProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val parentPropertyModel = JsonPropertyModel.getModelForCompletionOnInnerJsonProperty(parameters) ?: return
addEnumKeywords<KeyFrameField>(
result = result,
format = JsonObjectArrayTemplate,
existing = parentPropertyModel.declaredFieldNamesSet
)
}
}
/**
* Provides completion for the fields of KeyFrame children. A KeyFrame child can be any of [KeyFrameField].
*/
internal object KeyFrameChildFieldsCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
// TODO(b/207030860): For consistency, make it so that JsonPropertyModel may be used here. It currently won't work because the model
// doesn't consider a property defined by an array of objects.
// Obtain existing list of existing properties
val parentObject = parameters.position.parentOfType<JsonObject>(withSelf = false) ?: return
val existingFieldsSet = parentObject.propertyList.map { it.name }.toSet()
// We have to know the type of KeyFrame we are autocompleting for (KeyPositions, KeyAttributes, etc)
val keyFrameTypeName = parentObject.parentOfType<JsonProperty>(withSelf = false)?.name ?: return
// Look for the `frames` property, we want to know the size of its array (if present), since all other numeric properties should have an
// array of the same size
val framesProperty = parentObject.findProperty(KeyFrameChildCommonField.Frames.keyWord)
val arrayCountInFramesProperty = (framesProperty?.value as? JsonArray)?.valueList?.size ?: 1
// Create the template that will be used by any numeric property we autocomplete
val jsonNumberArrayTemplate = buildJsonNumberArrayTemplate(count = arrayCountInFramesProperty)
// We've done some read operations, check for cancellation
ProgressManager.checkCanceled()
// Common fields for any type of KeyFrame
KeyFrameChildCommonField.values().forEach {
if (existingFieldsSet.contains(it.keyWord)) {
return@forEach
}
when (it) {
KeyFrameChildCommonField.Frames -> result.addLookupElement(lookupString = it.keyWord, format = jsonNumberArrayTemplate)
else -> result.addLookupElement(lookupString = it.keyWord, format = JsonStringValueTemplate)
}
}
// Figure out which type of KeyFrame the completion is being called on, and offer completion for their respective fields
when (keyFrameTypeName) {
KeyFrameField.Positions.keyWord -> {
addKeyPositionFields(result, existingFieldsSet) {
// Some KeyPosition fields take either a Number Array value or a String value
if (isNumberArrayType(it)) jsonNumberArrayTemplate else JsonStringValueTemplate
}
}
KeyFrameField.Attributes.keyWord -> {
// KeyAttributes properties are the same as the RenderTransform fields
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
}
KeyFrameField.Cycles.keyWord -> {
// KeyCycles properties are a mix of RenderTransform fields and KeyCycles specific fields
addEnumKeywords<RenderTransform>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
addEnumKeywords<KeyCycleField>(result = result, format = jsonNumberArrayTemplate, existing = existingFieldsSet)
}
else -> {
thisLogger().warn("Completion on unknown KeyFrame type: $keyFrameTypeName")
}
}
}
/**
* Add LookupElements to the [result] for each non-repeated [KeyPositionField] using the [InsertionFormat] returned by [templateProvider].
*/
private fun addKeyPositionFields(
result: CompletionResultSet,
existing: Set<String>,
templateProvider: (KeyPositionField) -> InsertionFormat
) {
KeyPositionField.values().forEach { keyPositionField ->
if (existing.contains(keyPositionField.keyWord)) {
// Skip repeated fields
return@forEach
}
result.addLookupElement(lookupString = keyPositionField.keyWord, format = templateProvider(keyPositionField))
}
}
private fun isNumberArrayType(keyPositionField: KeyPositionField) =
when (keyPositionField) {
// Only some KeyPosition fields receive a Number value
KeyPositionField.PercentX,
KeyPositionField.PercentY,
KeyPositionField.PercentWidth,
KeyPositionField.PercentHeight -> true
else -> false
}
}
/**
* Provides plaint-text completion for each of the elements in the Enum.
*
* The provided values come from [ConstraintLayoutKeyWord.keyWord].
*/
internal class EnumValuesCompletionProvider<E>(private val enumClass: KClass<E>)
: CompletionProvider<CompletionParameters>() where E : Enum<E>, E : ConstraintLayoutKeyWord {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
enumClass.java.enumConstants.forEach {
result.addLookupElement(lookupString = it.keyWord)
}
}
}
/**
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a string for its value.
*/
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithStringValueTemplate(
existing: Set<String>
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
addEnumKeywords<E>(result = this, existing = existing, format = JsonStringValueTemplate)
}
/**
* Add the [ConstraintLayoutKeyWord.keyWord] of the enum constants as a completion result that takes a number for its value.
*/
private inline fun <reified E> CompletionResultSet.addEnumKeyWordsWithNumericValueTemplate(
existing: Set<String>
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
addEnumKeywords<E>(result = this, existing = existing, format = JsonNumericValueTemplate)
}
/**
* Helper function to simplify adding enum constant members to the completion result.
*/
private inline fun <reified E> addEnumKeywords(
result: CompletionResultSet,
existing: Set<String> = emptySet(),
format: InsertionFormat? = null
) where E : Enum<E>, E : ConstraintLayoutKeyWord {
E::class.java.enumConstants.forEach { constant ->
if (!existing.contains(constant.keyWord)) {
result.addLookupElement(lookupString = constant.keyWord, format = format)
}
}
}

101
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/BaseJsonElementModel.kt

@ -0,0 +1,101 @@
/*
* 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.provider.model
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.json.psi.JsonElement
import com.intellij.json.psi.JsonObject
import com.intellij.json.psi.JsonProperty
import com.intellij.openapi.progress.ProgressManager
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.util.parentOfType
import org.jetbrains.kotlin.psi.psiUtil.getChildOfType
/**
* Base model for [JsonElement], sets a pointer to avoid holding to the element itself.
*/
internal abstract class BaseJsonElementModel<E: JsonElement>(element: E) {
protected val elementPointer = SmartPointerManager.createPointer(element)
}
/**
* Base model for a [JsonProperty].
*
* Populates some common fields and provides useful function while avoiding holding to PsiElement instances.
*/
internal open class JsonPropertyModel(element: JsonProperty): BaseJsonElementModel<JsonProperty>(element) {
/**
* The [JsonObject] that describes this [JsonProperty].
*/
private val innerJsonObject: JsonObject? = elementPointer.element?.getChildOfType<JsonObject>()
/**
* A mapping of the containing [JsonProperty]s by their declare name.
*/
private val propertiesByName: Map<String, JsonProperty> =
innerJsonObject?.propertyList?.associateBy { it.name } ?: emptyMap()
/**
* [List] of all the children of this element that are [JsonProperty].
*/
protected val innerProperties: Collection<JsonProperty> = propertiesByName.values
/**
* Name of the [JsonProperty].
*/
val name: String?
get() = elementPointer.element?.name
/**
* A set of names for all declared properties in this [JsonProperty].
*/
val declaredFieldNamesSet: Set<String> = propertiesByName.keys
/**
* For the children of the current element, returns the [JsonProperty] which name matches the given [name]. Null if none of them does.
*/
protected fun findProperty(name: String): JsonProperty? = propertiesByName[name]
/**
* Returns true if this [JsonProperty] contains another [JsonProperty] declared by the given [name].
*/
fun containsPropertyOfName(name: String): Boolean = propertiesByName.containsKey(name)
/**
* Returns the containing [JsonProperty].
*
* May return null if this model is for a top level [JsonProperty].
*/
fun getParentProperty(): JsonProperty? = elementPointer.element?.parentOfType<JsonProperty>(withSelf = false)
companion object {
/**
* Returns the [JsonPropertyModel] where the completion is performed on an inner [JsonProperty], including if the completion is on the
* value side of the inner [JsonProperty].
*
* In other words, the model of the second [JsonProperty] parent if the element on [CompletionParameters.getPosition] is NOT a
* [JsonProperty].
*
* Or the model of the first [JsonProperty] parent if the element on [CompletionParameters.getPosition] is a [JsonProperty].
*/
fun getModelForCompletionOnInnerJsonProperty(parameters: CompletionParameters): JsonPropertyModel? {
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null
ProgressManager.checkCanceled()
return JsonPropertyModel(parentJsonProperty)
}
}
}

70
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetModel.kt

@ -0,0 +1,70 @@
/*
* 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.provider.model
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.android.tools.compose.code.completion.constraintlayout.getJsonPropertyParent
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.json.psi.JsonProperty
import com.intellij.json.psi.JsonStringLiteral
import com.intellij.openapi.progress.ProgressManager
/**
* Model for the JSON block corresponding to a single ConstraintSet.
*
* A ConstraintSet is a state that defines a specific layout of the contents in a ConstraintLayout.
*/
internal class ConstraintSetModel(jsonProperty: JsonProperty) : JsonPropertyModel(jsonProperty) {
/**
* List of properties that have a constraint block assigned to it.
*/
private val propertiesWithConstraints = innerProperties.filter { it.name != KeyWords.Extends }
/**
* Name of the ConstraintSet this is extending constraints from.
*/
val extendsFrom: String? = (findProperty(KeyWords.Extends)?.value as? JsonStringLiteral)?.value
/**
* List of IDs declared in this ConstraintSet.
*/
val declaredIds = propertiesWithConstraints.map { it.name }
/**
* The constraints (by widget ID) explicitly declared in this ConstraintSet.
*
* Note that it does not resolve constraints inherited from [extendsFrom].
*/
val constraintsById: Map<String, ConstraintsModel> =
propertiesWithConstraints.associate { property ->
property.name to ConstraintsModel(property)
}
// TODO(b/207030860): Add a method that can pull all resolved constraints for each widget ID, it could be useful to make sure we are not
// offering options that are implicitly present from the 'Extends' ConstraintSet
companion object {
/**
* Returns a [ConstraintSetModel], for when the completion is performed on a property or the value of a property within a ConstraintSet
* declaration.
*/
fun getModelForCompletionOnConstraintSetProperty(parameters: CompletionParameters): ConstraintSetModel? {
val parentJsonProperty = getJsonPropertyParent(parameters) ?: return null
ProgressManager.checkCanceled()
return ConstraintSetModel(parentJsonProperty)
}
}
}

66
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintSetsPropertyModel.kt

@ -0,0 +1,66 @@
/*
* 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.provider.model
import com.android.tools.compose.code.completion.constraintlayout.KeyWords
import com.intellij.json.psi.JsonProperty
/**
* Model for the `ConstraintSets` Json block.
*
* The `ConstraintSets` Json block, is a collection of different ConstraintSets, each of which describes a state of the layout by defining
* properties of each of its widgets such as width, height or their layout constraints.
*
* @param constraintSetsElement The PSI element of the `ConstraintSets` Json property
*/
internal class ConstraintSetsPropertyModel(
constraintSetsElement: JsonProperty
) : JsonPropertyModel(constraintSetsElement) {
// TODO(b/209839226): Explore how we could use these models to validate the syntax or structure of the JSON as well as to check logic
// correctness through Inspections/Lint
/**
* List of all ConstraintSet elements in the Json block.
*/
val constraintSets: List<ConstraintSetModel> = innerProperties.map { ConstraintSetModel(it) }
/**
* The names of all ConstraintSets in this block.
*/
fun getConstraintSetNames(): Collection<String> {
return declaredFieldNamesSet
}
/**
* Returns the remaining possible fields for the given [constraintSetName], this is done by reading all fields in all ConstraintSets and
* subtracting the fields already present in [constraintSetName]. Most of these should be the IDs that represent constrained widgets.
*/
fun getRemainingFieldsForConstraintSet(constraintSetName: String): List<String> {
val availableNames = mutableSetOf(KeyWords.Extends)
val usedNames = mutableSetOf<String>()
constraintSets.forEach { constraintSet ->
constraintSet.declaredFieldNamesSet.forEach { propertyName ->
if (constraintSet.name == constraintSetName) {
usedNames.add(propertyName)
}
else {
availableNames.add(propertyName)
}
}
}
availableNames.removeAll(usedNames)
return availableNames.toList()
}
}

29
idea-plugin/src/main/kotlin/com/android/tools/compose/code/completion/constraintlayout/provider/model/ConstraintsModel.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.code.completion.constraintlayout.provider.model
import com.intellij.json.psi.JsonProperty
/**
* Model for the JSON block that corresponds to the constraints applied on a widget (defined by an ID).
*
* Constraints are a set of instructions that define the widget's dimensions, position with respect to other widgets and render-time
* transforms.
*/
internal class ConstraintsModel(jsonProperty: JsonProperty): JsonPropertyModel(jsonProperty) {
// TODO(b/207030860): Fill the contents of this model as is necessary, keeping in mind that it would be useful to have fields like
// 'verticalConstraints', 'hasBaseline', 'dimensionBehavior', etc...
}

53
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/CompletionResultSetUtils.kt

@ -0,0 +1,53 @@
/*
* Copyright (C) 2022 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.completion
import com.android.tools.compose.completion.inserthandler.FormatWithCaretInsertHandler
import com.android.tools.compose.completion.inserthandler.FormatWithLiveTemplateInsertHandler
import com.android.tools.compose.completion.inserthandler.FormatWithNewLineInsertHandler
import com.android.tools.compose.completion.inserthandler.InsertionFormat
import com.android.tools.compose.completion.inserthandler.LiteralNewLineFormat
import com.android.tools.compose.completion.inserthandler.LiteralWithCaretFormat
import com.android.tools.compose.completion.inserthandler.LiveTemplateFormat
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElementBuilder
/**
* Utility function to simplify adding [com.intellij.codeInsight.lookup.LookupElement]s with [InsertionFormat] support.
*
* Note that the added lookup element is case-sensitive.
*
* @param lookupString The base text to autocomplete, also used to match the user input with a completion result.
* @param tailText Grayed out text shown after the LookupElement name, not part of the actual completion.
* @param format InsertionFormat to handle the rest of the completion. See different implementations of [InsertionFormat] for more.
*/
fun CompletionResultSet.addLookupElement(lookupString: String, tailText: String? = null, format: InsertionFormat? = null) {
// Populate the lookupObject param to allow multiple LookupElements with the same lookupString, differentiated by the tailText.
var lookupBuilder = LookupElementBuilder.create(tailText ?: lookupString, lookupString)
if (format != null) {
val insertionHandler = when (format) {
is LiteralWithCaretFormat -> FormatWithCaretInsertHandler(format)
is LiteralNewLineFormat -> FormatWithNewLineInsertHandler(format)
is LiveTemplateFormat -> FormatWithLiveTemplateInsertHandler(format)
}
lookupBuilder = lookupBuilder.withInsertHandler(insertionHandler)
}
lookupBuilder = lookupBuilder.withCaseSensitivity(true)
if (tailText != null) {
lookupBuilder = lookupBuilder.withTailText(tailText, true)
}
addElement(lookupBuilder)
}

46
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithCaretInsertHandler.kt

@ -0,0 +1,46 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.editor.EditorModificationUtil
import com.intellij.openapi.editor.actions.EditorActionUtil
import com.intellij.psi.PsiDocumentManager
/**
* Handles insertions of an [InsertionFormat], moving the caret at the position specified by the '|' character.
*/
class FormatWithCaretInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
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)
}
}
}
}

86
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithLiveTemplateInsertHandler.kt

@ -0,0 +1,86 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.daemon.impl.quickfix.EmptyExpression
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.ConstantNode
/**
* Handles insertions of an [InsertionFormat] using the [TemplateManager], stopping at every '<>' for user input.
*/
class FormatWithLiveTemplateInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val templateManager = TemplateManager.getInstance(context.project)
val template = templateManager.createTemplate("", "")
// Create template from the given format
getTemplateSegments(format).forEach { segment ->
val text = segment.textSegment
if (segment.takesUserInput) {
if (text.isNotEmpty()) {
template.addVariable(ConstantNode(text), true)
}
else {
template.addVariable(EmptyExpression(), true)
}
}
else {
template.addTextSegment(text)
}
}
templateManager.startTemplate(context.editor, template)
}
}
/**
* Extracts the insertable text segments from the [InsertionFormat] indicating whether each segment is simple text or if it expects user
* input.
*/
private fun getTemplateSegments(format: InsertionFormat): List<LiveTemplateSegment> {
val segments = mutableListOf<LiveTemplateSegment>()
val templateText = format.insertableString
var start = 0
var end = 0
// Normal text does not take user input
var isNormalText = true
while (end < templateText.length) {
val currentChar = templateText.elementAtOrNull(end)
if (currentChar == '<' || currentChar == '>') {
// Stop at the marker characters and add any pending segment
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end)))
isNormalText = currentChar == '>'
start = end + 1 // update start but skip this char
}
end++
}
if (end - start > 1) {
// Add the last segment if not empty (end index is exclusive)
segments.add(LiveTemplateSegment(takesUserInput = !isNormalText, templateText.substring(start, end)))
}
return segments
}
private data class LiveTemplateSegment(
val takesUserInput: Boolean,
val textSegment: String
)

58
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/FormatWithNewLineInsertHandler.kt

@ -0,0 +1,58 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
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
/**
* Handles insertions of an [InsertionFormat], applying new a line at the `\n` character.
*
* Applies the new line with [IdeActions.ACTION_EDITOR_ENTER] and moves the caret at the end of the new line.
*/
class FormatWithNewLineInsertHandler(private val format: InsertionFormat) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
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)
)
}
}
}

50
idea-plugin/src/main/kotlin/com/android/tools/compose/completion/inserthandler/InsertionFormat.kt

@ -0,0 +1,50 @@
/*
* Copyright (C) 2022 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.completion.inserthandler
/**
* Describes a string that may be automatically inserted when selecting an autocomplete option.
*/
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.
*/
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.
*/
class LiteralNewLineFormat(literalFormat: String) : InsertionFormat(literalFormat)
/**
* Inserts a string driven by Live templates. The string is inserted after the auto-completed value.
*
* Use '<' and '>' to delimit a range of text the user is expected to edit, may contain multiple instances of these delimiters.
*
* Eg: For the string `"<0123>, <text>"`. The '0123' will be selected in the editor for the user to modify, once they press Enter, it
* will select 'text' for the user to modify until all marked snippets of the strings are handled or the user presses ESC to keep the text
* as is.
*/
class LiveTemplateFormat(templateFormat: String) : InsertionFormat(templateFormat)

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;
}
}

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

@ -0,0 +1,150 @@
/*
* 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.MultiRequestPositionManager
import com.intellij.debugger.NoDataException
import com.intellij.debugger.SourcePosition
import com.intellij.debugger.engine.DebugProcess
import com.intellij.debugger.engine.DebugProcessImpl
import com.intellij.debugger.engine.PositionManagerWithMultipleStackFrames
import com.intellij.debugger.engine.evaluation.EvaluationContext
import com.intellij.debugger.jdi.StackFrameProxyImpl
import com.intellij.debugger.requests.ClassPrepareRequestor
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileTypes.FileType
import com.intellij.util.ThreeState
import com.intellij.xdebugger.frame.XStackFrame
import com.sun.jdi.Location
import com.sun.jdi.ReferenceType
import com.sun.jdi.request.ClassPrepareRequest
import org.jetbrains.kotlin.fileClasses.JvmFileClassUtil
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.debugger.KotlinPositionManager
import org.jetbrains.kotlin.load.kotlin.PackagePartClassUtils
import org.jetbrains.kotlin.psi.KtFile
/**
* 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.core.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.core.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.isModifierChainLongerThanTwo
import com.android.tools.compose.settings.ComposeCustomCodeStyleSettings
import com.android.tools.modules.*
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 &&
psiElement.inComposeModule() &&
!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)
}
}

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

@ -0,0 +1,113 @@
/*
* 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
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
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, it)) }
// 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
}
}

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

@ -0,0 +1,81 @@
/*
* 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_FQ_NAMES
import com.android.tools.compose.COMPOSE_PREVIEW_ANNOTATION_FQN
import com.android.tools.compose.ComposeBundle
import com.android.tools.compose.isComposableAnnotation
import com.android.tools.compose.fqNameMatches
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("@${COMPOSE_PREVIEW_ANNOTATION_FQN}")
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)
}
}
}
}

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

@ -0,0 +1,215 @@
/*
* 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.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.prevLeaf
import com.intellij.refactoring.suggested.endOffset
import com.intellij.refactoring.suggested.startOffset
import com.intellij.ui.popup.list.ListPopupImpl
import org.jetbrains.kotlin.idea.core.util.CodeInsightUtils
import org.jetbrains.kotlin.idea.util.ElementKind
import org.jetbrains.kotlin.idea.util.findElements
import org.jetbrains.kotlin.idea.util.isLineBreak
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedFunction
/**
* 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")
}
/**
* Finds the first [KtCallExpression] at the given offset stopping if it finds any [KtNamedFunction] so it does not
* exit the `Composable`.
*/
private fun PsiFile.findParentCallExpression(offset: Int): PsiElement? =
PsiTreeUtil.findElementOfClassAtOffsetWithStopSet(this, offset, KtCallExpression::class.java,
false, KtNamedFunction::class.java)
/**
* Finds the nearest surroundable [PsiElement] starting at the given offset and looking at the parents. If the offset is at
* the end of a line, this method might look in the immediately previous offset.
*/
private fun findNearestSurroundableElement(file: PsiFile, offset: Int): PsiElement? {
val nearestElement = file.findElementAt(offset)?.let {
if (it.isLineBreak()) {
file.findParentCallExpression(it.prevLeaf(true)?.startOffset ?: (offset - 1))
}
else it
} ?: return null
return file.findParentCallExpression(nearestElement.startOffset)
}
/**
* Finds the [TextRange] to surround based on the current [editor] selection. It returns null if there is no block that
* can be selected.
*/
fun findSurroundingSelectionRange(file: PsiFile, editor: Editor): TextRange? {
if (!editor.selectionModel.hasSelection()) return null
// We try to select full call elements to avoid the selection falling in the middle of, for example, a string.
// This way, selecting the middle of two strings would still wrap the parent calls like for the following example:
//
// Text("Hello <selection>world!")
// Button(...)
// Text("By</selection>e")
//
// Would wrap the three elements instead of just the Button.
val startSelectionOffset = findNearestSurroundableElement(file, editor.selectionModel.selectionStart)?.startOffset ?: Int.MAX_VALUE
val endSelectionOffset = findNearestSurroundableElement(file, editor.selectionModel.selectionEnd)?.endOffset ?: -1
val statements = findElements(file,
minOf(editor.selectionModel.selectionStart, startSelectionOffset),
maxOf(editor.selectionModel.selectionEnd, endSelectionOffset),
ElementKind.EXPRESSION)
.filter { it.isInsideComposableCode() }
if (statements.isNotEmpty()) {
return TextRange.create(statements.minOf { it.startOffset }, statements.maxOf { it.endOffset })
}
return null
}
/**
* 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
private fun findSurroundableRange(file: PsiFile, editor: Editor): TextRange? = if (editor.selectionModel.hasSelection()) {
findSurroundingSelectionRange(file, editor)
}
else {
findNearestSurroundableElement(file, editor.caretModel.offset)?.textRange
}
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = when {
!StudioFlags.COMPOSE_EDITOR_SUPPORT.get() -> false
file == null || editor == null -> false
!file.isWritable || file !is KtFile -> false
else -> findSurroundableRange(file, editor) != null
}
protected abstract fun getTemplate(): TemplateImpl?
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
if (editor == null || file == null) return
val surroundRange = findSurroundableRange(file, editor) ?: return
// Extend the selection if it does not match the inferred range
if (editor.selectionModel.selectionStart != surroundRange.startOffset ||
editor.selectionModel.selectionEnd != surroundRange.endOffset) {
editor.selectionModel.setSelection(surroundRange.startOffset, surroundRange.endOffset)
}
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))
}
}

32
idea-plugin/src/main/kotlin/com/android/tools/compose/settings/ComposeCustomCodeStyleSettings.java

@ -0,0 +1,32 @@
/*
* 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.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CustomCodeStyleSettings;
// Don't convert to Kotlin due to the serialization implementation for Settings.
public class ComposeCustomCodeStyleSettings extends CustomCodeStyleSettings {
public boolean USE_CUSTOM_FORMATTING_FOR_MODIFIERS = true;
protected ComposeCustomCodeStyleSettings(CodeStyleSettings container) {
super("ComposeCustomCodeStyleSettings", container);
}
public static ComposeCustomCodeStyleSettings getInstance(CodeStyleSettings settings) {
return settings.getCustomSettings(ComposeCustomCodeStyleSettings.class);
}
}

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()
}
}

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

@ -0,0 +1,23 @@
/*
* 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)
val COMPOSE_CONSTRAINTLAYOUT_COMPLETION = Flag(true)
val SAMPLES_SUPPORT_ENABLED = Flag(true)
}
class Flag<T>(val value: T) {
fun get(): T = value
}

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

@ -0,0 +1,28 @@
/*
* 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.android.tools.compose.*
import com.intellij.openapi.module.*
import com.intellij.openapi.roots.*
import com.intellij.psi.*
import com.intellij.psi.search.*
import com.intellij.psi.util.*
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
fun PsiElement.inComposeModule() = module?.isComposeModule() ?: false
fun Module.isComposeModule(): Boolean {
return CachedValuesManager.getManager(project).getCachedValue(this) {
val scope = GlobalSearchScope.moduleWithDependenciesAndLibrariesScope(this)
val hasComposable = COMPOSABLE_FQ_NAMES.any {
KotlinFullClassNameIndex.get(it, project, scope).any()
}
val rootModificationTracker = ProjectRootModificationTracker.getInstance(project)
CachedValueProvider.Result.create(hasComposable, rootModificationTracker)
}
}

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)
}
}
}

4
idea-plugin/src/main/kotlin/org/jetbrains/compose/web/ide/run/psiUtils.kt

@ -8,9 +8,9 @@ package org.jetbrains.compose.web.ide.run
import com.intellij.psi.PsiElement
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.base.facet.platform.platform
import org.jetbrains.kotlin.idea.base.util.module
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.project.platform
import org.jetbrains.kotlin.idea.util.module
import org.jetbrains.kotlin.platform.js.JsPlatforms
import org.jetbrains.kotlin.psi.KtNamedFunction
import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode

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

@ -7,14 +7,12 @@
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>
<!-- until-build is omitted, so we don't have to update it after each Intellij release -->
<idea-version since-build="213"/>
<idea-version since-build="231"/>
<!-- Product and plugin compatibility requirements -->
<!-- https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html -->
@ -23,7 +21,109 @@
<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"/>
<irGenerationExtension implementation="com.android.tools.compose.ComposePluginIrGenerationExtension"/>
<highlighterExtension implementation="com.android.tools.compose.ComposableHighlighter"/>
</extensions>
<extensions defaultExtensionNs="org.jetbrains.kotlin.extensions.internal">
<typeResolutionInterceptorExtension implementation="com.android.tools.compose.ComposePluginTypeResolutionInterceptorExtension"/>
</extensions>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceInterface="org.jetbrains.kotlin.idea.kdoc.KDocLinkResolutionService"
serviceImplementation="com.android.tools.compose.ComposeKDocLinkResolutionService"
overrides="true"
/>
<dependencySupport coordinate="androidx.compose.runtime:runtime" kind="java" displayName="Jetpack Compose"/>
<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"/>
@ -39,9 +139,7 @@
factoryClass="org.jetbrains.compose.desktop.ide.preview.PreviewToolWindow"
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