From ed4d01ff3be341531af0cedfd93c1833e6bd184e Mon Sep 17 00:00:00 2001 From: Pavel Kirpichenkov Date: Wed, 5 Nov 2025 13:37:13 +0200 Subject: [PATCH] [kotlin] KTIJ-35016 K2 Move: support file suffixes for expect/actual MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files in different source sets by convention should have different names. Mostly this is due to JVM limitations: file facade classes with the same name can lead to clashes. Move changes: — File destination field is treated as file name in the common source set for expect/actual move — Iff expect/actual move setting is enabled, platform file suffixes are added in different source sets UI changes: — Show expect/actual setting only for declaration movement with expects/actuals — Add an explanation for the file destination in case of expect/actual move — Add a warning for platform file suffix used for common file name — Disable dialog button when trying to move multiple declarations to the same file GitOrigin-RevId: 9c294d146366f55eed10fffc8e23cbefb8ebe0cf --- .../messages/RefactoringBundle.properties | 3 + .../messages/KotlinBundle.properties | 4 +- .../after/commonMain/commonMain.iml | 20 +++ .../after/commonMain/src/bar/target.kt | 3 + .../after/commonMain/src/foo/.keep | 0 .../after/commonMain/src/foo/base.kt | 4 + .../after/jsMain/jsMain.iml | 23 +++ .../after/jsMain/src/bar/target.js.kt | 6 + .../after/jsMain/src/foo/.keep | 0 .../after/jsMain/src/foo/base.js.kt | 4 + .../after/jvmMain/jvmMain.iml | 23 +++ .../after/jvmMain/src/bar/target.jvm.kt | 6 + .../after/jvmMain/src/foo/.keep | 0 .../before/commonMain/commonMain.iml | 20 +++ .../before/commonMain/src/foo/.keep | 0 .../before/commonMain/src/foo/base.kt | 6 + .../before/jsMain/jsMain.iml | 23 +++ .../before/jsMain/src/bar/target.js.kt | 4 + .../before/jsMain/src/foo/.keep | 0 .../before/jsMain/src/foo/base.js.kt | 6 + .../before/jvmMain/jvmMain.iml | 23 +++ .../before/jvmMain/src/foo/.keep | 0 .../before/jvmMain/src/foo/base.jvm.kt | 6 + ...larFunctionConventionalSourceSetNames.test | 8 + .../after/B/src/bar/{Foo.kt => Foo.B.kt} | 0 .../after/C/src/bar/{Foo.kt => Foo.C.kt} | 0 .../after/B/src/bar/{foo.kt => foo.B.kt} | 0 .../after/C/src/bar/{foo.kt => foo.C.kt} | 0 .../after/B/src/bar/{foo.kt => foo.B.kt} | 0 .../after/C/src/bar/{foo.kt => foo.C.kt} | 0 .../after/B/src/bar/{test.kt => test.B.kt} | 0 .../after/C/src/bar/{test.kt => test.C.kt} | 0 .../kotlin.refactorings.move.k2/BUILD.bazel | 4 + .../intellij.kotlin.refactorings.move.iml | 2 + .../intellij.kotlin.refactorings.move.xml | 1 + .../move/ui/ConstantObservableProperty.kt | 18 ++ .../k2/refactoring/move/ui/K2MoveModel.kt | 70 +++++--- .../refactoring/move/ui/K2MoveSourceModel.kt | 38 +++- .../refactoring/move/ui/K2MoveTargetModel.kt | 162 +++++++++++++----- .../move/ui/ObservableUiSettings.kt | 54 ++++-- .../idea/k2/refactoring/move/ui/moveUiUtil.kt | 31 ++++ ...actK2CheckDescriptorMultiModuleMoveTest.kt | 4 +- .../K2CheckDescriptorMultiModuleMoveTest.kt | 35 +++- 43 files changed, 512 insertions(+), 99 deletions(-) create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/commonMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/bar/target.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/base.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/jsMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/bar/target.js.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/base.js.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/jvmMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/bar/target.jvm.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/commonMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/base.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/jsMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/bar/target.js.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/base.js.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/jvmMain.iml create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/.keep create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/base.jvm.kt create mode 100644 plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/moveActualAndRegularFunctionConventionalSourceSetNames.test rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/B/src/bar/{Foo.kt => Foo.B.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/C/src/bar/{Foo.kt => Foo.C.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/B/src/bar/{foo.kt => foo.B.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/C/src/bar/{foo.kt => foo.C.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/B/src/bar/{foo.kt => foo.B.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/C/src/bar/{foo.kt => foo.C.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/B/src/bar/{test.kt => test.B.kt} (100%) rename plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/C/src/bar/{test.kt => test.C.kt} (100%) create mode 100644 plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ConstantObservableProperty.kt diff --git a/platform/refactoring/resources/messages/RefactoringBundle.properties b/platform/refactoring/resources/messages/RefactoringBundle.properties index 5c99771a573e..3d0289ce605c 100644 --- a/platform/refactoring/resources/messages/RefactoringBundle.properties +++ b/platform/refactoring/resources/messages/RefactoringBundle.properties @@ -48,6 +48,9 @@ visibility.combo.title=&Visibility: moveInstanceMethod.select.an.instance.parameter=Select an &instance expression: choose.destination.package=Choose Destination Package create.explicit.package.warning=Implicit package ''{0}'' will become explicit +move.added.kmp.suffixes.warning.0=Platform suffix ''.{0}'' will be copied across multiple source sets, consider removing it +move.added.kmp.suffixes.info=Multiplatform file suffixes will be added automatically +tooltip.text.move.kmp.target.file=Base file name in the common source set. Platform source sets will receive suffixes in accordance with the multiplatform conventions. interface.name.prompt=&Interface name: package.for.new.interface=&Package for new interface: pull.up.members.to=P&ull up members of {0} to: diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index c30ac6d5962f..79fae9bc139b 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -889,8 +889,8 @@ parameter.name.is.invalid=Parameter name ''{0}'' is invalid parameter.type.is.invalid=Parameter type ''{0}'' is invalid parameter.types.are.not.denotable=Cannot extract method since following types are not denotable in the target scope: refactoring.move.non.kotlin.file=Target must be a Kotlin file -refactoring.class.destination=Class destination -refactoring.file.destination=File destination +refactoring.class.destination=Class destination: +refactoring.file.destination=File destination: refactoring.cannot.find.target.class=Cannot find target class return.type.is.invalid=Return type is invalid searching.usages.of.0.parameter=Searching usages of ''{0}''parameter diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/commonMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/commonMain.iml new file mode 100644 index 000000000000..7cd42c203d42 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/commonMain.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/bar/target.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/bar/target.kt new file mode 100644 index 000000000000..bbc178233ae7 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/bar/target.kt @@ -0,0 +1,3 @@ +package bar + +expect fun foo(): Int \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/base.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/base.kt new file mode 100644 index 000000000000..bee4460472f2 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/commonMain/src/foo/base.kt @@ -0,0 +1,4 @@ +package foo + +fun regularCommon() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/jsMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/jsMain.iml new file mode 100644 index 000000000000..65b5735a6e4a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/jsMain.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + commonMain + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/bar/target.js.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/bar/target.js.kt new file mode 100644 index 000000000000..73303e977ddb --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/bar/target.js.kt @@ -0,0 +1,6 @@ +package bar + +fun existingInTargetPackageJs() { +} + +actual fun foo(): Int = 1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/base.js.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/base.js.kt new file mode 100644 index 000000000000..a3fdad49d202 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jsMain/src/foo/base.js.kt @@ -0,0 +1,4 @@ +package foo + +fun regularJs() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/jvmMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/jvmMain.iml new file mode 100644 index 000000000000..539cd10de04b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/jvmMain.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + commonMain + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/bar/target.jvm.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/bar/target.jvm.kt new file mode 100644 index 000000000000..2c2c08191dc9 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/bar/target.jvm.kt @@ -0,0 +1,6 @@ +package bar + +actual fun foo(): Int = 0 + +fun regularJvm() { +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/after/jvmMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/commonMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/commonMain.iml new file mode 100644 index 000000000000..7cd42c203d42 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/commonMain.iml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/base.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/base.kt new file mode 100644 index 000000000000..ca56072568a0 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/commonMain/src/foo/base.kt @@ -0,0 +1,6 @@ +package foo + +expect fun foo(): Int + +fun regularCommon() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/jsMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/jsMain.iml new file mode 100644 index 000000000000..65b5735a6e4a --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/jsMain.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + commonMain + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/bar/target.js.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/bar/target.js.kt new file mode 100644 index 000000000000..913a5d85a089 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/bar/target.js.kt @@ -0,0 +1,4 @@ +package bar + +fun existingInTargetPackageJs() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/base.js.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/base.js.kt new file mode 100644 index 000000000000..af95738a4315 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jsMain/src/foo/base.js.kt @@ -0,0 +1,6 @@ +package foo + +actual fun foo(): Int = 1 + +fun regularJs() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/jvmMain.iml b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/jvmMain.iml new file mode 100644 index 000000000000..539cd10de04b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/jvmMain.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + commonMain + + + + + + + \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/.keep b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/base.jvm.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/base.jvm.kt new file mode 100644 index 000000000000..61dbefbd63a9 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/before/jvmMain/src/foo/base.jvm.kt @@ -0,0 +1,6 @@ +package foo + +actual fun foo(): Int = 0 + +fun regularJvm() { +} diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/moveActualAndRegularFunctionConventionalSourceSetNames.test b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/moveActualAndRegularFunctionConventionalSourceSetNames.test new file mode 100644 index 000000000000..b9c3aebcc49e --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualAndRegularFunctionConventionalSourceSetNames/moveActualAndRegularFunctionConventionalSourceSetNames.test @@ -0,0 +1,8 @@ +{ + "mainFile": "jvmMain/src/foo/base.jvm.kt", + "targetPackage": "bar", + "withRuntime": "true", + "modulesWithCommonRuntime": ["commonMain"], + "modulesWithRuntime": ["jvmMain"], + "modulesWithJsRuntime": ["jsMain"] +} \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/B/src/bar/Foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/B/src/bar/Foo.B.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/B/src/bar/Foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/B/src/bar/Foo.B.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/C/src/bar/Foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/C/src/bar/Foo.C.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/C/src/bar/Foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualClass/after/C/src/bar/Foo.C.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/B/src/bar/foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/B/src/bar/foo.B.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/B/src/bar/foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/B/src/bar/foo.B.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/C/src/bar/foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/C/src/bar/foo.C.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/C/src/bar/foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveActualFunction/after/C/src/bar/foo.C.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/B/src/bar/foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/B/src/bar/foo.B.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/B/src/bar/foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/B/src/bar/foo.B.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/C/src/bar/foo.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/C/src/bar/foo.C.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/C/src/bar/foo.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectFunctionToExistingPackage/after/C/src/bar/foo.C.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/B/src/bar/test.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/B/src/bar/test.B.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/B/src/bar/test.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/B/src/bar/test.B.kt diff --git a/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/C/src/bar/test.kt b/plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/C/src/bar/test.C.kt similarity index 100% rename from plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/C/src/bar/test.kt rename to plugins/kotlin/idea/tests/testData/refactoring/moveDescriptors/moveExpectProperty/after/C/src/bar/test.C.kt diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/BUILD.bazel b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/BUILD.bazel index e0c720fdfc41..9e365634963d 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/BUILD.bazel +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/BUILD.bazel @@ -58,6 +58,8 @@ jvm_library( "//plugins/kotlin/base/facet", "//platform/observable", "//java/openapi:java", + "@lib//:kotlinc-analysis-api-platform-interface", + "//plugins/kotlin/base/external-build-system:kotlin-base-externalSystem", ] ) @@ -115,6 +117,8 @@ jvm_library( "//plugins/kotlin/base/facet", "//platform/observable", "//java/openapi:java", + "@lib//:kotlinc-analysis-api-platform-interface", + "//plugins/kotlin/base/external-build-system:kotlin-base-externalSystem", ] ) ### auto-generated section `build intellij.kotlin.refactorings.move` end diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/intellij.kotlin.refactorings.move.iml b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/intellij.kotlin.refactorings.move.iml index 178a8d50826b..5e5966dbbf5a 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/intellij.kotlin.refactorings.move.iml +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/intellij.kotlin.refactorings.move.iml @@ -68,5 +68,7 @@ + + \ No newline at end of file diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/resources/intellij.kotlin.refactorings.move.xml b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/resources/intellij.kotlin.refactorings.move.xml index a55d7f5dda0d..284e4db328ae 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/resources/intellij.kotlin.refactorings.move.xml +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/resources/intellij.kotlin.refactorings.move.xml @@ -1,6 +1,7 @@ + (private val value: T) : ObservableProperty { + final override fun get(): T = value + final override fun afterChange(listener: (T) -> Unit) {} + final override fun afterChange(parentDisposable: Disposable?, listener: (T) -> Unit) {} +} + +class ConstantBooleanObservableProperty(value: Boolean) : ConstantObservableProperty(value), ObservableBooleanProperty { + override fun afterSet(parentDisposable: Disposable?, listener: () -> Unit) {} + override fun afterReset(parentDisposable: Disposable?, listener: () -> Unit) {} +} diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt index 83e7d42e3bb4..de06116d358e 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveModel.kt @@ -4,7 +4,8 @@ package org.jetbrains.kotlin.idea.k2.refactoring.move.ui import com.intellij.openapi.editor.Editor import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.observable.properties.MutableBooleanProperty -import com.intellij.openapi.observable.properties.ObservableBooleanProperty +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.util.transform import com.intellij.openapi.project.Project import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.util.NlsContexts @@ -33,15 +34,13 @@ import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtNamedDeclaration -import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier -import org.jetbrains.kotlin.psi.psiUtil.isExpectDeclaration import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull /** * @see K2MoveDescriptor */ -sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveModelObservableSettings { +sealed class K2MoveModel(private val observableUiSettings: ObservableUiSettings) : K2MoveModelObservableSettings { init { observableUiSettings.registerK2MoveModelSettings(this) } @@ -76,6 +75,10 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod } open fun buildPanel(panel: Panel): Unit = with(panel) { + val mppDeclarationSelectedObservable = observableUiSettings.mppDeclarationsSelectedObservable.transform { isSelected -> + isSelected && inSourceRoot + } + row { panel { searchForText.createComboBox(this) @@ -83,21 +86,25 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod }.align(AlignY.TOP + AlignX.LEFT) panel { searchInComments.createComboBox(this) - mppDeclarations.createComboBox(this, inSourceRoot && this@K2MoveModel is Declarations) + mppDeclarations.createComboBox( + panel = this, + visible = mppDeclarationSelectedObservable, + enabled = mppDeclarationSelectedObservable, + ) }.align(AlignY.TOP + AlignX.RIGHT) } } - override val mppDeclarationsObservable: ObservableBooleanProperty + override val mppDeclarationsSettingObservable: ObservableProperty get() = mppDeclarations.observableProperty - override val searchForTextObservable: ObservableBooleanProperty + override val searchForTextSettingObservable: ObservableProperty get() = searchForText.observableProperty - override val searchInCommentsObservable: ObservableBooleanProperty + override val searchInCommentsSettingObservable: ObservableProperty get() = searchInComments.observableProperty - override val searchReferencesObservable: ObservableBooleanProperty + override val searchReferencesSettingObservable: ObservableProperty get() = searchReferences.observableProperty enum class Setting(private val text: @NlsContexts.Checkbox String) { @@ -143,19 +150,36 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod }; abstract var state: Boolean - // lazy prevents service access from constructor + // lazy to avoid service access from constructor internal val observableProperty: MutableBooleanProperty by lazy { AtomicBooleanProperty(state) } - fun createComboBox(panel: Panel, enabled: Boolean = true) { + fun createComboBox( + panel: Panel, + enabled: Boolean = true, + visible: Boolean = true, + ) { + createComboBox( + panel = panel, + enabled = ConstantBooleanObservableProperty(enabled), + visible = ConstantBooleanObservableProperty(visible) + ) + } + + fun createComboBox( + panel: Panel, + enabled: ObservableProperty, + visible: ObservableProperty, + ) { panel.row { - val checkBox = checkBox(text).enabled(enabled) - if (enabled) { - checkBox.bindSelected(::state) - // bind doesn't register state changes before dialog confirmation, but the UI should be updated on all state changes - checkBox.onChanged { observableProperty.set(it.isSelected) } - } else { - checkBox.selected(false) - } + val checkBox = checkBox(text).enabledIf(enabled).visibleIf(visible) + checkBox.onChanged { observableProperty.set(it.isSelected) } + // reset only is intentional + enabled.afterChange { isEnabled -> if (!isEnabled) checkBox.selected(false) } + checkBox.bindSelected(::state) + // value set in bindSelected doesn't trigger listeners + val initial = enabled.get() && state + checkBox.selected(initial) + observableProperty.set(initial) }.layout(RowLayout.PARENT_GRID) } } @@ -226,7 +250,7 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod && name == fileName } if (!fileName.isValidKotlinFile()) return false - val files = source.elements.map { it.containingFile } + val files = source.elements.map { it.containingFile }.toSet() return files.size != 1 || !(files.single() as KtFile).isTargetFile() || target.isMoveToExplicitPackage() } @@ -258,7 +282,7 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod val searchForReferences = if (inSourceRoot) searchReferences.state else false val searchForText = searchForText.state val searchInComments = searchInComments.state - if (mppDeclarations.state && declarations.any { it.isExpectDeclaration() || it.hasActualModifier() }) { + if (mppDeclarations.state && declarations.any { it.isExpectOrActual() }) { val descriptors = declarations.flatMap { elem -> ExpectActualUtils.withExpectedActuals(elem).filterIsInstance() }.groupBy { elem -> @@ -340,7 +364,7 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod || declarationsFromFiles.isEmpty() || (targetContainer is PsiDirectory && targetContainer.getPackage() == null) -> { // this move can contain foreign language files - val source = K2MoveSourceModel.FileSource(elementsToMove.toFileElements().toSet()) + val source = K2MoveSourceModel.FileSource(elementsToMove.toFileElements().toSet(), observableUiSettings) val target = if (targetContainer is PsiDirectory) { val pkg = targetContainer.getFqNameWithImplicitPrefixOrRoot() K2MoveTargetModel.SourceDirectory( @@ -360,7 +384,7 @@ sealed class K2MoveModel(observableUiSettings: ObservableUiSettings) : K2MoveMod } targetContainer is KtFile || targetContainer.isSingleClassContainer() || isSingleFileMove(elementsToMove) -> { - val source = K2MoveSourceModel.ElementSource(declarationsFromFiles.toSet()) + val source = K2MoveSourceModel.ElementSource(declarationsFromFiles.toSet(), observableUiSettings) val targetFile = targetContainer?.containingFile val target = if (targetFile is KtFile) { K2MoveTargetModel.File(targetFile, observableUiSettings) diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveSourceModel.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveSourceModel.kt index 8a8aa9e93710..9b89b1ca79a0 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveSourceModel.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveSourceModel.kt @@ -2,6 +2,9 @@ package org.jetbrains.kotlin.idea.k2.refactoring.move.ui import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.openapi.observable.properties.AtomicBooleanProperty +import com.intellij.openapi.observable.properties.MutableBooleanProperty +import com.intellij.openapi.observable.properties.ObservableBooleanProperty import com.intellij.platform.backend.presentation.TargetPresentation import com.intellij.psi.PsiElement import com.intellij.psi.PsiFileSystemItem @@ -13,6 +16,7 @@ import com.intellij.ui.dsl.builder.BottomGap import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.TopGap import com.intellij.ui.list.createTargetPresentationRenderer +import org.jetbrains.annotations.ApiStatus import org.jetbrains.kotlin.idea.base.resources.KotlinBundle import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveSourceDescriptor import org.jetbrains.kotlin.idea.refactoring.memberInfo.AbstractKotlinMemberInfoModel @@ -23,15 +27,25 @@ import org.jetbrains.kotlin.psi.KtDeclarationContainer import org.jetbrains.kotlin.psi.KtNamedDeclaration import javax.swing.JComponent -sealed interface K2MoveSourceModel { - val elements: Set +sealed class K2MoveSourceModel( + observableUiSettings: ObservableUiSettings +): K2SourceModelObservableSettings { + abstract val elements: Set - fun toDescriptor(): K2MoveSourceDescriptor? + abstract fun toDescriptor(): K2MoveSourceDescriptor? - fun buildPanel(panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) + abstract fun buildPanel(panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) - class FileSource(fsItems: Set) : K2MoveSourceModel { + init { + observableUiSettings.registerK2SourceModelSettings(this) + } + + class FileSource( + fsItems: Set, + observableUiSettings: ObservableUiSettings, + ) : K2MoveSourceModel(observableUiSettings) { override var elements: Set = fsItems + override val mppDeclarationsSelectedObservable: ObservableBooleanProperty = ConstantBooleanObservableProperty(false) override fun toDescriptor(): K2MoveSourceDescriptor.FileSource = K2MoveSourceDescriptor.FileSource(elements) @@ -47,9 +61,8 @@ sealed interface K2MoveSourceModel { } panel.group(RefactoringBundle.message("move.files.group")) { - lateinit var list: JBList row { - list = cell(JBList(CollectionListModel(presentableFiles)).apply { + cell(JBList(CollectionListModel(presentableFiles)).apply { cellRenderer = createTargetPresentationRenderer { it } }).align(Align.FILL).component }.resizableRow() @@ -57,9 +70,14 @@ sealed interface K2MoveSourceModel { } } - class ElementSource(declarations: Set) : K2MoveSourceModel { + class ElementSource( + declarations: Set, + observableUiSettings: ObservableUiSettings, + ) : K2MoveSourceModel(observableUiSettings) { override var elements: Set = declarations private set + override val mppDeclarationsSelectedObservable: MutableBooleanProperty = + AtomicBooleanProperty(hasExpectOrActualElements()) private lateinit var memberSelectionPanel: KotlinMemberSelectionPanel @@ -83,6 +101,7 @@ sealed interface K2MoveSourceModel { val table = memberSelectionPanel.table table.addMemberInfoChangeListener { elements = table.selectedMemberInfos.map { it.member }.toSet() + mppDeclarationsSelectedObservable.set(hasExpectOrActualElements()) if (elements.isEmpty()) { onError(KotlinBundle.message("text.no.elements.to.move.are.selected"), memberSelectionPanel.table) } else { @@ -116,6 +135,9 @@ sealed interface K2MoveSourceModel { isChecked = elementsToMove.contains(declaration) } } + + private fun hasExpectOrActualElements(): Boolean = + elements.any { it.isExpectOrActual() } } } diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt index 32931474806e..3472fdf95a04 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/K2MoveTargetModel.kt @@ -6,9 +6,11 @@ import com.intellij.ide.util.DirectoryChooser import com.intellij.ide.util.TreeJavaClassChooserDialog import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ReadAction -import com.intellij.openapi.observable.properties.AtomicBooleanProperty -import com.intellij.openapi.observable.properties.MutableBooleanProperty -import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.properties.* +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.equalsTo +import com.intellij.openapi.observable.util.operation +import com.intellij.openapi.observable.util.transform import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.util.registry.Registry @@ -33,37 +35,42 @@ import org.jetbrains.kotlin.idea.core.util.toPsiDirectory import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveTargetDescriptor import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox import org.jetbrains.kotlin.idea.refactoring.ui.KotlinFileChooserDialog +import org.jetbrains.kotlin.idea.util.sourceRoot import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtFile import javax.swing.JComponent import javax.swing.event.DocumentEvent -sealed interface K2MoveTargetModel { - val directory: PsiDirectory +sealed class K2MoveTargetModel( + protected val observableUiSettings: ObservableUiSettings +) : K2TargetModelObservableSettings { + init { + observableUiSettings.registerK2TargetModelSettings(this) + } - val pkgName: FqName + abstract val directory: PsiDirectory - val explicitPkgMoveFqName: FqName? + abstract val pkgName: FqName - val observableUiSettings: ObservableUiSettings + abstract val explicitPkgMoveFqName: FqName? /** * Creates a [K2MoveTargetDescriptor] from this model. * @param kmpSourceRoot source KMP directory that corresponds to the base [directory] in a different source set. * `null` for non expect-actual moves. */ - fun toDescriptor(kmpSourceRoot: PsiDirectory? = null): K2MoveTargetDescriptor + abstract fun toDescriptor(kmpSourceRoot: PsiDirectory? = null): K2MoveTargetDescriptor - fun buildPanel(panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) + abstract fun buildPanel(panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) @ApiStatus.Internal abstract class SourceDirectoryChooser( override var pkgName: FqName, override var directory: PsiDirectory, override val explicitPkgMoveFqName: FqName?, - override val observableUiSettings: ObservableUiSettings, - ) : K2MoveTargetModel { + observableUiSettings: ObservableUiSettings, + ) : K2MoveTargetModel(observableUiSettings) { private val initialDirectory = directory protected lateinit var pkgChooser: PackageNameReferenceEditorCombo @@ -163,20 +170,24 @@ sealed interface K2MoveTargetModel { override fun buildPanel(panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) { panel.installPkgChooser(onError, revalidateButtons) } + + override val sourceSetSuffix: ObservableProperty + get() = ConstantObservableProperty(null) } @ApiStatus.Internal abstract class FileChooser( - fileName: String, + @set:ApiStatus.Internal + var fileName: String, pkg: FqName, directory: PsiDirectory, explicitPkgMoveFqName: FqName?, observableUiSettings: ObservableUiSettings, ) : SourceDirectoryChooser(pkg, directory, explicitPkgMoveFqName, observableUiSettings) { - var fileName: String = fileName - protected set private var selectedFile: KtFile? = null + private val mppSuffixObservableProperty: AtomicProperty = + AtomicProperty(findFileSuffix(fileName)) override fun updateDirectory(onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit) { super.updateDirectory(onError, revalidateButtons) @@ -220,10 +231,21 @@ sealed interface K2MoveTargetModel { } else { onError(null, fileChooser) } + mppSuffixObservableProperty.set(findFileSuffix(fileName)) revalidateButtons() } }) } + + override val sourceSetSuffix: ObservableProperty + get() = mppSuffixObservableProperty + + private fun findFileSuffix(fileName: String): String? { + val sourceRootVirtualFile = directory.sourceRoot ?: return null + val sourceRootPsiDir = sourceRootVirtualFile.toPsiDirectory(directory.project) ?: return null + val suffix = findSourceSetNameStem(sourceRootPsiDir) + return if (fileName.endsWith(".$suffix.kt")) suffix else null + } } class File( @@ -274,7 +296,8 @@ sealed interface K2MoveTargetModel { return if (destinationTargetType == MoveTargetType.CLASS && selectedClass != null) { K2MoveTargetDescriptor.ClassOrObject(selectedClass) } else { - K2MoveTargetDescriptor.File(fileName, pkgName, kmpSourceRoot ?: directory) + val fileNameWithKmpSuffixHandled = findSuffixedFileName(fileName, kmpSourceRoot) + K2MoveTargetDescriptor.File(fileNameWithKmpSuffixHandled, pkgName, kmpSourceRoot ?: directory) } } @@ -322,43 +345,92 @@ sealed interface K2MoveTargetModel { }) } + private fun Panel.installTargetFileSelector( + onError: (String?, JComponent) -> Unit, + revalidateButtons: () -> Unit, + installTitleElement: Row.() -> Unit, + ) { + val iconInfoObservableProperty = observableUiSettings.mppDeclarationsSettingObservable + .and(observableUiSettings.sourceSetSuffix.transform { it.isNullOrBlank() }) + val iconWarningObservableProperty = observableUiSettings.mppDeclarationsSettingObservable + .and(observableUiSettings.sourceSetSuffix.transform { !it.isNullOrBlank() }) + + row { + panel { + row { + installTitleElement() + icon(AllIcons.General.ContextHelp).align(AlignX.RIGHT).gap(RightGap.SMALL).applyToComponent { + toolTipText = RefactoringBundle.message("tooltip.text.move.kmp.target.file") + }.visibleIf(observableUiSettings.mppDeclarationsSettingObservable) + } + } + installFileChooser(onError, revalidateButtons) + }.layout(RowLayout.LABEL_ALIGNED) + row("") { + icon(AllIcons.General.Warning).align(AlignX.LEFT).gap(RightGap.SMALL) + .visibleIf(iconWarningObservableProperty) + icon(AllIcons.General.Information).align(AlignX.LEFT).gap(RightGap.SMALL) + .visibleIf(iconInfoObservableProperty) + val warningLabel = label("").bindText( + operation(iconInfoObservableProperty, iconWarningObservableProperty) { info, warning -> + when { + info -> RefactoringBundle.message("move.added.kmp.suffixes.info") + warning -> RefactoringBundle.message( + "move.added.kmp.suffixes.warning.0", + observableUiSettings.sourceSetSuffix.get().orEmpty(), + ) + else -> "" + } + } + ).visibleIf(observableUiSettings.mppDeclarationsSettingObservable) + // placeholder to avoid resizing on changes in the settings + label("").visibleIf(observableUiSettings.mppDeclarationsSettingObservable.equalsTo(false)) + .applyToComponent { preferredSize = warningLabel.component.preferredSize } + } + } + + private fun Panel.installTargetSelectionRadioGroup( + onError: (String?, JComponent) -> Unit, + revalidateButtons: () -> Unit, + ) { + buttonsGroup(indent = false) { + installTargetFileSelector(onError, revalidateButtons) { + radioButton(KotlinBundle.message("refactoring.file.destination"), MoveTargetType.FILE) + .onChanged { + destinationTargetType = MoveTargetType.FILE + fileChooser.isEnabled = true + pkgChooser.isEnabled = true + destinationChooser.isEnabled = true + classChooser.isEnabled = false + revalidateButtons() + } + } + row { + radioButton(KotlinBundle.message("refactoring.class.destination"), MoveTargetType.CLASS) + .onChanged { + destinationTargetType = MoveTargetType.CLASS + fileChooser.isEnabled = false + pkgChooser.isEnabled = false + destinationChooser.isEnabled = false + classChooser.isEnabled = true + revalidateButtons() + } + installClassTargetChooser(onError, revalidateButtons) + }.layout(RowLayout.LABEL_ALIGNED) + }.bind(::destinationTargetType.toMutableProperty()) + } + override fun buildPanel( panel: Panel, onError: (String?, JComponent) -> Unit, revalidateButtons: () -> Unit ) { panel.installPkgChooser(onError, revalidateButtons) - if (Registry.`is`("kotlin.move.show.move.to.class")) { - panel.buttonsGroup(indent = true) { - panel.row { - radioButton(KotlinBundle.message("refactoring.file.destination"), MoveTargetType.FILE) - .onChanged { - destinationTargetType = MoveTargetType.FILE - fileChooser.isEnabled = true - pkgChooser.isEnabled = true - destinationChooser.isEnabled = true - classChooser.isEnabled = false - revalidateButtons() - } - installFileChooser(onError, revalidateButtons) - } - panel.row { - radioButton(KotlinBundle.message("refactoring.class.destination"), MoveTargetType.CLASS) - .onChanged { - destinationTargetType = MoveTargetType.CLASS - fileChooser.isEnabled = false - pkgChooser.isEnabled = false - destinationChooser.isEnabled = false - classChooser.isEnabled = true - revalidateButtons() - } - installClassTargetChooser(onError, revalidateButtons) - } - }.bind(::destinationTargetType.toMutableProperty()) + panel.installTargetSelectionRadioGroup(onError, revalidateButtons) } else { - panel.row { - installFileChooser(onError, revalidateButtons) + panel.installTargetFileSelector(onError, revalidateButtons) { + label(KotlinBundle.message("label.text.file")) } } } diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt index 005c5004377f..7eceed61f446 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/ObservableUiSettings.kt @@ -2,32 +2,58 @@ package org.jetbrains.kotlin.idea.k2.refactoring.move.ui -import com.intellij.openapi.observable.properties.ObservableBooleanProperty +import com.intellij.openapi.observable.properties.ObservableProperty -interface ObservableUiSettings : K2MoveModelObservableSettings { +interface ObservableUiSettings : K2MoveModelObservableSettings, K2SourceModelObservableSettings, K2TargetModelObservableSettings { fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings) + fun registerK2SourceModelSettings(settings: K2SourceModelObservableSettings) + fun registerK2TargetModelSettings(settings: K2TargetModelObservableSettings) } interface K2MoveModelObservableSettings { - val searchReferencesObservable: ObservableBooleanProperty - val searchForTextObservable: ObservableBooleanProperty - val searchInCommentsObservable: ObservableBooleanProperty - val mppDeclarationsObservable: ObservableBooleanProperty + val searchReferencesSettingObservable: ObservableProperty + val searchForTextSettingObservable: ObservableProperty + val searchInCommentsSettingObservable: ObservableProperty + val mppDeclarationsSettingObservable: ObservableProperty +} + +interface K2SourceModelObservableSettings { + val mppDeclarationsSelectedObservable: ObservableProperty +} + +interface K2TargetModelObservableSettings { + val sourceSetSuffix: ObservableProperty } internal class ObservableUiSettingsImpl : ObservableUiSettings { private lateinit var k2MoveModelSettings: K2MoveModelObservableSettings + private lateinit var k2TargetModelSettings: K2TargetModelObservableSettings + private lateinit var k2SourceModelSettings: K2SourceModelObservableSettings - override val searchReferencesObservable: ObservableBooleanProperty - get() = k2MoveModelSettings.searchReferencesObservable - override val searchForTextObservable: ObservableBooleanProperty - get() = k2MoveModelSettings.searchForTextObservable - override val searchInCommentsObservable: ObservableBooleanProperty - get() = k2MoveModelSettings.searchInCommentsObservable - override val mppDeclarationsObservable: ObservableBooleanProperty - get() = k2MoveModelSettings.mppDeclarationsObservable + override val searchReferencesSettingObservable: ObservableProperty + get() = k2MoveModelSettings.searchReferencesSettingObservable + override val searchForTextSettingObservable: ObservableProperty + get() = k2MoveModelSettings.searchForTextSettingObservable + override val searchInCommentsSettingObservable: ObservableProperty + get() = k2MoveModelSettings.searchInCommentsSettingObservable + override val mppDeclarationsSettingObservable: ObservableProperty + get() = k2MoveModelSettings.mppDeclarationsSettingObservable + + override val mppDeclarationsSelectedObservable: ObservableProperty + get() = k2SourceModelSettings.mppDeclarationsSelectedObservable + + override val sourceSetSuffix: ObservableProperty + get() = k2TargetModelSettings.sourceSetSuffix override fun registerK2MoveModelSettings(settings: K2MoveModelObservableSettings) { k2MoveModelSettings = settings } + + override fun registerK2SourceModelSettings(settings: K2SourceModelObservableSettings) { + k2SourceModelSettings = settings + } + + override fun registerK2TargetModelSettings(settings: K2TargetModelObservableSettings) { + k2TargetModelSettings = settings + } } diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/moveUiUtil.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/moveUiUtil.kt index a0d2b964c95d..5bd0e4055016 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/moveUiUtil.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/src/org/jetbrains/kotlin/idea/k2/refactoring/move/ui/moveUiUtil.kt @@ -7,13 +7,18 @@ import com.intellij.psi.PsiDirectory import com.intellij.psi.PsiElement import com.intellij.psi.PsiFileSystemItem import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.analysis.api.projectStructure.KaModuleProvider import org.jetbrains.kotlin.idea.KotlinLanguage +import org.jetbrains.kotlin.idea.base.externalSystem.KotlinBuildSystemFacade +import org.jetbrains.kotlin.idea.base.util.module import org.jetbrains.kotlin.idea.core.getImplicitPackagePrefix import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtElement import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtNamedDeclaration +import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier +import org.jetbrains.kotlin.psi.psiUtil.isExpectDeclaration internal fun String.isValidKotlinFile(): Boolean { return endsWith(KotlinLanguage.INSTANCE.associatedFileType?.defaultExtension ?: return false) || endsWith(".kts") @@ -65,3 +70,29 @@ internal fun findExplicitPkgMoveFqName(elementsToMove: List): FqName internal fun K2MoveTargetModel.isMoveToExplicitPackage(): Boolean = pkgName == explicitPkgMoveFqName + +internal fun KtNamedDeclaration.isExpectOrActual(): Boolean = + isExpectDeclaration() || hasActualModifier() + +internal fun findSourceSetNameStem(kmpSourceRoot: PsiDirectory): String? { + val project = kmpSourceRoot.project + val kaModule = KaModuleProvider.getModule(project, kmpSourceRoot, useSiteModule = null) + val dependsOnDependencies = kaModule.directDependsOnDependencies + if (dependsOnDependencies.isEmpty()) return null + val workspaceModule = kmpSourceRoot.module ?: return null + val sourceSet = KotlinBuildSystemFacade.getInstance().findSourceSet(workspaceModule) + val fullSourceSetName = sourceSet?.name ?: workspaceModule.name.split(".").last() + val sourceSetNameStem = kmpSourceSetDefaultSuffixes.firstOrNull { fullSourceSetName.endsWith(it) } + ?.let { sourceSetNameSuffix -> fullSourceSetName.removeSuffix(sourceSetNameSuffix) } + ?: fullSourceSetName + return sourceSetNameStem +} + +internal fun findSuffixedFileName(baseFileName: String, kmpSourceRoot: PsiDirectory?): String { + if (kmpSourceRoot == null) return baseFileName + val sourceSetSuffix = findSourceSetNameStem(kmpSourceRoot) ?: return baseFileName + val baseWithoutKt = baseFileName.substringBeforeLast(".kt") + return "$baseWithoutKt.$sourceSetSuffix.kt" +} + +private val kmpSourceSetDefaultSuffixes = listOf("Main", "Test") diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/AbstractK2CheckDescriptorMultiModuleMoveTest.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/AbstractK2CheckDescriptorMultiModuleMoveTest.kt index b2edb6369908..d4e09f1ddd00 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/AbstractK2CheckDescriptorMultiModuleMoveTest.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/AbstractK2CheckDescriptorMultiModuleMoveTest.kt @@ -41,8 +41,8 @@ abstract class AbstractK2CheckDescriptorMultiModuleMoveTest : KotlinMultiFileTes config: JsonObject ) { allowAnalysisOnEdt { - val elementAtCaret = elementsAtCaret.single() - val editor = elementAtCaret.findExistingEditor() + val firstElementAtCaret = elementsAtCaret.first() + val editor = firstElementAtCaret.findExistingEditor() val moveModel = K2MoveModel.create(elementsAtCaret.toTypedArray(), null, editor) ?: error("Failed to create move model") configureMoveModel(moveModel) diff --git a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/K2CheckDescriptorMultiModuleMoveTest.kt b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/K2CheckDescriptorMultiModuleMoveTest.kt index 19de7ae34952..b0216262e7a9 100644 --- a/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/K2CheckDescriptorMultiModuleMoveTest.kt +++ b/plugins/kotlin/refactorings/kotlin.refactorings.move.k2/test/org/jetbrains/kotlin/idea/k2/refactoring/move/K2CheckDescriptorMultiModuleMoveTest.kt @@ -54,6 +54,20 @@ class K2CheckDescriptorMultiModuleMoveTest : AbstractK2CheckDescriptorMultiModul ) } + @TestMetadata("moveActualAndRegularFunctionConventionalSourceSetNames") + @Throws(Exception::class) + fun testMoveActualAndRegularFunctionConventionalSourceSetNames() { + doTest( + "moveActualAndRegularFunctionConventionalSourceSetNames/moveActualAndRegularFunctionConventionalSourceSetNames.test", + configureMoveModel = { moveModel -> + setAllMoveSettingsOn(moveModel) + setNewTargetPackageInSameRoot(moveModel, FqName("bar")) + setTargetFile(moveModel, "target.kt") + }, + checkMoveDescriptor = { moveOperationDescriptor -> checkNoMoveOutsideSourceRoot(moveOperationDescriptor) }, + ) + } + @TestMetadata("moveActualClass") @Throws(Exception::class) fun testMoveActualClass() { @@ -112,17 +126,28 @@ class K2CheckDescriptorMultiModuleMoveTest : AbstractK2CheckDescriptorMultiModul targetModel.directory = packageSubdirectory } + /** + * Imitates target file change through the file chooser. + */ + private fun setTargetFile(moveModel: K2MoveModel, fileName: String) { + val moveTarget = moveModel.target + if (moveTarget !is K2MoveTargetModel.FileChooser) + throw AssertionError("Unexpected move target model: ${moveTarget::class.simpleName}") + moveTarget.fileName = fileName + } + private fun checkNoMoveOutsideSourceRoot(moveOperationDescriptor: K2MoveOperationDescriptor<*>) { moveOperationDescriptor.moveDescriptors.forEach { descriptor -> - val element = descriptor.source.elements.singleOrNull() ?: error("Single source element to move is expected") - val elementSourceDir = element.containingFile.sourceRoot ?: error("Can't find source root for element") - val targetRoot = descriptor.target.baseDirectory.sourceRoot ?: error("Can't find source root for target") - assert(elementSourceDir == targetRoot) { - """Element was unexpectedly moved outside of its source root: + descriptor.source.elements.forEach { element -> + val elementSourceDir = element.containingFile.sourceRoot ?: error("Can't find source root for element") + val targetRoot = descriptor.target.baseDirectory.sourceRoot ?: error("Can't find source root for target") + assert(elementSourceDir == targetRoot) { + """Element was unexpectedly moved outside of its source root: |${element.text} |Element root: ${elementSourceDir.path} |Target root: ${targetRoot.path} |""".trimMargin() + } } } }