diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt index b41e9836b64..96118a542ec 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandler.kt @@ -32,53 +32,92 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassesOrPackages import com.intellij.refactoring.util.CommonRefactoringUtil import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.refactoring.canRefactor +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinNestedClassesToUpperLevelDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject +import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext import java.util.* class MoveKotlinDeclarationsHandler : MoveHandlerDelegate() { - private fun getSourceDirectories(elements: Array) = elements.mapTo(LinkedHashSet()) { it.containingFile?.parent } + private fun getUniqueContainer(elements: Array): PsiElement? { + val getContainer: (PsiElement) -> PsiElement? = + if (elements.any { it.parent !is KtFile }) { e -> + (e as? KtNamedDeclaration)?.containingClassOrObject + } + else { e -> + e.containingFile?.parent + } + return elements.mapNotNullTo(LinkedHashSet(), getContainer).singleOrNull() + } + + private fun KtNamedDeclaration.canMove() = if (this is KtClassOrObject) !isLocal() else parent is KtFile private fun doMoveWithCheck( project: Project, elements: Array, targetContainer: PsiElement?, callback: MoveCallback?, editor: Editor? ): Boolean { if (!CommonRefactoringUtil.checkReadOnlyStatusRecursively(project, elements.toList(), true)) return false - if (getSourceDirectories(elements).singleOrNull() == null) { + + val container = getUniqueContainer(elements) + if (container == null) { CommonRefactoringUtil.showErrorHint( - project, editor, "All declarations must belong to the same directory", MOVE_DECLARATIONS, null + project, editor, "All declarations must belong to the same directory or class", MOVE_DECLARATIONS, null ) return false } val elementsToSearch = elements.mapTo(LinkedHashSet()) { it as KtNamedDeclaration } - if (elementsToSearch.any { it.parent !is KtFile }) { - val message = RefactoringBundle.getCannotRefactorMessage("Move declaration is only supported for top-level declarations") + // 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) return true } - val targetPackageName = MoveClassesOrPackagesImpl.getInitialTargetPackageName(targetContainer, elements) - val targetDirectory = MoveClassesOrPackagesImpl.getInitialTargetDirectory(targetContainer, elements) - val searchInComments = JavaRefactoringSettings.getInstance()!!.MOVE_SEARCH_IN_COMMENTS - val searchInText = JavaRefactoringSettings.getInstance()!!.MOVE_SEARCH_FOR_TEXT - val targetFile = targetContainer as? KtFile - val moveToPackage = targetContainer !is KtFile + 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) + return true + } - MoveKotlinTopLevelDeclarationsDialog( - project, elementsToSearch, targetPackageName, targetDirectory, targetFile, moveToPackage, searchInComments, searchInText, callback - ).show() + when (container) { + is PsiDirectory, is PsiPackage, is KtFile -> { + val targetPackageName = MoveClassesOrPackagesImpl.getInitialTargetPackageName(targetContainer, elements) + val targetDirectory = MoveClassesOrPackagesImpl.getInitialTargetDirectory(targetContainer, elements) + val searchInComments = JavaRefactoringSettings.getInstance()!!.MOVE_SEARCH_IN_COMMENTS + val searchInText = JavaRefactoringSettings.getInstance()!!.MOVE_SEARCH_FOR_TEXT + val targetFile = targetContainer as? KtFile + val moveToPackage = targetContainer !is KtFile + + MoveKotlinTopLevelDeclarationsDialog( + project, elementsToSearch, targetPackageName, targetDirectory, targetFile, moveToPackage, searchInComments, searchInText, callback + ).show() + } + + is KtClassOrObject -> { + if (elementsToSearch.size > 1) return false + + val outerClass = container.parent as KtClassOrObject + val newContainer = targetContainer + ?: outerClass.containingClassOrObject + ?: outerClass.containingFile.let { it.containingDirectory ?: it } + MoveKotlinNestedClassesToUpperLevelDialog(project, elementsToSearch.first() as KtClassOrObject, newContainer).show() + } + + else -> throw AssertionError("Unexpected container: ${container.getElementTextWithContext()}") + } return true } private fun canMove(elements: Array, targetContainer: PsiElement?, editorMode: Boolean): Boolean { if (!super.canMove(elements, targetContainer)) return false - if (getSourceDirectories(elements).singleOrNull() == null) return false + if (getUniqueContainer(elements) == null) return false return elements.all { e -> if (e is KtClass || (e is KtObjectDeclaration && !e.isObjectLiteral()) || e is KtNamedFunction || e is KtProperty) { - (editorMode || e.parent is KtFile) && e.canRefactor() + (editorMode || (e as KtNamedDeclaration).canMove()) && e.canRefactor() } else false } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.form b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.form new file mode 100644 index 00000000000..a68954fac59 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.form @@ -0,0 +1,102 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java new file mode 100644 index 00000000000..11a3f61ef34 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ui/MoveKotlinNestedClassesToUpperLevelDialog.java @@ -0,0 +1,404 @@ +/* + * Copyright 2010-2016 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.moveDeclarations.ui; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.JavaProjectRootsUtil; +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.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.collections.CollectionsKt; +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.refactoring.JetRefactoringUtilKt; +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.name.FqName; +import org.jetbrains.kotlin.name.Name; +import org.jetbrains.kotlin.psi.*; +import org.jetbrains.kotlin.resolve.DescriptorUtils; +import org.jetbrains.kotlin.types.KotlinType; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.List; + +public class MoveKotlinNestedClassesToUpperLevelDialog extends MoveDialogBase { + @NonNls private static final String RECENTS_KEY = MoveKotlinNestedClassesToUpperLevelDialog.class.getName() + ".RECENTS_KEY"; + + private final Project project; + private final KtClassOrObject innerClass; + private final ClassDescriptor innerClassDescriptor; + + private final PsiElement targetContainer; + private EditorTextField classNameField; + private NameSuggestionsField parameterField; + private JCheckBox passOuterClassCheckBox; + private JPanel panel; + private JCheckBox searchInCommentsCheckBox; + private JCheckBox searchForTextOccurrencesCheckBox; + private PackageNameReferenceEditorCombo packageNameField; + private JLabel packageNameLabel; + private JLabel classNameLabel; + private JLabel parameterNameLabel; + private JPanel openInEditorPanel; + + public MoveKotlinNestedClassesToUpperLevelDialog( + @NotNull Project project, + @NotNull KtClassOrObject innerClass, + @NotNull PsiElement targetContainer + ) { + super(project, true); + this.project = project; + this.innerClass = innerClass; + this.targetContainer = targetContainer; + this.innerClassDescriptor = (ClassDescriptor) ResolutionUtils.resolveToDescriptor(innerClass); + setTitle("Move Nested Classes to Upper Level"); + init(); + packageNameLabel.setLabelFor(packageNameField.getChildComponent()); + classNameLabel.setLabelFor(classNameField); + parameterNameLabel.setLabelFor(parameterField); + openInEditorPanel.add(initOpenInEditorCb(), BorderLayout.EAST); + } + + private void createUIComponents() { + parameterField = new NameSuggestionsField(project); + packageNameField = new PackageNameReferenceEditorCombo("", project, RECENTS_KEY, + RefactoringBundle.message("choose.destination.package")); + } + + @Override + protected String getMovePropertySuffix() { + return "Nested Classes to Upper Level"; + } + + @Override + protected String getHelpId() { + return HelpID.MOVE_INNER_UPPER; + } + + @Override + protected String getCbTitle() { + 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(); + } + + @Nullable + public String getParameterName() { + return parameterField != null ? parameterField.getEnteredName() : null; + } + + private boolean isThisNeeded() { + return innerClass instanceof KtClass && MoveUtilsKt.traverseOuterInstanceReferences((KtClass) innerClass, true); + } + + @Nullable + private FqName getTargetPackageFqName() { + if (targetContainer instanceof PsiDirectory) return innerClass.getContainingKtFile().getPackageFqName(); + if (targetContainer instanceof KtFile) return ((KtFile) targetContainer).getPackageFqName(); + return null; + } + + @NotNull + private KotlinType getOuterInstanceType() { + return ((ClassDescriptor) innerClassDescriptor.getContainingDeclaration()).getDefaultType(); + } + + @Override + protected void init() { + classNameField.setText(innerClass.getName()); + classNameField.selectAll(); + + if (innerClass instanceof KtClass && ((KtClass) innerClass).isInner()) { + passOuterClassCheckBox.setSelected(true); + passOuterClassCheckBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + parameterField.setEnabled(passOuterClassCheckBox.isSelected()); + } + }); + } + else { + passOuterClassCheckBox.setSelected(false); + passOuterClassCheckBox.setEnabled(false); + parameterField.setEnabled(false); + } + + if (passOuterClassCheckBox.isEnabled()) { + boolean thisNeeded = isThisNeeded(); + passOuterClassCheckBox.setSelected(thisNeeded); + parameterField.setEnabled(thisNeeded); + } + + passOuterClassCheckBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + boolean selected = passOuterClassCheckBox.isSelected(); + parameterField.getComponent().setEnabled(selected); + } + }); + + if (!(targetContainer instanceof PsiDirectory)) { + packageNameField.setVisible(false); + packageNameLabel.setVisible(false); + } + + if (innerClass instanceof KtClass && ((KtClass) innerClass).isInner()) { + KtClassBody innerClassBody = innerClass.getBody(); + Function1 validator = + innerClassBody != null + ? new NewDeclarationNameValidator(innerClassBody, (PsiElement) null, + NewDeclarationNameValidator.Target.VARIABLES) + : new CollectingNameValidator(); + List suggestions = KotlinNameSuggester.INSTANCE.suggestNamesByType(getOuterInstanceType(), validator, "outer"); + parameterField.setSuggestions(ArrayUtil.toStringArray(suggestions)); + } + else { + parameterField.getComponent().setEnabled(false); + } + + FqName packageFqName = getTargetPackageFqName(); + if (packageFqName != null) { + packageNameField.prependItem(packageFqName.asString()); + } + + super.init(); + } + + @Override + public JComponent getPreferredFocusedComponent() { + return classNameField; + } + + @Override + protected String getDimensionServiceKey() { + return "#com.intellij.refactoring.move.moveInner.MoveInnerDialog"; + } + + @Override + protected JComponent createNorthPanel() { + return panel; + } + + @Override + protected JComponent createCenterPanel() { + 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 contentSourceRoots = JavaProjectRootsUtil.getSuitableDestinationSourceRoots(project); + final PackageWrapper newPackage = new PackageWrapper(PsiManager.getInstance(project), targetName); + final 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(new NullableComputable() { + @Override + public PsiDirectory compute() { + 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; + } + + @Nullable + private String getErrorMessageIfAny() { + String className = getClassName(); + String parameterName = getParameterName(); + + if (className != null && className.isEmpty()) return RefactoringBundle.message("no.class.name.specified"); + if (!KotlinNameSuggester.INSTANCE.isIdentifier(className)) return RefactoringMessageUtil.getIncorrectIdentifierMessage(className); + + if (passOuterClassCheckBox.isSelected()) { + if (parameterName != null && parameterName.isEmpty()) return RefactoringBundle.message("no.parameter.name.specified"); + if (!KotlinNameSuggester.INSTANCE.isIdentifier(parameterName)) return 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())) { + return RefactoringBundle.message("inner.class.exists", className, targetClass.getName()); + } + } + } + + if (targetContainer instanceof PsiDirectory || targetContainer instanceof KtFile) { + FqName targetPackageFqName = getTargetPackageFqName(); + if (targetPackageFqName == null) return "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) return "Class " + className + " already exists in package " + targetPackageFqName; + + PsiDirectory targetDir = targetContainer instanceof PsiDirectory + ? (PsiDirectory) targetContainer + : targetContainer.getContainingFile().getContainingDirectory(); + return RefactoringMessageUtil.checkCanCreateFile(targetDir, className + ".kt"); + } + + return null; + } + + @Override + protected void doAction() { + String errorMessage = getErrorMessageIfAny(); + if (errorMessage != null) { + CommonRefactoringUtil.showErrorMessage(MoveInnerImpl.REFACTORING_NAME, errorMessage, HelpID.MOVE_INNER_UPPER, project); + return; + } + + PsiElement target = getTargetContainer(); + if (target == null) return; + + KotlinMoveTarget moveTarget; + if (target instanceof PsiDirectory) { + final PsiDirectory targetDir = (PsiDirectory) target; + + final FqName targetPackageFqName = getTargetPackageFqName(); + if (targetPackageFqName == null) return; + + final String targetFileName = KotlinNameSuggester.INSTANCE.suggestNameByName( + innerClass.getName() + "." + KotlinFileType.EXTENSION, + new Function1() { + @Override + public Boolean invoke(String s) { + return targetDir.findFile(s) == null; + } + } + ); + moveTarget = new KotlinMoveTargetForDeferredFile( + targetPackageFqName, + new Function1() { + @Override + public KtFile invoke(@NotNull KtFile originalFile) { + return JetRefactoringUtilKt.createKotlinFile(targetFileName, targetDir, targetPackageFqName.asString()); + } + } + ); + } + else { + //noinspection ConstantConditions + moveTarget = new KotlinMoveTargetForExistingElement((KtElement) target); + } + + String outerInstanceParameterName = passOuterClassCheckBox.isSelected() ? getParameterName() : null; + String newClassName = getClassName(); + MoveDeclarationsDelegate delegate = new MoveDeclarationsDelegate.NestedClass(newClassName, outerInstanceParameterName); + MoveDeclarationsDescriptor moveDescriptor = new MoveDeclarationsDescriptor( + CollectionsKt.listOf(innerClass), + moveTarget, + delegate, + isSearchInComments(), + isSearchInNonJavaFiles(), + true, + false, + null, + isOpenInEditor() + ); + saveOpenInEditorOption(); + + invokeRefactoring(new MoveKotlinDeclarationsProcessor(project, moveDescriptor, Mover.Default.INSTANCE)); + } +} \ No newline at end of file