diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/BUILD.bazel b/plugins/compose/intellij.compose.ide.plugin.k2/BUILD.bazel
index cc3b68a8e1cc..a99ae6946324 100644
--- a/plugins/compose/intellij.compose.ide.plugin.k2/BUILD.bazel
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/BUILD.bazel
@@ -29,6 +29,8 @@ jvm_library(
"//plugins/kotlin/highlighting/highlighting-k2:kotlin-highlighting-k2",
"//platform/util",
"//platform/analysis-impl",
+ "//plugins/kotlin/refactorings/kotlin.refactorings.common:kotlin-refactorings-common",
+ "//plugins/kotlin/refactorings/kotlin.refactorings.k2:kotlin-refactorings-k2",
"@lib//:kotlinc-kotlin-compiler-common-provided",
"@lib//:kotlinc-analysis-api-provided",
"//plugins/kotlin/base/analysis-api/analysis-api-utils:kotlin-base-analysis-api-utils",
@@ -58,7 +60,12 @@ jvm_library(
deps = [
"//plugins/compose/intellij.compose.ide.plugin.shared:ide-plugin-shared",
"//plugins/compose/intellij.compose.ide.plugin.shared:ide-plugin-shared-tests_test_lib",
+ "//java/testFramework",
+ "//plugins/kotlin/refactorings/kotlin.refactorings.k2:kotlin-refactorings-k2",
+ "//plugins/kotlin/refactorings/kotlin.refactorings.common:kotlin-refactorings-common",
+ "//platform/refactoring",
"//platform/core-api:core",
+ "//platform/core-ui",
"//platform/editor-ui-api:editor-ui",
"//plugins/kotlin/base/test:test_test_lib",
"//plugins/kotlin/base/plugin",
diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.iml b/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.iml
index 9a05e2b12e12..c47adf576d96 100644
--- a/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.iml
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.iml
@@ -33,6 +33,8 @@
+
+
diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.tests.iml b/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.tests.iml
index 5290edb4567f..1c14064cd0a0 100644
--- a/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.tests.iml
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/intellij.compose.ide.plugin.k2.tests.iml
@@ -26,7 +26,12 @@
+
+
+
+
+
diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/resources/intellij.compose.ide.plugin.k2.xml b/plugins/compose/intellij.compose.ide.plugin.k2/resources/intellij.compose.ide.plugin.k2.xml
index 377f1267708e..0fcff01ec057 100644
--- a/plugins/compose/intellij.compose.ide.plugin.k2/resources/intellij.compose.ide.plugin.k2.xml
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/resources/intellij.compose.ide.plugin.k2.xml
@@ -1,12 +1,15 @@
+
+
diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/src/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdder.kt b/plugins/compose/intellij.compose.ide.plugin.k2/src/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdder.kt
new file mode 100644
index 000000000000..db1e812cc142
--- /dev/null
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/src/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdder.kt
@@ -0,0 +1,20 @@
+package com.intellij.compose.ide.plugin.k2
+
+import com.intellij.compose.ide.plugin.shared.COMPOSABLE_ANNOTATION_FQ_NAME
+import com.intellij.compose.ide.plugin.shared.isFunctionExtractionInComposableControlFlow
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractFunctionDescriptorModifier
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractableCodeDescriptor
+
+/**
+ * Responsible for adding `@Composable` annotation to functions extracted from inside Composable control flow.
+ *
+ * For details see: [com.intellij.compose.ide.plugin.shared.isFunctionExtractionInComposableControlFlow]
+ */
+internal class K2ComposableAnnotationToExtractedFunctionAdder : ExtractFunctionDescriptorModifier {
+ override fun modifyDescriptor(descriptor: ExtractableCodeDescriptor): ExtractableCodeDescriptor =
+ if (composableAnnotationText !in descriptor.annotationsText && descriptor.extractionData.isFunctionExtractionInComposableControlFlow())
+ descriptor.copy(renderedAnnotations = descriptor.renderedAnnotations + "$composableAnnotationText ")
+ else descriptor
+}
+
+private val composableAnnotationText = "@${COMPOSABLE_ANNOTATION_FQ_NAME.asString()}"
\ No newline at end of file
diff --git a/plugins/compose/intellij.compose.ide.plugin.k2/tests/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdderTest.kt b/plugins/compose/intellij.compose.ide.plugin.k2/tests/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdderTest.kt
new file mode 100644
index 000000000000..b458f91dc0b1
--- /dev/null
+++ b/plugins/compose/intellij.compose.ide.plugin.k2/tests/com/intellij/compose/ide/plugin/k2/K2ComposableAnnotationToExtractedFunctionAdderTest.kt
@@ -0,0 +1,111 @@
+package com.intellij.compose.ide.plugin.k2
+
+import com.intellij.compose.ide.plugin.shared.ComposableAnnotationToExtractedFunctionAdderTest
+import com.intellij.openapi.application.Application
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture
+import org.jetbrains.kotlin.analysis.api.permissions.KaAllowAnalysisOnEdt
+import org.jetbrains.kotlin.analysis.api.permissions.allowAnalysisOnEdt
+import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractableCodeDescriptor
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractableCodeDescriptorWithConflicts
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractionGeneratorConfiguration
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.ExtractionResult
+import org.jetbrains.kotlin.idea.k2.refactoring.extractFunction.KotlinFirExtractFunctionHandler
+import org.jetbrains.kotlin.idea.k2.refactoring.introduce.extractionEngine.ExtractionEngineHelper
+import org.jetbrains.kotlin.idea.k2.refactoring.introduceConstant.INTRODUCE_CONSTANT
+import org.jetbrains.kotlin.idea.k2.refactoring.introduceConstant.KotlinIntroduceConstantHandler
+import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.EXTRACT_FUNCTION
+import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.ExtractionGeneratorOptions
+import org.jetbrains.kotlin.name.FqName
+import java.util.concurrent.Semaphore
+
+class K2ComposableAnnotationToExtractedFunctionAdderTest : ComposableAnnotationToExtractedFunctionAdderTest() {
+ override val pluginMode: KotlinPluginMode get() = KotlinPluginMode.K2
+
+ override fun JavaCodeInsightTestFixture.invokeExtractFunctionIn(application: Application, existingAnnotationFqNames: List) {
+ val helper = ExtractionHelper(existingAnnotationFqNames)
+ application.invokeAndWait {
+ KotlinFirExtractFunctionHandler(helper = helper)
+ .invoke(this.project, this.editor, this.file!!, null)
+ }
+ helper.waitUntilFinished()
+ }
+
+ override fun JavaCodeInsightTestFixture.invokeExtractConstantIn(application: Application) {
+ val helper = InteractiveExtractionHelper()
+ application.invokeAndWait {
+ KotlinIntroduceConstantHandler(helper = helper)
+ .invoke(this.project, this.editor, this.file!!, null)
+ }
+ helper.waitUntilFinished()
+ }
+
+}
+
+// Following 2 are heavily inspired/copied from AOSP
+private class ExtractionHelper(private val existingAnnotationFqNames: List) : ExtractionEngineHelper(EXTRACT_FUNCTION) {
+ private val finishedSemaphore = Semaphore(0)
+
+ fun waitUntilFinished() {
+ finishedSemaphore.acquire()
+ }
+
+ @OptIn(KaAllowAnalysisOnEdt::class)
+ override fun configureAndRun(
+ project: Project,
+ editor: Editor,
+ descriptorWithConflicts: ExtractableCodeDescriptorWithConflicts,
+ onFinish: (ExtractionResult) -> Unit,
+ ) {
+ // `descriptorWithConflicts.descriptor.copy(..)` runs the constructor of
+ // ExtractableCodeDescriptor
+ // that calls AA for return type check. Since the copied one has the exactly same type as the
+ // initial descriptor, if we only copy the boolean value, we do not need `allowAnalysisOnEdt`.
+ // To do so, we have to update `ExtractableCodeDescriptor`.
+ allowAnalysisOnEdt {
+ val newDescriptor =
+ descriptorWithConflicts.descriptor.copy(
+ suggestedNames = listOf("newFunction"),
+ renderedAnnotations = existingAnnotationFqNames.map { "@${it.asString()}" } +
+ descriptorWithConflicts.descriptor.renderedAnnotations
+ )
+ doRefactor(
+ ExtractionGeneratorConfiguration(newDescriptor, ExtractionGeneratorOptions.DEFAULT)
+ ) { er: ExtractionResult ->
+ onFinish(er)
+ finishedSemaphore.release()
+ }
+ }
+ }
+}
+
+private class InteractiveExtractionHelper : ExtractionEngineHelper(INTRODUCE_CONSTANT) {
+ private val finishedSemaphore = Semaphore(0)
+
+ fun waitUntilFinished() {
+ finishedSemaphore.acquire()
+ }
+
+ override fun validate(
+ descriptor: ExtractableCodeDescriptor
+ ): ExtractableCodeDescriptorWithConflicts =
+ KotlinIntroduceConstantHandler.InteractiveExtractionHelper.validate(descriptor)
+
+ override fun configureAndRun(
+ project: Project,
+ editor: Editor,
+ descriptorWithConflicts: ExtractableCodeDescriptorWithConflicts,
+ onFinish: (ExtractionResult) -> Unit,
+ ) {
+ KotlinIntroduceConstantHandler.InteractiveExtractionHelper.configureAndRun(
+ project,
+ editor,
+ descriptorWithConflicts,
+ ) { er: ExtractionResult ->
+ onFinish(er)
+ finishedSemaphore.release()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/BUILD.bazel b/plugins/compose/intellij.compose.ide.plugin.shared/BUILD.bazel
index d47a8b5a8582..158f3b903517 100644
--- a/plugins/compose/intellij.compose.ide.plugin.shared/BUILD.bazel
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/BUILD.bazel
@@ -1,6 +1,12 @@
### auto-generated section `build intellij.compose.ide.plugin.shared` start
+load("//build:compiler-options.bzl", "create_kotlinc_options")
load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_resources")
+create_kotlinc_options(
+ name = "custom",
+ context_receivers = True
+)
+
jvm_resources(
name = "ide-plugin-shared_resources",
files = glob(["resources/**/*"]),
@@ -12,6 +18,7 @@ jvm_library(
module_name = "intellij.compose.ide.plugin.shared",
visibility = ["//visibility:public"],
srcs = glob(["src/**/*.kt", "src/**/*.java"], allow_empty = True),
+ kotlinc_opts = ":custom",
deps = [
"//plugins/compose:compose-ide-plugin",
"//platform/analysis-api:analysis",
@@ -28,6 +35,7 @@ jvm_library(
"//uast/uast-common:uast",
"//platform/util",
"//plugins/kotlin/base/indices",
+ "//plugins/kotlin/refactorings/kotlin.refactorings.common:kotlin-refactorings-common",
"@lib//:kotlinc-kotlin-compiler-common-provided",
"@lib//:kotlinc-analysis-api-provided",
"//java/java-psi-api:psi",
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/intellij.compose.ide.plugin.shared.iml b/plugins/compose/intellij.compose.ide.plugin.shared/intellij.compose.ide.plugin.shared.iml
index 3ecd947c996e..426587d6d0f9 100644
--- a/plugins/compose/intellij.compose.ide.plugin.shared/intellij.compose.ide.plugin.shared.iml
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/intellij.compose.ide.plugin.shared.iml
@@ -1,5 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -23,6 +39,7 @@
+
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderUtil.kt b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderUtil.kt
new file mode 100644
index 000000000000..99924d813784
--- /dev/null
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderUtil.kt
@@ -0,0 +1,15 @@
+package com.intellij.compose.ide.plugin.shared
+
+import com.intellij.psi.PsiElement
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.IExtractionData
+
+/** Checks whether [this] represents the intention to extract a function inside a Composable control flow */
+@ApiStatus.Internal
+fun IExtractionData.isFunctionExtractionInComposableControlFlow(): Boolean =
+ !options.extractAsProperty // We only care for function extractions!
+ && composeIsEnabledInModuleOf(commonParent) // Cheap check for performance
+ && commonParent.isInsideComposableControlFlow()
+
+private fun composeIsEnabledInModuleOf(element: PsiElement): Boolean =
+ element.module.let { it != null && isComposeEnabledInModule(it) }
\ No newline at end of file
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposeLibraryNamespace.kt b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposeLibraryNamespace.kt
index f842b0d77072..1297b587dace 100644
--- a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposeLibraryNamespace.kt
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/ComposeLibraryNamespace.kt
@@ -11,6 +11,7 @@ val COMPOSABLE_ANNOTATION_CLASS_ID: ClassId = ClassId.topLevel(COMPOSABLE_ANNOTA
val COMPOSE_MODIFIER_NAME: Name = Name.identifier("Modifier")
val COMPOSE_MODIFIER_FQN: FqName = FqName("androidx.compose.ui.$COMPOSE_MODIFIER_NAME")
val COMPOSE_MODIFIER_CLASS_ID: ClassId = ClassId.topLevel(COMPOSE_MODIFIER_FQN)
+val DISALLOW_COMPOSABLE_CALLS_FQ_NAME: FqName = FqName("androidx.compose.runtime.DisallowComposableCalls")
val PREVIEW_CLASS_NAME: Name = Name.identifier("Preview")
val PREVIEW_PARAMETER_CLASS_NAME: Name = Name.identifier("PreviewParameter")
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/IsComposableControlFlowUtil.kt b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/IsComposableControlFlowUtil.kt
new file mode 100644
index 000000000000..393c70d70fc8
--- /dev/null
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/IsComposableControlFlowUtil.kt
@@ -0,0 +1,96 @@
+package com.intellij.compose.ide.plugin.shared
+
+import com.intellij.psi.PsiElement
+import org.jetbrains.kotlin.analysis.api.KaSession
+import org.jetbrains.kotlin.analysis.api.analyze
+import org.jetbrains.kotlin.analysis.api.annotations.KaAnnotated
+import org.jetbrains.kotlin.analysis.api.resolution.KaCallInfo
+import org.jetbrains.kotlin.analysis.api.resolution.singleConstructorCallOrNull
+import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull
+import org.jetbrains.kotlin.analysis.api.resolution.symbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaNamedFunctionSymbol
+import org.jetbrains.kotlin.analysis.api.symbols.KaValueParameterSymbol
+import org.jetbrains.kotlin.name.FqName
+import org.jetbrains.kotlin.psi.*
+import org.jetbrains.kotlin.psi.psiUtil.getAnnotationEntries
+
+/**
+ * Checks whether [this] is inside a Composable control flow.
+ * Examples:
+ * ```kt
+ * fun bar() {
+ * somePsiElement // <-- Will be false for this element. Because it would be a compiler error to call @Composable functions here.
+ * }
+ *
+ * @Composable fun foo() {
+ * somePsiElement // <-- Will be true for this element. Because it's legal to call @Composable functions here.
+ * }
+ * ```
+ * For detailed behavior, see: [com.intellij.compose.ide.plugin.shared.ComposableAnnotationToExtractedFunctionAddingAnalyserTest]
+ */
+internal fun PsiElement?.isInsideComposableControlFlow(): Boolean = when (this) {
+ null -> false // Reached root parent
+ is KtPropertyAccessor, is KtNamedFunction -> hasComposableAnnotation()
+ is KtLambdaExpression -> analyze(this) { ownsComposableControlFlow() }
+ is KtLambdaArgument -> analyze(this) { ownsComposableControlFlow() }
+ else -> parent.isInsideComposableControlFlow()
+}
+
+context(KaSession)
+private fun KtLambdaExpression.ownsComposableControlFlow(): Boolean =
+ (parent as? KtLambdaArgument)?.ownsComposableControlFlow() // Only check lambda that is NOT an argument
+ // One would simply expect us to infer the type and then check whether the type has a `@Composable` annotation.
+ // But currently, annotations are not inferred. See https://jetbrains.slack.com/archives/C061DS4G41J/p1738180610032509
+ ?: getAnnotationEntries().containsAnnotationNamed(COMPOSABLE_ANNOTATION_FQ_NAME)
+
+context(KaSession)
+private fun KtLambdaArgument.ownsComposableControlFlow(): Boolean =
+ (parent as? KtCallExpression)
+ ?.resolveToCall()
+ .let { functionCall -> functionCall != null && this.ownsComposableControlFlowWhenArgumentOf(functionCall) }
+
+context(KaSession)
+private fun KtLambdaArgument.ownsComposableControlFlowWhenArgumentOf(functionCall: KaCallInfo): Boolean =
+ this.hasAnnotationIn(functionCall, COMPOSABLE_ANNOTATION_FQ_NAME) || // We are explicitly in Composable flow!
+ (
+ !this.hasAnnotationIn(functionCall, DISALLOW_COMPOSABLE_CALLS_FQ_NAME) // If our flow is not explicitly forbidding Composable calls,
+ && this.isInlinedInside(functionCall) // and we are in an inlined lambda,
+ && parent.isInsideComposableControlFlow() // then check whether the parent is in Composable flow
+ )
+
+private fun KtLambdaArgument.hasAnnotationIn(function: KaCallInfo, fqName: FqName): Boolean = analyze(this) {
+ function.parameterSymbolOf(this@hasAnnotationIn)
+ ?.returnType
+ ?.isAnnotatedWith(fqName) // Not cached, but doesn't matter for this infrequently triggered extension
+ ?: false
+}
+
+private fun KaAnnotated.isAnnotatedWith(fqName: FqName): Boolean =
+ annotations.any { it.classId?.asSingleFqName() == fqName }
+
+context(KaSession)
+private fun KtLambdaArgument.isInlinedInside(function: KaCallInfo): Boolean =
+ function.isInline() &&
+ function.parameterSymbolOf(this@isInlinedInside)
+ ?.let { !it.isNoinline && !it.isCrossinline }
+ ?: true
+
+private fun KaCallInfo.parameterSymbolOf(argument: KtLambdaArgument): KaValueParameterSymbol? =
+ argument.getArgumentExpression()?.let { expression ->
+ successfulFunctionCallOrNull()
+ ?.argumentMapping
+ ?.get(expression)
+ ?.symbol
+ }
+
+private fun KaCallInfo.isInline(): Boolean =
+ successfulFunctionCallOrNull()
+ ?.symbol
+ ?.let { it as? KaNamedFunctionSymbol }
+ ?.isInline
+ ?: false
+
+/** Not cached, but doesn't matter for this infrequently triggered extension */
+context(KaSession)
+private fun Iterable.containsAnnotationNamed(fqName: FqName): Boolean =
+ any { it.resolveToCall()?.singleConstructorCallOrNull()?.symbol?.containingClassId?.asSingleFqName() == fqName }
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/PsiUtils.kt b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/PsiUtils.kt
index 04a8efbfbbdb..724b14b554e3 100644
--- a/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/PsiUtils.kt
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/src/com/intellij/compose/ide/plugin/shared/PsiUtils.kt
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.compose.ide.plugin.shared
+import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.roots.ProjectRootModificationTracker
import com.intellij.openapi.roots.impl.ProjectFileIndexFacade
@@ -57,7 +58,13 @@ fun isComposeEnabledForElementModule(element: PsiElement): Boolean {
}
internal fun PsiElement.isComposableFunction(): Boolean =
- (this as? KtNamedFunction)?.getAnnotationWithCaching(COMPOSABLE_FUNCTION_KEY) { it.isComposableAnnotation() } != null
+ this is KtNamedFunction && this.hasComposableAnnotation()
+
+internal fun KtAnnotated.hasComposableAnnotation(): Boolean =
+ this.getAnnotationWithCaching(COMPOSABLE_FUNCTION_KEY) { it.isComposableAnnotation() } != null
+
+internal val PsiElement.module: Module?
+ get() = ModuleUtilCore.findModuleForPsiElement(this)
private val COMPOSABLE_FUNCTION_KEY: Key> =
Key.create("com.intellij.compose.ide.plugin.shared.isComposableFunction")
diff --git a/plugins/compose/intellij.compose.ide.plugin.shared/tests/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderTest.kt b/plugins/compose/intellij.compose.ide.plugin.shared/tests/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderTest.kt
new file mode 100644
index 000000000000..1399e8652dc0
--- /dev/null
+++ b/plugins/compose/intellij.compose.ide.plugin.shared/tests/com/intellij/compose/ide/plugin/shared/ComposableAnnotationToExtractedFunctionAdderTest.kt
@@ -0,0 +1,1029 @@
+package com.intellij.compose.ide.plugin.shared
+
+import com.intellij.openapi.application.Application
+import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture
+import com.intellij.util.application
+import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
+import org.jetbrains.kotlin.name.FqName
+
+abstract class ComposableAnnotationToExtractedFunctionAdderTest : KotlinLightCodeInsightFixtureTestCase() {
+ override fun runInDispatchThread(): Boolean = false
+
+ /** Copied from AOSP. Regression test for https://issuetracker.google.com/issues/301481575 */
+ fun testConstantInComposableFunction() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun sourceFunction() {
+ print("foo")
+ }
+ """.trimIndent())
+ .invokingExtractConstant()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ private const val string = "foo"
+
+ @Composable
+ fun sourceFunction() {
+ print(string)
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside non-Composable function`() {
+ assertThatGivenComposeProjectWithState("""
+ fun MyComposable() {
+ println("a")
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ fun MyComposable() {
+ newFunction()
+ }
+
+ private fun newFunction() {
+ println("a")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside Composable function`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ newFunction()
+ }
+
+ @Composable
+ private fun newFunction() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside non-Composable property getter`() {
+ assertThatGivenComposeProjectWithState("""
+ val myProp: Int
+ get() {
+ println("a")
+ return 0
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ val myProp: Int
+ get() {
+ newFunction()
+ return 0
+ }
+
+ private fun newFunction() {
+ println("a")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside Composable property getter`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ val myProp: Int
+ @Composable
+ get() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ return 0
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ val myProp: Int
+ @Composable
+ get() {
+ newFunction()
+ return 0
+ }
+
+ @Composable
+ private fun newFunction() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside deeply nested Composable property getter`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ fun f(@Composable block: () -> Unit)
+
+ val myProp: Int
+ @Composable
+ get() {
+ Column {
+ Row {
+ LaunchedEffect(Unit) {
+ f {
+ Text("extract from here")
+ }
+ }
+ }
+ }
+ return 0
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ fun f(@Composable block: () -> Unit)
+
+ val myProp: Int
+ @Composable
+ get() {
+ Column {
+ Row {
+ LaunchedEffect(Unit) {
+ f {
+ newFunction()
+ }
+ }
+ }
+ }
+ return 0
+ }
+
+ private fun newFunction() {
+ Text("extract from here")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside non-inline Composable call with Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ Text("World")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline Composable call with Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ inline fun View(block: @Composable () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ Text("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ inline fun View(block: @Composable () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside non-inline Composable call with non-Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun View(block: () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ println("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun View(block: () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline Composable call with non-Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ inline fun View(block: () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ println("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ inline fun View(block: () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable() {
+ View {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ println("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline non-Composable lambda - inside Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ a?.let {
+ Text("Hello")
+ }
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ a?.let {
+ newFunction()
+ }
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline non-Composable lambda - inside Composable lambda - disallow composable calls`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.DisallowComposableCalls
+
+ inline fun f(block: @DisallowComposableCalls () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ f {
+ println("Hello")
+ }
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.DisallowComposableCalls
+
+ inline fun f(block: @DisallowComposableCalls () -> Unit) = Unit
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ f {
+ newFunction()
+ }
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside 2 inline non-Composable lambdas - inside Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?, b: Int) {
+ Column {
+ a?.let {
+ b.let {
+ Text("Hello")
+ }
+ }
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?, b: Int) {
+ Column {
+ a?.let {
+ b.let {
+ newFunction()
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline non-Composable call with Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ inline fun view(block: @Composable () -> Unit) = Unit
+
+ fun f() {
+ view {
+ Text("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ inline fun view(block: @Composable () -> Unit) = Unit
+
+ fun f() {
+ view {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside non-inline non-Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.LaunchedEffect
+
+ @Composable
+ fun MyComposable() {
+ LaunchedEffect(Unit) {
+ println("Hello world")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.LaunchedEffect
+
+ @Composable
+ fun MyComposable() {
+ LaunchedEffect(Unit) {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello world")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside inline non-Composable lambda - inside non-inline non-Composable lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.LaunchedEffect
+ import androidx.compose.material.Text
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ LaunchedEffect(Unit) {
+ a?.let {
+ Text("Hello")
+ }
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.LaunchedEffect
+ import androidx.compose.material.Text
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ LaunchedEffect(Unit) {
+ a?.let {
+ newFunction()
+ }
+ }
+ }
+
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test preserve existing annotations inside function`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunctionWithExistingAnnotations(COMPOSE_PREVIEW_ANNOTATION_FQN)
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ newFunction()
+ }
+
+ @androidx.compose.ui.tooling.preview.Preview
+ @Composable
+ private fun newFunction() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ }
+
+ fun `test preserve annotations from other analyzers inside lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ Text("Hello")
+ Text("World")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunctionWithExistingAnnotations(COMPOSE_PREVIEW_ANNOTATION_FQN)
+ .resultsIn("""
+ import androidx.compose.foundation.layout.Column
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable() {
+ Column {
+ newFunction()
+ }
+ }
+
+ @androidx.compose.ui.tooling.preview.Preview
+ @Composable
+ private fun newFunction() {
+ Text("Hello")
+ Text("World")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside unresolved call lambda - inside Composable function`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ Text("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.material.Text
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun MyComposable(a: Int?) {
+ Column {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test inside unresolved call lambda - inside non-Composable function`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.material.Text
+
+ fun f() {
+ Column {
+ Text("Hello")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.material.Text
+
+ fun f() {
+ Column {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ Text("Hello")
+ }
+ """.trimIndent())
+ }
+
+ fun `test noinline lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(noinline a: () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(noinline a: () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test crossinline lambda`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(crossinline a: () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(crossinline a: () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test noinline lambda explicitly Composable`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(noinline a: @Composable () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(noinline a: @Composable () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test crossinline lambda explicitly Composable`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(crossinline a: @Composable () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ inline fun foo(crossinline a: @Composable () -> Unit) {}
+
+ @Composable
+ fun f() {
+ foo {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test lambda that is not argument`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun f() {
+ val a = {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun f() {
+ val a = {
+ newFunction()
+ }
+ }
+
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test lambda that is not argument with explicit composability`() {
+ assertThatGivenComposeProjectWithState("""
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun f() {
+ val a = @Composable {
+ println("Hello, World!")
+ }
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun f() {
+ val a = @Composable {
+ newFunction()
+ }
+ }
+
+ @Composable
+ private fun newFunction() {
+ println("Hello, World!")
+ }
+ """.trimIndent())
+ }
+
+ fun `test Composable is not imported`() {
+ assertThatGivenComposeProjectWithState("""
+ @Composable
+ fun MyComposable() {
+ Text("World")
+ }
+ """.trimIndent())
+ .invokingExtractFunction()
+ .resultsIn("""
+ @Composable
+ fun MyComposable() {
+ newFunction()
+ }
+
+ private fun newFunction() {
+ Text("World")
+ }
+ """.trimIndent())
+ }
+
+ fun `test Composable is not on classpath`() {
+ myFixture.configureByText(
+ "test.kt",
+ """
+ import androidx.compose.material.Text
+
+ @Composable
+ fun MyComposable() {
+ Text("World")
+ }
+ """.trimIndent()
+ )
+
+ myFixture.invokeExtractFunctionIn(application, existingAnnotationFqNames = emptyList())
+
+ myFixture.checkResult("""
+ import androidx.compose.material.Text
+
+ @Composable
+ fun MyComposable() {
+ newFunction()
+ }
+
+ private fun newFunction() {
+ Text("World")
+ }
+ """.trimIndent())
+ }
+
+ private fun ComposeProjectStateWithRefactoring.resultsIn(@Language("kt") expectedResult: String) {
+ myFixture.configureStubbedComposeRuntime()
+ myFixture.configureStubbedComposeFoundation()
+ myFixture.configureStubbedComposeMaterial()
+ myFixture.configureStubbedStandardLibraryLet()
+ myFixture.configureByText("test.kt", initialState.text)
+
+ if (isConstantExtractionInsteadOfFunction) {
+ myFixture.invokeExtractConstantIn(application)
+ }
+ else {
+ myFixture.invokeExtractFunctionIn(application, existingAnnotationFqNames)
+ }
+
+ myFixture.checkResult(expectedResult)
+ }
+
+ protected abstract fun JavaCodeInsightTestFixture.invokeExtractFunctionIn(application: Application, existingAnnotationFqNames: List)
+ protected abstract fun JavaCodeInsightTestFixture.invokeExtractConstantIn(application: Application)
+}
+
+private data class ComposeProjectState(val text: String)
+private data class ComposeProjectStateWithRefactoring(
+ val initialState: ComposeProjectState,
+ val isConstantExtractionInsteadOfFunction: Boolean,
+ val existingAnnotationFqNames: List,
+)
+
+private fun assertThatGivenComposeProjectWithState(@Language("kt") editorStateText: String): ComposeProjectState =
+ ComposeProjectState(editorStateText)
+private fun ComposeProjectState.invokingExtractFunction(): ComposeProjectStateWithRefactoring =
+ invokingExtractFunctionWithExistingAnnotations()
+private fun ComposeProjectState.invokingExtractFunctionWithExistingAnnotations(vararg names: FqName): ComposeProjectStateWithRefactoring =
+ ComposeProjectStateWithRefactoring(this, isConstantExtractionInsteadOfFunction = false, names.toList())
+private fun ComposeProjectState.invokingExtractConstant(): ComposeProjectStateWithRefactoring =
+ ComposeProjectStateWithRefactoring(this, isConstantExtractionInsteadOfFunction = true, emptyList())
+
+private val COMPOSE_PREVIEW_ANNOTATION_FQN = FqName("androidx.compose.ui.tooling.preview.Preview")
+
+private fun JavaCodeInsightTestFixture.configureStubbedStandardLibraryLet() {
+ addFileToProject(
+ "Let.kt",
+ """
+ // Should be `package kotlin`, but then it doesn't work without import. So I just put it in root package as a hack.
+
+ inline fun T.let(block: (T) -> R): R = block(this)
+ """.trimIndent()
+ )
+}
+
+private fun JavaCodeInsightTestFixture.configureStubbedComposeMaterial() {
+ addFileToProject(
+ "Material.kt",
+ """
+ package androidx.compose.material
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Text(text: String) = Unit
+ """.trimIndent()
+ )
+}
+
+private fun JavaCodeInsightTestFixture.configureStubbedComposeFoundation() {
+ addFileToProject(
+ "Foundation.kt",
+ """
+ package androidx.compose.foundation.layout
+
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Column(block: @Composable () -> Unit) = Unit
+ """.trimIndent()
+ )
+}
+
+private fun JavaCodeInsightTestFixture.configureStubbedComposeRuntime() {
+ addFileToProject(
+ "Runtime.kt",
+ """
+ package androidx.compose.runtime
+
+ annotation class Composable
+ annotation class DisallowComposableCalls
+
+ @Composable
+ fun LaunchedEffect(key: Unit, block: suspend () -> Unit) = Unit
+ """.trimIndent()
+ )
+}