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");