Add expect/actual move refactoring support

Fixed #KT-27389
This commit is contained in:
Igor Yakovlev
2020-01-28 20:22:39 +03:00
parent bd851bf2fb
commit e05df9d838
9 changed files with 80 additions and 19 deletions
@@ -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}
@@ -156,6 +156,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep
searchInComments: Boolean,
searchForTextOccurrences: Boolean,
deleteEmptySourceFiles: Boolean,
moveMppDeclarations: Boolean,
moveCallback: MoveCallback?
) {
val selectedElementsToMove = mutableSetOf<KtNamedDeclaration>()
@@ -181,6 +182,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep
isDeleteEmptyFiles = randomBoolean(),
isUpdatePackageDirective = randomBoolean(),
isFullFileMove = randomBoolean(),
applyMPPDeclarations = true,
moveCallback = null
)
@@ -61,6 +61,9 @@ class KotlinRefactoringSettings : PersistentStateComponent<KotlinRefactoringSett
@JvmField
var MOVE_DELETE_EMPTY_SOURCE_FILES = true
@JvmField
var MOVE_MPP_DECLARATIONS = true
@JvmField
var EXTRACT_INTERFACE_JAVADOC: Int = 0
@@ -160,6 +160,7 @@ class ExtractDeclarationFromCurrentFileIntention :
/* searchInComments = */ true,
/* searchForTextOccurrences = */ true,
/* deleteEmptySourceFiles = */ true,
/* moveMppDeclarations = */ false,
callBack
).showWithTransaction()
}
@@ -71,6 +71,7 @@ private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerAction
searchInComments: Boolean,
searchForTextOccurrences: Boolean,
deleteEmptySourceFiles: Boolean,
moveMppDeclarations: Boolean,
moveCallback: MoveCallback?
) = MoveKotlinTopLevelDeclarationsDialog(
project,
@@ -82,6 +83,7 @@ private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerAction
searchInComments,
searchForTextOccurrences,
deleteEmptySourceFiles,
moveMppDeclarations,
moveCallback
).show()
@@ -127,19 +129,6 @@ class MoveKotlinDeclarationsHandler internal constructor(private val handlerActi
): Boolean {
if (!CommonRefactoringUtil.checkReadOnlyStatusRecursively(project, elements.toList(), true)) return false
if (!ApplicationManager.getApplication().isUnitTestMode &&
elements.any { it is KtDeclaration && (it.isExpectDeclaration() || it.isEffectivelyActual()) }
) {
val proceedWithIncompleteRefactoring = Messages.showYesNoDialog(
project,
"This refactoring will move selected declaration without it's expect/actual counterparts that may lead to compilation errors.\n" +
"Do you wish to proceed?",
"MPP declarations does not supported by this refactoring.",
Messages.getWarningIcon()
)
if (proceedWithIncompleteRefactoring != Messages.YES) return true
}
val container = getUniqueContainer(elements)
if (container == null) {
handlerActions.showErrorHint(
@@ -207,6 +196,7 @@ class MoveKotlinDeclarationsHandler internal constructor(private val handlerActi
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 moveMppDeclarations = KotlinRefactoringSettings.instance.MOVE_MPP_DECLARATIONS
val targetFile = targetContainer as? KtFile
val moveToPackage = targetContainer !is KtFile
@@ -220,6 +210,7 @@ class MoveKotlinDeclarationsHandler internal constructor(private val handlerActi
searchInComments,
searchInText,
deleteEmptySourceFiles,
moveMppDeclarations,
callback
)
}
@@ -34,6 +34,7 @@ internal interface MoveKotlinDeclarationsHandlerActions {
searchInComments: Boolean,
searchForTextOccurrences: Boolean,
deleteEmptySourceFiles: Boolean,
moveMppDeclarations: Boolean,
moveCallback: MoveCallback?
)
@@ -116,7 +116,7 @@
<text resource-bundle="messages/KotlinBundle" key="label.text.update.package.directive"/>
</properties>
</component>
<grid id="69aba" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<grid id="69aba" layout-manager="GridLayoutManager" row-count="4" 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"/>
@@ -160,6 +160,15 @@
<text resource-bundle="messages/KotlinBundle" key="checkbox.text.delete.empty.source.files"/>
</properties>
</component>
<component id="cc60d" class="javax.swing.JCheckBox" binding="cbApplyMPPDeclarationsMove">
<constraints>
<grid row="3" 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 resource-bundle="messages/KotlinBundle" key="label.text.move.expect.actual.counterparts"/>
</properties>
</component>
</children>
</grid>
</children>
@@ -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<KtNamedDeclaration, KotlinMemberInfo> {
@@ -100,6 +103,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
@Override
public void memberInfoChanged(@NotNull MemberInfoChange<KtNamedDeclaration, KotlinMemberInfo> 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
);
}
@@ -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<BaseRefactoringProcessor> {
@@ -59,6 +63,8 @@ internal class MoveKotlinTopLevelDeclarationsModel(
private val sourceFiles: Set<KtFile> = 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<KtNamedDeclaration>()
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,