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