From eca2707dbf5912bd40bcedd094a5bfe7c7669809 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Tue, 20 Aug 2019 12:05:35 +0300 Subject: [PATCH] Refactor of Move Refactoring The main purpose of this commit - to make this refactoring logic can be covered as much as possible. To make this possible we should to extract all possible UI code from business logic (and vice versa). Additional fixes was applied during refactoring process. There is come major steps made in this commit: 1) Extract business logic to separate models 2) Fixes of business models logic 3) Add "Delete empty source file" checkbox instead of modal message box 4) Improve error messaging in move refactoring dialogs 5) Inject interface into move handler that makes UI dialogs could be overrided 6) Inject flag into move handler that makes UI conflicts dialog could be overrided --- .../refactoring/KotlinRefactoringSettings.kt | 1 + ...ractDeclarationFromCurrentFileIntention.kt | 1 + ...linAwareMoveFilesOrDirectoriesProcessor.kt | 8 +- .../MoveKotlinDeclarationsHandler.kt | 91 ++- .../MoveKotlinDeclarationsHandlerActions.kt | 46 ++ .../MoveKotlinDeclarationsProcessor.kt | 8 +- .../MoveKotlinDeclarationsProcessor.kt.183 | 8 +- .../MoveToKotlinFileProcessor.kt | 8 +- .../RefactoringConflictsFoundException.kt | 8 + .../move/moveDeclarations/ui/Interfaces.kt | 16 + ...KotlinAwareMoveFilesOrDirectoriesDialog.kt | 113 ++-- .../KotlinAwareMoveFilesOrDirectoriesModel.kt | 112 ++++ ...otlinSelectNestedClassRefactoringDialog.kt | 7 +- .../ui/MoveKotlinNestedClassesDialog.java | 57 +- .../ui/MoveKotlinNestedClassesModel.kt | 69 +++ ...KotlinNestedClassesToUpperLevelDialog.java | 218 +------ ...oveKotlinNestedClassesToUpperLevelModel.kt | 204 +++++++ .../MoveKotlinTopLevelDeclarationsDialog.form | 11 +- .../MoveKotlinTopLevelDeclarationsDialog.java | 533 ++++-------------- .../ui/MoveKotlinTopLevelDeclarationsModel.kt | 270 +++++++++ .../KotlinMoveFilesOrDirectoriesHandler.kt | 37 +- .../idea/refactoring/move/moveFilesUtil.kt | 128 ----- .../kotlin/idea/refactoring/move/moveUtils.kt | 31 + 23 files changed, 1117 insertions(+), 868 deletions(-) create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/RefactoringConflictsFoundException.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/Interfaces.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesModel.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesModel.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelModel.kt create mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt delete mode 100644 idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesUtil.kt diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt index 66aa25ce33b..fe525821f0c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt @@ -43,6 +43,7 @@ class KotlinRefactoringSettings : PersistentStateComponent, usages: Array?): Boolean { + if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException() + return super.showConflicts(conflicts, usages) + } + private fun markPsiFiles(mark: PsiFile.() -> Unit) { fun PsiElement.doMark(mark: PsiFile.() -> Unit) { when (this) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt index f7bb6f330e8..6d5ad1171a1 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt @@ -31,7 +31,6 @@ import com.intellij.refactoring.util.CommonRefactoringUtil import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.canRefactor -import org.jetbrains.kotlin.idea.refactoring.move.invokeMoveFilesOrDirectoriesRefactoring import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinSelectNestedClassRefactoringDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesDialog @@ -43,7 +42,60 @@ import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext import org.jetbrains.kotlin.psi.psiUtil.isTopLevelInFileOrScript import java.util.* -class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { +private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerActions { + + override fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?) { + CommonRefactoringUtil.showErrorHint(project, editor, message, title, helpId) + } + + override fun invokeMoveKotlinNestedClassesRefactoring( + project: Project, + elementsToMove: List, + originalClass: KtClassOrObject, + targetClass: KtClassOrObject, + moveCallback: MoveCallback? + ) = MoveKotlinNestedClassesDialog(project, elementsToMove, originalClass, targetClass, moveCallback).show() + + override fun invokeMoveKotlinTopLevelDeclarationsRefactoring( + project: Project, + elementsToMove: Set, + targetPackageName: String, + targetDirectory: PsiDirectory?, + targetFile: KtFile?, + moveToPackage: Boolean, + searchInComments: Boolean, + searchForTextOccurrences: Boolean, + deleteEmptySourceFiles: Boolean, + moveCallback: MoveCallback? + ) = MoveKotlinTopLevelDeclarationsDialog( + project, + elementsToMove, + targetPackageName, + targetDirectory, + targetFile, + moveToPackage, + searchInComments, + searchForTextOccurrences, + deleteEmptySourceFiles, + moveCallback + ).show() + + override fun invokeKotlinSelectNestedClassChooser(nestedClass: KtClassOrObject, targetContainer: PsiElement?) = + KotlinSelectNestedClassRefactoringDialog.chooseNestedClassRefactoring(nestedClass, targetContainer) + + override fun invokeKotlinAwareMoveFilesOrDirectoriesRefactoring( + project: Project, + initialDirectory: PsiDirectory?, + elements: Array, + moveCallback: MoveCallback? + ) = KotlinAwareMoveFilesOrDirectoriesDialog(project, initialDirectory, elements, moveCallback).show() +} + +class MoveKotlinDeclarationsHandler internal constructor(private val handlerActions: MoveKotlinDeclarationsHandlerActions) : + MoveHandlerDelegate() { + + constructor() : this(defaultHandlerActions) + private fun getUniqueContainer(elements: Array): PsiElement? { val allTopLevel = elements.all { isTopLevelInFileOrScript(it) } @@ -72,7 +124,7 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { val container = getUniqueContainer(elements) if (container == null) { - CommonRefactoringUtil.showErrorHint( + handlerActions.showErrorHint( project, editor, "All declarations must belong to the same directory or class", MOVE_DECLARATIONS, null ) return false @@ -89,20 +141,20 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { // todo: allow moving companion object if (elementsToSearch.any { it is KtObjectDeclaration && it.isCompanion() }) { val message = RefactoringBundle.getCannotRefactorMessage("Move declaration is not supported for companion objects") - CommonRefactoringUtil.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) + handlerActions.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) return true } if (elementsToSearch.any { !it.canMove() }) { val message = RefactoringBundle.getCannotRefactorMessage("Move declaration is only supported for top-level declarations and nested classes") - CommonRefactoringUtil.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) + handlerActions.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) return true } if (elementsToSearch.any { it is KtEnumEntry }) { val message = RefactoringBundle.getCannotRefactorMessage("Move declaration is not supported for enum entries") - CommonRefactoringUtil.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) + handlerActions.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) return true } @@ -113,11 +165,10 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { else -> null } val initialTargetDirectory = MoveFilesOrDirectoriesUtil.resolveToDirectory(project, initialTargetElement) - val dialog = KotlinAwareMoveFilesOrDirectoriesDialog(project, initialTargetDirectory) { - invokeMoveFilesOrDirectoriesRefactoring(it, project, elements, initialTargetDirectory, callback) - } - dialog.setData(elements, initialTargetDirectory, "refactoring.moveFile") - dialog.show() + + handlerActions.invokeKotlinAwareMoveFilesOrDirectoriesRefactoring( + project, initialTargetDirectory, elements, callback + ) return true } @@ -128,12 +179,13 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { val targetDirectory = if (targetContainer != null) { MoveClassesOrPackagesImpl.getInitialTargetDirectory(targetContainer, elements) } else null - val searchInComments = KotlinRefactoringSettings.instance!!.MOVE_SEARCH_IN_COMMENTS - val searchInText = KotlinRefactoringSettings.instance!!.MOVE_SEARCH_FOR_TEXT + val searchInComments = KotlinRefactoringSettings.instance.MOVE_SEARCH_IN_COMMENTS + val searchInText = KotlinRefactoringSettings.instance.MOVE_SEARCH_FOR_TEXT + val deleteEmptySourceFiles = KotlinRefactoringSettings.instance.MOVE_DELETE_EMPTY_SOURCE_FILES val targetFile = targetContainer as? KtFile val moveToPackage = targetContainer !is KtFile - MoveKotlinTopLevelDeclarationsDialog( + handlerActions.invokeMoveKotlinTopLevelDeclarationsRefactoring( project, elementsToSearch, targetPackageName, @@ -142,8 +194,9 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { moveToPackage, searchInComments, searchInText, + deleteEmptySourceFiles, callback - ).show() + ) } is KtClassOrObject -> { @@ -152,20 +205,20 @@ class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { if (targetContainer !is KtClassOrObject) { val message = RefactoringBundle.getCannotRefactorMessage("Moving multiple nested classes to top-level is not supported") - CommonRefactoringUtil.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) + handlerActions.showErrorHint(project, editor, message, MOVE_DECLARATIONS, null) return true } @Suppress("UNCHECKED_CAST") - MoveKotlinNestedClassesDialog( + handlerActions.invokeMoveKotlinNestedClassesRefactoring( project, elementsToSearch.filterIsInstance(), container, targetContainer, callback - ).show() + ) return true } - KotlinSelectNestedClassRefactoringDialog.chooseNestedClassRefactoring( + handlerActions.invokeKotlinSelectNestedClassChooser( elementsToSearch.first() as KtClassOrObject, targetContainer ) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt new file mode 100644 index 00000000000..48da8a2d3bf --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.refactoring.move.MoveCallback +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedDeclaration + +internal interface MoveKotlinDeclarationsHandlerActions { + fun invokeMoveKotlinNestedClassesRefactoring( + project: Project, + elementsToMove: List, + originalClass: KtClassOrObject, + targetClass: KtClassOrObject, + moveCallback: MoveCallback? + ) + + fun invokeMoveKotlinTopLevelDeclarationsRefactoring( + project: Project, + elementsToMove: Set, + targetPackageName: String, + targetDirectory: PsiDirectory?, + targetFile: KtFile?, + moveToPackage: Boolean, + searchInComments: Boolean, + searchForTextOccurrences: Boolean, + deleteEmptySourceFiles: Boolean, + moveCallback: MoveCallback? + ) + + fun invokeKotlinSelectNestedClassChooser(nestedClass: KtClassOrObject, targetContainer: PsiElement?) + + fun invokeKotlinAwareMoveFilesOrDirectoriesRefactoring( + project: Project, initialDirectory: PsiDirectory?, elements: Array, moveCallback: MoveCallback? + ) + + fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?) +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt index 53befa2a9bb..abb76e8facd 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt @@ -131,7 +131,8 @@ private object ElementHashingStrategy : TObjectHashingStrategy { class MoveKotlinDeclarationsProcessor( val descriptor: MoveDeclarationsDescriptor, - val mover: Mover = Mover.Default + val mover: Mover = Mover.Default, + private val throwOnConflicts: Boolean = false ) : BaseRefactoringProcessor(descriptor.project) { companion object { private const val REFACTORING_NAME = "Move declarations" @@ -269,6 +270,11 @@ class MoveKotlinDeclarationsProcessor( return showConflicts(conflicts, refUsages.get()) } + override fun showConflicts(conflicts: MultiMap, usages: Array?): Boolean { + if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException() + return super.showConflicts(conflicts, usages) + } + override fun performRefactoring(usages: Array) = doPerformRefactoring(usages.toList()) internal fun doPerformRefactoring(usages: List) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt.183 b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt.183 index e8f7a4362dd..4ffc971b26e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt.183 +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsProcessor.kt.183 @@ -143,7 +143,8 @@ private object ElementHashingStrategy : TObjectHashingStrategy { class MoveKotlinDeclarationsProcessor( val descriptor: MoveDeclarationsDescriptor, - val mover: Mover = Mover.Default) : BaseRefactoringProcessor(descriptor.project) { + val mover: Mover = Mover.Default, + private val throwOnConflicts: Boolean = false) : BaseRefactoringProcessor(descriptor.project) { companion object { private val REFACTORING_NAME = "Move declarations" val REFACTORING_ID = "move.kotlin.declarations" @@ -296,6 +297,11 @@ class MoveKotlinDeclarationsProcessor( return showConflicts(conflicts, refUsages.get()) } + override fun showConflicts(conflicts: MultiMap, usages: Array?): Boolean { + if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException() + return super.showConflicts(conflicts, usages) + } + override fun performRefactoring(usages: Array) = doPerformRefactoring(usages.toList()) internal fun doPerformRefactoring(usages: List) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveToKotlinFileProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveToKotlinFileProcessor.kt index 702193785ed..52d696eca84 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveToKotlinFileProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveToKotlinFileProcessor.kt @@ -37,7 +37,8 @@ class MoveToKotlinFileProcessor @JvmOverloads constructor( searchInComments: Boolean, searchInNonJavaFiles: Boolean, moveCallback: MoveCallback?, - prepareSuccessfulCallback: Runnable = EmptyRunnable.INSTANCE + prepareSuccessfulCallback: Runnable = EmptyRunnable.INSTANCE, + private val throwOnConflicts: Boolean = false ) : MoveFilesOrDirectoriesProcessor( project, arrayOf(sourceFile), @@ -59,6 +60,11 @@ class MoveToKotlinFileProcessor @JvmOverloads constructor( return showConflicts(conflicts, usages) } + override fun showConflicts(conflicts: MultiMap, usages: Array?): Boolean { + if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException() + return super.showConflicts(conflicts, usages) + } + // Assign a temporary name to file-under-move to avoid naming conflict during the refactoring private fun renameFileTemporarily() { if (targetDirectory.findFile(targetFileName) == null) return diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/RefactoringConflictsFoundException.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/RefactoringConflictsFoundException.kt new file mode 100644 index 00000000000..6d9f933c547 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/RefactoringConflictsFoundException.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations + +internal class RefactoringConflictsFoundException : Exception() \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/Interfaces.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/Interfaces.kt new file mode 100644 index 00000000000..0d005b21b83 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/Interfaces.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.openapi.options.ConfigurationException + +internal interface Model { + @Throws(ConfigurationException::class) + fun computeModelResult(throwOnConflicts: Boolean = false): T + + @Throws(ConfigurationException::class) + fun computeModelResult(): T +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesDialog.kt index bc396e73565..96666e095b2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesDialog.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesDialog.kt @@ -5,49 +5,51 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui -import com.intellij.ide.util.DirectoryUtil import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserFactory -import com.intellij.openapi.help.HelpManager import com.intellij.openapi.keymap.KeymapUtil -import com.intellij.openapi.project.DumbService +import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.ui.Messages import com.intellij.openapi.ui.TextComponentAccessor import com.intellij.openapi.util.Disposer +import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.psi.* import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog +import com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler +import com.intellij.refactoring.move.MoveCallback +import com.intellij.refactoring.move.MoveHandler import com.intellij.refactoring.util.CommonRefactoringUtil import com.intellij.ui.NonFocusableCheckBox import com.intellij.ui.RecentsManager import com.intellij.ui.TextFieldWithHistoryWithBrowseButton import com.intellij.ui.components.JBLabelDecorator -import com.intellij.util.IncorrectOperationException import com.intellij.util.ui.FormBuilder import com.intellij.util.ui.UIUtil import org.jetbrains.kotlin.idea.core.packageMatchesDirectoryOrImplicit import org.jetbrains.kotlin.idea.core.util.onTextChange import org.jetbrains.kotlin.idea.refactoring.isInKotlinAwareSourceRoot +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinAwareMoveFilesOrDirectoriesProcessor import org.jetbrains.kotlin.idea.util.application.executeCommand -import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.psi.KtFile -import java.io.File import javax.swing.JComponent class KotlinAwareMoveFilesOrDirectoriesDialog( private val project: Project, private val initialDirectory: PsiDirectory?, - private val callback: (KotlinAwareMoveFilesOrDirectoriesDialog?) -> Unit + private val psiElements: Array, + private val callback: MoveCallback? ) : DialogWrapper(project, true) { + companion object { private const val RECENT_KEYS = "MoveFile.RECENT_KEYS" private const val MOVE_FILES_OPEN_IN_EDITOR = "MoveFile.OpenInEditor" + private const val HELP_ID = "refactoring.moveFile" } private val nameLabel = JBLabelDecorator.createJBLabelDecorator().setBold(true) @@ -56,26 +58,19 @@ class KotlinAwareMoveFilesOrDirectoriesDialog( private val openInEditorCb = NonFocusableCheckBox("Open moved files in editor") private val updatePackageDirectiveCb = NonFocusableCheckBox() - private var helpID: String? = null - var targetDirectory: PsiDirectory? = null - private set + override fun getHelpId() = HELP_ID init { title = RefactoringBundle.message("move.title") init() + initializeData() } - val updatePackageDirective: Boolean - get() = updatePackageDirectiveCb.isSelected - - val searchReferences: Boolean - get() = searchReferencesCb.isSelected - override fun createActions() = arrayOf(okAction, cancelAction, helpAction) override fun getPreferredFocusedComponent() = targetDirectoryField.childComponent - override fun createCenterPanel() = null + override fun createCenterPanel(): JComponent? = null override fun createNorthPanel(): JComponent { RecentsManager.getInstance(project).getRecentEntries(RECENT_KEYS)?.let { targetDirectoryField.childComponent.history = it } @@ -107,7 +102,7 @@ class KotlinAwareMoveFilesOrDirectoriesDialog( .panel } - fun setData(psiElements: Array, initialTargetDirectory: PsiDirectory?, helpID: String) { + private fun initializeData() { val psiElement = psiElements.singleOrNull() if (psiElement != null) { val shortenedPath = CopyFilesOrDirectoriesDialog.shortenPath((psiElement as PsiFileSystemItem).virtualFile) @@ -125,10 +120,9 @@ class KotlinAwareMoveFilesOrDirectoriesDialog( } } - targetDirectoryField.childComponent.text = initialTargetDirectory?.virtualFile?.presentableUrl ?: "" + targetDirectoryField.childComponent.text = initialDirectory?.virtualFile?.presentableUrl ?: "" validateOKButton() - this.helpID = helpID with(updatePackageDirectiveCb) { val jetFiles = psiElements.filterIsInstance().filter(KtFile::isInKotlinAwareSourceRoot) @@ -144,8 +138,6 @@ class KotlinAwareMoveFilesOrDirectoriesDialog( } } - override fun doHelpAction() = HelpManager.getInstance().invokeHelp(helpID) - private fun isOpenInEditor(): Boolean { if (ApplicationManager.getApplication().isUnitTestMode) return false return PropertiesComponent.getInstance().getBoolean(MOVE_FILES_OPEN_IN_EDITOR, false) @@ -155,41 +147,54 @@ class KotlinAwareMoveFilesOrDirectoriesDialog( isOKActionEnabled = targetDirectoryField.childComponent.text.isNotEmpty() } - override fun doOKAction() { - PropertiesComponent.getInstance().setValue(MOVE_FILES_OPEN_IN_EDITOR, openInEditorCb.isSelected, false) - RecentsManager.getInstance(project).registerRecentEntry(RECENT_KEYS, targetDirectoryField.childComponent.text) + private val selectedDirectoryName: String + get() = targetDirectoryField.childComponent.text.let { + when { + it.startsWith(".") -> (initialDirectory?.virtualFile?.path ?: "") + "/" + it + else -> it + } + } - if (DumbService.isDumb(project)) { - Messages.showMessageDialog(project, "Move refactoring is not available while indexing is in progress", "Indexing", null) + private fun getModel(): Model { + + val directory = LocalFileSystem.getInstance().findFileByPath(selectedDirectoryName)?.let { + PsiManager.getInstance(project).findDirectory(it) + } + + val elementsToMove: List = directory?.let { existentDirectory -> + val choice = if (psiElements.size > 1 || psiElements[0] is PsiDirectory) intArrayOf(-1) else null + psiElements.filterNot { + it is PsiFile && CopyFilesOrDirectoriesHandler.checkFileExist(existentDirectory, choice, it, it.name, "Move") + } + } ?: psiElements.toList() + + return KotlinAwareMoveFilesOrDirectoriesModel( + project, + elementsToMove, + selectedDirectoryName, + updatePackageDirectiveCb.isSelected, + searchReferencesCb.isSelected, + callback + ) + } + + override fun doOKAction() { + + val processor: KotlinAwareMoveFilesOrDirectoriesProcessor + try { + processor = getModel().computeModelResult() + } catch (e: ConfigurationException) { + setErrorText(e.message) return } - project.executeCommand(RefactoringBundle.message("move.title"), null) { - runWriteAction { - val directoryName = targetDirectoryField.childComponent.text.replace(File.separatorChar, '/').let { - when { - it.startsWith(".") -> (initialDirectory?.virtualFile?.path ?: "") + "/" + it - else -> it - } - } - try { - targetDirectory = DirectoryUtil.mkdirs(PsiManager.getInstance(project), directoryName) - } catch (e: IncorrectOperationException) { - // ignore - } - } - - if (targetDirectory == null) { - CommonRefactoringUtil.showErrorMessage( - title, - RefactoringBundle.message("cannot.create.directory"), - helpID, - project - ) - return@executeCommand - } - - callback(this@KotlinAwareMoveFilesOrDirectoriesDialog) + project.executeCommand(MoveHandler.REFACTORING_NAME) { + processor.run() } + + PropertiesComponent.getInstance().setValue(MOVE_FILES_OPEN_IN_EDITOR, openInEditorCb.isSelected, false) + RecentsManager.getInstance(project).registerRecentEntry(RECENT_KEYS, targetDirectoryField.childComponent.text) + + close(OK_EXIT_CODE, true) } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesModel.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesModel.kt new file mode 100644 index 00000000000..ed313f7d19f --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinAwareMoveFilesOrDirectoriesModel.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.refactoring.move.MoveCallback +import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil +import com.intellij.util.IncorrectOperationException +import org.jetbrains.kotlin.idea.refactoring.isInKotlinAwareSourceRoot +import org.jetbrains.kotlin.idea.refactoring.move.getOrCreateDirectory +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinAwareMoveFilesOrDirectoriesProcessor +import org.jetbrains.kotlin.idea.refactoring.move.updatePackageDirective +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import java.nio.file.InvalidPathException +import java.nio.file.Path +import java.nio.file.Paths + +class KotlinAwareMoveFilesOrDirectoriesModel( + val project: Project, + val elementsToMove: List, + val targetDirectoryName: String, + val updatePackageDirective: Boolean, + val searchReferences: Boolean, + val moveCallback: MoveCallback? +) : Model { + + private fun checkedGetElementsToMove(selectedDirectory: PsiDirectory): List { + + val preparedElementsToMove = elementsToMove + .filterNot { it is PsiFile && it.containingDirectory == selectedDirectory } + .sortedWith( // process Kotlin files first so that light classes are updated before changing references in Java files + java.util.Comparator { o1, o2 -> + when { + o1 is KtElement && o2 !is KtElement -> -1 + o1 !is KtElement && o2 is KtElement -> 1 + else -> 0 + } + }) + + try { + preparedElementsToMove.forEach { + MoveFilesOrDirectoriesUtil.checkMove(it, selectedDirectory) + if (it is KtFile && it.isInKotlinAwareSourceRoot()) { + it.updatePackageDirective = updatePackageDirective + } + } + } catch (e: IncorrectOperationException) { + throw ConfigurationException(e.message) + } + + return preparedElementsToMove + } + + private fun checkedGetTargetDirectory(): PsiDirectory { + try { + return getOrCreateDirectory(targetDirectoryName, project) + } catch (e: IncorrectOperationException) { + throw ConfigurationException("Cannot create target directory $targetDirectoryName") + } + } + + @Throws(ConfigurationException::class) + override fun computeModelResult() = computeModelResult(throwOnConflicts = false) + + private fun checkModel() { + + elementsToMove.firstOrNull { it !is PsiFile && it !is PsiDirectory }?.let { + throw ConfigurationException("Unexpected element type: $it") + } + + if (elementsToMove.isEmpty()) { + throw ConfigurationException("There is no given files to move") + } + + try { + Paths.get(targetDirectoryName) + } catch (e: InvalidPathException) { + throw ConfigurationException("Invalid target path $targetDirectoryName") + } + + if (DumbService.isDumb(project)) { + throw ConfigurationException("Move refactoring is not available while indexing is in progress") + } + } + + @Throws(ConfigurationException::class) + override fun computeModelResult(throwOnConflicts: Boolean): KotlinAwareMoveFilesOrDirectoriesProcessor { + + checkModel() + + val selectedDir = checkedGetTargetDirectory() + + return KotlinAwareMoveFilesOrDirectoriesProcessor( + project, + checkedGetElementsToMove(selectedDir), + selectedDir, + searchReferences = searchReferences, + searchInComments = false, + searchInNonJavaFiles = false, + moveCallback = moveCallback + ) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt index 0344bfe3c6c..024c94d174b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/KotlinSelectNestedClassRefactoringDialog.kt @@ -104,7 +104,12 @@ internal class KotlinSelectNestedClassRefactoringDialog private constructor( } nestedClass is KtEnumEntry -> return else -> { - val selectionDialog = KotlinSelectNestedClassRefactoringDialog(project, nestedClass, targetContainer) + val selectionDialog = + KotlinSelectNestedClassRefactoringDialog( + project, + nestedClass, + targetContainer + ) selectionDialog.show() if (selectionDialog.exitCode != OK_EXIT_CODE) return selectionDialog.getNextDialog() ?: return diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java index 3062e75a143..728c22f3610 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesDialog.java @@ -15,7 +15,6 @@ import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.search.GlobalSearchScope; -import com.intellij.psi.util.PsiTreeUtil; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; import com.intellij.refactoring.classMembers.MemberInfoChange; @@ -38,7 +37,6 @@ import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTab import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinTypeReferenceEditorComboWithBrowseButton; import org.jetbrains.kotlin.psi.*; - import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import java.awt.*; @@ -82,6 +80,8 @@ public class MoveKotlinNestedClassesDialog extends RefactoringDialog { initMemberInfo(elementsToMove); validateButtons(); + + myHelpAction.setEnabled(false); } private void initClassChooser(@NotNull KtClassOrObject initialTargetClass) { @@ -217,40 +217,29 @@ public class MoveKotlinNestedClassesDialog extends RefactoringDialog { return "#" + getClass().getName(); } - @Override - protected void canRun() throws ConfigurationException { - if (targetClass == null) throw new ConfigurationException("No destination class specified"); - - if (!(targetClass instanceof KtClassOrObject)) throw new ConfigurationException("Destination class must be a Kotlin class"); - - if (originalClass == targetClass) { - throw new ConfigurationException(RefactoringBundle.message("source.and.destination.classes.should.be.different")); - } - - for (KtClassOrObject classOrObject : getSelectedElementsToMove()) { - if (PsiTreeUtil.isAncestor(classOrObject, targetClass, false)) { - throw new ConfigurationException("Cannot move nested class " + classOrObject.getName() + " to itself"); - } - } + private Model getModel() { + return new MoveKotlinNestedClassesModel( + myProject, + openInEditorCheckBox.isSelected(), + getSelectedElementsToMove(), + originalClass, + targetClass, + moveCallback); } @Override protected void doAction() { - List elementsToMove = getSelectedElementsToMove(); - KotlinMoveTarget target = new KotlinMoveTargetForExistingElement((KtClassOrObject) targetClass); - MoveDeclarationsDelegate.NestedClass delegate = new MoveDeclarationsDelegate.NestedClass(); - MoveDeclarationsDescriptor descriptor = new MoveDeclarationsDescriptor( - myProject, - MoveKotlinDeclarationsProcessorKt.MoveSource(elementsToMove), - target, - delegate, - false, - false, - false, - moveCallback, - openInEditorCheckBox.isSelected() - ); - invokeRefactoring(new MoveKotlinDeclarationsProcessor(descriptor, Mover.Default.INSTANCE)); + + MoveKotlinDeclarationsProcessor processor; + try { + processor = getModel().computeModelResult(); + } + catch (ConfigurationException e) { + setErrorText(e.getMessage()); + return; + } + + invokeRefactoring(processor); } @Override @@ -258,7 +247,5 @@ public class MoveKotlinNestedClassesDialog extends RefactoringDialog { return targetClassChooser.getChildComponent(); } - private static class MemberInfoModelImpl extends AbstractMemberInfoModel { - - } + private static class MemberInfoModelImpl extends AbstractMemberInfoModel { } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesModel.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesModel.kt new file mode 100644 index 00000000000..cb74805fc37 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesModel.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.move.MoveCallback +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.* +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtElement + +internal class MoveKotlinNestedClassesModel( + val project: Project, + val openInEditorCheckBox: Boolean, + val selectedElementsToMove: List, + val originalClass: KtClassOrObject, + val targetClass: PsiElement?, + val moveCallback: MoveCallback? +) : Model { + + private fun getCheckedTargetClass(): KtElement { + val targetClass = this.targetClass ?: throw ConfigurationException("No destination class specified") + + if (targetClass !is KtClassOrObject){ + throw ConfigurationException("Destination class must be a Kotlin class") + } + + if (originalClass === targetClass) { + throw ConfigurationException(RefactoringBundle.message("source.and.destination.classes.should.be.different")) + } + + for (classOrObject in selectedElementsToMove) { + if (PsiTreeUtil.isAncestor(classOrObject, targetClass, false)) { + throw ConfigurationException("Cannot move nested class ${classOrObject.name} to itself") + } + } + + return targetClass + } + + @Throws(ConfigurationException::class) + override fun computeModelResult() = computeModelResult(throwOnConflicts = false) + + @Throws(ConfigurationException::class) + override fun computeModelResult(throwOnConflicts: Boolean): MoveKotlinDeclarationsProcessor { + val elementsToMove = selectedElementsToMove + val target = KotlinMoveTargetForExistingElement(getCheckedTargetClass()) + val delegate = MoveDeclarationsDelegate.NestedClass() + val descriptor = MoveDeclarationsDescriptor( + project, + MoveSource(elementsToMove), + target, + delegate, + searchInCommentsAndStrings = false, + searchInNonCode = false, + deleteSourceFiles = false, + moveCallback = moveCallback, + openInEditor = openInEditorCheckBox + ) + + return MoveKotlinDeclarationsProcessor(descriptor, Mover.Default, throwOnConflicts) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java index 9ce4cef6421..50f0703c266 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java @@ -5,53 +5,35 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ProjectRootManager; -import com.intellij.openapi.util.Comparing; -import com.intellij.openapi.util.NullableComputable; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiElement; import com.intellij.refactoring.HelpID; -import com.intellij.refactoring.PackageWrapper; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.move.MoveDialogBase; -import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil; -import com.intellij.refactoring.move.moveInner.MoveInnerImpl; import com.intellij.refactoring.ui.NameSuggestionsField; import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo; -import com.intellij.refactoring.util.CommonRefactoringUtil; -import com.intellij.refactoring.util.RefactoringMessageUtil; -import com.intellij.refactoring.util.RefactoringUtil; import com.intellij.ui.EditorTextField; import com.intellij.util.ArrayUtil; -import com.intellij.util.IncorrectOperationException; import kotlin.jvm.functions.Function1; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.descriptors.ClassDescriptor; -import org.jetbrains.kotlin.descriptors.ClassifierDescriptor; -import org.jetbrains.kotlin.idea.KotlinFileType; import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils; import org.jetbrains.kotlin.idea.core.CollectingNameValidator; import org.jetbrains.kotlin.idea.core.KotlinNameSuggester; import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator; -import org.jetbrains.kotlin.idea.core.PackageUtilsKt; import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings; -import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt; import org.jetbrains.kotlin.idea.refactoring.move.MoveUtilsKt; -import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*; -import org.jetbrains.kotlin.incremental.components.NoLookupLocation; +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsProcessor; import org.jetbrains.kotlin.name.FqName; -import org.jetbrains.kotlin.name.Name; -import org.jetbrains.kotlin.psi.*; -import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt; -import org.jetbrains.kotlin.resolve.DescriptorUtils; +import org.jetbrains.kotlin.psi.KtClass; +import org.jetbrains.kotlin.psi.KtClassBody; +import org.jetbrains.kotlin.psi.KtClassOrObject; import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode; import org.jetbrains.kotlin.types.KotlinType; -import static org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt.getSuitableDestinationSourceRoots; import javax.swing.*; import java.awt.*; @@ -96,16 +78,6 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { openInEditorPanel.add(initOpenInEditorCb(), BorderLayout.EAST); } - @Nullable - private static FqName getTargetPackageFqName(PsiElement targetContainer) { - if (targetContainer instanceof PsiDirectory) { - PsiPackage targetPackage = PackageUtilsKt.getPackage((PsiDirectory) targetContainer); - return targetPackage != null ? new FqName(targetPackage.getQualifiedName()) : null; - } - if (targetContainer instanceof KtFile) return ((KtFile) targetContainer).getPackageFqName(); - return null; - } - private void createUIComponents() { parameterField = new NameSuggestionsField(project); packageNameField = new PackageNameReferenceEditorCombo("", project, RECENTS_KEY, @@ -127,14 +99,6 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { return "Open moved member in editor"; } - public boolean isSearchInComments() { - return searchInCommentsCheckBox.isSelected(); - } - - public boolean isSearchInNonJavaFiles() { - return searchForTextOccurrencesCheckBox.isSelected(); - } - public String getClassName() { return classNameField.getText().trim(); } @@ -150,7 +114,7 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { @Nullable private FqName getTargetPackageFqName() { - return getTargetPackageFqName(targetContainer); + return MoveUtilsKt.getTargetPackageFqName(targetContainer); } @NotNull @@ -236,127 +200,30 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { return null; } - @Nullable - private PsiElement getTargetContainer() { - if (targetContainer instanceof PsiDirectory) { - PsiDirectory psiDirectory = (PsiDirectory) targetContainer; - FqName oldPackageFqName = getTargetPackageFqName(); - String targetName = packageNameField.getText(); - if (!Comparing.equal(oldPackageFqName != null ? oldPackageFqName.asString() : null, targetName)) { - ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project); - - List contentSourceRoots = getSuitableDestinationSourceRoots(project); - PackageWrapper newPackage = new PackageWrapper(PsiManager.getInstance(project), targetName); - VirtualFile targetSourceRoot; - - if (contentSourceRoots.size() > 1) { - PsiDirectory initialDir = null; - PsiPackage oldPackage = oldPackageFqName != null - ? JavaPsiFacade.getInstance(project).findPackage(oldPackageFqName.asString()) - : null; - if (oldPackage != null) { - PsiDirectory[] directories = oldPackage.getDirectories(); - VirtualFile root = projectRootManager.getFileIndex().getContentRootForFile(psiDirectory.getVirtualFile()); - for (PsiDirectory dir : directories) { - if (Comparing.equal(projectRootManager.getFileIndex().getContentRootForFile(dir.getVirtualFile()), root)) { - initialDir = dir; - } - } - } - VirtualFile sourceRoot = MoveClassesOrPackagesUtil.chooseSourceRoot(newPackage, contentSourceRoots, initialDir); - if (sourceRoot == null) return null; - targetSourceRoot = sourceRoot; - } - else { - targetSourceRoot = contentSourceRoots.get(0); - } - PsiDirectory dir = RefactoringUtil.findPackageDirectoryInSourceRoot(newPackage, targetSourceRoot); - if (dir == null) { - dir = ApplicationManager.getApplication().runWriteAction((NullableComputable) () -> { - try { - return RefactoringUtil.createPackageDirectoryInSourceRoot(newPackage, targetSourceRoot); - } - catch (IncorrectOperationException e) { - return null; - } - }); - } - return dir; - } - - return targetContainer; - } - - if (targetContainer instanceof KtFile || targetContainer instanceof KtClassOrObject) return targetContainer; - - return null; + private Model getModel() { + return new MoveKotlinNestedClassesToUpperLevelModel( + project, + innerClass, + targetContainer, + getParameterName(), + getClassName(), + passOuterClassCheckBox.isSelected(), + searchInCommentsCheckBox.isSelected(), + searchForTextOccurrencesCheckBox.isSelected(), + packageNameField.getText(), + isOpenInEditor()); } - @Nullable - private PsiElement getTargetContainerWithValidation() throws ConfigurationException { - String className = getClassName(); - String parameterName = getParameterName(); - - if (className != null && className.isEmpty()) { - throw new ConfigurationException(RefactoringBundle.message("no.class.name.specified")); - } - if (!KtPsiUtilKt.isIdentifier(className)) { - throw new ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(className)); - } - - if (passOuterClassCheckBox.isSelected()) { - if (parameterName != null && parameterName.isEmpty()) { - throw new ConfigurationException(RefactoringBundle.message("no.parameter.name.specified")); - } - if (!KtPsiUtilKt.isIdentifier(parameterName)) { - throw new ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(parameterName)); - } - } - - PsiElement targetContainer = getTargetContainer(); - - if (targetContainer instanceof KtClassOrObject) { - KtClassOrObject targetClass = (KtClassOrObject) targetContainer; - for (KtDeclaration member : targetClass.getDeclarations()) { - if (member instanceof KtClassOrObject && className != null && className.equals(member.getName())) { - throw new ConfigurationException(RefactoringBundle.message("inner.class.exists", className, targetClass.getName())); - } - } - } - - if (targetContainer instanceof PsiDirectory || targetContainer instanceof KtFile) { - FqName targetPackageFqName = getTargetPackageFqName(); - if (targetPackageFqName == null) throw new ConfigurationException("No package corresponds to this directory"); - - //noinspection ConstantConditions - ClassifierDescriptor existingClass = DescriptorUtils - .getContainingModule(innerClassDescriptor) - .getPackage(targetPackageFqName) - .getMemberScope() - .getContributedClassifier(Name.identifier(className), NoLookupLocation.FROM_IDE); - if (existingClass != null) { - throw new ConfigurationException("Class " + className + " already exists in package " + targetPackageFqName); - } - - PsiDirectory targetDir = targetContainer instanceof PsiDirectory - ? (PsiDirectory) targetContainer - : targetContainer.getContainingFile().getContainingDirectory(); - String message = RefactoringMessageUtil.checkCanCreateFile(targetDir, className + ".kt"); - if (message != null) throw new ConfigurationException(message); - } - - return targetContainer; - } @Override protected void doAction() { - PsiElement target; + + MoveKotlinDeclarationsProcessor processor; try { - target = getTargetContainerWithValidation(); - if (target == null) return; + processor = getModel().computeModelResult(); } catch (ConfigurationException e) { - CommonRefactoringUtil.showErrorMessage(MoveInnerImpl.REFACTORING_NAME, e.getMessage(), HelpID.MOVE_INNER_UPPER, project); + setErrorText(e.getMessage()); return; } @@ -364,45 +231,8 @@ public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { settings.MOVE_TO_UPPER_LEVEL_SEARCH_FOR_TEXT = searchForTextOccurrencesCheckBox.isSelected(); settings.MOVE_TO_UPPER_LEVEL_SEARCH_IN_COMMENTS = searchInCommentsCheckBox.isSelected(); - String newClassName = getClassName(); - - KotlinMoveTarget moveTarget; - if (target instanceof PsiDirectory) { - PsiDirectory targetDir = (PsiDirectory) target; - - FqName targetPackageFqName = getTargetPackageFqName(target); - if (targetPackageFqName == null) return; - - String targetFileName = KotlinNameSuggester.INSTANCE.suggestNameByName( - newClassName, - s -> targetDir.findFile(s + "." + KotlinFileType.EXTENSION) == null - ) + "." + KotlinFileType.EXTENSION; - moveTarget = new KotlinMoveTargetForDeferredFile( - targetPackageFqName, - targetDir, - null, - originalFile -> KotlinRefactoringUtilKt.createKotlinFile(targetFileName, targetDir, targetPackageFqName.asString()) - ); - } - else { - moveTarget = new KotlinMoveTargetForExistingElement((KtElement) target); - } - - String outerInstanceParameterName = passOuterClassCheckBox.isSelected() ? getParameterName() : null; - MoveDeclarationsDelegate delegate = new MoveDeclarationsDelegate.NestedClass(newClassName, outerInstanceParameterName); - MoveDeclarationsDescriptor moveDescriptor = new MoveDeclarationsDescriptor( - project, - MoveKotlinDeclarationsProcessorKt.MoveSource(innerClass), - moveTarget, - delegate, - isSearchInComments(), - isSearchInNonJavaFiles(), - false, - null, - isOpenInEditor() - ); saveOpenInEditorOption(); - invokeRefactoring(new MoveKotlinDeclarationsProcessor(moveDescriptor, Mover.Default.INSTANCE)); + invokeRefactoring(processor); } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelModel.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelModel.kt new file mode 100644 index 00000000000..1e2bab28977 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelModel.kt @@ -0,0 +1,204 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.util.Comparing +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.refactoring.PackageWrapper +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil +import com.intellij.refactoring.util.RefactoringMessageUtil +import com.intellij.refactoring.util.RefactoringUtil +import com.intellij.util.IncorrectOperationException +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor +import org.jetbrains.kotlin.idea.core.KotlinNameSuggester +import org.jetbrains.kotlin.idea.refactoring.createKotlinFile +import org.jetbrains.kotlin.idea.refactoring.move.getTargetPackageFqName +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.* +import org.jetbrains.kotlin.idea.roots.getSuitableDestinationSourceRoots +import org.jetbrains.kotlin.incremental.components.NoLookupLocation +import org.jetbrains.kotlin.name.Name +import org.jetbrains.kotlin.psi.KtClassOrObject +import org.jetbrains.kotlin.psi.KtElement +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.psiUtil.isIdentifier +import org.jetbrains.kotlin.resolve.DescriptorUtils +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode + +internal class MoveKotlinNestedClassesToUpperLevelModel( + val project: Project, + val innerClass: KtClassOrObject, + val target: PsiElement, + val parameter: String?, + val className: String, + val passOuterClass: Boolean, + val searchInComments: Boolean, + val isSearchInNonJavaFiles: Boolean, + val packageName: String, + val isOpenInEditor: Boolean +) : Model { + + private val innerClassDescriptor = innerClass.unsafeResolveToDescriptor(BodyResolveMode.FULL) as ClassDescriptor + + private fun getTargetContainer(): PsiElement? { + if (target is PsiDirectory) { + val oldPackageFqName = getTargetPackageFqName(target) + val targetName = packageName + if (!Comparing.equal(oldPackageFqName?.asString(), targetName)) { + val projectRootManager = ProjectRootManager.getInstance(project) + + val contentSourceRoots = getSuitableDestinationSourceRoots(project) + val newPackage = PackageWrapper(PsiManager.getInstance(project), targetName) + + val targetSourceRoot: VirtualFile + if (contentSourceRoots.size > 1) { + val oldPackage = oldPackageFqName?.let { + JavaPsiFacade.getInstance(project).findPackage(it.asString()) + } + + var initialDir: PsiDirectory? = null + if (oldPackage != null) { + val root = projectRootManager.fileIndex.getContentRootForFile(target.virtualFile) + initialDir = oldPackage.directories.firstOrNull { + Comparing.equal(projectRootManager.fileIndex.getContentRootForFile(it.virtualFile), root) + } + } + + targetSourceRoot = MoveClassesOrPackagesUtil.chooseSourceRoot(newPackage, contentSourceRoots, initialDir) ?: return null + } else { + targetSourceRoot = contentSourceRoots[0] + } + + var directory = RefactoringUtil.findPackageDirectoryInSourceRoot(newPackage, targetSourceRoot); + if (directory === null) { + runWriteAction { + try { + directory = RefactoringUtil.createPackageDirectoryInSourceRoot(newPackage, targetSourceRoot) + } catch (e: IncorrectOperationException) { + directory = null; + } + } + } + return directory; + } + + return target + } + + return if (target is KtFile || target is KtClassOrObject) target else null + + } + + @Throws(ConfigurationException::class) + private fun getTargetContainerWithValidation(): PsiElement { + + if (className.isEmpty()) { + throw ConfigurationException(RefactoringBundle.message("no.class.name.specified")) + } + if (!className.isIdentifier()) { + throw ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(className)) + } + + if (passOuterClass) { + if (parameter.isNullOrEmpty()) { + throw ConfigurationException(RefactoringBundle.message("no.parameter.name.specified")) + } + if (!parameter.isIdentifier()) { + throw ConfigurationException(RefactoringMessageUtil.getIncorrectIdentifierMessage(parameter)) + } + } + + val targetContainer = getTargetContainer() + + if (targetContainer is KtClassOrObject) { + val targetClass = targetContainer as KtClassOrObject? + for (member in targetClass!!.declarations) { + if (member is KtClassOrObject && className == member.getName()) { + throw ConfigurationException(RefactoringBundle.message("inner.class.exists", className, targetClass.name)) + } + } + } + + if (targetContainer is PsiDirectory || targetContainer is KtFile) { + val targetPackageFqName = getTargetPackageFqName(target) + ?: throw ConfigurationException("No package corresponds to this directory") + + val existingClass = DescriptorUtils + .getContainingModule(innerClassDescriptor) + .getPackage(targetPackageFqName) + .memberScope + .getContributedClassifier(Name.identifier(className), NoLookupLocation.FROM_IDE) + if (existingClass != null) { + throw ConfigurationException("Class $className already exists in package $targetPackageFqName") + } + + val targetDir = targetContainer as? PsiDirectory ?: targetContainer.containingFile.containingDirectory + val message = RefactoringMessageUtil.checkCanCreateFile(targetDir, "$className.kt") + if (message != null) throw ConfigurationException(message) + } + + return targetContainer ?: throw ConfigurationException("Invalid target specified") + } + + @Throws(ConfigurationException::class) + private fun getMoveTarget(): KotlinMoveTarget { + val target = getTargetContainerWithValidation() + if (target is PsiDirectory) { + val targetDir = target + + val targetPackageFqName = getTargetPackageFqName(target) + ?: throw ConfigurationException("Cannot find target package name") + + val suggestedName = KotlinNameSuggester.suggestNameByName(className) { + targetDir.findFile(it + "." + KotlinFileType.EXTENSION) == null + } + + val targetFileName = suggestedName + "." + KotlinFileType.EXTENSION + + return KotlinMoveTargetForDeferredFile( + targetPackageFqName, + targetDir, null + ) { createKotlinFile(targetFileName, targetDir, targetPackageFqName.asString()) } + } else { + return KotlinMoveTargetForExistingElement(target as KtElement) + } + } + + @Throws(ConfigurationException::class) + override fun computeModelResult() = computeModelResult(throwOnConflicts = false) + + @Throws(ConfigurationException::class) + override fun computeModelResult(throwOnConflicts: Boolean): MoveKotlinDeclarationsProcessor { + + val moveTarget = getMoveTarget() + + val outerInstanceParameterName = if (passOuterClass) packageName else null + val delegate = MoveDeclarationsDelegate.NestedClass(className, outerInstanceParameterName) + val moveDescriptor = MoveDeclarationsDescriptor( + project, + MoveSource(innerClass), + moveTarget, + delegate, + searchInComments, + isSearchInNonJavaFiles, + deleteSourceFiles = false, + moveCallback = null, + openInEditor = isOpenInEditor + ) + + return MoveKotlinDeclarationsProcessor(moveDescriptor, Mover.Default, throwOnConflicts) + } +} diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.form b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.form index 656468c3f24..297fd6552f3 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.form +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.form @@ -116,7 +116,7 @@ - + @@ -151,6 +151,15 @@ + + + + + + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java index 66a5fb95b8a..32c831eaeae 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java @@ -6,31 +6,20 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui; import com.intellij.ide.util.DirectoryChooser; -import com.intellij.ide.util.DirectoryUtil; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.event.DocumentEvent; -import com.intellij.openapi.editor.event.DocumentListener; -import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.Project; -import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.TextFieldWithBrowseButton; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Pass; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; -import com.intellij.psi.impl.source.PsiFileImpl; -import com.intellij.refactoring.*; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiFile; +import com.intellij.refactoring.BaseRefactoringProcessor; +import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; import com.intellij.refactoring.classMembers.MemberInfoBase; import com.intellij.refactoring.classMembers.MemberInfoChange; import com.intellij.refactoring.classMembers.MemberInfoChangeListener; import com.intellij.refactoring.move.MoveCallback; import com.intellij.refactoring.move.MoveHandler; -import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination; -import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination; import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo; import com.intellij.refactoring.ui.RefactoringDialog; import com.intellij.refactoring.util.CommonRefactoringUtil; @@ -42,35 +31,29 @@ import com.intellij.util.ui.UIUtil; import kotlin.collections.CollectionsKt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.idea.KotlinFileType; import org.jetbrains.kotlin.idea.core.PackageUtilsKt; import org.jetbrains.kotlin.idea.core.util.PhysicalFileSystemUtilsKt; -import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle; import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings; -import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtilKt; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTable; import org.jetbrains.kotlin.idea.refactoring.move.MoveUtilsKt; -import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinFileChooserDialog; -import org.jetbrains.kotlin.idea.util.application.ApplicationUtilsKt; -import org.jetbrains.kotlin.name.FqName; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.psi.KtNamedDeclaration; import org.jetbrains.kotlin.psi.KtPureElement; import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt; -import static org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt.getSuitableDestinationSourceRoots; import javax.swing.*; import java.awt.*; import java.io.File; -import java.nio.file.Path; +import java.util.Collection; import java.util.List; -import java.util.*; +import java.util.Map; +import java.util.Set; -import static java.util.Collections.emptyList; +import static org.jetbrains.kotlin.idea.roots.ProjectRootUtilsKt.getSuitableDestinationSourceRoots; public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private static final String RECENTS_KEY = "MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY"; @@ -89,9 +72,42 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private JTextField tfFileNameInPackage; private JCheckBox cbSpecifyFileNameInPackage; private JCheckBox cbUpdatePackageDirective; + private JCheckBox cbDeleteEmptySourceFiles; private JCheckBox cbSearchReferences; private KotlinMemberSelectionTable memberTable; + private class MemberSelectionerInfoChangeListener implements MemberInfoChangeListener { + + private final List memberInfos; + + public MemberSelectionerInfoChangeListener(List memberInfos) { + this.memberInfos = memberInfos; + } + + private boolean shouldUpdateFileNameField(Collection changedMembers) { + if (!tfFileNameInPackage.isEnabled()) return true; + + Collection previousDeclarations = CollectionsKt.filterNotNull( + CollectionsKt.map( + memberInfos, + info -> changedMembers.contains(info) != info.isChecked() ? info.getMember() : null + ) + ); + String suggestedText = previousDeclarations.isEmpty() ? "" : MoveUtilsKt.guessNewFileName(previousDeclarations); + return tfFileNameInPackage.getText().equals(suggestedText); + } + + @Override + public void memberInfoChanged(@NotNull MemberInfoChange event) { + updatePackageDirectiveCheckBox(); + updateFileNameInPackageField(); + // Update file name field only if it user hasn't changed it to some non-default value + if (shouldUpdateFileNameField(event.getChangedMembers())) { + updateSuggestedFileName(); + } + } + } + public MoveKotlinTopLevelDeclarationsDialog( @NotNull Project project, @NotNull Set elementsToMove, @@ -100,7 +116,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @Nullable KtFile targetFile, boolean moveToPackage, boolean searchInComments, - boolean searchForTextOccurences, + boolean searchForTextOccurrences, + boolean deleteEmptySourceFiles, @Nullable MoveCallback moveCallback ) { super(project, true); @@ -114,7 +131,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { setTitle(MoveHandler.REFACTORING_NAME); - initSearchOptions(searchInComments, searchForTextOccurences); + initSearchOptions(searchInComments, searchForTextOccurrences, deleteEmptySourceFiles); initPackageChooser(targetPackageName, targetDirectory, sourceFiles); @@ -136,18 +153,6 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ); } - @NotNull - private static PsiDirectory getSourceDirectory(@NotNull Collection sourceFiles) { - return CollectionsKt.single( - CollectionsKt.distinct( - CollectionsKt.map( - sourceFiles, - PsiFileImpl::getParent - ) - ) - ); - } - private static List getAllDeclarations(Collection sourceFiles) { return CollectionsKt.filterIsInstance( CollectionsKt.flatMap( @@ -165,30 +170,6 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { return true; } - @NotNull - private static List getFilesExistingInTargetDir( - @NotNull List sourceFiles, - @Nullable String targetFileName, - @Nullable PsiDirectory targetDirectory - ) { - if (targetDirectory == null) return emptyList(); - - List fileNames = - targetFileName != null - ? Collections.singletonList(targetFileName) - : CollectionsKt.map( - sourceFiles, - PsiFileImpl::getName - ); - - return CollectionsKt.filterNotNull( - CollectionsKt.map( - fileNames, - targetDirectory::findFile - ) - ); - } - private void initMemberInfo( @NotNull Set elementsToMove, @NotNull List sourceFiles @@ -207,34 +188,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { memberInfoModel.memberInfoChanged(new MemberInfoChange<>(memberInfos)); selectionPanel.getTable().setMemberInfoModel(memberInfoModel); selectionPanel.getTable().addMemberInfoChangeListener(memberInfoModel); - selectionPanel.getTable().addMemberInfoChangeListener( - new MemberInfoChangeListener() { - private boolean shouldUpdateFileNameField(Collection changedMembers) { - if (!tfFileNameInPackage.isEnabled()) return true; - - Collection previousDeclarations = CollectionsKt.filterNotNull( - CollectionsKt.map( - memberInfos, - info -> changedMembers.contains(info) != info.isChecked() ? info.getMember() : null - ) - ); - String suggestedText = previousDeclarations.isEmpty() - ? "" - : MoveUtilsKt.guessNewFileName(previousDeclarations); - return tfFileNameInPackage.getText().equals(suggestedText); - } - - @Override - public void memberInfoChanged(@NotNull MemberInfoChange event) { - updatePackageDirectiveCheckBox(); - updateFileNameInPackageField(); - // Update file name field only if it user hasn't changed it to some non-default value - if (shouldUpdateFileNameField(event.getChangedMembers())) { - updateSuggestedFileName(); - } - } - } - ); + selectionPanel.getTable().addMemberInfoChangeListener(new MemberSelectionerInfoChangeListener(memberInfos)); memberInfoPanel.add(selectionPanel, BorderLayout.CENTER); } @@ -243,8 +197,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } private void updateFileNameInPackageField() { - boolean movingSingleFileToPackage = isMoveToPackage() - && getSourceFiles(getSelectedElementsToMove()).size() == 1; + boolean movingSingleFileToPackage = rbMoveToPackage.isSelected() && getSourceFiles(getSelectedElementsToMove()).size() == 1; cbSpecifyFileNameInPackage.setEnabled(movingSingleFileToPackage); tfFileNameInPackage.setEnabled(movingSingleFileToPackage && cbSpecifyFileNameInPackage.isSelected()); } @@ -275,9 +228,10 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { cbUpdatePackageDirective.setSelected(arePackagesAndDirectoryMatched(sourceFiles)); } - private void initSearchOptions(boolean searchInComments, boolean searchForTextOccurences) { + private void initSearchOptions(boolean searchInComments, boolean searchForTextOccurences, boolean deleteEmptySourceFiles) { cbSearchInComments.setSelected(searchInComments); cbSearchTextOccurrences.setSelected(searchForTextOccurences); + cbDeleteEmptySourceFiles.setSelected(deleteEmptySourceFiles); } private void initMoveToButtons(boolean moveToPackage) { @@ -309,13 +263,14 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @NotNull List sourceFiles ) { PsiDirectory sourceDir = sourceFiles.get(0).getParent(); - assert sourceDir != null : sourceFiles.get(0).getVirtualFile().getPath(); + if (sourceDir == null) { + throw new AssertionError("File chooser initialization failed"); + } - fileChooser.addActionListener( - e -> { + fileChooser.addActionListener(e -> { KotlinFileChooserDialog dialog = new KotlinFileChooserDialog("Choose Containing File", myProject); - File targetFile1 = new File(getTargetFilePath()); + File targetFile1 = new File(fileChooser.getText()); PsiFile targetPsiFile = PhysicalFileSystemUtilsKt.toPsiFile(targetFile1, myProject); if (targetPsiFile instanceof KtFile) { dialog.select((KtFile) targetPsiFile); @@ -336,12 +291,9 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } ); - String initialTargetPath = - targetFile != null + String initialTargetPath = targetFile != null ? targetFile.getVirtualFile().getPath() - : sourceFiles.get(0).getVirtualFile().getParent().getPath() + - "/" + - MoveUtilsKt.guessNewFileName(elementsToMove); + : sourceFiles.get(0).getVirtualFile().getParent().getPath() + "/" + MoveUtilsKt.guessNewFileName(elementsToMove); fileChooser.setText(initialTargetPath); } @@ -357,28 +309,23 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } private ReferenceEditorComboWithBrowseButton createPackageChooser() { - ReferenceEditorComboWithBrowseButton packageChooser = - new PackageNameReferenceEditorCombo("", myProject, RECENTS_KEY, RefactoringBundle.message("choose.destination.package")); - Document document = packageChooser.getChildComponent().getDocument(); - document.addDocumentListener(new DocumentListener() { - @Override - public void documentChanged(@NotNull DocumentEvent e) { - validateButtons(); - } - }); - - return packageChooser; + return new PackageNameReferenceEditorCombo( + "", + myProject, + RECENTS_KEY, + RefactoringBundle.message("choose.destination.package") + ); } private void updateControls() { - boolean moveToPackage = isMoveToPackage(); + boolean moveToPackage = rbMoveToPackage.isSelected(); classPackageChooser.setEnabled(moveToPackage); updateFileNameInPackageField(); fileChooser.setEnabled(!moveToPackage); updatePackageDirectiveCheckBox(); UIUtil.setEnabled(targetPanel, moveToPackage && hasAnySourceRoots(), true); updateSuggestedFileName(); - validateButtons(); + myHelpAction.setEnabled(false); } private boolean isFullFileMove() { @@ -393,7 +340,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } private void updatePackageDirectiveCheckBox() { - cbUpdatePackageDirective.setEnabled(isMoveToPackage() && isFullFileMove()); + cbUpdatePackageDirective.setEnabled(rbMoveToPackage.isSelected() && isFullFileMove()); } private boolean hasAnySourceRoots() { @@ -402,206 +349,12 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private void saveRefactoringSettings() { KotlinRefactoringSettings refactoringSettings = KotlinRefactoringSettings.getInstance(); - refactoringSettings.MOVE_SEARCH_IN_COMMENTS = isSearchInComments(); - refactoringSettings.MOVE_SEARCH_FOR_TEXT = isSearchInNonJavaFiles(); + refactoringSettings.MOVE_SEARCH_IN_COMMENTS = cbSearchInComments.isSelected(); + refactoringSettings.MOVE_SEARCH_FOR_TEXT = cbSearchTextOccurrences.isSelected(); + refactoringSettings.MOVE_DELETE_EMPTY_SOURCE_FILES = cbDeleteEmptySourceFiles.isSelected(); refactoringSettings.MOVE_PREVIEW_USAGES = isPreviewUsages(); - } - @Nullable - private Pair selectPackageBasedTargetDirAndDestination(boolean askIfDoesNotExist) { - String packageName = getTargetPackage(); - - RecentsManager.getInstance(myProject).registerRecentEntry(RECENTS_KEY, packageName); - PackageWrapper targetPackage = new PackageWrapper(PsiManager.getInstance(myProject), packageName); - if (!targetPackage.exists() && askIfDoesNotExist) { - int ret = Messages.showYesNoDialog(myProject, RefactoringBundle.message("package.does.not.exist", packageName), - RefactoringBundle.message("move.title"), Messages.getQuestionIcon()); - if (ret != Messages.YES) return null; - } - - DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) destinationFolderCB.getComboBox().getSelectedItem(); - PsiDirectory selectedPsiDirectory = selectedItem != null ? selectedItem.getDirectory() : null; - if (selectedPsiDirectory == null) { - if (initialTargetDirectory != null) { - selectedPsiDirectory = initialTargetDirectory; - } - else { - return Pair.create(null, new MultipleRootsMoveDestination(targetPackage)); - } - } - - VirtualFile targetDirectory = selectedPsiDirectory.getVirtualFile(); - return Pair.create(targetDirectory, new AutocreatingSingleSourceRootMoveDestination(targetPackage, targetDirectory)); - } - - private boolean checkTargetFileName(String fileName) { - if (FileTypeManager.getInstance().getFileTypeByFileName(fileName) == KotlinFileType.INSTANCE) return true; - setErrorText("Can't move to non-Kotlin file"); - return false; - } - - @Nullable - private KotlinMoveTarget selectMoveTarget() { - String message = verifyBeforeRun(); - if (message != null) { - setErrorText(message); - return null; - } - - setErrorText(null); - - List sourceFiles = getSourceFiles(getSelectedElementsToMove()); - PsiDirectory sourceDirectory = getSourceDirectory(sourceFiles); - - if (isMoveToPackage()) { - Pair targetDirWithMoveDestination = selectPackageBasedTargetDirAndDestination(true); - if (targetDirWithMoveDestination == null) return null; - - VirtualFile targetDir = targetDirWithMoveDestination.getFirst(); - MoveDestination moveDestination = targetDirWithMoveDestination.getSecond(); - - String targetFileName = sourceFiles.size() > 1 ? null : tfFileNameInPackage.getText(); - if (targetFileName != null && !checkTargetFileName(targetFileName)) return null; - - PsiDirectory targetDirectory = moveDestination.getTargetIfExists(sourceDirectory); - - List filesExistingInTargetDir = getFilesExistingInTargetDir(sourceFiles, targetFileName, targetDirectory); - if (!filesExistingInTargetDir.isEmpty()) { - if (filesExistingInTargetDir.size() > 1) { - String filePathsToReport = StringUtil.join( - filesExistingInTargetDir, - file -> file.getVirtualFile().getPath(), - "\n" - ); - Messages.showErrorDialog( - myProject, - "Cannot perform refactoring since the following files already exist:\n\n" + filePathsToReport, - RefactoringBundle.message("move.title") - ); - return null; - } - - PsiFile targetFile = filesExistingInTargetDir.get(0); - - if (!sourceFiles.contains(targetFile)) { - String question = String.format( - "File '%s' already exists. Do you want to move selected declarations to this file?", - targetFile.getVirtualFile().getPath() - ); - int ret = - Messages.showYesNoDialog(myProject, question, RefactoringBundle.message("move.title"), - Messages.getQuestionIcon()); - if (ret != Messages.YES) return null; - } - - if (targetFile instanceof KtFile) { - return new KotlinMoveTargetForExistingElement((KtFile) targetFile); - } - } - - // All source files must be in the same directory - return new KotlinMoveTargetForDeferredFile( - new FqName(getTargetPackage()), - moveDestination.getTargetIfExists(sourceFiles.get(0)), - targetDir, - originalFile -> KotlinRefactoringUtilKt.getOrCreateKotlinFile( - targetFileName != null ? targetFileName : originalFile.getName(), - moveDestination.getTargetDirectory(originalFile) - ) - ); - } - - File targetFile = new File(getTargetFilePath()); - if (!checkTargetFileName(targetFile.getName())) return null; - KtFile jetFile = (KtFile) PhysicalFileSystemUtilsKt.toPsiFile(targetFile, myProject); - if (jetFile != null) { - if (sourceFiles.size() == 1 && sourceFiles.contains(jetFile)) { - setErrorText("Can't move to the original file"); - return null; - } - - return new KotlinMoveTargetForExistingElement(jetFile); - } - - Path targetFilePath = targetFile.toPath(); - Path targetDirPath = targetFilePath.getParent(); - if (targetDirPath == null || !targetDirPath.startsWith(Objects.requireNonNull(getProject().getBasePath()))) { - setErrorText("Incorrect target path. Directory " + targetDirPath + " does not belong to current project."); - return null; - } - if (PhysicalFileSystemUtilsKt.toPsiDirectory(targetDirPath.toFile(), myProject) == null) { - int ret = Messages.showYesNoDialog( - myProject, - "You are about to move all declarations to the directory that does not exist. Do you want to create it?", - RefactoringBundle.message("move.title"), - Messages.getQuestionIcon() - ); - if (ret == Messages.YES) { - try { - ApplicationManager.getApplication().runWriteAction(() -> { - DirectoryUtil.mkdirs(PsiManager.getInstance(getProject()), targetDirPath.toString()); - }); - } - catch (IncorrectOperationException e) { - setErrorText("Failed to create parent directory: " + targetDirPath); - return null; - } - } - } - - File targetDir = targetDirPath.toFile(); - PsiDirectory psiDirectory = targetDir != null ? PhysicalFileSystemUtilsKt.toPsiDirectory(targetDir, myProject) : null; - if (psiDirectory == null) { - setErrorText("No directory found for file: " + targetFile.getPath()); - return null; - } - - Set sourcePackageFqNames = CollectionsKt.mapTo( - sourceFiles, - new LinkedHashSet<>(), - KtFile::getPackageFqName - ); - FqName targetPackageFqName = CollectionsKt.singleOrNull(sourcePackageFqNames); - if (targetPackageFqName == null) { - PsiPackage psiPackage = JavaDirectoryService.getInstance().getPackage(psiDirectory); - if (psiPackage == null) { - setErrorText("Could not find package corresponding to " + targetDir.getPath()); - return null; - } - targetPackageFqName = new FqName(psiPackage.getQualifiedName()); - } - - String finalTargetPackageFqName = targetPackageFqName.asString(); - return new KotlinMoveTargetForDeferredFile( - targetPackageFqName, - psiDirectory, - null, - originalFile -> KotlinRefactoringUtilKt.getOrCreateKotlinFile(targetFile.getName(), psiDirectory, finalTargetPackageFqName) - ); - } - - @Nullable - private String verifyBeforeRun() { - if (memberTable.getSelectedMemberInfos().isEmpty()) return "At least one member must be selected"; - - if (isMoveToPackage()) { - String name = getTargetPackage(); - if (name.length() != 0 && !PsiNameHelper.getInstance(myProject).isQualifiedName(name)) { - return "\'" + name + "\' is invalid destination package name"; - } - } - else { - PsiFile targetFile = PhysicalFileSystemUtilsKt.toPsiFile(new File(getTargetFilePath()), myProject); - if (!(targetFile == null || targetFile instanceof KtFile)) { - return KotlinRefactoringBundle.message("refactoring.move.non.kotlin.file"); - } - } - - if (getSourceFiles(getSelectedElementsToMove()).size() == 1 && tfFileNameInPackage.getText().isEmpty()) { - return "File name may not be empty"; - } - - return null; + RecentsManager.getInstance(myProject).registerRecentEntry(RECENTS_KEY, getTargetPackage()); } private List getSelectedElementsToMove() { @@ -621,136 +374,66 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { return "#" + getClass().getName(); } - protected final String getTargetPackage() { + private String getTargetPackage() { return classPackageChooser.getText().trim(); } - protected final String getTargetFilePath() { - return fileChooser.getText(); + private List getSelectedElementsToMoveChecked() throws ConfigurationException { + List elementsToMove = getSelectedElementsToMove(); + if (elementsToMove.isEmpty()) { + throw new ConfigurationException("No elements to move are selected"); + } + return elementsToMove; } - @Override - protected void canRun() throws ConfigurationException { - String message = verifyBeforeRun(); - if (message != null) { - throw new ConfigurationException(message); - } + private Model getModel() throws ConfigurationException { + + DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) destinationFolderCB.getComboBox().getSelectedItem(); + PsiDirectory selectedPsiDirectory = selectedItem != null ? selectedItem.getDirectory() : initialTargetDirectory; + + return new MoveKotlinTopLevelDeclarationsModel( + myProject, + getSelectedElementsToMoveChecked(), + getTargetPackage(), + selectedPsiDirectory, + tfFileNameInPackage.getText(), + fileChooser.getText(), + rbMoveToPackage.isSelected(), + cbSearchReferences.isSelected(), + cbSearchInComments.isSelected(), + cbSearchTextOccurrences.isSelected(), + cbDeleteEmptySourceFiles.isSelected(), + cbUpdatePackageDirective.isSelected(), + isFullFileMove(), + moveCallback + ); } @Override protected void doAction() { - KotlinMoveTarget target = selectMoveTarget(); - if (target == null) return; + + BaseRefactoringProcessor processor; + try { + processor = getModel().computeModelResult(); + } + catch (ConfigurationException e) { + setErrorText(e.getMessage()); + return; + } saveRefactoringSettings(); - List elementsToMove = getSelectedElementsToMove(); - List sourceFiles = getSourceFiles(elementsToMove); - PsiDirectory sourceDirectory = getSourceDirectory(sourceFiles); - - for (PsiElement element : elementsToMove) { - String message = target.verify(element.getContainingFile()); - if (message != null) { - CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), message, null, myProject); - return; - } - } - try { - boolean deleteSourceFile = false; - - if (isFullFileMove()) { - if (isMoveToPackage()) { - Pair sourceRootWithMoveDestination = - selectPackageBasedTargetDirAndDestination(false); - //noinspection ConstantConditions - MoveDestination moveDestination = sourceRootWithMoveDestination.getSecond(); - - PsiDirectory targetDir = moveDestination.getTargetIfExists(sourceDirectory); - String targetFileName = sourceFiles.size() > 1 ? null : tfFileNameInPackage.getText(); - List filesExistingInTargetDir = getFilesExistingInTargetDir(sourceFiles, targetFileName, targetDir); - if (filesExistingInTargetDir.isEmpty() - || (filesExistingInTargetDir.size() == 1 && sourceFiles.contains(filesExistingInTargetDir.get(0)))) { - PsiDirectory targetDirectory = ApplicationUtilsKt.runWriteAction( - () -> moveDestination.getTargetDirectory(sourceDirectory) - ); - - for (KtFile sourceFile : sourceFiles) { - MoveUtilsKt.setUpdatePackageDirective(sourceFile, cbUpdatePackageDirective.isSelected()); - } - - BaseRefactoringProcessor processor; - processor = sourceFiles.size() == 1 && targetFileName != null - ? new MoveToKotlinFileProcessor(myProject, - CollectionsKt.single(sourceFiles), - targetDirectory, - targetFileName, - isSearchInComments(), - isSearchInNonJavaFiles(), - moveCallback) - : new KotlinAwareMoveFilesOrDirectoriesProcessor(myProject, - sourceFiles, - targetDirectory, - cbSearchReferences.isSelected(), - isSearchInComments(), - isSearchInNonJavaFiles(), - moveCallback); - - invokeRefactoring(processor); - - return; - } - } - - int ret = Messages.showYesNoCancelDialog( - myProject, - "You are about to move all declarations out of the source file(s). Do you want to delete empty files?", - RefactoringBundle.message("move.title"), - Messages.getQuestionIcon() - ); - if (ret == Messages.CANCEL) return; - deleteSourceFile = ret == Messages.YES; - } - - MoveDeclarationsDescriptor options = new MoveDeclarationsDescriptor( - myProject, - MoveKotlinDeclarationsProcessorKt.MoveSource(elementsToMove), - target, - MoveDeclarationsDelegate.TopLevel.INSTANCE, - isSearchInComments(), - isSearchInNonJavaFiles(), - deleteSourceFile, - moveCallback, - false, - null, - true, - cbSearchReferences.isSelected() - ); - invokeRefactoring(new MoveKotlinDeclarationsProcessor(options, Mover.Default.INSTANCE)); - } - catch (IncorrectOperationException e) { + invokeRefactoring(processor); + } catch (IncorrectOperationException e) { CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.getMessage(), null, myProject); } } - private boolean isSearchInNonJavaFiles() { - return cbSearchTextOccurrences.isSelected(); - } - - private boolean isSearchInComments() { - return cbSearchInComments.isSelected(); - } - - private boolean isMoveToPackage() { - return rbMoveToPackage.isSelected(); - } - @Override public JComponent getPreferredFocusedComponent() { return classPackageChooser.getChildComponent(); } - private static class MemberInfoModelImpl extends AbstractMemberInfoModel { - - } + private static class MemberInfoModelImpl extends AbstractMemberInfoModel { } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt new file mode 100644 index 00000000000..9720f90d0e2 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt @@ -0,0 +1,270 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui + +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.options.ConfigurationException +import com.intellij.psi.* +import com.intellij.refactoring.BaseRefactoringProcessor +import com.intellij.refactoring.MoveDestination +import com.intellij.refactoring.PackageWrapper +import com.intellij.refactoring.RefactoringBundle +import com.intellij.refactoring.move.MoveCallback +import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination +import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination +import com.intellij.util.IncorrectOperationException +import org.jetbrains.kotlin.idea.KotlinFileType +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle +import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile +import org.jetbrains.kotlin.idea.refactoring.move.getOrCreateDirectory +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.* +import org.jetbrains.kotlin.idea.refactoring.move.updatePackageDirective +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.psi.KtFile +import org.jetbrains.kotlin.psi.KtNamedDeclaration +import java.io.File +import java.nio.file.InvalidPathException +import java.nio.file.Paths + +internal class MoveKotlinTopLevelDeclarationsModel( + val project: Project, + val elementsToMove: List, + val targetPackage: String, + val selectedPsiDirectory: PsiDirectory?, + val fileNameInPackage: String, + val targetFilePath: String, + val isMoveToPackage: Boolean, + val isSearchReferences: Boolean, + val isSearchInComments: Boolean, + val isSearchInNonJavaFiles: Boolean, + val isDeleteEmptyFiles: Boolean, + val isUpdatePackageDirective: Boolean, + val isFullFileMove: Boolean, + val moveCallback: MoveCallback? +) : Model { + + private val sourceDirectory by lazy { + sourceFiles.singleOrNull { it.parent !== null }?.parent ?: throw ConfigurationException("Can't determine sources directory") + } + + private val sourceFiles = elementsToMove.map { it.containingKtFile }.distinct() + + private data class TargetDirAndDestination(val targetDir: VirtualFile?, val destination: MoveDestination) + + private fun selectPackageBasedTargetDirAndDestination(): TargetDirAndDestination { + + val targetPackageWrapper = PackageWrapper(PsiManager.getInstance(project), targetPackage) + + return if (selectedPsiDirectory === null) + TargetDirAndDestination(null, MultipleRootsMoveDestination(targetPackageWrapper)) + else { + TargetDirAndDestination( + selectedPsiDirectory.virtualFile, + AutocreatingSingleSourceRootMoveDestination(targetPackageWrapper, selectedPsiDirectory.virtualFile) + ) + } + } + + private fun checkTargetFileName(fileName: String) { + if (FileTypeManager.getInstance().getFileTypeByFileName(fileName) != KotlinFileType.INSTANCE) { + throw ConfigurationException(KotlinRefactoringBundle.message("refactoring.move.non.kotlin.file")) + } + } + + private fun getFilesExistingInTargetDir( + targetFileName: String?, + targetDirectory: PsiDirectory? + ): List { + targetDirectory ?: return emptyList() + + val fileNames = targetFileName?.let { listOf(it) } ?: sourceFiles.map { it.name } + + return fileNames + .distinct() + .mapNotNull { targetDirectory.findFile(it) } + } + + private fun selectMoveTargetToPackage(): KotlinMoveTarget { + + if (sourceFiles.size > 1) throw ConfigurationException("Can't move from multiply source files") + + checkTargetFileName(fileNameInPackage) + + val (targetDir, moveDestination) = selectPackageBasedTargetDirAndDestination() + + val targetDirectory = moveDestination.getTargetIfExists(sourceDirectory) + + val filesExistingInTargetDir = getFilesExistingInTargetDir(fileNameInPackage, targetDirectory) + if (filesExistingInTargetDir.isNotEmpty()) { + if (filesExistingInTargetDir.size > 1) { + val filePathsToReport = filesExistingInTargetDir.joinToString( + separator = "\n", + prefix = "Cannot perform refactoring since the following files already exist:\n\n" + ) { it.virtualFile.path } + throw ConfigurationException(filePathsToReport) + } + + (filesExistingInTargetDir[0] as? KtFile)?.let { + return KotlinMoveTargetForExistingElement(it) + } + } + + // All source files must be in the same directory + return KotlinMoveTargetForDeferredFile( + FqName(targetPackage), + moveDestination.getTargetIfExists(sourceFiles[0]), + targetDir + ) { getOrCreateKotlinFile(fileNameInPackage, moveDestination.getTargetDirectory(it)) } + } + + private fun selectMoveTargetToFile(): KotlinMoveTarget { + + try { + Paths.get(targetFilePath) + } catch (e: InvalidPathException) { + throw ConfigurationException("Invalid target path $targetFilePath") + } + + val targetFile = File(targetFilePath) + checkTargetFileName(targetFile.name) + + val jetFile = targetFile.toPsiFile(project) as? KtFile + if (jetFile !== null) { + if (sourceFiles.size == 1 && sourceFiles.contains(jetFile)) { + throw ConfigurationException("Can't move to the original file") + } + return KotlinMoveTargetForExistingElement(jetFile) + } + + val targetDirPath = targetFile.toPath().parent + val projectBasePath = project.basePath ?: throw ConfigurationException("Can't move for current project") + if (targetDirPath === null || !targetDirPath.startsWith(projectBasePath)) { + throw ConfigurationException("Incorrect target path. Directory $targetDirPath does not belong to current project.") + } + + val absoluteTargetDirPath = targetDirPath.toString() + val psiDirectory: PsiDirectory + try { + psiDirectory = getOrCreateDirectory(absoluteTargetDirPath, project) + } catch (e: IncorrectOperationException) { + throw ConfigurationException("Failed to create parent directory: $absoluteTargetDirPath") + } + + val targetPackageFqName = sourceFiles.singleOrNull()?.packageFqName + ?: JavaDirectoryService.getInstance().getPackage(psiDirectory)?.let { FqName(it.qualifiedName) } + ?: throw ConfigurationException("Could not find package corresponding to $absoluteTargetDirPath") + + val finalTargetPackageFqName = targetPackageFqName.asString() + return KotlinMoveTargetForDeferredFile( + targetPackageFqName, + psiDirectory, + targetFile = null + ) { getOrCreateKotlinFile(targetFile.name, psiDirectory, finalTargetPackageFqName) } + } + + private fun selectMoveTarget() = + if (isMoveToPackage) selectMoveTargetToPackage() else selectMoveTargetToFile() + + private fun verifyBeforeRun(target: KotlinMoveTarget) { + + if (elementsToMove.isEmpty()) throw ConfigurationException("At least one member must be selected") + + for (element in elementsToMove) { + target.verify(element.containingFile)?.let { throw ConfigurationException(it) } + } + + if (isMoveToPackage) { + if (targetPackage.isNotEmpty() && !PsiNameHelper.getInstance(project).isQualifiedName(targetPackage)) { + throw ConfigurationException("\'$targetPackage\' is invalid destination package name") + } + } else { + val targetFile = File(targetFilePath).toPsiFile(project) + if (targetFile !== null && targetFile !is KtFile) { + throw ConfigurationException(KotlinRefactoringBundle.message("refactoring.move.non.kotlin.file")) + } + } + + if (sourceFiles.size == 1 && fileNameInPackage.isEmpty()) { + throw ConfigurationException("File name may not be empty") + } + } + + private val verifiedMoveTarget get() = selectMoveTarget().also { verifyBeforeRun(it) } + + @Throws(ConfigurationException::class) + override fun computeModelResult() = computeModelResult(throwOnConflicts = false) + + @Throws(ConfigurationException::class) + override fun computeModelResult(throwOnConflicts: Boolean): BaseRefactoringProcessor { + + val target = verifiedMoveTarget + + if (isFullFileMove && isMoveToPackage) { + val (_, moveDestination) = selectPackageBasedTargetDirAndDestination() + + val targetDir = moveDestination.getTargetIfExists(sourceDirectory) + val targetFileName = if (sourceFiles.size > 1) null else fileNameInPackage + + val filesExistingInTargetDir = getFilesExistingInTargetDir(targetFileName, targetDir) + + if (filesExistingInTargetDir.isEmpty() || + (filesExistingInTargetDir.size == 1 && sourceFiles.contains(filesExistingInTargetDir[0])) + ) { + val targetDirectory = project.executeCommand(RefactoringBundle.message("move.title"), null) { + runWriteAction { + moveDestination.getTargetDirectory(sourceDirectory) + } + } + + sourceFiles.forEach { it.updatePackageDirective = isUpdatePackageDirective } + + return if (sourceFiles.size == 1 && targetFileName !== null) + MoveToKotlinFileProcessor( + project, + sourceFiles[0], + targetDirectory, + targetFileName, + searchInComments = isSearchInComments, + searchInNonJavaFiles = isSearchInNonJavaFiles, + moveCallback = moveCallback, + throwOnConflicts = throwOnConflicts + ) + else + KotlinAwareMoveFilesOrDirectoriesProcessor( + project, + sourceFiles, + targetDirectory, + isSearchReferences, + searchInComments = isSearchInComments, + searchInNonJavaFiles = isSearchInNonJavaFiles, + moveCallback = moveCallback, + throwOnConflicts = throwOnConflicts + ) + } + } + + val options = MoveDeclarationsDescriptor( + project, + MoveSource(elementsToMove), + target, + MoveDeclarationsDelegate.TopLevel, + isSearchInComments, + isSearchInNonJavaFiles, + deleteSourceFiles = isFullFileMove && isDeleteEmptyFiles, + moveCallback = moveCallback, + openInEditor = false, + allElementsToMove = null, + analyzeConflicts = true, + searchReferences = isSearchReferences + ) + return MoveKotlinDeclarationsProcessor(options, Mover.Default, throwOnConflicts) + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesOrDirectories/KotlinMoveFilesOrDirectoriesHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesOrDirectories/KotlinMoveFilesOrDirectoriesHandler.kt index 74cfb9fe9ba..902e61facc7 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesOrDirectories/KotlinMoveFilesOrDirectoriesHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesOrDirectories/KotlinMoveFilesOrDirectoriesHandler.kt @@ -7,13 +7,18 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.LangDataKeys +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.psi.* import com.intellij.refactoring.move.MoveCallback +import com.intellij.refactoring.move.MoveHandler import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesHandler +import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil import org.jetbrains.kotlin.asJava.classes.KtLightClassForFacade -import org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesDialog +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesModel +import org.jetbrains.kotlin.idea.util.application.executeCommand import org.jetbrains.kotlin.psi.KtClassOrObject import org.jetbrains.kotlin.psi.KtFile @@ -43,12 +48,30 @@ class KotlinMoveFilesOrDirectoriesHandler : MoveFilesOrDirectoriesHandler() { override fun doMove(project: Project, elements: Array, targetContainer: PsiElement?, callback: MoveCallback?) { if (!(targetContainer == null || targetContainer is PsiDirectory || targetContainer is PsiDirectoryContainer)) return - moveFilesOrDirectories( - project, - adjustForMove(project, elements, targetContainer) ?: return, - targetContainer, - callback?.let { MoveCallback { it.refactoringCompleted() } } - ) + val adjustedElementsToMove = adjustForMove(project, elements, targetContainer)?.toList() ?: return + + val targetDirectory = MoveFilesOrDirectoriesUtil.resolveToDirectory(project, targetContainer) + if (targetContainer != null && targetDirectory == null) return + + val initialTargetDirectory = MoveFilesOrDirectoriesUtil.getInitialTargetDirectory(targetDirectory, elements) + + if (ApplicationManager.getApplication().isUnitTestMode && initialTargetDirectory !== null) { + KotlinAwareMoveFilesOrDirectoriesModel( + project, + adjustedElementsToMove, + initialTargetDirectory.virtualFile.presentableUrl, + updatePackageDirective = true, + searchReferences = true, + moveCallback = callback + ).run { + project.executeCommand(MoveHandler.REFACTORING_NAME) { + computeModelResult().run() + } + } + return + } + + KotlinAwareMoveFilesOrDirectoriesDialog(project, initialTargetDirectory, elements, callback).show() } override fun tryToMove( diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesUtil.kt deleted file mode 100644 index 8fa695b12d3..00000000000 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveFilesUtil.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.openapi.application.ApplicationManager -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogWrapper -import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.refactoring.RefactoringBundle -import com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler -import com.intellij.refactoring.move.MoveCallback -import com.intellij.refactoring.move.MoveHandler -import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil -import com.intellij.refactoring.util.CommonRefactoringUtil -import com.intellij.util.IncorrectOperationException -import org.jetbrains.kotlin.idea.refactoring.isInKotlinAwareSourceRoot -import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.KotlinAwareMoveFilesOrDirectoriesProcessor -import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesDialog -import org.jetbrains.kotlin.idea.util.application.executeCommand -import org.jetbrains.kotlin.idea.util.application.runWriteAction -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtFile - -fun invokeMoveFilesOrDirectoriesRefactoring( - moveDialog: KotlinAwareMoveFilesOrDirectoriesDialog?, - project: Project, - elements: Array, - initialTargetDirectory: PsiDirectory?, - moveCallback: MoveCallback? -) { - fun closeDialog() { - moveDialog?.close(DialogWrapper.CANCEL_EXIT_CODE) - } - - project.executeCommand(MoveHandler.REFACTORING_NAME) { - val selectedDir = (if (moveDialog != null) moveDialog.targetDirectory else initialTargetDirectory) ?: return@executeCommand - val updatePackageDirective = moveDialog?.updatePackageDirective - - try { - val choice = if (elements.size > 1 || elements[0] is PsiDirectory) intArrayOf(-1) else null - val elementsToMove = elements - .filterNot { - it is PsiFile - && CopyFilesOrDirectoriesHandler.checkFileExist(selectedDir, choice, it, it.name, "Move") - } - .sortedWith( // process Kotlin files first so that light classes are updated before changing references in Java files - java.util.Comparator { o1, o2 -> - when { - o1 is KtElement && o2 !is KtElement -> -1 - o1 !is KtElement && o2 is KtElement -> 1 - else -> 0 - } - } - ) - - elementsToMove.forEach { - MoveFilesOrDirectoriesUtil.checkMove(it, selectedDir) - if (it is KtFile && it.isInKotlinAwareSourceRoot()) { - it.updatePackageDirective = updatePackageDirective - } - } - - if (elementsToMove.isNotEmpty()) { - @Suppress("UNCHECKED_CAST") - val processor = KotlinAwareMoveFilesOrDirectoriesProcessor( - project, - elementsToMove as List, - selectedDir, - searchReferences = moveDialog?.searchReferences ?: true, - searchInComments = false, - searchInNonJavaFiles = false, - moveCallback = moveCallback, - prepareSuccessfulCallback = Runnable(::closeDialog) - ) - processor.run() - } else { - closeDialog() - } - } catch (e: IncorrectOperationException) { - CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("error.title"), e.message, "refactoring.moveFile", project) - } - } -} - -// Mostly copied from MoveFilesOrDirectoriesUtil.doMove() -fun moveFilesOrDirectories( - project: Project, - elements: Array, - targetElement: PsiElement?, - moveCallback: MoveCallback? = null -) { - elements.forEach { if (it !is PsiFile && it !is PsiDirectory) throw IllegalArgumentException("unexpected element type: $it") } - - val targetDirectory = MoveFilesOrDirectoriesUtil.resolveToDirectory(project, targetElement) - if (targetElement != null && targetDirectory == null) return - - val initialTargetDirectory = MoveFilesOrDirectoriesUtil.getInitialTargetDirectory(targetDirectory, elements) - - fun doRun(moveDialog: KotlinAwareMoveFilesOrDirectoriesDialog?) { - invokeMoveFilesOrDirectoriesRefactoring(moveDialog, project, elements, initialTargetDirectory, moveCallback) - } - - if (ApplicationManager.getApplication().isUnitTestMode) { - doRun(null) - return - } - - with(KotlinAwareMoveFilesOrDirectoriesDialog(project, initialTargetDirectory, ::doRun)) { - setData(elements, initialTargetDirectory, "refactoring.moveFile") - show() - } -} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt index 31f42459077..09a8433405b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt @@ -5,10 +5,14 @@ package org.jetbrains.kotlin.idea.refactoring.move +import com.intellij.ide.util.DirectoryUtil +import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project import com.intellij.openapi.util.Comparing import com.intellij.openapi.util.Key import com.intellij.psi.* +import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.move.moveMembers.MockMoveMembersOptions import com.intellij.refactoring.move.moveMembers.MoveMemberHandler import com.intellij.refactoring.move.moveMembers.MoveMembersProcessor @@ -27,11 +31,15 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.codeInsight.shorten.addDelayedImportRequest +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory import org.jetbrains.kotlin.idea.imports.importableFqName import org.jetbrains.kotlin.idea.refactoring.fqName.isImported import org.jetbrains.kotlin.idea.references.KtSimpleNameReference import org.jetbrains.kotlin.idea.references.KtSimpleNameReference.ShorteningMode import org.jetbrains.kotlin.idea.references.mainReference +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.load.java.descriptors.JavaCallableMemberDescriptor import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.* @@ -46,6 +54,8 @@ import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitClassReceiver import org.jetbrains.kotlin.resolve.scopes.receivers.ImplicitReceiver import org.jetbrains.kotlin.types.expressions.DoubleColonLHS import org.jetbrains.kotlin.utils.addIfNotNull +import java.io.File +import java.nio.file.Path import java.util.* sealed class ContainerInfo { @@ -638,4 +648,25 @@ fun collectOuterInstanceReferences(member: KtNamedDeclaration): List() traverseOuterInstanceReferences(member, false) { result += it } return result +} + +@Throws(IncorrectOperationException::class) +internal fun getOrCreateDirectory(path: String, project: Project): PsiDirectory { + + File(path).toPsiDirectory(project)?.let { return it } + + return project.executeCommand(RefactoringBundle.message("move.title"), null) { + runWriteAction { + val fixUpSeparators = path.replace(File.separatorChar, '/') + DirectoryUtil.mkdirs(PsiManager.getInstance(project), fixUpSeparators) + } + } +} + +internal fun getTargetPackageFqName(targetContainer: PsiElement): FqName? { + if (targetContainer is PsiDirectory) { + val targetPackage = targetContainer.getPackage() + return if (targetPackage != null) FqName(targetPackage.qualifiedName) else null + } + return if (targetContainer is KtFile) targetContainer.packageFqName else null } \ No newline at end of file