diff --git a/ChangeLog.md b/ChangeLog.md index 5d72381eb7f..d95c8358da5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -246,6 +246,7 @@ Pull Up: Support properties declared in the primary constructor Pull Up: Support members declared in the companion object of the original class Pull Up: Show member dependencies in the refactoring dialog - [`KT-9485`](https://youtrack.jetbrains.com/issue/KT-9485) Push Down: Support moving members from Java to Kotlin class +- [`KT-13963`](https://youtrack.jetbrains.com/issue/KT-13963) Rename: Implement popup chooser for overriding members #### Android Lint diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt index e7a7c2915b4..c31dcf39db1 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt @@ -75,6 +75,8 @@ class KotlinRefactoringSupportProvider : RefactoringSupportProvider() { return false } + override fun isMemberInplaceRenameAvailable(element: PsiElement, context: PsiElement?) = element is KtNamedDeclaration + override fun getChangeSignatureHandler() = KotlinChangeSignatureHandler() override fun getPullUpHandler() = KotlinPullUpHandler() diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt index 07f266775ca..6c5e7cc5d7d 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/kotlinRefactoringUtil.kt @@ -38,10 +38,7 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project import com.intellij.openapi.roots.JavaProjectRootsUtil import com.intellij.openapi.ui.Messages -import com.intellij.openapi.ui.popup.JBPopup -import com.intellij.openapi.ui.popup.JBPopupAdapter -import com.intellij.openapi.ui.popup.LightweightWindowEvent -import com.intellij.openapi.ui.popup.PopupChooserBuilder +import com.intellij.openapi.ui.popup.* import com.intellij.openapi.util.Pass import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil @@ -49,6 +46,7 @@ import com.intellij.openapi.vfs.LocalFileSystem import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.* import com.intellij.psi.impl.light.LightElement +import com.intellij.psi.presentation.java.SymbolPresentationUtil import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException import com.intellij.refactoring.changeSignature.ChangeSignatureUtil @@ -58,10 +56,12 @@ import com.intellij.refactoring.ui.ConflictsDialog import com.intellij.refactoring.util.ConflictsUtil import com.intellij.refactoring.util.RefactoringUIUtil import com.intellij.ui.components.JBList +import com.intellij.usageView.UsageViewTypeLocation import com.intellij.util.VisibilityUtil import com.intellij.util.containers.MultiMap import org.jetbrains.kotlin.asJava.LightClassUtil import org.jetbrains.kotlin.asJava.elements.KtLightMethod +import org.jetbrains.kotlin.asJava.namedUnwrappedElement import org.jetbrains.kotlin.asJava.toLightClass import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.descriptors.impl.AnonymousFunctionDescriptor @@ -241,7 +241,7 @@ fun reportDeclarationConflict( conflicts.putValue(declaration, message(RefactoringUIUtil.getDescription(declaration, true).capitalize())) } -fun getPsiElementPopup( +fun getPsiElementPopup( editor: Editor, elements: List, renderer: PsiElementListCellRenderer, @@ -270,7 +270,7 @@ fun getPsiElementPopup( processor(elements[index]) } } - addListener(object: JBPopupAdapter() { + addListener(object : JBPopupAdapter() { override fun onClosed(event: LightweightWindowEvent?) { highlighter?.dropHighlight() } @@ -656,7 +656,7 @@ fun (() -> Any).runRefactoringWithPostprocessing( ) { val connection = project.messageBus.connect() connection.subscribe(RefactoringEventListener.REFACTORING_EVENT_TOPIC, - object: RefactoringEventListener { + object : RefactoringEventListener { override fun undoRefactoring(refactoringId: String) { } @@ -702,7 +702,7 @@ fun Project.runSynchronouslyWithProgress(progressTitle: String, canBeC fun invokeOnceOnCommandFinish(action: () -> Unit) { val commandProcessor = CommandProcessor.getInstance() - val listener = object: CommandAdapter() { + val listener = object : CommandAdapter() { override fun beforeCommandFinished(event: CommandEvent?) { action() commandProcessor.removeCommandListener(this) @@ -752,7 +752,7 @@ fun replaceListPsiAndKeepDelimiters( return originalList } -fun Pass(body: (T) -> Unit) = object: Pass() { +fun Pass(body: (T) -> Unit) = object : Pass() { override fun pass(t: T) = body(t) } @@ -887,6 +887,61 @@ fun checkSuperMethods( return askUserForMethodsToSearch(declarationDescriptor, overriddenElementsToDescriptor) } +fun checkSuperMethodsWithPopup( + declaration: KtNamedDeclaration, + deepestSuperMethods: List, + actionString: String, + editor: Editor, + action: (List) -> Unit +) { + if (deepestSuperMethods.isEmpty()) return action(listOf(declaration)) + + val superMethod = deepestSuperMethods.first() + + val superClass = superMethod.containingClass ?: return action(listOf(declaration)) + + if (ApplicationManager.getApplication().isUnitTestMode) return action(deepestSuperMethods) + + val kind = when (declaration) { + is KtNamedFunction -> "function" + is KtProperty, is KtParameter -> "property" + else -> return + } + + val unwrappedSupers = deepestSuperMethods.mapNotNull { it.namedUnwrappedElement } + val hasJavaMethods = unwrappedSupers.any { it is PsiMethod } + val hasKtMembers = unwrappedSupers.any { it is KtNamedDeclaration } + val superKind = when { + hasJavaMethods && hasKtMembers -> "member" + hasJavaMethods -> "method" + else -> kind + } + + val renameBase = actionString + " base $superKind" + (if (deepestSuperMethods.size > 1) "s" else "") + val renameCurrent = actionString + " only current $kind" + val title = buildString { + append(declaration.name) + append(if (superMethod.hasModifierProperty(PsiModifier.ABSTRACT)) " implements " else " overrides ") + append(ElementDescriptionUtil.getElementDescription(superMethod, UsageViewTypeLocation.INSTANCE)) + append(" of ") + append(SymbolPresentationUtil.getSymbolPresentableText(superClass)) + } + val list = JBList(renameBase, renameCurrent) + JBPopupFactory.getInstance() + .createListPopupBuilder(list) + .setTitle(title) + .setMovable(false) + .setResizable(false) + .setRequestFocus(true) + .setItemChoosenCallback { + val value = list.selectedValue as? String ?: return@setItemChoosenCallback + val chosenElements = if (value == renameBase) deepestSuperMethods + declaration else listOf(declaration) + action(chosenElements) + } + .createPopup() + .showInBestPositionFor(editor) +} + fun KtNamedDeclaration.isCompanionMemberOf(klass: KtClassOrObject): Boolean { val containingObject = containingClassOrObject as? KtObjectDeclaration ?: return false return containingObject.isCompanion() && containingObject.containingClassOrObject == klass diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinFunctionProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinFunctionProcessor.kt index 5265157bcac..b50a5e61024 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinFunctionProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinFunctionProcessor.kt @@ -18,6 +18,7 @@ package org.jetbrains.kotlin.idea.refactoring.rename import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Pass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod import com.intellij.psi.PsiNamedElement @@ -37,7 +38,9 @@ import org.jetbrains.kotlin.asJava.elements.KtLightMethod import org.jetbrains.kotlin.asJava.unwrapped import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.refactoring.Pass import org.jetbrains.kotlin.idea.refactoring.checkSuperMethods +import org.jetbrains.kotlin.idea.refactoring.checkSuperMethodsWithPopup import org.jetbrains.kotlin.idea.refactoring.dropOverrideKeywordIfNecessary import org.jetbrains.kotlin.idea.references.KtReference import org.jetbrains.kotlin.idea.util.application.runReadAction @@ -125,6 +128,34 @@ class RenameKotlinFunctionProcessor : RenameKotlinPsiProcessor() { return substitutedJavaElement } + override fun substituteElementToRename(element: PsiElement, editor: Editor, renameCallback: Pass) { + fun preprocessAndPass(substitutedJavaElement: PsiElement) { + val elementToProcess = if (substitutedJavaElement is KtLightMethod && element is KtDeclaration) { + substitutedJavaElement.kotlinOrigin as? KtNamedFunction + } + else { + substitutedJavaElement + } + renameCallback.pass(elementToProcess) + } + + val wrappedMethod = wrapPsiMethod(element) ?: return + + val deepestSuperMethods = wrappedMethod.findDeepestSuperMethods() + when { + deepestSuperMethods.isEmpty() -> return + wrappedMethod.isConstructor || element !is KtNamedFunction -> { + javaMethodProcessorInstance.substituteElementToRename(wrappedMethod, editor, Pass(::preprocessAndPass)) + } + else -> { + val declaration = element.unwrapped as? KtNamedDeclaration ?: return + checkSuperMethodsWithPopup(declaration, deepestSuperMethods.toList(), "Rename", editor) { + preprocessAndPass(if (it.size > 1) FunctionWithSupersWrapper(element, it) else wrappedMethod) + } + } + } + } + override fun createRenameDialog(project: Project, element: PsiElement, nameSuggestionContext: PsiElement?, editor: Editor?): RenameDialog { val elementForDialog = (element as? FunctionWithSupersWrapper)?.originalDeclaration ?: element return object : RenameDialog(project, elementForDialog, nameSuggestionContext, editor) { @@ -176,7 +207,7 @@ class RenameKotlinFunctionProcessor : RenameKotlinPsiProcessor() { } private fun wrapPsiMethod(element: PsiElement?): PsiMethod? = when (element) { - is KtLightMethod -> element + is PsiMethod -> element is KtNamedFunction, is KtSecondaryConstructor -> runReadAction { LightClassUtil.getLightClassMethod(element as KtFunction) } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinPropertyProcessor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinPropertyProcessor.kt index 8c9377135a7..88408649515 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinPropertyProcessor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/rename/RenameKotlinPropertyProcessor.kt @@ -20,6 +20,7 @@ import com.intellij.navigation.NavigationItem import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.Pass import com.intellij.psi.* import com.intellij.psi.search.SearchScope import com.intellij.psi.search.searches.DirectClassInheritorsSearch @@ -43,6 +44,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.core.getDeepestSuperDeclarations import org.jetbrains.kotlin.idea.core.isEnumCompanionPropertyWithEntryConflict +import org.jetbrains.kotlin.idea.refactoring.checkSuperMethodsWithPopup import org.jetbrains.kotlin.idea.refactoring.dropOverrideKeywordIfNecessary import org.jetbrains.kotlin.idea.references.KtReference import org.jetbrains.kotlin.idea.references.KtSimpleNameReference @@ -62,6 +64,7 @@ import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.util.findCallableMemberBySignature import org.jetbrains.kotlin.utils.DFS import org.jetbrains.kotlin.utils.SmartList +import org.jetbrains.kotlin.utils.singletonOrEmptyList class RenameKotlinPropertyProcessor : RenameKotlinPsiProcessor() { override fun canProcessElement(element: PsiElement): Boolean { @@ -81,9 +84,6 @@ class RenameKotlinPropertyProcessor : RenameKotlinPsiProcessor() { JavaRefactoringSettings.getInstance().RENAME_SEARCH_FOR_TEXT_FOR_FIELD = enabled } - /* Can't properly update getters and setters in Java */ - override fun isInplaceRenameSupported() = false - private fun getJvmNames(element: PsiElement): Pair { val descriptor = (element.unwrapped as? KtDeclaration)?.resolveToDescriptor() as? PropertyDescriptor ?: return null to null val getterName = descriptor.getter?.let { DescriptorUtils.getJvmName(it) } @@ -253,6 +253,38 @@ class RenameKotlinPropertyProcessor : RenameKotlinPsiProcessor() { return declarationToRename } + override fun substituteElementToRename(element: PsiElement, editor: Editor, renameCallback: Pass) { + val namedUnwrappedElement = element.namedUnwrappedElement ?: return + + val callableDeclaration = namedUnwrappedElement as? KtCallableDeclaration + ?: throw IllegalStateException("Can't be for element $element there because of canProcessElement()") + + fun preprocessAndPass(substitutedJavaElement: PsiElement) { + val (getterJvmName, setterJvmName) = getJvmNames(namedUnwrappedElement) + val elementToProcess = if (element is KtLightMethod) { + val name = element.name + if (element.name != getterJvmName && element.name != setterJvmName) { + substitutedJavaElement + } + else { + substitutedJavaElement.toLightMethods().firstOrNull { it.name == name } + } + } + else substitutedJavaElement + renameCallback.pass(elementToProcess) + } + + val deepestSuperDeclaration = findDeepestOverriddenDeclaration(callableDeclaration) + if (deepestSuperDeclaration == null || deepestSuperDeclaration == callableDeclaration) { + return preprocessAndPass(callableDeclaration) + } + + val superPsiMethods = deepestSuperDeclaration.getRepresentativeLightMethod().singletonOrEmptyList() + checkSuperMethodsWithPopup(callableDeclaration, superPsiMethods, "Rename", editor) { + preprocessAndPass(if (it.size > 1) deepestSuperDeclaration else callableDeclaration) + } + } + class PropertyMethodWrapper(val propertyMethod: PsiMethod) : PsiNamedElement by propertyMethod, NavigationItem by propertyMethod { override fun getName() = propertyMethod.name override fun setName(name: String) = this