[kotlin] Port ExtractDeclarationFromCurrentFileIntention to K2

Also adds support for testing K2 multi file intentions. #KTIJ-22984 Fixed

GitOrigin-RevId: 72d5acee00c31c059184dcd68bc68fbd041eef49
This commit is contained in:
Bart van Helvert
2024-07-10 23:56:51 +00:00
committed by intellij-monorepo-bot
parent a8c764bc9c
commit d857a06b2d
15 changed files with 221 additions and 10 deletions
@@ -54,5 +54,6 @@
<orderEntry type="module" module-name="kotlin.refactorings.k2" />
<orderEntry type="module" module-name="kotlin.base.analysis" />
<orderEntry type="module" module-name="kotlin.base.facet" />
<orderEntry type="module" module-name="kotlin.refactorings.move.k2" />
</component>
</module>
@@ -170,6 +170,13 @@
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention</className>
<bundleName>messages.KotlinBundle</bundleName>
<categoryKey>group.names.kotlin</categoryKey>
</intentionAction>
<intentionAction>
<language>kotlin</language>
<className>org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ConvertForEachToForLoopIntention</className>
@@ -0,0 +1,118 @@
// 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.codeInsight.intention.LowPriorityAction
import com.intellij.codeInsight.navigation.activateFileWithPsiElement
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiFile
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.move.MoveCallback
import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
import org.jetbrains.kotlin.idea.codeinsight.api.classic.intentions.SelfTargetingRangeIntention
import org.jetbrains.kotlin.idea.core.moveCaret
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.idea.k2.refactoring.move.ui.K2MoveDialog
import org.jetbrains.kotlin.idea.k2.refactoring.move.ui.K2MoveModel
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
import org.jetbrains.kotlin.lexer.KtTokens
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class ExtractDeclarationFromCurrentFileIntention : SelfTargetingRangeIntention<KtClassOrObject>(
KtClassOrObject::class.java,
KotlinBundle.lazyMessage("intention.extract.declarations.from.file.text")
), LowPriorityAction {
override fun applicabilityRange(element: KtClassOrObject): TextRange? {
if (element.name == null) return null
if (element.parent !is KtFile) return null
if (element.hasModifier(KtTokens.PRIVATE_KEYWORD)) return null
if (element.containingKtFile.run { declarations.size == 1 || containingDirectory === null }) return null
val startOffset = when (element) {
is KtClass -> element.startOffset
is KtObjectDeclaration -> element.getObjectKeyword()?.startOffset
else -> return null
} ?: return null
val endOffset = element.nameIdentifier?.endOffset ?: return null
setTextGetter(
KotlinBundle.lazyMessage(
"intention.extract.declarations.from.file.text.details",
element.name.toString(),
0
)
)
return TextRange(startOffset, endOffset)
}
override fun startInWriteAction() = false
override fun applyTo(element: KtClassOrObject, editor: Editor?) {
requireNotNull(editor) { "This intention requires an editor" }
val containingFile = element.containingKtFile
val project = containingFile.project
val originalOffset = editor.caretModel.offset - element.startOffset
val directory = containingFile.containingDirectory ?: return
val packageName = containingFile.packageFqName
val targetFileName = "${element.name}.kt"
val targetFile = directory.findFile(targetFileName)
val moveCallBack = MoveCallback {
val newFile = directory.findFile(targetFileName) as KtFile
val newDeclaration = newFile.declarations.first()
activateFileWithPsiElement(newFile)
FileEditorManager.getInstance(project).selectedTextEditor?.moveCaret(newDeclaration.startOffset + originalOffset)
}
if (targetFile != null) {
if (isUnitTestMode()) {
throw CommonRefactoringUtil.RefactoringErrorHintException(RefactoringBundle.message("file.already.exist", targetFileName))
}
// If an automatic move is not possible, fall back to full-fledged Move Declarations refactoring
showRefactoringDialog(project, editor, element, targetFile, moveCallBack)
return
}
val moveDescriptor = K2MoveDescriptor.Declarations(
project,
K2MoveSourceDescriptor.ElementSource(setOf(element)),
K2MoveTargetDescriptor.File(targetFileName, packageName, directory)
)
K2MoveOperationDescriptor.Declarations(
project,
listOf(moveDescriptor),
searchForText = false,
searchInComments = false,
searchReferences = true,
dirStructureMatchesPkg = false,
moveCallBack
).refactoringProcessor().run()
}
private fun showRefactoringDialog(
project: Project,
editor: Editor,
element: KtClassOrObject,
targetFile: PsiFile,
callBack: MoveCallback
) {
val model = K2MoveModel.create(arrayOf(element), targetFile, editor, callBack) ?: return
val dialog = K2MoveDialog(project, model)
dialog.show()
}
}
@@ -0,0 +1,8 @@
// 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.intentions.tests
import org.jetbrains.kotlin.idea.intentions.AbstractMultiFileIntentionTest
abstract class AbstractK2MultiFileIntentionTest : AbstractMultiFileIntentionTest() {
override val classId: String = "intentionK2Class"
}
@@ -0,0 +1,57 @@
// 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.intentions.tests;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode;
import org.jetbrains.kotlin.idea.base.test.TestRoot;
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
/**
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
* DO NOT MODIFY MANUALLY.
*/
@SuppressWarnings("all")
@TestRoot("code-insight/intentions-k2/tests")
@TestDataPath("$CONTENT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile")
public class K2MultiFileIntentionTestGenerated extends AbstractK2MultiFileIntentionTest {
@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("moveClassToExistingFile/moveClassToExistingFile.test")
public void testMoveClassToExistingFile_MoveClassToExistingFile() throws Exception {
runTest("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test");
}
@TestMetadata("moveClassToFileInDefaultPackage/moveClassToFileInDefaultPackage.test")
public void testMoveClassToFileInDefaultPackage_MoveClassToFileInDefaultPackage() throws Exception {
runTest("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFileInDefaultPackage/moveClassToFileInDefaultPackage.test");
}
@TestMetadata("moveClassToFile/moveClassToFile.test")
public void testMoveClassToFile_MoveClassToFile() throws Exception {
runTest("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test");
}
@TestMetadata("moveSingleToFile/moveSingleToFile.test")
public void testMoveSingleToFile_MoveSingleToFile() throws Exception {
runTest("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test");
}
@TestMetadata("optimizeImports/optimizeImports.test")
public void testOptimizeImports_OptimizeImports() throws Exception {
runTest("../../../idea/tests/testData/multiFileIntentions/moveDeclarationToSeparateFile/optimizeImports/optimizeImports.test");
}
}
@@ -0,0 +1,7 @@
// 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.intentions
abstract class AbstractK1MultiFileIntentionTest : AbstractMultiFileIntentionTest() {
override val classId: String = "intentionClass"
}
@@ -1,4 +1,4 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// 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.intentions
@@ -27,6 +27,8 @@ import org.junit.Assert
import java.io.File
abstract class AbstractMultiFileIntentionTest : KotlinLightCodeInsightFixtureTestCase() {
abstract val classId: String
override fun getProjectDescriptor(): LightProjectDescriptor {
val testFile = File(testDataDirectory, fileName())
val config = JsonParser.parseString(FileUtil.loadFile(testFile, true)) as JsonObject
@@ -41,7 +43,7 @@ abstract class AbstractMultiFileIntentionTest : KotlinLightCodeInsightFixtureTes
val testFile = File(path)
val config = JsonParser.parseString(FileUtil.loadFile(testFile, true)) as JsonObject
val mainFilePath = config.getString("mainFile")
val intentionAction = Class.forName(config.getString("intentionClass")).getDeclaredConstructor().newInstance() as IntentionAction
val intentionAction = Class.forName(config.getString(classId)).getDeclaredConstructor().newInstance() as IntentionAction
val isApplicableExpected = config["isApplicable"]?.asBoolean ?: true
doTest(path) { rootDir ->
@@ -19,7 +19,7 @@ import org.junit.runner.RunWith;
@TestDataPath("$CONTENT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@TestMetadata("testData/multiFileIntentions")
public class MultiFileIntentionTestGenerated extends AbstractMultiFileIntentionTest {
public class K1MultiFileIntentionTestGenerated extends AbstractK1MultiFileIntentionTest {
@java.lang.Override
@org.jetbrains.annotations.NotNull
public final KotlinPluginMode getPluginMode() {
@@ -1,4 +1,5 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention"
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention",
"intentionK2Class": "org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention"
}
@@ -1,4 +1,5 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention"
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention",
"intentionK2Class": "org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention"
}
@@ -1,4 +1,5 @@
{
"mainFile": "test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention"
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention",
"intentionK2Class": "org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention"
}
@@ -1,5 +1,6 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention",
"intentionK2Class": "org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention",
"isApplicable": "false"
}
@@ -1,5 +1,6 @@
{
"mainFile": "test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ExtractDeclarationFromCurrentFileIntention",
"intentionK2Class": "org.jetbrains.kotlin.idea.k2.codeinsight.intentions.ExtractDeclarationFromCurrentFileIntention",
"isApplicable": "true"
}
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.fe10.testGenerator
import com.intellij.testFramework.TestIndexingModeSupporter.IndexingMode
@@ -82,7 +82,7 @@ import org.jetbrains.kotlin.idea.inspections.AbstractViewOfflineInspectionTest
import org.jetbrains.kotlin.idea.intentions.AbstractConcatenatedStringGeneratorTest
import org.jetbrains.kotlin.idea.intentions.AbstractK1IntentionTest
import org.jetbrains.kotlin.idea.intentions.AbstractK1IntentionTest2
import org.jetbrains.kotlin.idea.intentions.AbstractMultiFileIntentionTest
import org.jetbrains.kotlin.idea.intentions.AbstractK1MultiFileIntentionTest
import org.jetbrains.kotlin.idea.intentions.declarations.AbstractJoinLinesTest
import org.jetbrains.kotlin.idea.internal.AbstractBytecodeToolWindowMultiplatformTest
import org.jetbrains.kotlin.idea.internal.AbstractBytecodeToolWindowTest
@@ -823,7 +823,7 @@ private fun assembleWorkspace(): TWorkspace = workspace(KotlinPluginMode.K1) {
}
testGroup("idea/tests", category = INTENTIONS) {
testClass<AbstractMultiFileIntentionTest> {
testClass<AbstractK1MultiFileIntentionTest> {
model("multiFileIntentions", pattern = TEST, flatten = true)
}
@@ -1,11 +1,13 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// 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.fir.testGenerator.codeinsight
import org.jetbrains.kotlin.idea.k2.codeInsight.intentions.shared.AbstractSharedK2IntentionTest
import org.jetbrains.kotlin.idea.k2.intentions.tests.AbstractK2GotoTestOrCodeActionTest
import org.jetbrains.kotlin.idea.k2.intentions.tests.AbstractK2IntentionTest
import org.jetbrains.kotlin.idea.k2.intentions.tests.AbstractK2MultiFileIntentionTest
import org.jetbrains.kotlin.testGenerator.model.*
import org.jetbrains.kotlin.testGenerator.model.GroupCategory.*
import org.jetbrains.kotlin.testGenerator.model.Patterns.TEST
internal fun MutableTWorkspace.generateK2IntentionTests() {
@@ -183,6 +185,10 @@ internal fun MutableTWorkspace.generateK2IntentionTests() {
model("${idea}intentions/convertLambdaToMultiLine", pattern = pattern, isIgnored = true)
}
testClass<AbstractK2MultiFileIntentionTest> {
model("${idea}/multiFileIntentions/moveDeclarationToSeparateFile", pattern = TEST, flatten = true)
}
testClass<AbstractK2GotoTestOrCodeActionTest> {
model("${idea}navigation/gotoTestOrCode", pattern = Patterns.forRegex("^(.+)\\.main\\..+\$"))
}