diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationsHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationsHandler.kt index 482e4aeb461..4602bdb04b5 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationsHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/copy/CopyKotlinDeclarationsHandler.kt @@ -10,11 +10,8 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Key -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiFileSystemItem +import com.intellij.openapi.vfs.* +import com.intellij.psi.* import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.searches.ReferencesSearch import com.intellij.refactoring.BaseRefactoringProcessor @@ -52,7 +49,7 @@ import org.jetbrains.kotlin.utils.ifEmpty class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { companion object { - private const val COMMAND_NAME = "Copy Declarations" + private val commandName = RefactoringBundle.message("copy.handler.copy.files.directories") private val isUnitTestMode get() = ApplicationManager.getApplication().isUnitTestMode @@ -133,7 +130,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { val answer = Messages.showOkCancelDialog( "File $targetFileName already exists in ${targetDirectory.virtualFile.path}", - COMMAND_NAME, + commandName, "Overwrite", "Cancel", Messages.getQuestionIcon() @@ -144,7 +141,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { val answer = Messages.showYesNoCancelDialog( "File $targetFileName already exists in ${targetDirectory.virtualFile.path}", - COMMAND_NAME, + commandName, "Append", "Overwrite", "Cancel", @@ -198,7 +195,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { if (singleNamedSourceElement !== null) { val dialog = CopyKotlinDeclarationDialog(singleNamedSourceElement, sourceData.initialTargetDirectory, sourceData.project) - dialog.title = COMMAND_NAME + dialog.title = commandName if (!dialog.showAndGet()) return null openInEditor = dialog.openInEditor @@ -246,14 +243,62 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { } } - private fun getTargetData(sourceData: SourceData) = - if (isUnitTestMode) getTargetDataForUnitTest(sourceData) - else getTargetDataForUX(sourceData) + private fun trackedCopyFiles(sourceFiles: Array, initialTargetDirectory: PsiDirectory?): Set { + val mapper = object : VirtualFileListener { + val filesCopied = mutableSetOf() + + override fun fileCopied(event: VirtualFileCopyEvent) { + filesCopied.add(event.file) + } + + override fun fileCreated(event: VirtualFileEvent) { + filesCopied.add(event.file) + } + } + + with(VirtualFileManager.getInstance()) { + try { + addVirtualFileListener(mapper) + copyFilesHandler.doCopy(sourceFiles, initialTargetDirectory) + } finally { + removeVirtualFileListener(mapper) + } + } + return mapper.filesCopied + } + + private fun doCopyFiles(filesToCopy: Array, initialTargetDirectory: PsiDirectory?) { + + if (filesToCopy.isEmpty()) return + + val project = filesToCopy.first().project + val psiManager = PsiManager.getInstance(project) + + project.executeCommand(commandName) { + val copiedFiles = trackedCopyFiles(filesToCopy, initialTargetDirectory) + + copiedFiles.forEach { copiedFile -> + val targetKtFile = psiManager.findFile(copiedFile) as? KtFile + if (targetKtFile !== null) { + runWriteAction { + if (!targetKtFile.packageMatchesDirectoryOrImplicit()) { + targetKtFile.containingDirectory?.getFqNameWithImplicitPrefix()?.let { targetDirectoryFqName -> + targetKtFile.packageFqName = targetDirectoryFqName + } + } + performDelayedRefactoringRequests(project) + } + } + } + } + } override fun doCopy(elements: Array, defaultTargetDirectory: PsiDirectory?) { + + if (elements.isEmpty()) return + if (!canCopyDeclarations(elements)) { - val sourceFiles = getSourceFiles(elements) ?: return - return copyFilesHandler.doCopy(sourceFiles, defaultTargetDirectory) + return doCopyFiles(elements, defaultTargetDirectory) } val elementsToCopy = elements.mapNotNull { it.getCopyableElement() } @@ -273,7 +318,9 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { originalFile = originalFile, initialTargetDirectory = initialTargetDirectory ) - val targetData = getTargetData(sourceData) ?: return + + val targetData = if (isUnitTestMode) getTargetDataForUnitTest(sourceData) else getTargetDataForUX(sourceData) + targetData ?: return val internalUsages = collectInternalUsages(sourceData, targetData) markInternalUsages(internalUsages) @@ -282,7 +329,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { project.checkConflictsInteractively(conflicts) { try { - project.executeCommand(COMMAND_NAME) { + project.executeCommand(commandName) { doRefactor(sourceData, targetData) } } finally { @@ -297,6 +344,10 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { val restoredInternalUsages: List? = null ) + private fun getTargetFileName(sourceData: SourceData, targetData: TargetData) = + if (targetData.newName.contains(".")) targetData.newName + else targetData.newName + "." + sourceData.originalFile.virtualFile.extension + private fun doRefactor(sourceData: SourceData, targetData: TargetData) { var refactoringResult: RefactoringResult? = null @@ -305,9 +356,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { targetData.targetDirWrapper.getOrCreateDirectory(sourceData.initialTargetDirectory) } - val targetFileName = - if (targetData.newName.contains(".")) targetData.newName - else targetData.newName + "." + sourceData.originalFile.virtualFile.extension + val targetFileName = getTargetFileName(sourceData, targetData) val isSingleDeclarationInFile = sourceData.singleElementToCopy is KtNamedDeclaration && @@ -322,7 +371,8 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { refactoringResult = if (fileToCopy !== null) { doRefactoringOnFile(fileToCopy, sourceData, targetDirectory, targetFileName, isSingleDeclarationInFile) } else { - val targetFile = getOrCreateTargetFile(sourceData.originalFile, targetDirectory, targetFileName) ?: return + val targetFile = getOrCreateTargetFile(sourceData.originalFile, targetDirectory, targetFileName) + ?: throw IncorrectOperationException("Could not create target file.") doRefactoringOnElement(sourceData, targetFile) } @@ -336,7 +386,7 @@ class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() { } if (targetData.openInEditor) { - EditorHelper.openFilesInEditor(arrayOf(refactoringResult.targetFile)) + EditorHelper.openInEditor(refactoringResult.targetFile) } } catch (e: IncorrectOperationException) { Messages.showMessageDialog(sourceData.project, e.message, RefactoringBundle.message("error.title"), Messages.getErrorIcon()) diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test.kt new file mode 100644 index 00000000000..e30b31b39b1 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test.kt @@ -0,0 +1,5 @@ +package bar + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test2.kt new file mode 100644 index 00000000000..e62c5217b0e --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/bar/test2.kt @@ -0,0 +1,5 @@ +package bar + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/boo/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/boo/test2.kt new file mode 100644 index 00000000000..a301d069c0d --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/boo/test2.kt @@ -0,0 +1,5 @@ +package boo + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/foo/test.kt new file mode 100644 index 00000000000..c2f32b2d8c3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/after/foo/test.kt @@ -0,0 +1,5 @@ +package foo + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/boo/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/boo/test2.kt new file mode 100644 index 00000000000..a301d069c0d --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/boo/test2.kt @@ -0,0 +1,5 @@ +package boo + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/foo/test.kt new file mode 100644 index 00000000000..c2f32b2d8c3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/before/foo/test.kt @@ -0,0 +1,5 @@ +package foo + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/copyMultipleFilesDifferentDirectoriesToNewDirectory.test b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/copyMultipleFilesDifferentDirectoriesToNewDirectory.test new file mode 100644 index 00000000000..fa94df72c4a --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/copyMultipleFilesDifferentDirectoriesToNewDirectory.test @@ -0,0 +1,5 @@ +{ + "filesToMove": ["foo/test.kt", "boo/test2.kt"], + "targetPackage": "bar", + "type": "COPY_FILES" +} diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test.kt new file mode 100644 index 00000000000..e30b31b39b1 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test.kt @@ -0,0 +1,5 @@ +package bar + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test2.kt new file mode 100644 index 00000000000..e62c5217b0e --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/bar/test2.kt @@ -0,0 +1,5 @@ +package bar + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test.kt new file mode 100644 index 00000000000..c2f32b2d8c3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test.kt @@ -0,0 +1,5 @@ +package foo + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test2.kt new file mode 100644 index 00000000000..0e2cff9128d --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/after/foo/test2.kt @@ -0,0 +1,5 @@ +package foo + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test.kt new file mode 100644 index 00000000000..c2f32b2d8c3 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test.kt @@ -0,0 +1,5 @@ +package foo + +class A { + val a: A = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test2.kt b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test2.kt new file mode 100644 index 00000000000..0e2cff9128d --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/before/foo/test2.kt @@ -0,0 +1,5 @@ +package foo + +class B { + val b: B = B() +} \ No newline at end of file diff --git a/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/copyMultipleFilesToNewDirectory.test b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/copyMultipleFilesToNewDirectory.test new file mode 100644 index 00000000000..255b9365960 --- /dev/null +++ b/idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/copyMultipleFilesToNewDirectory.test @@ -0,0 +1,5 @@ +{ + "filesToMove": ["foo/test.kt", "foo/test2.kt"], + "targetPackage": "bar", + "type": "COPY_FILES" +} 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 d64571e09c2..bc8b3cf43b1 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/AbstractCopyTest.kt @@ -14,6 +14,7 @@ import com.intellij.refactoring.PackageWrapper import com.intellij.refactoring.copy.CopyHandler import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.core.util.toPsiFile import org.jetbrains.kotlin.idea.jsonUtils.getNullableString import org.jetbrains.kotlin.idea.jsonUtils.getString import org.jetbrains.kotlin.idea.refactoring.AbstractMultifileRefactoringTest @@ -22,12 +23,20 @@ import org.jetbrains.kotlin.idea.refactoring.runRefactoringTest import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.utils.ifEmpty -abstract class AbstractCopyTest : AbstractMultifileRefactoringTest() { - companion object : RefactoringAction { - fun runCopyRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) { - runRefactoringTest(path, config, rootDir, project, this) - } +private enum class CopyAction : AbstractMultifileRefactoringTest.RefactoringAction { + COPY_FILES { + override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List, config: JsonObject) { + val project = mainFile.project + val elementsToCopy = config.getAsJsonArray("filesToMove").map { + val virtualFile = rootDir.findFileByRelativePath(it.asString)!! + if (virtualFile.isDirectory) virtualFile.toPsiDirectory(project)!! else virtualFile.toPsiFile(project)!! + } + DEFAULT.runRefactoring(rootDir, mainFile, elementsToCopy, config) + } + }, + + DEFAULT { override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List, config: JsonObject) { val project = mainFile.project @@ -37,16 +46,26 @@ abstract class AbstractCopyTest : AbstractMultifileRefactoringTest() { val targetDirectory = config.getNullableString("targetDirectory")?.let { rootDir.findFileByRelativePath(it)?.toPsiDirectory(project) } - ?: run { - val packageWrapper = PackageWrapper(mainFile.manager, config.getString("targetPackage")) - runWriteAction { MultipleRootsMoveDestination(packageWrapper).getTargetDirectory(mainFile) } - } + ?: run { + val packageWrapper = PackageWrapper(mainFile.manager, config.getString("targetPackage")) + runWriteAction { MultipleRootsMoveDestination(packageWrapper).getTargetDirectory(mainFile) } + } project.newName = config.getNullableString("newName") CopyHandler.doCopy(elementsToCopy, targetDirectory) } } +} + +abstract class AbstractCopyTest : AbstractMultifileRefactoringTest() { + + companion object { + fun runCopyRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) { + val action = config.getNullableString("type")?.let { CopyAction.valueOf(it) } ?: CopyAction.DEFAULT + runRefactoringTest(path, config, rootDir, project, action) + } + } override fun runRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) { runCopyRefactoring(path, config, rootDir, project) 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 99f072b810e..75a8c897ab0 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/copy/CopyTestGenerated.java @@ -143,6 +143,16 @@ public class CopyTestGenerated extends AbstractCopyTest { runTest("idea/testData/refactoring/copy/copyMultipleDeclarations/copyMultipleDeclarations.test"); } + @TestMetadata("copyMultipleFilesDifferentDirectoriesToNewDirectory/copyMultipleFilesDifferentDirectoriesToNewDirectory.test") + public void testCopyMultipleFilesDifferentDirectoriesToNewDirectory_CopyMultipleFilesDifferentDirectoriesToNewDirectory() throws Exception { + runTest("idea/testData/refactoring/copy/copyMultipleFilesDifferentDirectoriesToNewDirectory/copyMultipleFilesDifferentDirectoriesToNewDirectory.test"); + } + + @TestMetadata("copyMultipleFilesToNewDirectory/copyMultipleFilesToNewDirectory.test") + public void testCopyMultipleFilesToNewDirectory_CopyMultipleFilesToNewDirectory() throws Exception { + runTest("idea/testData/refactoring/copy/copyMultipleFilesToNewDirectory/copyMultipleFilesToNewDirectory.test"); + } + @TestMetadata("copyNestedClass/copyNestedClass.test") public void testCopyNestedClass_CopyNestedClass() throws Exception { runTest("idea/testData/refactoring/copy/copyNestedClass/copyNestedClass.test");