From 3edc903daf7812c019396c1b54396b85fe75276d Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Wed, 12 Aug 2015 14:03:46 +0300 Subject: [PATCH] Move Declarations: Support multiple files #KT-8431 Fixed --- .../KotlinMoveTarget.kt | 12 +- .../MoveKotlinTopLevelDeclarationsHandler.kt | 31 +- .../MoveKotlinTopLevelDeclarationsDialog.java | 349 ++++++++++++------ .../kotlin/idea/refactoring/move/moveUtils.kt | 17 +- .../refactoring/move/AbstractJetMoveTest.kt | 16 +- 5 files changed, 277 insertions(+), 148 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/KotlinMoveTarget.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/KotlinMoveTarget.kt index 39b8727184a..6b86afe3f07 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/KotlinMoveTarget.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/KotlinMoveTarget.kt @@ -23,6 +23,7 @@ import com.intellij.psi.PsiManager import com.intellij.refactoring.PackageWrapper import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.JetFile +import java.util.* public interface KotlinMoveTarget { val packageWrapper: PackageWrapper? @@ -48,13 +49,16 @@ public class JetFileKotlinMoveTarget(val targetFile: JetFile): KotlinMoveTarget public class DeferredJetFileKotlinMoveTarget( project: Project, - val packageFqName: FqName, - createFile: () -> JetFile?): KotlinMoveTarget { - val createdFile: JetFile? by lazy(createFile) + private val packageFqName: FqName, + private val createFile: (JetFile) -> JetFile?): KotlinMoveTarget { + private val createdFiles = HashMap() override val packageWrapper: PackageWrapper = PackageWrapper(PsiManager.getInstance(project), packageFqName.asString()) - override fun getOrCreateTargetPsi(originalPsi: PsiElement) = createdFile + override fun getOrCreateTargetPsi(originalPsi: PsiElement): PsiFile? { + val originalFile = originalPsi.containingFile as? JetFile ?: return null + return createdFiles.getOrPut(originalFile) { createFile(originalFile) } + } override fun getTargetPsiIfExists(originalPsi: PsiElement) = null diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveKotlinTopLevelDeclarationsHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveKotlinTopLevelDeclarationsHandler.kt index 6c1b76e3c23..9e0791be73c 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveKotlinTopLevelDeclarationsHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveKotlinTopLevelDeclarationsHandler.kt @@ -20,42 +20,39 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.LangDataKeys import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project -import com.intellij.psi.* +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiPackage +import com.intellij.psi.PsiReference import com.intellij.refactoring.JavaRefactoringSettings import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.move.MoveCallback import com.intellij.refactoring.move.MoveHandlerDelegate import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackagesImpl -import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesUtil import com.intellij.refactoring.util.CommonRefactoringUtil +import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.core.refactoring.isInJavaSourceRoot -import org.jetbrains.kotlin.idea.refactoring.move.* import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog import org.jetbrains.kotlin.psi.* -import org.jetbrains.kotlin.idea.core.getPackage -import org.jetbrains.kotlin.idea.refactoring.move.guessNewFileName -import java.util.HashSet import java.util.LinkedHashSet public class MoveKotlinTopLevelDeclarationsHandler : MoveHandlerDelegate() { + private fun getSourceDirectories(elements: Array) = elements.mapTo(LinkedHashSet()) { it.containingFile?.parent } + private fun doMoveWithCheck( project: Project, elements: Array, targetContainer: PsiElement?, callback: MoveCallback? ): Boolean { if (!CommonRefactoringUtil.checkReadOnlyStatusRecursively(project, elements.toList(), true)) return false - - @suppress("UNCHECKED_CAST") - val elementsToSearch = elements.toSet() as Set - - val sourceFiles = elementsToSearch.mapTo(LinkedHashSet()) { it.getContainingJetFile() } - val sourceFile = sourceFiles.singleOrNull() - if (sourceFile == null) { + if (getSourceDirectories(elements).singleOrNull() == null) { CommonRefactoringUtil.showErrorMessage(RefactoringBundle.message("move.title"), - "All declarations must belong to the same file", + "All declarations must belong to the same directory", null, project) return false } + val elementsToSearch = elements.mapTo(LinkedHashSet()) { it as JetNamedDeclaration } + val targetPackageName = MoveClassesOrPackagesImpl.getInitialTargetPackageName(targetContainer, elements) val targetDirectory = MoveClassesOrPackagesImpl.getInitialTargetDirectory(targetContainer, elements) val searchInComments = JavaRefactoringSettings.getInstance()!!.MOVE_SEARCH_IN_COMMENTS @@ -64,7 +61,7 @@ public class MoveKotlinTopLevelDeclarationsHandler : MoveHandlerDelegate() { val moveToPackage = targetContainer !is JetFile MoveKotlinTopLevelDeclarationsDialog( - project, sourceFile, elementsToSearch, targetPackageName, targetDirectory, targetFile, moveToPackage, searchInComments, searchInText, callback + project, elementsToSearch, targetPackageName, targetDirectory, targetFile, moveToPackage, searchInComments, searchInText, callback ).show() return true @@ -72,11 +69,11 @@ public class MoveKotlinTopLevelDeclarationsHandler : MoveHandlerDelegate() { override fun canMove(elements: Array, targetContainer: PsiElement?): Boolean { if (!super.canMove(elements, targetContainer)) return false - if (elements.map { it.getContainingFile() }.distinct().size() != 1) return false + if (getSourceDirectories(elements).singleOrNull() == null) return false return elements.all { e -> if (e is JetClass || (e is JetObjectDeclaration && !e.isObjectLiteral()) || e is JetNamedFunction || e is JetProperty) { - val parent = e.getParent() + val parent = e.parent parent is JetFile && parent.isInJavaSourceRoot() } else false diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java index cf4e2a9a730..dcdceec5982 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java @@ -31,13 +31,15 @@ import com.intellij.openapi.ui.TextComponentAccessor; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.EmptyRunnable; import com.intellij.openapi.util.Pass; -import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.*; -import com.intellij.refactoring.*; -import com.intellij.refactoring.classMembers.DependencyMemberInfoModel; +import com.intellij.refactoring.JavaRefactoringSettings; +import com.intellij.refactoring.MoveDestination; +import com.intellij.refactoring.PackageWrapper; +import com.intellij.refactoring.RefactoringBundle; +import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; import com.intellij.refactoring.classMembers.MemberInfoChange; import com.intellij.refactoring.classMembers.MemberInfoChangeListener; -import com.intellij.refactoring.classMembers.UsesMemberDependencyGraph; import com.intellij.refactoring.move.MoveCallback; import com.intellij.refactoring.move.MoveHandler; import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox; @@ -49,6 +51,7 @@ import com.intellij.ui.ComboboxWithBrowseButton; import com.intellij.ui.RecentsManager; import com.intellij.ui.ReferenceEditorComboWithBrowseButton; import com.intellij.usageView.UsageInfo; +import com.intellij.util.Function; import com.intellij.util.IncorrectOperationException; import com.intellij.util.text.UniqueNameGenerator; import com.intellij.util.ui.UIUtil; @@ -62,10 +65,10 @@ import org.jetbrains.kotlin.idea.core.CorePackage; import org.jetbrains.kotlin.idea.core.refactoring.RefactoringPackage; import org.jetbrains.kotlin.idea.refactoring.JetRefactoringBundle; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo; -import org.jetbrains.kotlin.idea.refactoring.move.MovePackage; -import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.*; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionPanel; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTable; +import org.jetbrains.kotlin.idea.refactoring.move.MovePackage; +import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.*; import org.jetbrains.kotlin.idea.util.application.ApplicationPackage; import org.jetbrains.kotlin.name.FqName; import org.jetbrains.kotlin.psi.JetFile; @@ -76,28 +79,14 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; -import java.util.Collection; +import java.util.*; import java.util.List; -import java.util.Set; public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private static final String RECENTS_KEY = "MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY"; - private class MemberInfoModelImpl extends DependencyMemberInfoModel { - public MemberInfoModelImpl() { - super(new UsesMemberDependencyGraph(sourceFile, null, false), WARNING); - } + private static class MemberInfoModelImpl extends AbstractMemberInfoModel { - @Override - @Nullable - public Boolean isFixedAbstract(KotlinMemberInfo member) { - return null; - } - - @Override - public boolean isCheckedWhenDisabled(KotlinMemberInfo member) { - return false; - } } private JCheckBox cbSearchInComments; @@ -115,12 +104,10 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private JCheckBox cbUpdatePackageDirective; private KotlinMemberSelectionTable memberTable; - private final JetFile sourceFile; private final MoveCallback moveCallback; public MoveKotlinTopLevelDeclarationsDialog( @NotNull Project project, - @NotNull JetFile sourceFile, @NotNull Set elementsToMove, @Nullable String targetPackageName, @Nullable PsiDirectory targetDirectory, @@ -132,7 +119,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ) { super(project, true); - this.sourceFile = sourceFile; + List sourceFiles = getSourceFiles(elementsToMove); + this.moveCallback = moveCallback; init(); @@ -141,22 +129,78 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { initSearchOptions(searchInComments, searchForTextOccurences); - initPackageChooser(targetPackageName, targetDirectory); + initPackageChooser(targetPackageName, targetDirectory, sourceFiles); - initFileChooser(targetFile, elementsToMove); + initFileChooser(targetFile, elementsToMove, sourceFiles); initMoveToButtons(moveToPackage); - initMemberInfo(elementsToMove); + initMemberInfo(elementsToMove, sourceFiles); updateControls(); pack(); } - private void initMemberInfo(@NotNull final Set elementsToMove) { + private static List getSourceFiles(@NotNull Collection elementsToMove) { + return KotlinPackage.distinct( + KotlinPackage.map( + elementsToMove, + new Function1() { + @Override + public JetFile invoke(JetNamedDeclaration declaration) { + return declaration.getContainingJetFile(); + } + } + ) + ); + } + + @NotNull + private static PsiDirectory getSourceDirectory(@NotNull Collection sourceFiles) { + return KotlinPackage.single( + KotlinPackage.distinct( + KotlinPackage.map( + sourceFiles, + new Function1() { + @Override + public PsiDirectory invoke(JetFile jetFile) { + return jetFile.getParent(); + } + } + ) + ) + ); + } + + private static List getAllDeclarations(Collection sourceFiles) { + return KotlinPackage.filterIsInstance( + KotlinPackage.flatMap( + sourceFiles, + new Function1>() { + @Override + public Iterable invoke(JetFile jetFile) { + return jetFile.getDeclarations(); + } + } + ), + JetNamedDeclaration.class + ); + } + + private static boolean arePackagesAndDirectoryMatched(List sourceFiles) { + for (JetFile sourceFile : sourceFiles) { + if (!CorePackage.packageMatchesDirectory(sourceFile)) return false; + } + return true; + } + + private void initMemberInfo( + @NotNull final Set elementsToMove, + @NotNull List sourceFiles + ) { final List memberInfos = KotlinPackage.map( - KotlinPackage.filterIsInstance(sourceFile.getDeclarations(), JetNamedDeclaration.class), + getAllDeclarations(sourceFiles), new Function1() { @Override public KotlinMemberInfo invoke(JetNamedDeclaration declaration) { @@ -190,13 +234,14 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ); String suggestedText = previousDeclarations.isEmpty() ? "" - : MovePackage.guessNewFileName(sourceFile, previousDeclarations); + : MovePackage.guessNewFileName(previousDeclarations); return tfFileNameInPackage.getText().equals(suggestedText); } @Override public void memberInfoChanged(MemberInfoChange event) { updatePackageDirectiveCheckBox(); + updateFileNameInPackageField(); // Update file name field only if it user hasn't changed it to some non-default value if (shouldUpdateFileNameField(event.getChangedMembers())) { updateSuggestedFileName(); @@ -205,19 +250,25 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } ); memberInfoPanel.add(selectionPanel, BorderLayout.CENTER); - } private void updateSuggestedFileName() { - tfFileNameInPackage.setText(MovePackage.guessNewFileName(sourceFile, getSelectedElementsToMove())); + tfFileNameInPackage.setText(MovePackage.guessNewFileName(getSelectedElementsToMove())); + } private void updateFileNameInPackageField() { - cbSpecifyFileNameInPackage.setEnabled(isMoveToPackage()); - tfFileNameInPackage.setEnabled(isMoveToPackage() && cbSpecifyFileNameInPackage.isSelected()); + boolean movingSingleFileToPackage = isMoveToPackage() + && getSourceFiles(getSelectedElementsToMove()).size() == 1; + cbSpecifyFileNameInPackage.setEnabled(movingSingleFileToPackage); + tfFileNameInPackage.setEnabled(movingSingleFileToPackage && cbSpecifyFileNameInPackage.isSelected()); } - private void initPackageChooser(String targetPackageName, PsiDirectory targetDirectory) { + private void initPackageChooser( + String targetPackageName, + PsiDirectory targetDirectory, + List sourceFiles + ) { if (targetPackageName != null) { classPackageChooser.prependItem(targetPackageName); } @@ -243,7 +294,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } ); - cbUpdatePackageDirective.setSelected(CorePackage.packageMatchesDirectory(sourceFile)); + cbUpdatePackageDirective.setSelected(arePackagesAndDirectoryMatched(sourceFiles)); } private void initSearchOptions(boolean searchInComments, boolean searchForTextOccurences) { @@ -280,7 +331,11 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ); } - private void initFileChooser(@Nullable JetFile targetFile, @NotNull Set elementsToMove) { + private void initFileChooser( + @Nullable JetFile targetFile, + @NotNull Set elementsToMove, + @NotNull List sourceFiles + ) { FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() .withRoots(ProjectRootManager.getInstance(myProject).getContentRoots()) .withTreeRootVisible(true); @@ -291,7 +346,9 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { String initialTargetPath = targetFile != null ? targetFile.getVirtualFile().getPath() - : sourceFile.getVirtualFile().getParent().getPath() + "/" + MovePackage.guessNewFileName(sourceFile, elementsToMove); + : sourceFiles.get(0).getVirtualFile().getParent().getPath() + + "/" + + MovePackage.guessNewFileName(elementsToMove); fileChooser.setText(initialTargetPath); } @@ -331,8 +388,24 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { validateButtons(); } + private boolean isFullFileMove() { + Map> fileToElements = KotlinPackage.groupBy( + getSelectedElementsToMove(), + new Function1() { + @Override + public JetFile invoke(JetNamedDeclaration declaration) { + return declaration.getContainingJetFile(); + } + } + ); + for (Map.Entry> entry : fileToElements.entrySet()) { + if (entry.getKey().getDeclarations().size() != entry.getValue().size()) return false; + } + return true; + } + private void updatePackageDirectiveCheckBox() { - cbUpdatePackageDirective.setEnabled(isMoveToPackage() && getSelectedElementsToMove().size() == sourceFile.getDeclarations().size()); + cbUpdatePackageDirective.setEnabled(isMoveToPackage() && isFullFileMove()); } private boolean hasAnySourceRoots() { @@ -367,6 +440,40 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { return false; } + @NotNull + private static List getFilesExistingInTargetDir( + @NotNull List sourceFiles, + @Nullable String targetFileName, + @Nullable final PsiDirectory targetDirectory + ) { + if (targetDirectory == null) return Collections.emptyList(); + + List fileNames = + targetFileName != null + ? Collections.singletonList(targetFileName) + : KotlinPackage.map( + sourceFiles, + new Function1() { + @Override + public String invoke(JetFile jetFile) { + return jetFile.getName(); + } + } + ); + + return KotlinPackage.filterNotNull( + KotlinPackage.map( + fileNames, + new Function1() { + @Override + public PsiFile invoke(String s) { + return targetDirectory.findFile(s); + } + } + ) + ); + } + @Nullable private KotlinMoveTarget selectMoveTarget() { String message = verifyBeforeRun(); @@ -377,39 +484,63 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { setErrorText(null); + List sourceFiles = getSourceFiles(getSelectedElementsToMove()); + PsiDirectory sourceDirectory = getSourceDirectory(sourceFiles); + if (isMoveToPackage()) { final MoveDestination moveDestination = selectPackageBasedMoveDestination(true); if (moveDestination == null) return null; - final String targetFileName = tfFileNameInPackage.getText(); - if (!checkTargetFileName(targetFileName)) return null; + final String targetFileName = sourceFiles.size() > 1 ? null : tfFileNameInPackage.getText(); + if (targetFileName != null && !checkTargetFileName(targetFileName)) return null; - PsiDirectory directory = moveDestination.getTargetIfExists(sourceFile); - PsiFile targetFile = directory != null ? directory.findFile(targetFileName) : null; - if (targetFile != null) { - if (targetFile == sourceFile) { - setErrorText("Can't move to the original file"); + PsiDirectory targetDirectory = moveDestination.getTargetIfExists(sourceDirectory); + + List filesExistingInTargetDir = getFilesExistingInTargetDir(sourceFiles, targetFileName, targetDirectory); + if (!filesExistingInTargetDir.isEmpty()) { + if (!KotlinPackage.intersect(sourceFiles, filesExistingInTargetDir).isEmpty()) { + setErrorText("Can't move to the original file(s)"); return null; } - String question = "File '" + - directory.getVirtualFile().getPath() + - "/" + - targetFileName + - "' already exists." + - "Do you want to move selected declarations to this file?"; - int ret = Messages.showYesNoDialog(myProject, question, RefactoringBundle.message("move.title"), Messages.getQuestionIcon()); + if (filesExistingInTargetDir.size() > 1) { + String filePathsToReport = StringUtil.join( + filesExistingInTargetDir, + new Function() { + @Override + public String fun(PsiFile file) { + return 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; + } + + String question = String.format( + "File '%s' already exists. Do you want to move selected declarations to this file?", + filesExistingInTargetDir.get(0).getVirtualFile().getPath() + ); + int ret = + Messages.showYesNoDialog(myProject, question, RefactoringBundle.message("move.title"), Messages.getQuestionIcon()); if (ret != Messages.YES) return null; } return new DeferredJetFileKotlinMoveTarget( myProject, new FqName(getTargetPackage()), - new Function0() { + new Function1() { @Override - public JetFile invoke() { - PsiDirectory directory = moveDestination.getTargetDirectory(sourceFile); - return RefactoringPackage.getOrCreateKotlinFile(targetFileName, directory); + public JetFile invoke(@NotNull JetFile originalFile) { + return RefactoringPackage.getOrCreateKotlinFile( + targetFileName != null ? targetFileName : originalFile.getName(), + moveDestination.getTargetDirectory(originalFile) + ); } } ); @@ -419,7 +550,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { if (!checkTargetFileName(targetFile.getName())) return null; JetFile jetFile = (JetFile) RefactoringPackage.toPsiFile(targetFile, myProject); if (jetFile != null) { - if (jetFile == sourceFile) { + if (sourceFiles.size() == 1 && sourceFiles.contains(jetFile)) { setErrorText("Can't move to the original file"); return null; } @@ -427,14 +558,6 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { return new JetFileKotlinMoveTarget(jetFile); } - int ret = Messages.showYesNoDialog( - myProject, - JetRefactoringBundle.message("file.does.not.exist", targetFile.getName()), - RefactoringBundle.message("move.title"), - Messages.getQuestionIcon() - ); - if (ret != Messages.YES) return null; - File targetDir = targetFile.getParentFile(); final PsiDirectory psiDirectory = RefactoringPackage.toPsiDirectory(targetDir, myProject); assert psiDirectory != null : "No directory found: " + targetDir.getPath(); @@ -448,9 +571,9 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { return new DeferredJetFileKotlinMoveTarget( myProject, new FqName(psiPackage.getQualifiedName()), - new Function0() { + new Function1() { @Override - public JetFile invoke() { + public JetFile invoke(@NotNull JetFile originalFile) { return RefactoringPackage.getOrCreateKotlinFile(targetFile.getName(), psiDirectory); } } @@ -474,7 +597,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { } } - if (tfFileNameInPackage.getText().isEmpty()) { + if (getSourceFiles(getSelectedElementsToMove()).size() == 1 && tfFileNameInPackage.getText().isEmpty()) { return "File name may not be empty"; } @@ -527,6 +650,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { saveRefactoringSettings(); List elementsToMove = getSelectedElementsToMove(); + final List sourceFiles = getSourceFiles(elementsToMove); + final PsiDirectory sourceDirectory = getSourceDirectory(sourceFiles); for (PsiElement element : elementsToMove) { String message = target.verify(element.getContainingFile()); @@ -538,27 +663,32 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { try { boolean deleteSourceFile = false; - - if (elementsToMove.size() == sourceFile.getDeclarations().size()) { + + if (isFullFileMove()) { if (isMoveToPackage()) { final MoveDestination moveDestination = selectPackageBasedMoveDestination(false); //noinspection ConstantConditions - PsiDirectory targetDir = moveDestination.getTargetIfExists(sourceFile); - final String targetFileName = tfFileNameInPackage.getText(); - if (targetDir == null || targetDir.findFile(targetFileName) == null) { + PsiDirectory targetDir = moveDestination.getTargetIfExists(sourceDirectory); + final String targetFileName = sourceFiles.size() > 1 ? null : tfFileNameInPackage.getText(); + List filesExistingInTargetDir = getFilesExistingInTargetDir(sourceFiles, targetFileName, targetDir); + if (filesExistingInTargetDir.isEmpty()) { PsiDirectory targetDirectory = ApplicationPackage.runWriteAction( new Function0() { @Override public PsiDirectory invoke() { - return moveDestination.getTargetDirectory(sourceFile); + return moveDestination.getTargetDirectory(sourceDirectory); } } ); - MovePackage.setUpdatePackageDirective(sourceFile, cbUpdatePackageDirective.isSelected()); + + for (JetFile sourceFile : sourceFiles) { + MovePackage.setUpdatePackageDirective(sourceFile, cbUpdatePackageDirective.isSelected()); + } + invokeRefactoring( new MoveFilesOrDirectoriesProcessor( myProject, - new PsiElement[] {sourceFile}, + sourceFiles.toArray(new PsiElement[sourceFiles.size()]), targetDirectory, true, isSearchInComments(), @@ -567,7 +697,9 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @Override public void refactoringCompleted() { try { - sourceFile.setName(targetFileName); + if (targetFileName != null) { + KotlinPackage.single(sourceFiles).setName(targetFileName); + } } finally { if (moveCallback != null) { @@ -580,47 +712,48 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ) { @Override protected String getCommandName() { - return "Move " + sourceFile.getName(); + return targetFileName != null ? "Move " + KotlinPackage.single(sourceFiles).getName() : "Move"; } @Override - protected void performRefactoring(UsageInfo[] usages) { - //noinspection ConstantConditions - String temporaryName = UniqueNameGenerator.generateUniqueName( - "temp", - "", - ".kt", - KotlinPackage.map( - sourceFile.getContainingDirectory().getFiles(), - new Function1() { - @Override - public String invoke(PsiFile file) { - return file.getName(); + protected void performRefactoring(@NotNull UsageInfo[] usages) { + if (targetFileName != null) { + JetFile sourceFile = KotlinPackage.single(sourceFiles); + //noinspection ConstantConditions + String temporaryName = UniqueNameGenerator.generateUniqueName( + "temp", + "", + ".kt", + KotlinPackage.map( + sourceFile.getContainingDirectory().getFiles(), + new Function1() { + @Override + public String invoke(PsiFile file) { + return file.getName(); + } } - } - ) - ); - sourceFile.setName(temporaryName); + ) + ); + sourceFile.setName(temporaryName); + } super.performRefactoring(usages); } } ); - + return; } - - int ret = Messages.showYesNoCancelDialog( - myProject, - "You are going to move all declarations out of '" + - sourceFile.getVirtualFile().getPath() + - "'. Do you want to delete this file?", - RefactoringBundle.message("move.title"), - Messages.getQuestionIcon() - ); - if (ret == Messages.CANCEL) return; - deleteSourceFile = ret == Messages.YES; } + + 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; } MoveKotlinTopLevelDeclarationsOptions options = new MoveKotlinTopLevelDeclarationsOptions( diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt index 47cdd1e7c42..989192a5554 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveUtils.kt @@ -17,18 +17,15 @@ package org.jetbrains.kotlin.idea.refactoring.move import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.Comparing -import com.intellij.openapi.util.Computable import com.intellij.openapi.util.Key import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.* import com.intellij.refactoring.RefactoringBundle import com.intellij.refactoring.RefactoringSettings import com.intellij.refactoring.copy.CopyFilesOrDirectoriesHandler -import com.intellij.refactoring.move.MoveCallback import com.intellij.refactoring.move.MoveHandler import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesDialog import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectoriesProcessor @@ -40,12 +37,10 @@ import com.intellij.refactoring.util.CommonRefactoringUtil import com.intellij.refactoring.util.MoveRenameUsageInfo import com.intellij.refactoring.util.NonCodeUsageInfo import com.intellij.usageView.UsageInfo -import com.intellij.util.Function import com.intellij.util.IncorrectOperationException import org.jetbrains.kotlin.asJava.namedUnwrappedElement import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.descriptors.* -import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind import org.jetbrains.kotlin.idea.JetFileType import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde @@ -172,14 +167,14 @@ fun createMoveUsageInfoIfPossible( return MoveRenameUsageInfo(element, reference, startOffset, endOffset, referencedElement, false) } -public fun guessNewFileName(sourceFile: JetFile, declarationsToMove: Collection): String? { - assert(sourceFile.getDeclarations().containsAll(declarationsToMove)) - +public fun guessNewFileName(declarationsToMove: Collection): String? { if (declarationsToMove.isEmpty()) return null - if (sourceFile.getDeclarations().size() == declarationsToMove.size()) return sourceFile.getName() - val representative = declarationsToMove.firstOrNull { it is JetClassOrObject } ?: declarationsToMove.first() - return "${representative.getName()}.${JetFileType.EXTENSION}" + val representative = declarationsToMove.singleOrNull() + ?: declarationsToMove.filterIsInstance().singleOrNull() + representative?.let { return "${it.name}.${JetFileType.EXTENSION}" } + + return declarationsToMove.first().containingFile.name } // returns true if successful diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractJetMoveTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractJetMoveTest.kt index caf5c541475..b4e9cca6a67 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractJetMoveTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/move/AbstractJetMoveTest.kt @@ -19,7 +19,6 @@ package org.jetbrains.kotlin.idea.refactoring.move import com.google.gson.JsonObject import com.google.gson.JsonParser import com.intellij.codeInsight.TargetElementUtilBase -import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.util.io.FileUtil @@ -41,16 +40,18 @@ import org.jetbrains.kotlin.idea.core.refactoring.createKotlinFile import org.jetbrains.kotlin.idea.jsonUtils.getNullableString import org.jetbrains.kotlin.idea.jsonUtils.getString import org.jetbrains.kotlin.idea.refactoring.move.changePackage.KotlinChangePackageRefactoring -import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.* +import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.DeferredJetFileKotlinMoveTarget +import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.JetFileKotlinMoveTarget +import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsOptions +import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveKotlinTopLevelDeclarationsProcessor +import org.jetbrains.kotlin.idea.search.allScope +import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase import org.jetbrains.kotlin.idea.test.PluginTestCaseBase -import org.jetbrains.kotlin.idea.search.allScope -import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.JetFile import org.jetbrains.kotlin.psi.JetNamedDeclaration import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType -import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil -import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.test.JetTestUtils import java.io.File @@ -248,8 +249,7 @@ enum class MoveAction { val moveTarget = config.getNullableString("targetPackage")?.let { packageName -> DeferredJetFileKotlinMoveTarget(project, FqName(packageName)) { val moveDestination = MultipleRootsMoveDestination(PackageWrapper(mainFile.getManager(), packageName)) - createKotlinFile(guessNewFileName(mainFile as JetFile, listOf(elementToMove))!!, - moveDestination.getTargetDirectory(mainFile)) + createKotlinFile(guessNewFileName(listOf(elementToMove))!!, moveDestination.getTargetDirectory(mainFile)) } } ?: config.getString("targetFile").let { filePath -> JetFileKotlinMoveTarget(PsiManager.getInstance(project).findFile(rootDir.findFileByRelativePath(filePath)!!) as JetFile)