Refactor of Move Refactoring

The main purpose of this commit - to make this refactoring logic can be covered as much as possible.
To make this possible we should to extract all possible UI code from business logic (and vice versa).
Additional fixes was applied during refactoring process.

There is come major steps made in this commit:
1) Extract business logic to separate models
2) Fixes of business models logic
3) Add "Delete empty source file" checkbox instead of modal message box
4) Improve error messaging in move refactoring dialogs
5) Inject interface into move handler that makes UI dialogs could be overrided
6) Inject flag into move handler that makes UI conflicts dialog could be overrided
This commit is contained in:
Igor Yakovlev
2019-08-20 12:05:35 +03:00
parent d6c5fbace0
commit eca2707dbf
23 changed files with 1117 additions and 868 deletions
@@ -43,6 +43,7 @@ class KotlinRefactoringSettings : PersistentStateComponent<KotlinRefactoringSett
@JvmField var MOVE_PREVIEW_USAGES = true
@JvmField var MOVE_SEARCH_IN_COMMENTS = true
@JvmField var MOVE_SEARCH_FOR_TEXT = true
@JvmField var MOVE_DELETE_EMPTY_SOURCE_FILES = true
@JvmField var EXTRACT_INTERFACE_JAVADOC: Int = 0
@JvmField var EXTRACT_SUPERCLASS_JAVADOC: Int = 0
@@ -95,6 +95,7 @@ class ExtractDeclarationFromCurrentFileIntention :
true,
true,
true,
true,
MoveCallback {
runBlocking {
withTimeoutOrNull(TIMEOUT_FOR_IMPORT_OPTIMIZING_MS) {
@@ -29,7 +29,8 @@ class KotlinAwareMoveFilesOrDirectoriesProcessor @JvmOverloads constructor(
searchInComments: Boolean,
searchInNonJavaFiles: Boolean,
moveCallback: MoveCallback?,
prepareSuccessfulCallback: Runnable = EmptyRunnable.INSTANCE
prepareSuccessfulCallback: Runnable = EmptyRunnable.INSTANCE,
private val throwOnConflicts: Boolean = false
) : MoveFilesOrDirectoriesProcessor(
project,
elementsToMove.toTypedArray(),
@@ -58,6 +59,11 @@ class KotlinAwareMoveFilesOrDirectoriesProcessor @JvmOverloads constructor(
return showConflicts(conflicts, usages)
}
override fun showConflicts(conflicts: MultiMap<PsiElement, String>, usages: Array<out UsageInfo>?): 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) {
@@ -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<KtClassOrObject>,
originalClass: KtClassOrObject,
targetClass: KtClassOrObject,
moveCallback: MoveCallback?
) = MoveKotlinNestedClassesDialog(project, elementsToMove, originalClass, targetClass, moveCallback).show()
override fun invokeMoveKotlinTopLevelDeclarationsRefactoring(
project: Project,
elementsToMove: Set<KtNamedDeclaration>,
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<out PsiElement>,
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<out PsiElement>): 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<KtClassOrObject>(),
container,
targetContainer,
callback
).show()
)
return true
}
KotlinSelectNestedClassRefactoringDialog.chooseNestedClassRefactoring(
handlerActions.invokeKotlinSelectNestedClassChooser(
elementsToSearch.first() as KtClassOrObject,
targetContainer
)
@@ -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<KtClassOrObject>,
originalClass: KtClassOrObject,
targetClass: KtClassOrObject,
moveCallback: MoveCallback?
)
fun invokeMoveKotlinTopLevelDeclarationsRefactoring(
project: Project,
elementsToMove: Set<KtNamedDeclaration>,
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<out PsiElement>, moveCallback: MoveCallback?
)
fun showErrorHint(project: Project, editor: Editor?, message: String, title: String, helpId: String?)
}
@@ -131,7 +131,8 @@ private object ElementHashingStrategy : TObjectHashingStrategy<PsiElement> {
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<PsiElement, String>, usages: Array<out UsageInfo>?): Boolean {
if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException()
return super.showConflicts(conflicts, usages)
}
override fun performRefactoring(usages: Array<out UsageInfo>) = doPerformRefactoring(usages.toList())
internal fun doPerformRefactoring(usages: List<UsageInfo>) {
@@ -143,7 +143,8 @@ private object ElementHashingStrategy : TObjectHashingStrategy<PsiElement> {
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<PsiElement, String>, usages: Array<out UsageInfo>?): Boolean {
if (throwOnConflicts && !conflicts.isEmpty) throw RefactoringConflictsFoundException()
return super.showConflicts(conflicts, usages)
}
override fun performRefactoring(usages: Array<out UsageInfo>) = doPerformRefactoring(usages.toList())
internal fun doPerformRefactoring(usages: List<UsageInfo>) {
@@ -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<PsiElement, String>, usages: Array<out UsageInfo>?): 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
@@ -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()
@@ -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<out T> {
@Throws(ConfigurationException::class)
fun computeModelResult(throwOnConflicts: Boolean = false): T
@Throws(ConfigurationException::class)
fun computeModelResult(): T
}
@@ -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<out PsiElement>,
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<out PsiElement>, 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<KtFile>().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<KotlinAwareMoveFilesOrDirectoriesProcessor> {
val directory = LocalFileSystem.getInstance().findFileByPath(selectedDirectoryName)?.let {
PsiManager.getInstance(project).findDirectory(it)
}
val elementsToMove: List<PsiElement> = 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)
}
}
@@ -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<PsiElement>,
val targetDirectoryName: String,
val updatePackageDirective: Boolean,
val searchReferences: Boolean,
val moveCallback: MoveCallback?
) : Model<KotlinAwareMoveFilesOrDirectoriesProcessor> {
private fun checkedGetElementsToMove(selectedDirectory: PsiDirectory): List<PsiElement> {
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
)
}
}
@@ -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
@@ -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<MoveKotlinDeclarationsProcessor> getModel() {
return new MoveKotlinNestedClassesModel(
myProject,
openInEditorCheckBox.isSelected(),
getSelectedElementsToMove(),
originalClass,
targetClass,
moveCallback);
}
@Override
protected void doAction() {
List<KtClassOrObject> 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<KtNamedDeclaration, KotlinMemberInfo> {
}
private static class MemberInfoModelImpl extends AbstractMemberInfoModel<KtNamedDeclaration, KotlinMemberInfo> { }
}
@@ -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<KtClassOrObject>,
val originalClass: KtClassOrObject,
val targetClass: PsiElement?,
val moveCallback: MoveCallback?
) : Model<MoveKotlinDeclarationsProcessor> {
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)
}
}
@@ -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<VirtualFile> 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<PsiDirectory>) () -> {
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<MoveKotlinDeclarationsProcessor> 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);
}
}
@@ -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<MoveKotlinDeclarationsProcessor> {
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)
}
}
@@ -116,7 +116,7 @@
<text value="&amp;Update package directive"/>
</properties>
</component>
<grid id="69aba" layout-manager="GridLayoutManager" row-count="2" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="69aba" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<grid row="5" column="0" row-span="1" col-span="2" vsize-policy="3" hsize-policy="3" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
@@ -151,6 +151,15 @@
<text value="Search &amp;references"/>
</properties>
</component>
<component id="cc60e" class="javax.swing.JCheckBox" binding="cbDeleteEmptySourceFiles">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<selected value="true"/>
<text value="&amp;Delete empty source files"/>
</properties>
</component>
</children>
</grid>
</children>
@@ -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<KtNamedDeclaration, KotlinMemberInfo> {
private final List<KotlinMemberInfo> memberInfos;
public MemberSelectionerInfoChangeListener(List<KotlinMemberInfo> memberInfos) {
this.memberInfos = memberInfos;
}
private boolean shouldUpdateFileNameField(Collection<KotlinMemberInfo> changedMembers) {
if (!tfFileNameInPackage.isEnabled()) return true;
Collection<KtNamedDeclaration> 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<KtNamedDeclaration, KotlinMemberInfo> 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<KtNamedDeclaration> 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<KtFile> sourceFiles) {
return CollectionsKt.single(
CollectionsKt.distinct(
CollectionsKt.map(
sourceFiles,
PsiFileImpl::getParent
)
)
);
}
private static List<KtNamedDeclaration> getAllDeclarations(Collection<KtFile> sourceFiles) {
return CollectionsKt.filterIsInstance(
CollectionsKt.flatMap(
@@ -165,30 +170,6 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
return true;
}
@NotNull
private static List<PsiFile> getFilesExistingInTargetDir(
@NotNull List<KtFile> sourceFiles,
@Nullable String targetFileName,
@Nullable PsiDirectory targetDirectory
) {
if (targetDirectory == null) return emptyList();
List<String> fileNames =
targetFileName != null
? Collections.singletonList(targetFileName)
: CollectionsKt.map(
sourceFiles,
PsiFileImpl::getName
);
return CollectionsKt.filterNotNull(
CollectionsKt.map(
fileNames,
targetDirectory::findFile
)
);
}
private void initMemberInfo(
@NotNull Set<KtNamedDeclaration> elementsToMove,
@NotNull List<KtFile> 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<KtNamedDeclaration, KotlinMemberInfo>() {
private boolean shouldUpdateFileNameField(Collection<KotlinMemberInfo> changedMembers) {
if (!tfFileNameInPackage.isEnabled()) return true;
Collection<KtNamedDeclaration> 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<KtNamedDeclaration, KotlinMemberInfo> 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<KtFile> 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<VirtualFile, ? extends MoveDestination> 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<KtFile> sourceFiles = getSourceFiles(getSelectedElementsToMove());
PsiDirectory sourceDirectory = getSourceDirectory(sourceFiles);
if (isMoveToPackage()) {
Pair<VirtualFile, ? extends MoveDestination> 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<PsiFile> 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<FqName> 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<KtNamedDeclaration> 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<KtNamedDeclaration> getSelectedElementsToMoveChecked() throws ConfigurationException {
List<KtNamedDeclaration> 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<BaseRefactoringProcessor> 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<KtNamedDeclaration> elementsToMove = getSelectedElementsToMove();
List<KtFile> 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<VirtualFile, ? extends MoveDestination> sourceRootWithMoveDestination =
selectPackageBasedTargetDirAndDestination(false);
//noinspection ConstantConditions
MoveDestination moveDestination = sourceRootWithMoveDestination.getSecond();
PsiDirectory targetDir = moveDestination.getTargetIfExists(sourceDirectory);
String targetFileName = sourceFiles.size() > 1 ? null : tfFileNameInPackage.getText();
List<PsiFile> 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<KtNamedDeclaration, KotlinMemberInfo> {
}
private static class MemberInfoModelImpl extends AbstractMemberInfoModel<KtNamedDeclaration, KotlinMemberInfo> { }
}
@@ -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<KtNamedDeclaration>,
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<BaseRefactoringProcessor> {
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<PsiFile> {
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<PsiDirectory> {
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)
}
}
@@ -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<out PsiElement>, 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(
@@ -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<out PsiElement>,
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<KtFile>,
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<PsiElement>,
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()
}
}
@@ -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<OuterInstan
val result = SmartList<OuterInstanceReferenceUsageInfo>()
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
}