diff --git a/idea/resources/messages/KotlinBundle.properties b/idea/resources/messages/KotlinBundle.properties index 2616ec0049a..42d7db813e6 100644 --- a/idea/resources/messages/KotlinBundle.properties +++ b/idea/resources/messages/KotlinBundle.properties @@ -816,7 +816,7 @@ label.text.target.file.name=Target file name: label.text.to.file=To &file: label.text.to.package=To p&ackage: label.text.type=&Type:\u0020 -label.text.update.package.directive=&Update package directive +label.text.move.expect.actual.counterparts=&Move expect/actual counterparts label.text.visibility=&Visibility:\u0020 member.info.abstract.0=abstract {0} member.info.companion.0=companion {0} @@ -876,6 +876,7 @@ text.cannot.inline.0.1.usages=Cannot inline {0}/{1} usages text.cannot.move.for.current.project=Can't move for current project text.cannot.move.inner.class.0.into.itself=Cannot move nested class {0} to itself text.cannot.move.to.original.file=Can't move to the original file +text.cannot.move.expect.actual.declaration.to.file=Could't move expect/actual declaration to file text.change.file.package.to.0=Change file''s package to ''{0}'' text.choose.containing.file=Choose Containing File text.class.0.already.contains.member.1={0} already contains {1} diff --git a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt index 9a669bb4437..bc5b89c41ed 100644 --- a/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt +++ b/idea/src/org/jetbrains/kotlin/idea/actions/internal/refactoringTesting/cases/MoveKotlinDeclarationsHandlerTestActions.kt @@ -156,6 +156,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep searchInComments: Boolean, searchForTextOccurrences: Boolean, deleteEmptySourceFiles: Boolean, + moveMppDeclarations: Boolean, moveCallback: MoveCallback? ) { val selectedElementsToMove = mutableSetOf() @@ -181,6 +182,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep isDeleteEmptyFiles = randomBoolean(), isUpdatePackageDirective = randomBoolean(), isFullFileMove = randomBoolean(), + applyMPPDeclarations = true, moveCallback = null ) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt index afdbe5437ad..29c10ecc4d2 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSettings.kt @@ -61,6 +61,9 @@ class KotlinRefactoringSettings : PersistentStateComponent - + @@ -160,6 +160,15 @@ + + + + + + + + + diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java index 6b3fe7581eb..fa40fbb6463 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsDialog.java @@ -41,6 +41,8 @@ import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberSelectionTab import org.jetbrains.kotlin.idea.refactoring.move.MoveUtilsKt; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinDestinationFolderComboBox; import org.jetbrains.kotlin.idea.refactoring.ui.KotlinFileChooserDialog; +import org.jetbrains.kotlin.idea.util.ExpectActualUtilKt; +import org.jetbrains.kotlin.psi.KtElement; import org.jetbrains.kotlin.psi.KtFile; import org.jetbrains.kotlin.psi.KtNamedDeclaration; import org.jetbrains.kotlin.psi.KtPureElement; @@ -75,6 +77,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private JCheckBox cbUpdatePackageDirective; private JCheckBox cbDeleteEmptySourceFiles; private JCheckBox cbSearchReferences; + private JCheckBox cbApplyMPPDeclarationsMove; private KotlinMemberSelectionTable memberTable; private class MemberSelectionerInfoChangeListener implements MemberInfoChangeListener { @@ -100,6 +103,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @Override public void memberInfoChanged(@NotNull MemberInfoChange event) { + updateControls(); updatePackageDirectiveCheckBox(); updateFileNameInPackageField(); // Update file name field only if it user hasn't changed it to some non-default value @@ -119,6 +123,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { boolean searchInComments, boolean searchForTextOccurrences, boolean deleteEmptySourceFiles, + boolean moveMppDeclarations, @Nullable MoveCallback moveCallback ) { super(project, true); @@ -132,7 +137,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { setTitle(MoveHandler.REFACTORING_NAME); - initSearchOptions(searchInComments, searchForTextOccurrences, deleteEmptySourceFiles); + initSearchOptions(searchInComments, searchForTextOccurrences, deleteEmptySourceFiles, moveMppDeclarations); initPackageChooser(targetPackageName, targetDirectory, sourceFiles); @@ -190,6 +195,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { selectionPanel.getTable().setMemberInfoModel(memberInfoModel); selectionPanel.getTable().addMemberInfoChangeListener(memberInfoModel); selectionPanel.getTable().addMemberInfoChangeListener(new MemberSelectionerInfoChangeListener(memberInfos)); + cbApplyMPPDeclarationsMove.addChangeListener(e -> updateControls()); memberInfoPanel.add(selectionPanel, BorderLayout.CENTER); } @@ -229,10 +235,11 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { cbUpdatePackageDirective.setSelected(arePackagesAndDirectoryMatched(sourceFiles)); } - private void initSearchOptions(boolean searchInComments, boolean searchForTextOccurences, boolean deleteEmptySourceFiles) { + private void initSearchOptions(boolean searchInComments, boolean searchForTextOccurences, boolean deleteEmptySourceFiles, boolean moveMppDeclarations) { cbSearchInComments.setSelected(searchInComments); cbSearchTextOccurrences.setSelected(searchForTextOccurences); cbDeleteEmptySourceFiles.setSelected(deleteEmptySourceFiles); + cbApplyMPPDeclarationsMove.setSelected(moveMppDeclarations); } private void initMoveToButtons(boolean moveToPackage) { @@ -327,7 +334,30 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ); } + private boolean isMppDeclarationSelected() { + for(KtNamedDeclaration element : getSelectedElementsToMove()) { + if (ExpectActualUtilKt.isEffectivelyActual(element, true) || + ExpectActualUtilKt.isExpectDeclaration(element)) { + return true; + } + } + return false; + } + private void updateControls() { + + boolean mppDeclarationSelected = isMppDeclarationSelected(); + cbApplyMPPDeclarationsMove.setEnabled(mppDeclarationSelected); + + boolean needToMoveMPPDeclarations = mppDeclarationSelected && cbApplyMPPDeclarationsMove.isSelected(); + + if (needToMoveMPPDeclarations) { + if (!rbMoveToPackage.isSelected()) { + rbMoveToPackage.setSelected(true); + } + } + UIUtil.setEnabled(rbMoveToFile, !needToMoveMPPDeclarations, true); + boolean moveToPackage = rbMoveToPackage.isSelected(); classPackageChooser.setEnabled(moveToPackage); updateFileNameInPackageField(); @@ -363,6 +393,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { refactoringSettings.MOVE_SEARCH_FOR_TEXT = cbSearchTextOccurrences.isSelected(); refactoringSettings.MOVE_DELETE_EMPTY_SOURCE_FILES = cbDeleteEmptySourceFiles.isSelected(); refactoringSettings.MOVE_PREVIEW_USAGES = isPreviewUsages(); + refactoringSettings.MOVE_MPP_DECLARATIONS = cbApplyMPPDeclarationsMove.isSelected(); RecentsManager.getInstance(myProject).registerRecentEntry(RECENTS_KEY, getTargetPackage()); } @@ -401,6 +432,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { DirectoryChooser.ItemWrapper selectedItem = (DirectoryChooser.ItemWrapper) destinationFolderCB.getComboBox().getSelectedItem(); PsiDirectory selectedPsiDirectory = selectedItem != null ? selectedItem.getDirectory() : initialTargetDirectory; + boolean mppDeclarationSelected = cbApplyMPPDeclarationsMove.isSelected() && isMppDeclarationSelected(); + return new MoveKotlinTopLevelDeclarationsModel( myProject, getSelectedElementsToMoveChecked(), @@ -415,6 +448,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { cbDeleteEmptySourceFiles.isSelected(), cbUpdatePackageDirective.isSelected(), isFullFileMove(), + mppDeclarationSelected, moveCallback ); } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt index a4aa8b79728..2bc330db392 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinTopLevelDeclarationsModel.kt @@ -27,6 +27,9 @@ 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.collectAllExpectAndActualDeclaration +import org.jetbrains.kotlin.idea.util.isEffectivelyActual +import org.jetbrains.kotlin.idea.util.isExpectDeclaration import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtNamedDeclaration @@ -48,6 +51,7 @@ internal class MoveKotlinTopLevelDeclarationsModel( val isDeleteEmptyFiles: Boolean, val isUpdatePackageDirective: Boolean, val isFullFileMove: Boolean, + val applyMPPDeclarations: Boolean, val moveCallback: MoveCallback? ) : Model { @@ -59,6 +63,8 @@ internal class MoveKotlinTopLevelDeclarationsModel( private val sourceFiles: Set = elementsToMove.mapTo(mutableSetOf()) { it.containingKtFile } + private val elementsToMoveHasMPP = applyMPPDeclarations && elementsToMove.any { it.isEffectivelyActual() || it.isExpectDeclaration() } + private val singleSourceFileMode = sourceFiles.size == 1 private fun selectPackageBasedDestination(): MoveDestination { @@ -187,7 +193,11 @@ internal class MoveKotlinTopLevelDeclarationsModel( if (isMoveToPackage) selectMoveTargetToPackage() else selectMoveTargetToFile() private fun verifyBeforeRun() { + if (!isMoveToPackage && elementsToMoveHasMPP) + throw ConfigurationException(KotlinBundle.message("text.cannot.move.expect.actual.declaration.to.file")) + if (elementsToMove.isEmpty()) throw ConfigurationException(KotlinBundle.message("text.at.least.one.file.must.be.selected")) + if (sourceFiles.isEmpty()) throw ConfigurationException("None elements were selected") if (singleSourceFileMode && fileNameInPackage.isBlank()) throw ConfigurationException(KotlinBundle.message("text.file.name.cannot.be.empty")) @@ -220,6 +230,8 @@ internal class MoveKotlinTopLevelDeclarationsModel( private fun tryMoveFile(throwOnConflicts: Boolean): BaseRefactoringProcessor? { + if (elementsToMoveHasMPP) return null + val targetFileName = if (sourceFiles.size > 1) null else fileNameInPackage if (targetFileName != null) checkTargetFileName(targetFileName) @@ -260,14 +272,21 @@ internal class MoveKotlinTopLevelDeclarationsModel( } private fun moveDeclaration(throwOnConflicts: Boolean): BaseRefactoringProcessor { + + val elementsWithMPPIfNeeded = + if (elementsToMoveHasMPP) elementsToMove + .flatMap { it.collectAllExpectAndActualDeclaration() } + .filterIsInstance() + else elementsToMove + val target = selectMoveTarget() - for (element in elementsToMove) { + for (element in elementsWithMPPIfNeeded) { target.verify(element.containingFile)?.let { throw ConfigurationException(it) } } val options = MoveDeclarationsDescriptor( project, - MoveSource(elementsToMove), + MoveSource(elementsWithMPPIfNeeded), target, MoveDeclarationsDelegate.TopLevel, isSearchInComments,