KT-44043 [Sealed interfaces]: quickfix for top level abstractions

This commit is contained in:
Andrei Klunnyi
2020-12-22 18:24:14 +01:00
parent a6b51da308
commit 521bebee0f
9 changed files with 143 additions and 9 deletions
@@ -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.family=Change file's package to match directory
fix.change.package.text=Change file''s package to {0} 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 action.add.import.chooser.title=Imports
goto.super.chooser.function.title=Choose super function goto.super.chooser.function.title=Choose super function
@@ -150,6 +150,7 @@ internal class MoveKotlinDeclarationsHandlerTestActions(private val caseDataKeep
targetPackageName: String, targetPackageName: String,
targetDirectory: PsiDirectory?, targetDirectory: PsiDirectory?,
targetFile: KtFile?, targetFile: KtFile?,
freezeTargets: Boolean,
moveToPackage: Boolean, moveToPackage: Boolean,
moveCallback: MoveCallback? moveCallback: MoveCallback?
) { ) {
@@ -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<KtTypeReference>(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<KtClass>() ?: 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<KtClass>() ?: 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)
}
}
}
@@ -664,5 +664,8 @@ class QuickFixRegistrar : QuickFixContributor {
INCOMPATIBLE_THROWS_OVERRIDE.registerFactory(RemoveAnnotationFix) INCOMPATIBLE_THROWS_OVERRIDE.registerFactory(RemoveAnnotationFix)
INLINE_CLASS_CONSTRUCTOR_NOT_FINAL_READ_ONLY_PARAMETER.registerFactory(InlineClassConstructorNotValParameterFactory) INLINE_CLASS_CONSTRUCTOR_NOT_FINAL_READ_ONLY_PARAMETER.registerFactory(InlineClassConstructorNotValParameterFactory)
SEALED_INHERITOR_IN_DIFFERENT_PACKAGE.registerFactory(MoveToSealedMatchingPackageFix)
SEALED_INHERITOR_IN_DIFFERENT_MODULE.registerFactory(MoveToSealedMatchingPackageFix)
} }
} }
@@ -155,6 +155,7 @@ class ExtractDeclarationFromCurrentFileIntention : SelfTargetingRangeIntention<K
packageName.asString(), packageName.asString(),
directory, directory,
targetFile as? KtFile, targetFile as? KtFile,
/* freezeTargets */ false,
/* moveToPackage = */ true, /* moveToPackage = */ true,
/* searchInComments = */ true, /* searchInComments = */ true,
/* searchForTextOccurrences = */ true, /* searchForTextOccurrences = */ true,
@@ -32,7 +32,6 @@ import com.intellij.refactoring.util.CommonRefactoringUtil
import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.asJava.unwrapped
import org.jetbrains.kotlin.idea.KotlinBundle import org.jetbrains.kotlin.idea.KotlinBundle
import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings
import org.jetbrains.kotlin.idea.refactoring.canRefactor import org.jetbrains.kotlin.idea.refactoring.canRefactor
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinAwareMoveFilesOrDirectoriesDialog
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinSelectNestedClassRefactoringDialog import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.KotlinSelectNestedClassRefactoringDialog
@@ -67,6 +66,7 @@ private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerAction
targetPackageName: String, targetPackageName: String,
targetDirectory: PsiDirectory?, targetDirectory: PsiDirectory?,
targetFile: KtFile?, targetFile: KtFile?,
freezeTargets: Boolean,
moveToPackage: Boolean, moveToPackage: Boolean,
moveCallback: MoveCallback? moveCallback: MoveCallback?
) = MoveKotlinTopLevelDeclarationsDialog( ) = MoveKotlinTopLevelDeclarationsDialog(
@@ -75,6 +75,7 @@ private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerAction
targetPackageName, targetPackageName,
targetDirectory, targetDirectory,
targetFile, targetFile,
freezeTargets,
moveToPackage, moveToPackage,
moveCallback moveCallback
).show() ).show()
@@ -93,8 +94,14 @@ private val defaultHandlerActions = object : MoveKotlinDeclarationsHandlerAction
class MoveKotlinDeclarationsHandler internal constructor(private val handlerActions: MoveKotlinDeclarationsHandlerActions) : class MoveKotlinDeclarationsHandler internal constructor(private val handlerActions: MoveKotlinDeclarationsHandlerActions) :
MoveHandlerDelegate() { MoveHandlerDelegate() {
private var freezeTargets: Boolean = true
constructor() : this(defaultHandlerActions) constructor() : this(defaultHandlerActions)
constructor(freezeTargets: Boolean) : this() {
this.freezeTargets = freezeTargets
}
private fun getUniqueContainer(elements: Array<out PsiElement>): PsiElement? { private fun getUniqueContainer(elements: Array<out PsiElement>): PsiElement? {
val allTopLevel = elements.all { isTopLevelInFileOrScript(it) } val allTopLevel = elements.all { isTopLevelInFileOrScript(it) }
@@ -203,6 +210,7 @@ class MoveKotlinDeclarationsHandler internal constructor(private val handlerActi
targetPackageName, targetPackageName,
targetDirectory, targetDirectory,
targetFile, targetFile,
freezeTargets,
moveToPackage, moveToPackage,
callback callback
) )
@@ -30,6 +30,7 @@ internal interface MoveKotlinDeclarationsHandlerActions {
targetPackageName: String, targetPackageName: String,
targetDirectory: PsiDirectory?, targetDirectory: PsiDirectory?,
targetFile: KtFile?, targetFile: KtFile?,
freezeTargets: Boolean,
moveToPackage: Boolean, moveToPackage: Boolean,
moveCallback: MoveCallback? moveCallback: MoveCallback?
) )
@@ -6,12 +6,15 @@
package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui; package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui;
import com.intellij.ide.util.DirectoryChooser; 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.options.ConfigurationException;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.TextFieldWithBrowseButton; import com.intellij.openapi.ui.TextFieldWithBrowseButton;
import com.intellij.openapi.util.Pass; import com.intellij.openapi.util.Pass;
import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFile;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.classMembers.AbstractMemberInfoModel; import com.intellij.refactoring.classMembers.AbstractMemberInfoModel;
import com.intellij.refactoring.classMembers.MemberInfoBase; import com.intellij.refactoring.classMembers.MemberInfoBase;
@@ -30,6 +33,7 @@ import kotlin.collections.CollectionsKt;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.idea.KotlinBundle; 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.core.util.PhysicalFileSystemUtilsKt;
import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings; import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringSettings;
import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo; import org.jetbrains.kotlin.idea.refactoring.memberInfo.KotlinMemberInfo;
@@ -74,12 +78,15 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
private JCheckBox cbApplyMPPDeclarationsMove; private JCheckBox cbApplyMPPDeclarationsMove;
private KotlinMemberSelectionTable memberTable; private KotlinMemberSelectionTable memberTable;
private final boolean freezeTargets;
public MoveKotlinTopLevelDeclarationsDialog( public MoveKotlinTopLevelDeclarationsDialog(
@NotNull Project project, @NotNull Project project,
@NotNull Set<KtNamedDeclaration> elementsToMove, @NotNull Set<KtNamedDeclaration> elementsToMove,
@Nullable String targetPackageName, @Nullable String targetPackageName,
@Nullable PsiDirectory targetDirectory, @Nullable PsiDirectory targetDirectory,
@Nullable KtFile targetFile, @Nullable KtFile targetFile,
boolean freezeTargets,
boolean moveToPackage, boolean moveToPackage,
@Nullable MoveCallback moveCallback @Nullable MoveCallback moveCallback
) { ) {
@@ -88,6 +95,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
targetPackageName, targetPackageName,
targetDirectory, targetDirectory,
targetFile, targetFile,
freezeTargets,
moveToPackage, moveToPackage,
KotlinRefactoringSettings.getInstance().MOVE_SEARCH_IN_COMMENTS, KotlinRefactoringSettings.getInstance().MOVE_SEARCH_IN_COMMENTS,
KotlinRefactoringSettings.getInstance().MOVE_SEARCH_FOR_TEXT, KotlinRefactoringSettings.getInstance().MOVE_SEARCH_FOR_TEXT,
@@ -102,6 +110,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
@Nullable String targetPackageName, @Nullable String targetPackageName,
@Nullable PsiDirectory targetDirectory, @Nullable PsiDirectory targetDirectory,
@Nullable KtFile targetFile, @Nullable KtFile targetFile,
boolean freezeTargets,
boolean moveToPackage, boolean moveToPackage,
boolean searchInComments, boolean searchInComments,
boolean searchForTextOccurrences, boolean searchForTextOccurrences,
@@ -110,6 +119,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
@Nullable MoveCallback moveCallback @Nullable MoveCallback moveCallback
) { ) {
super(project, true); super(project, true);
this.freezeTargets = freezeTargets;
init(); init();
@@ -127,7 +137,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
initPackageChooser(targetPackageName, targetDirectory, sourceFiles); initPackageChooser(targetPackageName, targetDirectory, sourceFiles);
initFileChooser(targetFile, elementsToMove, sourceFiles); initFileChooser(targetFile, freezeTargets ? targetDirectory : null, elementsToMove, sourceFiles);
initMoveToButtons(moveToPackage); initMoveToButtons(moveToPackage);
@@ -214,6 +224,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
) { ) {
if (targetPackageName != null) { if (targetPackageName != null) {
classPackageChooser.prependItem(targetPackageName); classPackageChooser.prependItem(targetPackageName);
classPackageChooser.setEnabled(freezeTargets);
} }
((KotlinDestinationFolderComboBox) destinationFolderCB).setData( ((KotlinDestinationFolderComboBox) destinationFolderCB).setData(
@@ -225,7 +236,8 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
setErrorText(s); setErrorText(s);
} }
}, },
classPackageChooser.getChildComponent() classPackageChooser.getChildComponent(),
!freezeTargets
); );
} }
@@ -268,6 +280,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
private void initFileChooser( private void initFileChooser(
@Nullable KtFile targetFile, @Nullable KtFile targetFile,
@Nullable PsiDirectory targetDirectory,
@NotNull Set<KtNamedDeclaration> elementsToMove, @NotNull Set<KtNamedDeclaration> elementsToMove,
@NotNull List<KtFile> sourceFiles @NotNull List<KtFile> sourceFiles
) { ) {
@@ -276,11 +289,15 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
throw new AssertionError("File chooser initialization failed"); 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 -> { fileChooser.addActionListener(e -> {
KotlinFileChooserDialog dialog = new KotlinFileChooserDialog( KotlinFileChooserDialog dialog = new KotlinFileChooserDialog(
KotlinBundle.message("text.choose.containing.file"), KotlinBundle.message("text.choose.containing.file"),
myProject myProject,
); targetModuleScope, getTargetPackage());
File targetFile1 = new File(fileChooser.getText()); File targetFile1 = new File(fileChooser.getText());
PsiFile targetPsiFile = PhysicalFileSystemUtilsKt.toPsiFile(targetFile1, myProject); PsiFile targetPsiFile = PhysicalFileSystemUtilsKt.toPsiFile(targetFile1, myProject);
@@ -364,7 +381,7 @@ public class MoveKotlinTopLevelDeclarationsDialog extends RefactoringDialog {
UIUtil.setEnabled(rbMoveToFile, !needToMoveMPPDeclarations, true); UIUtil.setEnabled(rbMoveToFile, !needToMoveMPPDeclarations, true);
boolean moveToPackage = rbMoveToPackage.isSelected(); boolean moveToPackage = rbMoveToPackage.isSelected();
classPackageChooser.setEnabled(moveToPackage); classPackageChooser.setEnabled(moveToPackage && freezeTargets);
updateFileNameInPackageField(); updateFileNameInPackageField();
fileChooser.setEnabled(!moveToPackage); fileChooser.setEnabled(!moveToPackage);
UIUtil.setEnabled(targetPanel, moveToPackage && !needToMoveMPPDeclarations && hasAnySourceRoots(), true); UIUtil.setEnabled(targetPanel, moveToPackage && !needToMoveMPPDeclarations && hasAnySourceRoots(), true);
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.idea.refactoring.ui package org.jetbrains.kotlin.idea.refactoring.ui
import com.intellij.ide.util.AbstractTreeClassChooserDialog import com.intellij.ide.util.AbstractTreeClassChooserDialog
import com.intellij.ide.util.TreeChooser
import com.intellij.ide.util.gotoByName.GotoFileModel import com.intellij.ide.util.gotoByName.GotoFileModel
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.search.FilenameIndex import com.intellij.psi.search.FilenameIndex
@@ -19,13 +20,15 @@ import javax.swing.tree.DefaultMutableTreeNode
class KotlinFileChooserDialog( class KotlinFileChooserDialog(
title: String, title: String,
project: Project project: Project,
searchScope: GlobalSearchScope?,
packageName: String?
) : AbstractTreeClassChooserDialog<KtFile>( ) : AbstractTreeClassChooserDialog<KtFile>(
title, title,
project, project,
project.projectScope().restrictToKotlinSources(), searchScope ?: project.projectScope().restrictToKotlinSources(),
KtFile::class.java, KtFile::class.java,
null, ScopeAwareClassFilter(searchScope, packageName),
null, null,
null, null,
false, false,
@@ -45,4 +48,20 @@ class KotlinFileChooserDialog(
} }
override fun createChooseByNameModel() = GotoFileModel(this.project) 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<KtFile> {
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
}
}
} }