From 4c1c1a989a436646f17d67412e793e3f4c02440f Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Tue, 16 May 2017 12:54:08 +0300 Subject: [PATCH] Copy: Support multiple classes in the same file --- idea/src/META-INF/plugin.xml | 2 +- ...Handler.kt => CopyKotlinClassesHandler.kt} | 96 +++++++++++-------- .../move/AutocreatingPsiDirectoryWrapper.kt | 39 ++++++++ .../copy/copyMultiClassFile/after/bar/test.kt | 11 +++ .../copy/copyMultiClassFile/after/foo/test.kt | 11 +++ .../copyMultiClassFile/before/foo/test.kt | 11 +++ .../copyMultiClassFile.test | 4 + .../after/bar/test.kt | 19 ++++ .../after/foo/test.kt | 19 ++++ .../before/bar/test.kt | 5 + .../before/foo/test.kt | 19 ++++ .../copyMultipleClassesToExistingFile.test | 4 + .../after/bar/test.kt | 15 +++ .../after/foo/test.kt | 19 ++++ .../before/foo/test.kt | 19 ++++ .../copyMultipleClassesToNewFile.test | 4 + .../idea/refactoring/copy/AbstractCopyTest.kt | 2 +- .../refactoring/copy/CopyTestGenerated.java | 18 ++++ 18 files changed, 275 insertions(+), 42 deletions(-) rename idea/src/org/jetbrains/kotlin/idea/refactoring/copy/{CopyKotlinClassHandler.kt => CopyKotlinClassesHandler.kt} (66%) create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/AutocreatingPsiDirectoryWrapper.kt create mode 100644 idea/testData/refactoring/copy/copyMultiClassFile/after/bar/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultiClassFile/after/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultiClassFile/before/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/bar/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/bar/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/bar/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToNewFile/before/foo/test.kt create mode 100644 idea/testData/refactoring/copy/copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml index 1bfb2a32cf2..8610d5ed7c2 100644 --- a/idea/src/META-INF/plugin.xml +++ b/idea/src/META-INF/plugin.xml @@ -398,7 +398,7 @@ language="kotlin" /> { val classOrFile = parentsWithSelf.firstOrNull { it is KtFile || (it is KtClassOrObject && it.isTopLevel()) } return when (classOrFile) { - is KtFile -> classOrFile.declarations.singleOrNull() as? KtClassOrObject - is KtClassOrObject -> classOrFile - else -> null + is KtFile -> classOrFile.declarations.filterIsInstance() + is KtClassOrObject -> listOf(classOrFile) + else -> emptyList() } } - - private fun getClassToCopy(elements: Array) = elements.singleOrNull()?.getTopLevelClass() } - override fun canCopy(elements: Array, fromUpdate: Boolean) = getClassToCopy(elements) != null + override fun canCopy(elements: Array, fromUpdate: Boolean): Boolean { + return elements.flatMap { it.getTopLevelClasses().ifEmpty { return false } }.distinctBy { it.containingFile }.size == 1 + } enum class ExistingFilePolicy { APPEND, OVERWRITE, SKIP @@ -135,9 +135,12 @@ class CopyKotlinClassHandler : CopyHandlerDelegateBase() { } override fun doCopy(elements: Array, defaultTargetDirectory: PsiDirectory?) { - val classToCopy = getClassToCopy(elements) ?: return + val classesToCopy = elements.flatMap { it.getTopLevelClasses() } + if (classesToCopy.isEmpty()) return - val originalFile = classToCopy.containingKtFile + val singleClassToCopy = classesToCopy.singleOrNull() + + val originalFile = classesToCopy.first().containingKtFile val initialTargetDirectory = defaultTargetDirectory ?: originalFile.containingDirectory ?: return val project = initialTargetDirectory.project @@ -147,38 +150,45 @@ class CopyKotlinClassHandler : CopyHandlerDelegateBase() { val commandName = RefactoringBundle.message("copy.handler.copy.class") var openInEditor = false - var newClassName: String? = classToCopy.name - var moveDestination: MoveDestination? = null + var newName: String? = singleClassToCopy?.name ?: originalFile.name + var targetDirWrapper: AutocreatingPsiDirectoryWrapper = initialTargetDirectory.toDirectoryWrapper() if (!ApplicationManager.getApplication().isUnitTestMode) { - val dialog = CopyKotlinClassDialog(classToCopy, initialTargetDirectory, project) - dialog.title = commandName - if (!dialog.showAndGet()) return + if (singleClassToCopy != null) { + val dialog = CopyKotlinClassDialog(singleClassToCopy, initialTargetDirectory, project) + dialog.title = commandName + if (!dialog.showAndGet()) return - openInEditor = dialog.openInEditor - newClassName = dialog.className - moveDestination = dialog.targetDirectory ?: return + openInEditor = dialog.openInEditor + newName = dialog.className ?: singleClassToCopy.name + targetDirWrapper = dialog.targetDirectory?.toDirectoryWrapper() ?: return + } + else { + val dialog = CopyFilesOrDirectoriesDialog(arrayOf(originalFile), initialTargetDirectory, project, false) + if (!dialog.showAndGet()) return + openInEditor = dialog.openInEditor() + newName = dialog.newName + targetDirWrapper = dialog.targetDirectory?.toDirectoryWrapper() ?: return + } } else { - project.newName?.let { newClassName = it } + project.newName?.let { newName = it } } - if (newClassName.isNullOrEmpty()) return + if (singleClassToCopy != null && newName.isNullOrEmpty()) return val internalUsages = runReadAction { - val targetPackageName = moveDestination?.targetPackage?.qualifiedName - ?: initialTargetDirectory.getPackage()?.qualifiedName - ?: "" + val targetPackageName = targetDirWrapper.getPackageName() val changeInfo = ContainerChangeInfo( ContainerInfo.Package(originalFile.packageFqName), ContainerInfo.Package(FqName(targetPackageName)) ) - classToCopy - .getInternalReferencesToUpdateOnPackageNameChange(changeInfo) - .filter { - val referencedElement = (it as? MoveRenameUsageInfo)?.referencedElement - referencedElement == null || !classToCopy.isAncestor(referencedElement) - } + classesToCopy.flatMap { classToCopy -> + classToCopy.getInternalReferencesToUpdateOnPackageNameChange(changeInfo).filter { + val referencedElement = (it as? MoveRenameUsageInfo)?.referencedElement + referencedElement == null || !classToCopy.isAncestor(referencedElement) + } + } } markInternalUsages(internalUsages) @@ -186,22 +196,28 @@ class CopyKotlinClassHandler : CopyHandlerDelegateBase() { project.executeCommand(commandName) { try { - val targetDirectory = runWriteAction { moveDestination?.getTargetDirectory(initialTargetDirectory) ?: initialTargetDirectory } - val targetFileName = newClassName + "." + originalFile.virtualFile.extension + val targetDirectory = runWriteAction { targetDirWrapper.getOrCreateDirectory(initialTargetDirectory) } + val targetFileName = if (newName?.contains(".") ?: false) newName!! else newName + "." + originalFile.virtualFile.extension val targetFile = getOrCreateTargetFile(originalFile, targetDirectory, targetFileName, commandName) ?: return@executeCommand - val newClass = runWriteAction { - val newClass = targetFile.add(classToCopy.copy()) as KtClassOrObject - val oldToNewElementsMapping: Map = mapOf(classToCopy to newClass) - restoredInternalUsages += restoreInternalUsages(newClass, oldToNewElementsMapping, true) - postProcessMoveUsages(restoredInternalUsages, oldToNewElementsMapping) + val newClasses = runWriteAction { + val newClasses = classesToCopy.map { targetFile.add(it.copy()) as KtClassOrObject } + val oldToNewElementsMapping = classesToCopy.zip(newClasses).toMap() + + for (newClass in newClasses) { + restoredInternalUsages += restoreInternalUsages(newClass, oldToNewElementsMapping, true) + postProcessMoveUsages(restoredInternalUsages, oldToNewElementsMapping) + } + performDelayedRefactoringRequests(project) - newClass + newClasses } - RenameProcessor(project, newClass, newClassName!!.quoteIfNeeded(), false, false).run() + newClasses.singleOrNull()?.let { + RenameProcessor(project, it, newName!!.quoteIfNeeded(), false, false).run() + } if (openInEditor) { EditorHelper.openFilesInEditor(arrayOf(targetFile)) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/AutocreatingPsiDirectoryWrapper.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/AutocreatingPsiDirectoryWrapper.kt new file mode 100644 index 00000000000..042c473838a --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/AutocreatingPsiDirectoryWrapper.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.move + +import com.intellij.psi.PsiDirectory +import com.intellij.refactoring.MoveDestination +import org.jetbrains.kotlin.idea.core.getPackage + +sealed class AutocreatingPsiDirectoryWrapper { + class ByPsiDirectory(val psiDirectory: PsiDirectory) : AutocreatingPsiDirectoryWrapper() { + override fun getPackageName(): String = psiDirectory.getPackage()?.qualifiedName ?: "" + override fun getOrCreateDirectory(source: PsiDirectory) = psiDirectory + } + + class ByMoveDestination(val moveDestination: MoveDestination) : AutocreatingPsiDirectoryWrapper() { + override fun getPackageName() = moveDestination.targetPackage.qualifiedName + override fun getOrCreateDirectory(source: PsiDirectory) = moveDestination.getTargetDirectory(source) + } + + abstract fun getPackageName(): String + abstract fun getOrCreateDirectory(source: PsiDirectory): PsiDirectory +} + +fun MoveDestination.toDirectoryWrapper() = AutocreatingPsiDirectoryWrapper.ByMoveDestination(this) +fun PsiDirectory.toDirectoryWrapper() = AutocreatingPsiDirectoryWrapper.ByPsiDirectory(this) \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultiClassFile/after/bar/test.kt b/idea/testData/refactoring/copy/copyMultiClassFile/after/bar/test.kt new file mode 100644 index 00000000000..c52f950b8cf --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultiClassFile/after/bar/test.kt @@ -0,0 +1,11 @@ +package bar + +class A { + val a: A = A() + val b: B = B() +} + +class B { + val a: A = A() + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultiClassFile/after/foo/test.kt b/idea/testData/refactoring/copy/copyMultiClassFile/after/foo/test.kt new file mode 100644 index 00000000000..c6688a1d8c2 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultiClassFile/after/foo/test.kt @@ -0,0 +1,11 @@ +package foo + +class A { + val a: A = A() + val b: B = B() +} + +class B { + val a: A = A() + val b: B = B() +} diff --git a/idea/testData/refactoring/copy/copyMultiClassFile/before/foo/test.kt b/idea/testData/refactoring/copy/copyMultiClassFile/before/foo/test.kt new file mode 100644 index 00000000000..c6688a1d8c2 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultiClassFile/before/foo/test.kt @@ -0,0 +1,11 @@ +package foo + +class A { + val a: A = A() + val b: B = B() +} + +class B { + val a: A = A() + val b: B = B() +} diff --git a/idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test b/idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test new file mode 100644 index 00000000000..f169eabdfa3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test @@ -0,0 +1,4 @@ +{ + "mainFile": "foo/test.kt", + "targetPackage": "bar" +} diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/bar/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/bar/test.kt new file mode 100644 index 00000000000..bce14d66792 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/bar/test.kt @@ -0,0 +1,19 @@ +package bar + +import foo.B + +fun test() { + +} + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/foo/test.kt new file mode 100644 index 00000000000..a122f713c54 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/after/foo/test.kt @@ -0,0 +1,19 @@ +package foo + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class B { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/bar/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/bar/test.kt new file mode 100644 index 00000000000..1b3d83bdabb --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/bar/test.kt @@ -0,0 +1,5 @@ +package bar + +fun test() { + +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/foo/test.kt new file mode 100644 index 00000000000..f31f1b61697 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/before/foo/test.kt @@ -0,0 +1,19 @@ +package foo + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class B { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test new file mode 100644 index 00000000000..f169eabdfa3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test @@ -0,0 +1,4 @@ +{ + "mainFile": "foo/test.kt", + "targetPackage": "bar" +} diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/bar/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/bar/test.kt new file mode 100644 index 00000000000..aba70cab06b --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/bar/test.kt @@ -0,0 +1,15 @@ +package bar + +import foo.B + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/foo/test.kt new file mode 100644 index 00000000000..a122f713c54 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/after/foo/test.kt @@ -0,0 +1,19 @@ +package foo + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class B { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/before/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/before/foo/test.kt new file mode 100644 index 00000000000..f31f1b61697 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/before/foo/test.kt @@ -0,0 +1,19 @@ +package foo + +class A { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class B { + val a: A = A() + val b: B = B() + val c: C = C() +} + +class C { + val a: A = A() + val b: B = B() + val c: C = C() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test new file mode 100644 index 00000000000..f169eabdfa3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test @@ -0,0 +1,4 @@ +{ + "mainFile": "foo/test.kt", + "targetPackage": "bar" +} diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt index 9a67c57ab14..9893fa35f96 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt @@ -27,7 +27,7 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDest import org.jetbrains.kotlin.idea.jsonUtils.getNullableString import org.jetbrains.kotlin.idea.jsonUtils.getString import org.jetbrains.kotlin.idea.refactoring.AbstractMultifileRefactoringTest -import org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinClassHandler.Companion.newName +import org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinClassesHandler.Companion.newName import org.jetbrains.kotlin.idea.refactoring.runRefactoringTest import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.utils.ifEmpty diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java index 4d3ca5cb27d..215a778b97d 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java @@ -66,6 +66,24 @@ public class CopyTestGenerated extends AbstractCopyTest { doTest(fileName); } + @TestMetadata("copyMultiClassFile/copyMultiClassFile.test") + public void testCopyMultiClassFile_CopyMultiClassFile() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test"); + doTest(fileName); + } + + @TestMetadata("copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test") + public void testCopyMultipleClassesToExistingFile_CopyMultipleClassesToExistingFile() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMultipleClassesToExistingFile/copyMultipleClassesToExistingFile.test"); + doTest(fileName); + } + + @TestMetadata("copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test") + public void testCopyMultipleClassesToNewFile_CopyMultipleClassesToNewFile() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMultipleClassesToNewFile/copyMultipleClassesToNewFile.test"); + doTest(fileName); + } + @TestMetadata("copyNestedClass/copyNestedClass.test") public void testCopyNestedClass_CopyNestedClass() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyNestedClass/copyNestedClass.test");