Move top level declaration refactoring

This commit is contained in:
Igor Yakovlev
2019-12-24 23:23:07 +03:00
parent f58a6e054c
commit a68a6b77df
2 changed files with 77 additions and 96 deletions
@@ -17,13 +17,11 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiManager import com.intellij.psi.PsiManager
import com.intellij.refactoring.PackageWrapper import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesUtil
import com.intellij.refactoring.util.RefactoringMessageUtil import com.intellij.refactoring.util.RefactoringMessageUtil
import com.intellij.refactoring.util.RefactoringUtil import com.intellij.refactoring.util.RefactoringUtil
import com.intellij.util.IncorrectOperationException import com.intellij.util.IncorrectOperationException
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.caches.resolve.unsafeResolveToDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.core.KotlinNameSuggester
import org.jetbrains.kotlin.idea.refactoring.createKotlinFile import org.jetbrains.kotlin.idea.refactoring.createKotlinFile
import org.jetbrains.kotlin.idea.refactoring.move.getTargetPackageFqName import org.jetbrains.kotlin.idea.refactoring.move.getTargetPackageFqName
@@ -57,13 +55,13 @@ internal abstract class MoveKotlinNestedClassesToUpperLevelModel(
initialDir: PsiDirectory? initialDir: PsiDirectory?
): VirtualFile? ): VirtualFile?
private val innerClassDescriptor = innerClass.unsafeResolveToDescriptor(BodyResolveMode.FULL) as ClassDescriptor private val innerClassDescriptor = innerClass.resolveToDescriptorIfAny(BodyResolveMode.FULL)
private fun getTargetContainer(): PsiElement? { private fun getTargetContainer(): PsiElement? {
if (target is PsiDirectory) { if (target is PsiDirectory) {
val oldPackageFqName = getTargetPackageFqName(target) val oldPackageFqName = getTargetPackageFqName(target)
val targetName = packageName val targetName = packageName
if (!Comparing.equal(oldPackageFqName?.asString(), targetName)) { if (oldPackageFqName?.asString() != targetName) {
val projectRootManager = ProjectRootManager.getInstance(project) val projectRootManager = ProjectRootManager.getInstance(project)
val contentSourceRoots = getSuitableDestinationSourceRoots(project) val contentSourceRoots = getSuitableDestinationSourceRoots(project)
@@ -88,24 +86,21 @@ internal abstract class MoveKotlinNestedClassesToUpperLevelModel(
targetSourceRoot = contentSourceRoots[0] targetSourceRoot = contentSourceRoots[0]
} }
var directory = RefactoringUtil.findPackageDirectoryInSourceRoot(newPackage, targetSourceRoot); RefactoringUtil.findPackageDirectoryInSourceRoot(newPackage, targetSourceRoot)?.let { return it }
if (directory === null) {
runWriteAction { return runWriteAction {
try { try {
directory = RefactoringUtil.createPackageDirectoryInSourceRoot(newPackage, targetSourceRoot) RefactoringUtil.createPackageDirectoryInSourceRoot(newPackage, targetSourceRoot)
} catch (e: IncorrectOperationException) { } catch (e: IncorrectOperationException) {
directory = null; null
}
} }
} }
return directory;
} }
return target return target
} }
return if (target is KtFile || target is KtClassOrObject) target else null return target as? KtFile ?: target as? KtClassOrObject
} }
@Throws(ConfigurationException::class) @Throws(ConfigurationException::class)
@@ -142,11 +137,10 @@ internal abstract class MoveKotlinNestedClassesToUpperLevelModel(
val targetPackageFqName = getTargetPackageFqName(target) val targetPackageFqName = getTargetPackageFqName(target)
?: throw ConfigurationException("No package corresponds to this directory") ?: throw ConfigurationException("No package corresponds to this directory")
val existingClass = DescriptorUtils val existingClass = innerClassDescriptor?.let { DescriptorUtils.getContainingModule(it) }
.getContainingModule(innerClassDescriptor) ?.getPackage(targetPackageFqName)
.getPackage(targetPackageFqName) ?.memberScope
.memberScope ?.getContributedClassifier(Name.identifier(className), NoLookupLocation.FROM_IDE)
.getContributedClassifier(Name.identifier(className), NoLookupLocation.FROM_IDE)
if (existingClass != null) { if (existingClass != null) {
throw ConfigurationException("Class $className already exists in package $targetPackageFqName") throw ConfigurationException("Class $className already exists in package $targetPackageFqName")
} }
@@ -163,21 +157,20 @@ internal abstract class MoveKotlinNestedClassesToUpperLevelModel(
private fun getMoveTarget(): KotlinMoveTarget { private fun getMoveTarget(): KotlinMoveTarget {
val target = getTargetContainerWithValidation() val target = getTargetContainerWithValidation()
if (target is PsiDirectory) { if (target is PsiDirectory) {
val targetDir = target
val targetPackageFqName = getTargetPackageFqName(target) val targetPackageFqName = getTargetPackageFqName(target)
?: throw ConfigurationException("Cannot find target package name") ?: throw ConfigurationException("Cannot find target package name")
val suggestedName = KotlinNameSuggester.suggestNameByName(className) { val suggestedName = KotlinNameSuggester.suggestNameByName(className) {
targetDir.findFile(it + "." + KotlinFileType.EXTENSION) == null target.findFile(it + "." + KotlinFileType.EXTENSION) == null
} }
val targetFileName = suggestedName + "." + KotlinFileType.EXTENSION val targetFileName = suggestedName + "." + KotlinFileType.EXTENSION
return KotlinMoveTargetForDeferredFile( return KotlinMoveTargetForDeferredFile(
targetPackageFqName, targetPackageFqName,
targetDir, null target,
) { createKotlinFile(targetFileName, targetDir, targetPackageFqName.asString()) } targetFile = null
) { createKotlinFile(targetFileName, target, targetPackageFqName.asString()) }
} else { } else {
return KotlinMoveTargetForExistingElement(target as KtElement) return KotlinMoveTargetForExistingElement(target as KtElement)
} }
@@ -7,26 +7,23 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui
import com.intellij.openapi.fileTypes.FileTypeManager import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.ConfigurationException
import com.intellij.psi.* import com.intellij.psi.*
import com.intellij.refactoring.BaseRefactoringProcessor import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.refactoring.MoveDestination import com.intellij.refactoring.MoveDestination
import com.intellij.refactoring.PackageWrapper import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.move.MoveCallback import com.intellij.refactoring.move.MoveCallback
import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination import com.intellij.refactoring.move.moveClassesOrPackages.AutocreatingSingleSourceRootMoveDestination
import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination
import com.intellij.util.IncorrectOperationException
import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.core.util.toPsiDirectory
import org.jetbrains.kotlin.idea.core.util.toPsiFile import org.jetbrains.kotlin.idea.core.util.toPsiFile
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.refactoring.move.getOrCreateDirectory import org.jetbrains.kotlin.idea.refactoring.move.getOrCreateDirectory
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.* import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.refactoring.move.updatePackageDirective 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.name.FqName
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration import org.jetbrains.kotlin.psi.KtNamedDeclaration
@@ -51,29 +48,24 @@ internal class MoveKotlinTopLevelDeclarationsModel(
val moveCallback: MoveCallback? val moveCallback: MoveCallback?
) : Model<BaseRefactoringProcessor> { ) : Model<BaseRefactoringProcessor> {
private inline fun <T, K> List<T>.mapToSingleOrNull(transform: (T) -> K?): K? = private inline fun <T, K> Set<T>.mapToSingleOrNull(transform: (T) -> K?): K? =
mapTo(mutableSetOf(), transform).singleOrNull() mapTo(mutableSetOf(), transform).singleOrNull()
private val sourceDirectory by lazy { private fun checkedGetSourceDirectory() =
sourceFiles.mapToSingleOrNull { it.parent } ?: throw ConfigurationException("Can't determine sources directory") sourceFiles.mapToSingleOrNull { it.parent } ?: throw ConfigurationException("Can't determine sources directory")
}
private val sourceFiles = elementsToMove.map { it.containingKtFile }.distinct() private val sourceFiles: Set<KtFile> = elementsToMove.mapTo(mutableSetOf()) { it.containingKtFile }
private data class TargetDirAndDestination(val targetDir: VirtualFile?, val destination: MoveDestination) private val singleSourceFileMode = sourceFiles.size == 1
private fun selectPackageBasedTargetDirAndDestination(): TargetDirAndDestination { private fun selectPackageBasedDestination(): MoveDestination {
val targetPackageWrapper = PackageWrapper(PsiManager.getInstance(project), targetPackage) val targetPackageWrapper = PackageWrapper(PsiManager.getInstance(project), targetPackage)
return if (selectedPsiDirectory == null) return if (selectedPsiDirectory == null)
TargetDirAndDestination(null, MultipleRootsMoveDestination(targetPackageWrapper)) MultipleRootsMoveDestination(targetPackageWrapper)
else { else
TargetDirAndDestination( AutocreatingSingleSourceRootMoveDestination(targetPackageWrapper, selectedPsiDirectory.virtualFile)
selectedPsiDirectory.virtualFile,
AutocreatingSingleSourceRootMoveDestination(targetPackageWrapper, selectedPsiDirectory.virtualFile)
)
}
} }
private fun checkTargetFileName(fileName: String) { private fun checkTargetFileName(fileName: String) {
@@ -82,7 +74,7 @@ internal class MoveKotlinTopLevelDeclarationsModel(
} }
} }
private fun getFilesExistingInTargetDir( private fun getFilesExistingInTargetDirectory(
targetFileName: String?, targetFileName: String?,
targetDirectory: PsiDirectory targetDirectory: PsiDirectory
): Set<PsiFile> { ): Set<PsiFile> {
@@ -93,41 +85,43 @@ internal class MoveKotlinTopLevelDeclarationsModel(
} }
} }
private fun tryMoveToPackageForExistingDirectory(targetFileName: String?, targetDirectory: PsiDirectory): KotlinMoveTarget? {
val filesExistingInTargetDir = getFilesExistingInTargetDirectory(targetFileName, targetDirectory)
if (filesExistingInTargetDir.isEmpty()) return null
if (singleSourceFileMode) {
val singeTargetFile = filesExistingInTargetDir.single() as? KtFile
if (singeTargetFile != null) {
return KotlinMoveTargetForExistingElement(singeTargetFile)
}
} else {
val filePathsToReport = filesExistingInTargetDir.joinToString(
separator = "\n",
prefix = "Cannot perform refactoring since the following files already exist:\n\n"
) { it.virtualFile.path }
throw ConfigurationException(filePathsToReport)
}
return null
}
private fun selectMoveTargetToPackage(): KotlinMoveTarget { private fun selectMoveTargetToPackage(): KotlinMoveTarget {
require(sourceFiles.isNotEmpty()) require(sourceFiles.isNotEmpty())
val (targetDir, moveDestination) = selectPackageBasedTargetDirAndDestination() val moveDestination = selectPackageBasedDestination()
val targetDirectory = moveDestination.getTargetIfExists(sourceDirectory) val targetDirectory: PsiDirectory? = moveDestination.getTargetIfExists(checkedGetSourceDirectory())
?: throw ConfigurationException("Can't get target directory for selected package")
val destination = sourceFiles
.mapToSingleOrNull { moveDestination.getTargetIfExists(it) }
?: throw ConfigurationException("Can't get target for all source elements")
val singleSourceFileMode = sourceFiles.size == 1
val targetFileName = if (singleSourceFileMode) fileNameInPackage.also(::checkTargetFileName) else null val targetFileName = if (singleSourceFileMode) fileNameInPackage.also(::checkTargetFileName) else null
val filesExistingInTargetDir = getFilesExistingInTargetDir(targetFileName, targetDirectory)
if (filesExistingInTargetDir.isNotEmpty()) { if (targetDirectory != null) {
if (singleSourceFileMode) { tryMoveToPackageForExistingDirectory(targetFileName, targetDirectory)?.let { return it }
val singeTargetFile = filesExistingInTargetDir.single() as? KtFile
if (singeTargetFile != null) {
return KotlinMoveTargetForExistingElement(singeTargetFile)
}
} else {
val filePathsToReport = filesExistingInTargetDir.joinToString(
separator = "\n",
prefix = "Cannot perform refactoring since the following files already exist:\n\n"
) { it.virtualFile.path }
throw ConfigurationException(filePathsToReport)
}
} }
return KotlinMoveTargetForDeferredFile( return KotlinMoveTargetForDeferredFile(
FqName(targetPackage), FqName(targetPackage),
destination, targetDirectory
targetDir
) { ) {
val deferredFileName = if (singleSourceFileMode) fileNameInPackage else it.name val deferredFileName = if (singleSourceFileMode) fileNameInPackage else it.name
val deferredFileDirectory = moveDestination.getTargetDirectory(it) val deferredFileDirectory = moveDestination.getTargetDirectory(it)
@@ -137,51 +131,49 @@ internal class MoveKotlinTopLevelDeclarationsModel(
private fun selectMoveTargetToFile(): KotlinMoveTarget { private fun selectMoveTargetToFile(): KotlinMoveTarget {
try { val targetFile = try {
Paths.get(targetFilePath) Paths.get(targetFilePath).toFile()
} catch (e: InvalidPathException) { } catch (e: InvalidPathException) {
throw ConfigurationException("Invalid target path $targetFilePath") throw ConfigurationException("Invalid target path $targetFilePath")
} }
val targetFile = File(targetFilePath)
checkTargetFileName(targetFile.name) checkTargetFileName(targetFile.name)
val jetFile = targetFile.toPsiFile(project) as? KtFile val jetFile = targetFile.toPsiFile(project) as? KtFile
if (jetFile != null) { if (jetFile != null) {
if (sourceFiles.size == 1 && sourceFiles.contains(jetFile)) { if (sourceFiles.singleOrNull() == jetFile) {
throw ConfigurationException("Can't move to the original file") throw ConfigurationException("Can't move to the original file")
} }
return KotlinMoveTargetForExistingElement(jetFile) return KotlinMoveTargetForExistingElement(jetFile)
} }
val targetDirPath = targetFile.toPath().parent val targetDirectoryPath = targetFile.toPath().parent
?: throw ConfigurationException("Incorrect target path. Directory is not specified.") ?: throw ConfigurationException("Incorrect target path. Directory is not specified.")
val projectBasePath = project.basePath val projectBasePath = project.basePath
?: throw ConfigurationException("Can't move for current project") ?: throw ConfigurationException("Can't move for current project")
if (!targetDirPath.startsWith(projectBasePath)) { if (!targetDirectoryPath.startsWith(projectBasePath)) {
throw ConfigurationException("Incorrect target path. Directory $targetDirPath does not belong to current project.") throw ConfigurationException("Incorrect target path. Directory $targetDirectoryPath does not belong to current project.")
} }
val absoluteTargetDirPath = targetDirPath.toString() val psiDirectory = targetDirectoryPath.toFile().toPsiDirectory(project)
val psiDirectory: PsiDirectory
try {
psiDirectory = getOrCreateDirectory(absoluteTargetDirPath, project)
} catch (e: IncorrectOperationException) {
throw ConfigurationException("Failed to create parent directory: $absoluteTargetDirPath")
}
val targetPackageFqName = sourceFiles.singleOrNull()?.packageFqName val targetPackageFqName = sourceFiles.singleOrNull()?.packageFqName
?: JavaDirectoryService.getInstance().getPackage(psiDirectory)?.let { FqName(it.qualifiedName) } ?: psiDirectory?.getPackage()?.let { FqName(it.qualifiedName) }
?: throw ConfigurationException("Could not find package corresponding to $absoluteTargetDirPath") ?: throw ConfigurationException("Could not find package corresponding to $targetDirectoryPath")
val targetDirectoryPathString = targetDirectoryPath.toString()
val finalTargetPackageFqName = targetPackageFqName.asString() val finalTargetPackageFqName = targetPackageFqName.asString()
return KotlinMoveTargetForDeferredFile( return KotlinMoveTargetForDeferredFile(
targetPackageFqName, targetPackageFqName,
psiDirectory, psiDirectory,
targetFile = null targetFile = null
) { getOrCreateKotlinFile(targetFile.name, psiDirectory, finalTargetPackageFqName) } ) {
val actualPsiDirectory = psiDirectory ?: getOrCreateDirectory(targetDirectoryPathString, project)
getOrCreateKotlinFile(targetFile.name, actualPsiDirectory, finalTargetPackageFqName)
}
} }
private fun selectMoveTarget() = private fun selectMoveTarget() =
@@ -191,7 +183,7 @@ internal class MoveKotlinTopLevelDeclarationsModel(
if (elementsToMove.isEmpty()) throw ConfigurationException("At least one member must be selected") if (elementsToMove.isEmpty()) throw ConfigurationException("At least one member must be selected")
if (sourceFiles.isEmpty()) throw ConfigurationException("None elements were selected") if (sourceFiles.isEmpty()) throw ConfigurationException("None elements were selected")
if (sourceFiles.size == 1 && fileNameInPackage.isBlank()) throw ConfigurationException("File name may not be empty") if (singleSourceFileMode && fileNameInPackage.isBlank()) throw ConfigurationException("File name may not be empty")
if (isMoveToPackage) { if (isMoveToPackage) {
if (targetPackage.isNotEmpty() && !PsiNameHelper.getInstance(project).isQualifiedName(targetPackage)) { if (targetPackage.isNotEmpty() && !PsiNameHelper.getInstance(project).isQualifiedName(targetPackage)) {
@@ -225,20 +217,16 @@ internal class MoveKotlinTopLevelDeclarationsModel(
val targetFileName = if (sourceFiles.size > 1) null else fileNameInPackage val targetFileName = if (sourceFiles.size > 1) null else fileNameInPackage
if (targetFileName != null) checkTargetFileName(targetFileName) if (targetFileName != null) checkTargetFileName(targetFileName)
val moveDestination = selectPackageBasedTargetDirAndDestination().destination val moveDestination = selectPackageBasedDestination()
val targetDir = moveDestination.getTargetIfExists(sourceDirectory) ?: return null val targetDirectory = moveDestination.getTargetIfExists(checkedGetSourceDirectory()) ?: return null
val filesExistingInTargetDir = getFilesExistingInTargetDir(targetFileName, targetDir) val filesExistingInTargetDir = getFilesExistingInTargetDirectory(targetFileName, targetDirectory)
val moveAsFile = filesExistingInTargetDir.isEmpty() || val moveAsFile = filesExistingInTargetDir.isEmpty() ||
filesExistingInTargetDir.singleOrNull()?.let { sourceFiles.contains(it) } == true filesExistingInTargetDir.singleOrNull()?.let { sourceFiles.contains(it) } == true
if (!moveAsFile) return null if (!moveAsFile) return null
val targetDirectory = project.executeCommand(RefactoringBundle.message("move.title"), null) {
runWriteAction<PsiDirectory> { moveDestination.getTargetDirectory(sourceDirectory) }
}
sourceFiles.forEach { it.updatePackageDirective = isUpdatePackageDirective } sourceFiles.forEach { it.updatePackageDirective = isUpdatePackageDirective }
return if (targetFileName != null) return if (targetFileName != null)
@@ -255,7 +243,7 @@ internal class MoveKotlinTopLevelDeclarationsModel(
else else
KotlinAwareMoveFilesOrDirectoriesProcessor( KotlinAwareMoveFilesOrDirectoriesProcessor(
project, project,
sourceFiles, sourceFiles.toList(),
targetDirectory, targetDirectory,
isSearchReferences, isSearchReferences,
searchInComments = isSearchInComments, searchInComments = isSearchInComments,