diff --git a/idea/resources-en/messages/KotlinBundle.properties b/idea/resources-en/messages/KotlinBundle.properties index 2bcbf9fcc28..2c44c4921b0 100644 --- a/idea/resources-en/messages/KotlinBundle.properties +++ b/idea/resources-en/messages/KotlinBundle.properties @@ -174,6 +174,9 @@ fix.move.file.to.package.text=Move file to {0} fix.change.package.family=Change file's package to match directory fix.change.package.text=Change file''s package to {0} +fix.move.to.sealed.family=Move hierarchy member to the package/module of its sealed parent +fix.move.to.sealed.text=Move {0} to the package/module of {1} + action.add.import.chooser.title=Imports goto.super.chooser.function.title=Choose super function 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 bd4eabd2ecf..d4fda4dc1be 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 @@ -150,6 +150,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep targetPackageName: String, targetDirectory: PsiDirectory?, targetFile: KtFile?, + freezeTargets: Boolean, moveToPackage: Boolean, moveCallback: MoveCallback? ) { diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt new file mode 100644 index 00000000000..0f88b4a7042 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/MoveToSealedMatchingPackageFix.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.idea.quickfix + +import com.intellij.openapi.actionSystem.LangDataKeys +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.psi.PsiDirectory +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.parentOfType +import org.jetbrains.kotlin.descriptors.containingPackage +import org.jetbrains.kotlin.diagnostics.Diagnostic +import org.jetbrains.kotlin.idea.KotlinBundle +import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.MoveKotlinDeclarationsHandler +import org.jetbrains.kotlin.idea.references.resolveMainReferenceToDescriptors +import org.jetbrains.kotlin.idea.util.projectStructure.module +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.resolve.DescriptorToSourceUtils +import org.jetbrains.kotlin.resolve.jvm.KotlinJavaPsiFacade + +class MoveToSealedMatchingPackageFix(element: KtTypeReference) : KotlinQuickFixAction(element) { + + private val moveHandler = MoveKotlinDeclarationsHandler(false) + + override fun invoke(project: Project, editor: Editor?, file: KtFile) { + val typeReference = element ?: return + + // 'element' references sealed class/interface in extension list + val classToMove = typeReference.parentOfType() ?: return + val defaultTargetDir = typeReference.resolveToDir() ?: return + + val parentContext = SimpleDataContext.getProjectContext(project) + val context = SimpleDataContext.getSimpleContext(LangDataKeys.TARGET_PSI_ELEMENT.name, defaultTargetDir, parentContext) + + moveHandler.tryToMove(classToMove, project, context, null, editor) + } + + private fun KtTypeReference.resolveToDir(): PsiDirectory? { + val ktUserType = typeElement as? KtUserType ?: return null + val ktNameReferenceExpression = ktUserType.referenceExpression as? KtNameReferenceExpression ?: return null + val declDescriptor = ktNameReferenceExpression.resolveMainReferenceToDescriptors().singleOrNull() ?: return null + + val packageName = declDescriptor.containingPackage()?.asString() ?: return null + + val projectFileIndex = ProjectFileIndex.getInstance(project) + val ktClassInQuestion = DescriptorToSourceUtils.getSourceFromDescriptor(declDescriptor) as? KtClass ?: return null + val module = projectFileIndex.getModuleForFile(ktClassInQuestion.containingFile.virtualFile) ?: return null + val psiPackage = + KotlinJavaPsiFacade.getInstance(project).findPackage(packageName, GlobalSearchScope.moduleScope(module)) ?: return null + + return psiPackage.directories.find { it.module == module } + } + + override fun startInWriteAction(): Boolean { + return false + } + + override fun getText(): String { + val typeReference = element ?: return "" + val referencedName = (typeReference.typeElement as? KtUserType)?.referenceExpression?.getReferencedName() ?: return "" + + val classToMove = typeReference.parentOfType() ?: return "" + return KotlinBundle.message("fix.move.to.sealed.text", classToMove.nameAsSafeName.asString(), referencedName) + } + + override fun getFamilyName(): String { + return KotlinBundle.message("fix.move.to.sealed.family") + } + + companion object : KotlinSingleIntentionActionFactory() { + override fun createAction(diagnostic: Diagnostic): MoveToSealedMatchingPackageFix? { + val annotationEntry = diagnostic.psiElement as? KtTypeReference ?: return null + return MoveToSealedMatchingPackageFix(annotationEntry) + } + } +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index 4a5a367c68f..99488a1231a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -664,5 +664,8 @@ class QuickFixRegistrar : QuickFixContributor { INCOMPATIBLE_THROWS_OVERRIDE.registerFactory(RemoveAnnotationFix) INLINE_CLASS_CONSTRUCTOR_NOT_FINAL_READ_ONLY_PARAMETER.registerFactory(InlineClassConstructorNotValParameterFactory) + + SEALED_INHERITOR_IN_DIFFERENT_PACKAGE.registerFactory(MoveToSealedMatchingPackageFix) + SEALED_INHERITOR_IN_DIFFERENT_MODULE.registerFactory(MoveToSealedMatchingPackageFix) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ExtractDeclarationFromCurrentFileIntention.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ExtractDeclarationFromCurrentFileIntention.kt index 6b12ec6c8fd..98874f50ced 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ExtractDeclarationFromCurrentFileIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/ExtractDeclarationFromCurrentFileIntention.kt @@ -155,6 +155,7 @@ class ExtractDeclarationFromCurrentFileIntention : SelfTargetingRangeIntention): PsiElement? { val allTopLevel = elements.all { isTopLevelInFileOrScript(it) } @@ -203,6 +210,7 @@ class MoveKotlinDeclarationsHandler internal constructor(private val handlerActi targetPackageName, targetDirectory, targetFile, + freezeTargets, moveToPackage, callback ) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt index 384e75d0988..f008e97c6ef 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveDeclarations/MoveKotlinDeclarationsHandlerActions.kt @@ -30,6 +30,7 @@ internal interface MoveKotlinDeclarationsHandlerActions { targetPackageName: String, targetDirectory: PsiDirectory?, targetFile: KtFile?, + freezeTargets: Boolean, moveToPackage: Boolean, moveCallback: MoveCallback? ) 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 84e14b2176b..0b26153f43d 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 @@ -6,12 +6,15 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui; import com.intellij.ide.util.DirectoryChooser; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.util.Pass; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; +import com.intellij.psi.search.GlobalSearchScope; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; import com.intellij.refactoring.classMembers.MemberInfoBase; @@ -30,6 +33,7 @@ import kotlin.collections.CollectionsKt; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.idea.KotlinBundle; +import org.jetbrains.kotlin.idea.KotlinFileType; import org.jetbrains.kotlin.idea.core.util.PhysicalFileSystemUtilsKt; import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo; @@ -74,12 +78,15 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private JCheckBox cbApplyMPPDeclarationsMove; private KotlinMemberSelectionTable memberTable; + private final boolean freezeTargets; + public MoveKotlinTopLevelDeclarationsDialog( @NotNull Project project, @NotNull Set elementsToMove, @Nullable String targetPackageName, @Nullable PsiDirectory targetDirectory, @Nullable KtFile targetFile, + boolean freezeTargets, boolean moveToPackage, @Nullable MoveCallback moveCallback ) { @@ -88,6 +95,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { targetPackageName, targetDirectory, targetFile, + freezeTargets, moveToPackage, KotlinRefactoringSettings.getInstance().MOVE_SEARCH_IN_COMMENTS, KotlinRefactoringSettings.getInstance().MOVE_SEARCH_FOR_TEXT, @@ -102,6 +110,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @Nullable String targetPackageName, @Nullable PsiDirectory targetDirectory, @Nullable KtFile targetFile, + boolean freezeTargets, boolean moveToPackage, boolean searchInComments, boolean searchForTextOccurrences, @@ -110,6 +119,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { @Nullable MoveCallback moveCallback ) { super(project, true); + this.freezeTargets = freezeTargets; init(); @@ -127,7 +137,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { initPackageChooser(targetPackageName, targetDirectory, sourceFiles); - initFileChooser(targetFile, elementsToMove, sourceFiles); + initFileChooser(targetFile, freezeTargets ? targetDirectory : null, elementsToMove, sourceFiles); initMoveToButtons(moveToPackage); @@ -214,6 +224,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { ) { if (targetPackageName != null) { classPackageChooser.prependItem(targetPackageName); + classPackageChooser.setEnabled(freezeTargets); } ((KotlinDestinationFolderComboBox) destinationFolderCB).setData( @@ -225,7 +236,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { setErrorText(s); } }, - classPackageChooser.getChildComponent() + classPackageChooser.getChildComponent(), + !freezeTargets ); } @@ -268,6 +280,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { private void initFileChooser( @Nullable KtFile targetFile, + @Nullable PsiDirectory targetDirectory, @NotNull Set elementsToMove, @NotNull List sourceFiles ) { @@ -276,11 +289,15 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { throw new AssertionError("File chooser initialization failed"); } + Module targetModule = (targetDirectory != null)? ModuleUtilCore.findModuleForPsiElement(targetDirectory) : null; + GlobalSearchScope targetModuleScope = targetModule == null ? null + : GlobalSearchScope.getScopeRestrictedByFileTypes(targetModule.getModuleScope(), KotlinFileType.INSTANCE); + fileChooser.addActionListener(e -> { KotlinFileChooserDialog dialog = new KotlinFileChooserDialog( KotlinBundle.message("text.choose.containing.file"), - myProject - ); + myProject, + targetModuleScope, getTargetPackage()); File targetFile1 = new File(fileChooser.getText()); PsiFile targetPsiFile = PhysicalFileSystemUtilsKt.toPsiFile(targetFile1, myProject); @@ -364,7 +381,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog { UIUtil.setEnabled(rbMoveToFile, !needToMoveMPPDeclarations, true); boolean moveToPackage = rbMoveToPackage.isSelected(); - classPackageChooser.setEnabled(moveToPackage); + classPackageChooser.setEnabled(moveToPackage && freezeTargets); updateFileNameInPackageField(); fileChooser.setEnabled(!moveToPackage); UIUtil.setEnabled(targetPanel, moveToPackage && !needToMoveMPPDeclarations && hasAnySourceRoots(), true); diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinFileChooserDialog.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinFileChooserDialog.kt index d7f88a1d9ce..3d2dfe7e47a 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinFileChooserDialog.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/ui/KotlinFileChooserDialog.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.idea.refactoring.ui import com.intellij.ide.util.AbstractTreeClassChooserDialog +import com.intellij.ide.util.TreeChooser import com.intellij.ide.util.gotoByName.GotoFileModel import com.intellij.openapi.project.Project import com.intellij.psi.search.FilenameIndex @@ -19,13 +20,15 @@ import javax.swing.tree.DefaultMutableTreeNode class KotlinFileChooserDialog( title: String, - project: Project + project: Project, + searchScope: GlobalSearchScope?, + packageName: String? ) : AbstractTreeClassChooserDialog( title, project, - project.projectScope().restrictToKotlinSources(), + searchScope ?: project.projectScope().restrictToKotlinSources(), KtFile::class.java, - null, + ScopeAwareClassFilter(searchScope, packageName), null, null, false, @@ -45,4 +48,20 @@ class KotlinFileChooserDialog( } override fun createChooseByNameModel() = GotoFileModel(this.project) + + /** + * Base class [AbstractTreeClassChooserDialog] unfortunately doesn't filter the file tree according to the provided "scope". + * As a workaround we use filter preventing wrong file selection. + */ + private class ScopeAwareClassFilter(val searchScope: GlobalSearchScope?, val packageName: String?) : TreeChooser.Filter { + override fun isAccepted(element: KtFile?): Boolean { + if (element == null) return false + if (searchScope == null && packageName == null) return true + + val matchesSearchScope = searchScope?.accept(element.virtualFile) ?: true + val matchesPackage = packageName?.let { element.packageFqName.asString() == it } ?: true + + return matchesSearchScope && matchesPackage + } + } } \ No newline at end of file