diff --git a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml index 9037daa2d6c9..b197890aa8fb 100644 --- a/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml +++ b/plugins/kotlin/code-insight/intentions-k2/resources/kotlin.code-insight.intentions.k2.xml @@ -436,5 +436,12 @@ group.names.kotlin + + kotlin + org.jetbrains.kotlin.idea.k2.codeinsight.intentions.MoveMemberToTopLevelIntention + messages.KotlinBundle + group.names.kotlin + + diff --git a/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/MoveMemberToTopLevelIntention.kt b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/MoveMemberToTopLevelIntention.kt new file mode 100644 index 000000000000..de10bce3fe5c --- /dev/null +++ b/plugins/kotlin/code-insight/intentions-k2/src/org/jetbrains/kotlin/idea/k2/codeinsight/intentions/MoveMemberToTopLevelIntention.kt @@ -0,0 +1,60 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.kotlin.idea.k2.codeinsight.intentions + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.TextRange +import org.jetbrains.kotlin.idea.base.resources.KotlinBundle +import org.jetbrains.kotlin.idea.codeinsight.api.classic.intentions.SelfTargetingRangeIntention +import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveDescriptor +import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveOperationDescriptor +import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveSourceDescriptor +import org.jetbrains.kotlin.idea.k2.refactoring.move.descriptor.K2MoveTargetDescriptor +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.util.capitalizeDecapitalize.decapitalizeAsciiOnly + +class MoveMemberToTopLevelIntention : SelfTargetingRangeIntention( + elementType = KtNamedDeclaration::class.java, + textGetter = KotlinBundle.lazyMessage("move.to.top.level") +) { + override fun applicabilityRange(element: KtNamedDeclaration): TextRange? { + if (element !is KtNamedFunction && element !is KtProperty && element !is KtClassOrObject) return null + if (element.containingClassOrObject !is KtClassOrObject) return null + if (element is KtObjectDeclaration && element.isCompanion()) return null + return element.nameIdentifier?.textRange + } + + override fun startInWriteAction(): Boolean = false + + override fun applyTo(element: KtNamedDeclaration, editor: Editor?) { + val moveDescriptor = K2MoveDescriptor.Declarations( + element.project, + K2MoveSourceDescriptor.ElementSource(setOf(element)), + K2MoveTargetDescriptor.File(element.containingKtFile) + ) + + val containingClass = element.containingClassOrObject ?: return + val instanceName = containingClass.takeIf { + it !is KtObjectDeclaration && + (element !is KtClass || element.isInner()) + && element !is KtProperty + }?.name?.decapitalizeAsciiOnly() + + // This intention used to also delete containing objects if they are empty after moving the declaration out, but + // this seems rather dangerous and only rarely useful, so it is not enabled in K2. + val processor = K2MoveOperationDescriptor.NestedDeclarations( + element.project, + listOf(moveDescriptor), + searchForText = false, + searchInComments = false, + searchReferences = true, + dirStructureMatchesPkg = false, + outerInstanceParameterName = instanceName, + moveCallBack = { } + ).refactoringProcessor() + + // Need to set this for the conflict dialog to be shown + processor.setPrepareSuccessfulSwingThreadCallback { } + processor.run() + } +} \ No newline at end of file diff --git a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java index e327d993920f..7bec08ea89a2 100644 --- a/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java +++ b/plugins/kotlin/code-insight/intentions-k2/tests/test/org/jetbrains/kotlin/idea/k2/intentions/tests/K2IntentionTestGenerated.java @@ -10031,7 +10031,84 @@ public abstract class K2IntentionTestGenerated extends AbstractK2IntentionTest { } } + @RunWith(JUnit3RunnerWithInners.class) + @TestMetadata("../../../idea/tests/testData/intentions/moveMemberToTopLevel") + public static class MoveMemberToTopLevel extends AbstractK2IntentionTest { + @java.lang.Override + @org.jetbrains.annotations.NotNull + public final KotlinPluginMode getPluginMode() { + return KotlinPluginMode.K2; + } + private void runTest(String testDataFilePath) throws Exception { + KotlinTestUtils.runTest(this::doTest, this, testDataFilePath); + } + + @TestMetadata("abstractFunction.kt") + public void testAbstractFunction() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/abstractFunction.kt"); + } + + @TestMetadata("abstractProperty.kt") + public void testAbstractProperty() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/abstractProperty.kt"); + } + + @TestMetadata("function.kt") + public void testFunction() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/function.kt"); + } + + @TestMetadata("functionInCompanion.kt") + public void testFunctionInCompanion() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/functionInCompanion.kt"); + } + + @TestMetadata("property.kt") + public void testProperty() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/property.kt"); + } + + @TestMetadata("propertyInCompanion.kt") + public void testPropertyInCompanion() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/propertyInCompanion.kt"); + } + + @TestMetadata("redeclarationConflict.kt") + public void testRedeclarationConflict() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt"); + } + + @TestMetadata("redeclarationConflictK2.kt") + public void testRedeclarationConflictK2() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictK2.kt"); + } + + @TestMetadata("redeclarationConflictWithPackage.kt") + public void testRedeclarationConflictWithPackage() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt"); + } + + @TestMetadata("redeclarationConflictWithPackageK2.kt") + public void testRedeclarationConflictWithPackageK2() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackageK2.kt"); + } + + @TestMetadata("redeclarationPropertyConflict.kt") + public void testRedeclarationPropertyConflict() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt"); + } + + @TestMetadata("redeclarationPropertyConflictK2.kt") + public void testRedeclarationPropertyConflictK2() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflictK2.kt"); + } + + @TestMetadata("script.kts") + public void testScript() throws Exception { + runTest("../../../idea/tests/testData/intentions/moveMemberToTopLevel/script.kts"); + } + } @RunWith(JUnit3RunnerWithInners.class) @TestMetadata("../../../idea/tests/testData/intentions/anonymousFunctionToLambda") diff --git a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/intentions/K1IntentionTestGenerated.java b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/intentions/K1IntentionTestGenerated.java index 156a003276f5..1e50892488a2 100644 --- a/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/intentions/K1IntentionTestGenerated.java +++ b/plugins/kotlin/idea/tests/test/org/jetbrains/kotlin/idea/intentions/K1IntentionTestGenerated.java @@ -15338,16 +15338,31 @@ public abstract class K1IntentionTestGenerated extends AbstractK1IntentionTest { runTest("testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt"); } + @TestMetadata("redeclarationConflictK2.kt") + public void testRedeclarationConflictK2() throws Exception { + runTest("testData/intentions/moveMemberToTopLevel/redeclarationConflictK2.kt"); + } + @TestMetadata("redeclarationConflictWithPackage.kt") public void testRedeclarationConflictWithPackage() throws Exception { runTest("testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt"); } + @TestMetadata("redeclarationConflictWithPackageK2.kt") + public void testRedeclarationConflictWithPackageK2() throws Exception { + runTest("testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackageK2.kt"); + } + @TestMetadata("redeclarationPropertyConflict.kt") public void testRedeclarationPropertyConflict() throws Exception { runTest("testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt"); } + @TestMetadata("redeclarationPropertyConflictK2.kt") + public void testRedeclarationPropertyConflictK2() throws Exception { + runTest("testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflictK2.kt"); + } + @TestMetadata("script.kts") public void testScript() throws Exception { runTest("testData/intentions/moveMemberToTopLevel/script.kts"); diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/.k2Intention b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/.k2Intention new file mode 100644 index 000000000000..ec0282b1cb65 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/.k2Intention @@ -0,0 +1 @@ +org.jetbrains.kotlin.idea.k2.codeinsight.intentions.MoveMemberToTopLevelIntention \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/property.kt.after.k2 b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/property.kt.after.k2 new file mode 100644 index 000000000000..5cc1a88b8182 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/property.kt.after.k2 @@ -0,0 +1,6 @@ +// WITH_STDLIB + +object A { +} + +val foo: Int = 1 \ No newline at end of file diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt index 3bf8edd15c3d..8cdd616f060c 100644 --- a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflict.kt @@ -1,6 +1,7 @@ // SHOULD_FAIL_WITH: Package 'default' already contains function f1(Int) // AFTER-WARNING: Parameter 'n' is never used // AFTER-WARNING: Parameter 'n' is never used +// IGNORE_K2 object Test { fun f1(n: Int) {} } diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictK2.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictK2.kt new file mode 100644 index 000000000000..83a024b4376f --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictK2.kt @@ -0,0 +1,9 @@ +// SHOULD_FAIL_WITH: Following declarations would clash: to move function 'fun f1(n: Int)' and destination function 'fun f1(n: Int)' declared in scope default +// AFTER-WARNING: Parameter 'n' is never used +// AFTER-WARNING: Parameter 'n' is never used +// IGNORE_K1 +object Test { + fun f1(n: Int) {} +} + +fun f1(n: Int) {} diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt index 4e4e5bbbd041..d02c62eaac16 100644 --- a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackage.kt @@ -1,6 +1,7 @@ // SHOULD_FAIL_WITH: Package 'foo.bar' already contains function f1(Int) // AFTER-WARNING: Parameter 'n' is never used // AFTER-WARNING: Parameter 'n' is never used +// IGNORE_K2 package foo.bar object Test { diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackageK2.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackageK2.kt new file mode 100644 index 000000000000..1cd82fd89b6b --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationConflictWithPackageK2.kt @@ -0,0 +1,11 @@ +// SHOULD_FAIL_WITH: Following declarations would clash: to move function 'fun f1(n: Int)' and destination function 'fun f1(n: Int)' declared in scope foo.bar +// AFTER-WARNING: Parameter 'n' is never used +// AFTER-WARNING: Parameter 'n' is never used +// IGNORE_K1 +package foo.bar + +object Test { + fun f1(n: Int) {} +} + +fun f1(n: Int) {} diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt index d081c8145987..b60934523b19 100644 --- a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflict.kt @@ -1,4 +1,5 @@ // SHOULD_FAIL_WITH: Package 'default' already contains property f1 +// IGNORE_K2 object Test { val f1 = 1 } diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflictK2.kt b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflictK2.kt new file mode 100644 index 000000000000..bb2d3a28a192 --- /dev/null +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/redeclarationPropertyConflictK2.kt @@ -0,0 +1,7 @@ +// SHOULD_FAIL_WITH: Following declarations would clash: to move property 'val f1: Int' and destination property 'val f1: Int' declared in scope default +// IGNORE_K1 +object Test { + val f1 = 1 +} + +val f1 = 1 diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts index 35fba43f0d23..1a05aac73909 100644 --- a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts @@ -1,4 +1,5 @@ // RUNTIME_WITH_SCRIPT_RUNTIME +// IGNORE_K2 object Foo { fun foo() = 1 val a = 1 diff --git a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts.after b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts.after index f20263622cd2..d1c0dc3457a3 100644 --- a/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts.after +++ b/plugins/kotlin/idea/tests/testData/intentions/moveMemberToTopLevel/script.kts.after @@ -1,4 +1,5 @@ // RUNTIME_WITH_SCRIPT_RUNTIME +// IGNORE_K2 object Foo { val a = 1 }