From d652623f907a3df19c2d990b15e29319839cb355 Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Wed, 11 Nov 2015 14:25:39 +0300 Subject: [PATCH] J2K: KotlinIntroduceVariableHandler --- .../KotlinRefactoringSupportProvider.kt | 2 +- .../KotlinIntroduceVariableHandler.kt | 997 ++++++++---------- .../idea/refactoring/jetRefactoringUtil.kt | 5 + .../introduce/AbstractExtractionTest.kt | 12 +- 4 files changed, 444 insertions(+), 572 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt index d8a954d5109..ac079574b15 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringSupportProvider.kt @@ -35,7 +35,7 @@ import org.jetbrains.kotlin.psi.* public class KotlinRefactoringSupportProvider : RefactoringSupportProvider() { override fun isSafeDeleteAvailable(element: PsiElement) = element.canDeleteElement() - override fun getIntroduceVariableHandler() = KotlinIntroduceVariableHandler() + override fun getIntroduceVariableHandler() = KotlinIntroduceVariableHandler override fun getIntroduceParameterHandler() = KotlinIntroduceParameterHandler() diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt index facf609bb9c..80046fb9fa4 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt @@ -14,617 +14,480 @@ * limitations under the License. */ -package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable; +package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable -import com.intellij.openapi.actionSystem.DataContext; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.Pass; -import com.intellij.openapi.util.Ref; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiWhiteSpace; -import com.intellij.psi.util.PsiTreeUtil; -import com.intellij.refactoring.HelpID; -import com.intellij.refactoring.introduce.inplace.OccurrencesChooser; -import kotlin.CollectionsKt; -import kotlin.Unit; -import kotlin.jvm.functions.Function1; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.kotlin.builtins.KotlinBuiltIns; -import org.jetbrains.kotlin.idea.analysis.AnalyzerUtilKt; -import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils; -import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils; -import org.jetbrains.kotlin.idea.core.KotlinNameSuggester; -import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator; -import org.jetbrains.kotlin.idea.core.PsiModificationUtilsKt; -import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention; -import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention; -import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle; -import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtil; -import org.jetbrains.kotlin.idea.refactoring.introduce.IntroduceUtilKt; -import org.jetbrains.kotlin.idea.refactoring.introduce.KotlinIntroduceHandlerBase; -import org.jetbrains.kotlin.idea.resolve.ResolutionFacade; -import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers; -import org.jetbrains.kotlin.idea.util.ScopeUtils; -import org.jetbrains.kotlin.idea.util.ShortenReferences; -import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange; -import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRangeKt; -import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier; -import org.jetbrains.kotlin.lexer.KtTokens; -import org.jetbrains.kotlin.psi.*; -import org.jetbrains.kotlin.psi.psiUtil.KtPsiUtilKt; -import org.jetbrains.kotlin.resolve.BindingContext; -import org.jetbrains.kotlin.resolve.BindingTraceContext; -import org.jetbrains.kotlin.resolve.ObservableBindingTrace; -import org.jetbrains.kotlin.resolve.bindingContextUtil.BindingContextUtilsKt; -import org.jetbrains.kotlin.resolve.calls.smartcasts.DataFlowInfo; -import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode; -import org.jetbrains.kotlin.resolve.scopes.LexicalScope; -import org.jetbrains.kotlin.types.KotlinType; -import org.jetbrains.kotlin.types.checker.KotlinTypeChecker; +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.HelpID +import com.intellij.refactoring.introduce.inplace.OccurrencesChooser +import com.intellij.refactoring.util.CommonRefactoringUtil +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.idea.analysis.computeTypeInfoInContext +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.core.KotlinNameSuggester +import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator +import org.jetbrains.kotlin.idea.core.moveInsideParenthesesAndReplaceWith +import org.jetbrains.kotlin.idea.core.refactoring.Pass +import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention +import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention +import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle +import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtil +import org.jetbrains.kotlin.idea.refactoring.introduce.KotlinIntroduceHandlerBase +import org.jetbrains.kotlin.idea.refactoring.introduce.findElementByCopyableDataAndClearIt +import org.jetbrains.kotlin.idea.refactoring.introduce.findExpressionByCopyableDataAndClearIt +import org.jetbrains.kotlin.idea.refactoring.introduce.findExpressionsByCopyableDataAndClearIt +import org.jetbrains.kotlin.idea.resolve.ResolutionFacade +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.ShortenReferences +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier +import org.jetbrains.kotlin.idea.util.psi.patternMatching.toRange +import org.jetbrains.kotlin.lexer.KtTokens +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.BindingTraceContext +import org.jetbrains.kotlin.resolve.ObservableBindingTrace +import org.jetbrains.kotlin.resolve.bindingContextUtil.getDataFlowInfo +import org.jetbrains.kotlin.resolve.bindingContextUtil.isUsedAsExpression +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.checker.KotlinTypeChecker +import org.jetbrains.kotlin.utils.addIfNotNull +import org.jetbrains.kotlin.utils.sure +import java.util.* -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { + val INTRODUCE_VARIABLE = KotlinRefactoringBundle.message("introduce.variable") -public class KotlinIntroduceVariableHandler extends KotlinIntroduceHandlerBase { + private val EXPRESSION_KEY = Key.create("EXPRESSION_KEY") + private val REPLACE_KEY = Key.create("REPLACE_KEY") + private val COMMON_PARENT_KEY = Key.create("COMMON_PARENT_KEY") - public static final String INTRODUCE_VARIABLE = KotlinRefactoringBundle.message("introduce.variable"); + private var KtExpression.isOccurrence: Boolean by NotNullableCopyableUserDataProperty(Key.create("OCCURRENCE"), false) - @Override - public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull PsiFile file, DataContext dataContext) { - KotlinRefactoringUtil.SelectExpressionCallback callback = new KotlinRefactoringUtil.SelectExpressionCallback() { - @Override - public void run(@Nullable KtExpression expression) { - doRefactoring(project, editor, expression, null, null); - } - }; - try { - KotlinRefactoringUtil.selectExpression(editor, file, callback); - } - catch (KotlinRefactoringUtil.IntroduceRefactoringException e) { - showErrorHint(project, editor, e.getMessage()); - } - } - - - public static void doRefactoring( - @NotNull final Project project, - @Nullable final Editor editor, - @Nullable KtExpression _expression, - @Nullable List occurrencesToReplace, - @Nullable final Function1 onNonInteractiveFinish + private class IntroduceVariableContext( + private val expression: KtExpression, + private val nameSuggestion: String, + private val allReplaces: List, + private val commonContainer: PsiElement, + private val commonParent: PsiElement, + private val replaceOccurrence: Boolean, + private val noTypeInference: Boolean, + private val expressionType: KotlinType?, + private val bindingContext: BindingContext, + private val resolutionFacade: ResolutionFacade ) { - if (_expression == null) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")); - return; + private val psiFactory = KtPsiFactory(expression) + + var propertyRef: KtProperty? = null + var reference: KtExpression? = null + val references = ArrayList() + + private fun findElementByOffsetAndText(offset: Int, text: String, newContainer: PsiElement): PsiElement? { + return newContainer.findElementAt(offset)?.parentsWithSelf?.firstOrNull { (it as? KtExpression)?.text == text } } - if (_expression.getParent() instanceof KtParenthesizedExpression) { - _expression = (KtExpression)_expression.getParent(); - } - final KtExpression expression = _expression; - boolean noTypeInference = false; - if (expression.getParent() instanceof KtQualifiedExpression) { - KtQualifiedExpression qualifiedExpression = (KtQualifiedExpression)expression.getParent(); - if (qualifiedExpression.getReceiverExpression() != expression) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")); - return; + + private fun replaceExpression(replace: KtExpression, addToReferences: Boolean): KtExpression { + val isActualExpression = expression == replace + + val replacement = psiFactory.createExpression(nameSuggestion) + var result = if (replace.isFunctionLiteralOutsideParentheses()) { + val functionLiteralArgument = replace.getStrictParentOfType()!! + val newCallExpression = functionLiteralArgument.moveInsideParenthesesAndReplaceWith(replacement, bindingContext) + newCallExpression.valueArguments.last().getArgumentExpression()!! } - } - else if (expression instanceof KtStatementExpression) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")); - return; - } - else if (expression.getParent() instanceof KtOperationExpression) { - KtOperationExpression operationExpression = (KtOperationExpression)expression.getParent(); - if (operationExpression.getOperationReference() == expression) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")); - return; - } - } - - //noinspection unchecked - if (PsiTreeUtil.getNonStrictParentOfType(expression, - KtTypeReference.class, KtConstructorCalleeExpression.class, KtSuperExpression.class) != null) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")); - return; - } - - ResolutionFacade resolutionFacade = ResolutionUtils.getResolutionFacade(expression); - final BindingContext bindingContext = resolutionFacade.analyze(expression, BodyResolveMode.FULL); - final KotlinType expressionType = bindingContext.getType(expression); //can be null or error type - LexicalScope scope = ScopeUtils.getResolutionScope(expression, bindingContext, resolutionFacade); - DataFlowInfo dataFlowInfo = BindingContextUtilsKt.getDataFlowInfo(bindingContext, expression); - - ObservableBindingTrace bindingTrace = new ObservableBindingTrace(new BindingTraceContext()); - KotlinType typeNoExpectedType = AnalyzerUtilKt.computeTypeInfoInContext( - expression, scope, expression, bindingTrace, dataFlowInfo - ).getType(); - if (expressionType != null && typeNoExpectedType != null && !KotlinTypeChecker.DEFAULT.equalTypes(expressionType, - typeNoExpectedType)) { - noTypeInference = true; - } - - if (expressionType == null && bindingContext.get(BindingContext.QUALIFIER, expression) != null) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.package.expression")); - return; - } - if (expressionType != null && KotlinBuiltIns.isUnit(expressionType)) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.expression.has.unit.type")); - return; - } - final PsiElement container = getContainer(expression); - PsiElement occurrenceContainer = getOccurrenceContainer(expression); - if (container == null) { - showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")); - return; - } - final boolean isInplaceAvailableOnDataContext = - editor != null && - editor.getSettings().isVariableInplaceRenameEnabled() && - !ApplicationManager.getApplication().isUnitTestMode(); - - final List allOccurrences; - if (occurrencesToReplace == null) { - allOccurrences = findOccurrences(occurrenceContainer, expression); - } - else { - allOccurrences = occurrencesToReplace; - } - - final boolean finalNoTypeInference = noTypeInference; - Pass callback = new Pass() { - @Override - public void pass(OccurrencesChooser.ReplaceChoice replaceChoice) { - boolean replaceOccurrence = shouldReplaceOccurrence(expression, bindingContext, container); - List allReplaces; - if (OccurrencesChooser.ReplaceChoice.ALL == replaceChoice) { - if (allOccurrences.size() > 1) replaceOccurrence = true; - allReplaces = allOccurrences; - } - else { - allReplaces = Collections.singletonList(expression); - } - - PsiElement commonParent = PsiTreeUtil.findCommonParent(allReplaces); - PsiElement commonContainer = getContainer(commonParent); - NewDeclarationNameValidator validator = new NewDeclarationNameValidator( - commonContainer, - calculateAnchor(commonParent, commonContainer, allReplaces), - NewDeclarationNameValidator.Target.VARIABLES - ); - final Collection suggestedNames = KotlinNameSuggester.INSTANCE$.suggestNamesByExpressionAndType( - expression, ResolutionUtils.analyze(expression, BodyResolveMode.PARTIAL), validator, "value"); - final Ref propertyRef = new Ref(); - final ArrayList references = new ArrayList(); - final Ref reference = new Ref(); - final Runnable introduceRunnable = introduceVariable( - expression, suggestedNames.iterator().next(), allReplaces, commonContainer, - commonParent, replaceOccurrence, propertyRef, references, - reference, finalNoTypeInference, expressionType, bindingContext); - CommandProcessor.getInstance().executeCommand(project, new Runnable() { - @Override - public void run() { - ApplicationManager.getApplication().runWriteAction(introduceRunnable); - KtProperty property = propertyRef.get(); - if (property != null) { - if (editor != null) { - editor.getCaretModel().moveToOffset(property.getTextOffset()); - editor.getSelectionModel().removeSelection(); - if (isInplaceAvailableOnDataContext) { - PsiDocumentManager.getInstance(project).commitDocument(editor.getDocument()); - PsiDocumentManager.getInstance(project). - doPostponedOperationsAndUnblockDocument(editor.getDocument()); - KotlinVariableInplaceIntroducer variableIntroducer = - new KotlinVariableInplaceIntroducer(property, - reference.get(), - references.toArray(new KtExpression[references.size()]), - suggestedNames, - /*todo*/ false, - /*todo*/ false, - expressionType, - finalNoTypeInference, - project, - editor); - variableIntroducer.startInplaceIntroduceTemplate(); - } - } - else if (onNonInteractiveFinish != null) { - onNonInteractiveFinish.invoke(property); - } - } - } - }, INTRODUCE_VARIABLE, null); - } - }; - if (isInplaceAvailableOnDataContext && occurrencesToReplace == null) { - OccurrencesChooser.simpleChooser(editor). - showChooser(expression, allOccurrences, callback); - } - else { - callback.pass(OccurrencesChooser.ReplaceChoice.ALL); - } - } - - private static Key OCCURRENCE = Key.create("OCCURRENCE"); - - private static Runnable introduceVariable( - final KtExpression expression, - final String nameSuggestion, - final List allReplaces, - final PsiElement commonContainer, - final PsiElement commonParent, - final boolean replaceOccurrence, - final Ref propertyRef, - final ArrayList references, - final Ref reference, - final boolean noTypeInference, - final KotlinType expressionType, - final BindingContext bindingContext - ) { - final KtPsiFactory psiFactory = KtPsiFactoryKt.KtPsiFactory(expression); - return new Runnable() { - @Override - public void run() { - if (commonContainer instanceof KtDeclarationWithBody) { - KtDeclarationWithBody originalDeclaration = (KtDeclarationWithBody) commonContainer; - final KtExpression originalBody = originalDeclaration.getBodyExpression(); - assert originalBody != null : "Original body is not found: " + originalDeclaration; - - Key EXPRESSION_KEY = Key.create("EXPRESSION_KEY"); - Key REPLACE_KEY = Key.create("REPLACE_KEY"); - Key COMMON_PARENT_KEY = Key.create("COMMON_PARENT_KEY"); - expression.putCopyableUserData(EXPRESSION_KEY, true); - for (KtExpression replace : allReplaces) { - replace.putCopyableUserData(REPLACE_KEY, true); - } - commonParent.putCopyableUserData(COMMON_PARENT_KEY, true); - - KtDeclarationWithBody newDeclaration = ConvertToBlockBodyIntention.Companion.convert(originalDeclaration); - - KtBlockExpression newCommonContainer = (KtBlockExpression) newDeclaration.getBodyExpression(); - assert newCommonContainer != null : "New body is not found: " + newDeclaration; - - KtExpression newExpression = IntroduceUtilKt.findExpressionByCopyableDataAndClearIt(newCommonContainer, EXPRESSION_KEY); - PsiElement newCommonParent = IntroduceUtilKt.findElementByCopyableDataAndClearIt(newCommonContainer, COMMON_PARENT_KEY); - List newAllReplaces = IntroduceUtilKt - .findExpressionsByCopyableDataAndClearIt(newCommonContainer, REPLACE_KEY); - - run(newExpression, newCommonContainer, newCommonParent, newAllReplaces); - } - else { - run(expression, commonContainer, commonParent, allReplaces); - } + else { + replace.replace(replacement) as KtExpression } - private void run( - KtExpression expression, - PsiElement commonContainer, - PsiElement commonParent, - List allReplaces - ) { - String variableText = "val " + nameSuggestion; + val parent = result.parent + if (parent is KtBlockStringTemplateEntry) { + val intention = RemoveCurlyBracesFromTemplateIntention() + val newEntry = if (intention.isApplicableTo(parent)) intention.applyTo(parent) else parent + result = newEntry.expression!! + } + + if (addToReferences) references.addIfNotNull(result) + + if (isActualExpression) reference = result + + return result + } + + private fun runRefactoring ( + expression: KtExpression, + commonContainer: PsiElement, + commonParent: PsiElement, + allReplaces: List + ) { + val variableText = StringBuilder("val ").apply { + append(nameSuggestion) if (noTypeInference) { - variableText += ": " + IdeDescriptorRenderers.SOURCE_CODE.renderType(expressionType); + val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType + append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender)) } - variableText += " = "; - if (expression instanceof KtParenthesizedExpression) { - KtParenthesizedExpression parenthesizedExpression = (KtParenthesizedExpression)expression; - KtExpression innerExpression = parenthesizedExpression.getExpression(); - if (innerExpression != null) { - variableText += innerExpression.getText(); + append(" = ") + + append(((expression as? KtParenthesizedExpression)?.expression ?: expression).text) + }.toString() + var property = psiFactory.createProperty(variableText) + + var anchor = calculateAnchor(commonParent, commonContainer, allReplaces) ?: return + val needBraces = commonContainer !is KtBlockExpression + if (!needBraces) { + property = commonContainer.addBefore(property, anchor) as KtProperty + commonContainer.addBefore(psiFactory.createNewLine(), anchor) + } + else { + var emptyBody: KtExpression = psiFactory.createEmptyBody() + val firstChild = emptyBody.firstChild + emptyBody.addAfter(psiFactory.createNewLine(), firstChild) + + if (replaceOccurrence) { + for (replace in allReplaces) { + val exprAfterReplace = replaceExpression(replace, false) + exprAfterReplace.isOccurrence = true + if (anchor == replace) { + anchor = exprAfterReplace + } } - else { - variableText += expression.getText(); + + var oldElement: PsiElement = commonContainer + if (commonContainer is KtWhenEntry) { + val body = commonContainer.expression + if (body != null) { + oldElement = body + } } + else if (commonContainer is KtContainerNode) { + val children = commonContainer.children + for (child in children) { + if (child is KtExpression) { + oldElement = child + } + } + } + //ugly logic to make sure we are working with right actual expression + var actualExpression = reference!! + var diff = actualExpression.textRange.startOffset - oldElement.textRange.startOffset + var actualExpressionText = actualExpression.text + val newElement = emptyBody.addAfter(oldElement, firstChild) + var elem: PsiElement? = findElementByOffsetAndText(diff, actualExpressionText, newElement) + if (elem != null) { + reference = elem as KtExpression + } + emptyBody.addAfter(psiFactory.createNewLine(), firstChild) + property = emptyBody.addAfter(property, firstChild) as KtProperty + emptyBody.addAfter(psiFactory.createNewLine(), firstChild) + actualExpression = reference!! + diff = actualExpression.textRange.startOffset - emptyBody.textRange.startOffset + actualExpressionText = actualExpression.text + emptyBody = anchor!!.replace(emptyBody) as KtBlockExpression + elem = findElementByOffsetAndText(diff, actualExpressionText, emptyBody) + if (elem != null) { + reference = elem as KtExpression + } + + emptyBody.accept( + object : KtTreeVisitorVoid() { + override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) { + if (!expression.isOccurrence) return + + expression.isOccurrence = false + references.add(expression) + } + }) } else { - variableText += expression.getText(); - } - KtProperty property = psiFactory.createProperty(variableText); - PsiElement anchor = calculateAnchor(commonParent, commonContainer, allReplaces); - if (anchor == null) return; - boolean needBraces = !(commonContainer instanceof KtBlockExpression); - if (!needBraces) { - property = (KtProperty)commonContainer.addBefore(property, anchor); - commonContainer.addBefore(psiFactory.createNewLine(), anchor); - } - else { - KtExpression emptyBody = psiFactory.createEmptyBody(); - PsiElement firstChild = emptyBody.getFirstChild(); - emptyBody.addAfter(psiFactory.createNewLine(), firstChild); + val parent = anchor.parent + val copyTo = parent.lastChild + val copyFrom = anchor.nextSibling - if (replaceOccurrence && commonContainer != null) { - for (KtExpression replace : allReplaces) { - KtExpression exprAfterReplace = replaceExpression(replace, false); - exprAfterReplace.putCopyableUserData(OCCURRENCE, true); - if (anchor == replace) { - anchor = exprAfterReplace; - } - } - - PsiElement oldElement = commonContainer; - if (commonContainer instanceof KtWhenEntry) { - KtExpression body = ((KtWhenEntry)commonContainer).getExpression(); - if (body != null) { - oldElement = body; - } - } - else if (commonContainer instanceof KtContainerNode) { - KtContainerNode container = (KtContainerNode)commonContainer; - PsiElement[] children = container.getChildren(); - for (PsiElement child : children) { - if (child instanceof KtExpression) { - oldElement = child; + property = emptyBody.addAfter(property, firstChild) as KtProperty + emptyBody.addAfter(psiFactory.createNewLine(), firstChild) + if (copyFrom != null && copyTo != null) { + emptyBody.addRangeAfter(copyFrom, copyTo, property) + parent.deleteChildRange(copyFrom, copyTo) + } + emptyBody = anchor.replace(emptyBody) as KtBlockExpression + } + for (child in emptyBody.children) { + if (child is KtProperty) { + property = child + } + } + if (commonContainer is KtContainerNode) { + if (commonContainer.parent is KtIfExpression) { + val next = commonContainer.nextSibling + if (next != null) { + val nextnext = next.nextSibling + if (nextnext != null && nextnext.node.elementType == KtTokens.ELSE_KEYWORD) { + if (next is PsiWhiteSpace) { + next.replace(psiFactory.createWhiteSpace()) } } } - //ugly logic to make sure we are working with right actual expression - KtExpression actualExpression = reference.get(); - int diff = actualExpression.getTextRange().getStartOffset() - oldElement.getTextRange().getStartOffset(); - String actualExpressionText = actualExpression.getText(); - PsiElement newElement = emptyBody.addAfter(oldElement, firstChild); - PsiElement elem = findElementByOffsetAndText(diff, actualExpressionText, newElement); - if (elem != null) { - reference.set((KtExpression)elem); - } - emptyBody.addAfter(psiFactory.createNewLine(), firstChild); - property = (KtProperty)emptyBody.addAfter(property, firstChild); - emptyBody.addAfter(psiFactory.createNewLine(), firstChild); - actualExpression = reference.get(); - diff = actualExpression.getTextRange().getStartOffset() - emptyBody.getTextRange().getStartOffset(); - actualExpressionText = actualExpression.getText(); - emptyBody = (KtBlockExpression) anchor.replace(emptyBody); - elem = findElementByOffsetAndText(diff, actualExpressionText, emptyBody); - if (elem != null) { - reference.set((KtExpression)elem); - } + } + } + } + if (!needBraces) { + for (i in allReplaces.indices) { + val replace = allReplaces[i] - emptyBody.accept( - new KtTreeVisitorVoid() { - @Override - public void visitSimpleNameExpression(@NotNull KtSimpleNameExpression expression) { - if (expression.getCopyableUserData(OCCURRENCE) == null) return; - - expression.putCopyableUserData(OCCURRENCE, null); - references.add(expression); - } - } - ); + if (if (i != 0) replaceOccurrence else replace.shouldReplaceOccurrence(bindingContext, commonContainer)) { + replaceExpression(replace, true) } else { - PsiElement parent = anchor.getParent(); - PsiElement copyTo = parent.getLastChild(); - PsiElement copyFrom = anchor.getNextSibling(); - - property = (KtProperty)emptyBody.addAfter(property, firstChild); - emptyBody.addAfter(psiFactory.createNewLine(), firstChild); - if (copyFrom != null && copyTo != null) { - emptyBody.addRangeAfter(copyFrom, copyTo, property); - parent.deleteChildRange(copyFrom, copyTo); - } - emptyBody = (KtBlockExpression) anchor.replace(emptyBody); - } - for (PsiElement child : emptyBody.getChildren()) { - if (child instanceof KtProperty) { - property = (KtProperty)child; - } - } - if (commonContainer instanceof KtContainerNode) { - KtContainerNode node = (KtContainerNode)commonContainer; - if (node.getParent() instanceof KtIfExpression) { - PsiElement next = node.getNextSibling(); - if (next != null) { - PsiElement nextnext = next.getNextSibling(); - if (nextnext != null && nextnext.getNode().getElementType() == KtTokens.ELSE_KEYWORD) { - if (next instanceof PsiWhiteSpace) { - next.replace(psiFactory.createWhiteSpace()); - } - } - } - } - } - } - if (!needBraces) { - for (int i = 0; i < allReplaces.size(); i++) { - KtExpression replace = allReplaces.get(i); - - if (i != 0 ? replaceOccurrence : shouldReplaceOccurrence(replace, bindingContext, commonContainer)) { - replaceExpression(replace, true); + val sibling = PsiTreeUtil.skipSiblingsBackward(replace, PsiWhiteSpace::class.java) + if (sibling == property) { + replace.parent.deleteChildRange(property.nextSibling, replace) } else { - PsiElement sibling = PsiTreeUtil.skipSiblingsBackward(replace, PsiWhiteSpace.class); - if (sibling == property) { - replace.getParent().deleteChildRange(property.getNextSibling(), replace); - } - else { - replace.delete(); - } + replace.delete() } } } - propertyRef.set(property); - if (noTypeInference) { - ShortenReferences.DEFAULT.process(property); - } } - - private PsiElement findElementByOffsetAndText(int offset, String text, PsiElement newContainer) { - PsiElement elem = newContainer.findElementAt(offset); - while (elem != null && !(elem instanceof KtExpression && text.equals(elem.getText()))) { - elem = elem.getParent(); - } - return elem; + propertyRef = property + if (noTypeInference) { + ShortenReferences.DEFAULT.process(property) } + } - private KtExpression replaceExpression(KtExpression replace, boolean addToReferences) { - boolean isActualExpression = expression == replace; + fun runRefactoring() { + if (commonContainer !is KtDeclarationWithBody) return runRefactoring(expression, commonContainer, commonParent, allReplaces) - KtExpression replacement = psiFactory.createExpression(nameSuggestion); - KtExpression result; - if (KtPsiUtilKt.isFunctionLiteralOutsideParentheses(replace)) { - KtFunctionLiteralArgument functionLiteralArgument = - PsiTreeUtil.getParentOfType(replace, KtFunctionLiteralArgument.class); - KtCallExpression newCallExpression = PsiModificationUtilsKt - .moveInsideParenthesesAndReplaceWith(functionLiteralArgument, replacement, bindingContext); - result = CollectionsKt.last(newCallExpression.getValueArguments()).getArgumentExpression(); - } - else { - result = (KtExpression)replace.replace(replacement); - } + commonContainer.bodyExpression.sure { "Original body is not found: " + commonContainer } - PsiElement parent = result != null ? result.getParent() : null; - if (parent instanceof KtBlockStringTemplateEntry) { - RemoveCurlyBracesFromTemplateIntention intention = new RemoveCurlyBracesFromTemplateIntention(); - KtBlockStringTemplateEntry entry = (KtBlockStringTemplateEntry) parent; - KtStringTemplateEntryWithExpression newEntry = intention.isApplicableTo(entry) ? intention.applyTo(entry) : entry; - result = newEntry.getExpression(); - } - - if (addToReferences) { - references.add(result); - } - if (isActualExpression) reference.set(result); - - return result; + expression.putCopyableUserData(EXPRESSION_KEY, true) + for (replace in allReplaces) { + replace.putCopyableUserData(REPLACE_KEY, true) } - }; + commonParent.putCopyableUserData(COMMON_PARENT_KEY, true) + + val newDeclaration = ConvertToBlockBodyIntention.convert(commonContainer) + + val newCommonContainer = (newDeclaration.bodyExpression as KtBlockExpression?) + .sure { "New body is not found: " + newDeclaration } + + val newExpression = newCommonContainer.findExpressionByCopyableDataAndClearIt(EXPRESSION_KEY) + val newCommonParent = newCommonContainer.findElementByCopyableDataAndClearIt(COMMON_PARENT_KEY) + val newAllReplaces = newCommonContainer.findExpressionsByCopyableDataAndClearIt(REPLACE_KEY) + + runRefactoring(newExpression, newCommonContainer, newCommonParent, newAllReplaces) + } } - private static PsiElement calculateAnchor(PsiElement commonParent, PsiElement commonContainer, - List allReplaces) { - PsiElement anchor = commonParent; - if (anchor != commonContainer) { - while (anchor.getParent() != commonContainer) { - anchor = anchor.getParent(); + private fun calculateAnchor(commonParent: PsiElement, commonContainer: PsiElement, allReplaces: List): PsiElement? { + if (commonParent != commonContainer) return commonParent.parentsWithSelf.firstOrNull { it.parent == commonContainer } + + val startOffset = allReplaces.fold(commonContainer.endOffset) { offset, expr -> Math.min(offset, expr.startOffset) } + return commonContainer.allChildren.lastOrNull { it.textRange.contains(startOffset) } ?: return null + } + + private fun KtExpression.findOccurrences(occurrenceContainer: PsiElement): List { + return toRange() + .match(occurrenceContainer, KotlinPsiUnifier.DEFAULT) + .map { + val candidate = it.range.elements.first() + when (candidate) { + is KtExpression -> candidate + is KtStringTemplateEntryWithExpression -> candidate.expression + else -> throw AssertionError("Unexpected candidate element: " + candidate.text) + } as? KtExpression + } + .filterNotNull() + } + + private fun KtExpression.shouldReplaceOccurrence(bindingContext: BindingContext, container: PsiElement?): Boolean { + return isUsedAsExpression(bindingContext) || container != parent + } + + private fun PsiElement.getContainer(): PsiElement? { + if (this is KtBlockExpression) return this + + return (parentsWithSelf zip parents).firstOrNull { + val (place, parent) = it + when (parent) { + is KtContainerNode -> !parent.isBadContainerNode(place) + is KtBlockExpression -> true + is KtWhenEntry -> place == parent.expression + is KtDeclarationWithBody -> parent.bodyExpression == place + else -> false } + }?.second + } + + private fun KtContainerNode.isBadContainerNode(place: PsiElement): Boolean { + val parent = parent + return when (parent) { + is KtIfExpression -> parent.condition == place + is KtLoopExpression -> parent.body != place + is KtArrayAccessExpression -> true + else -> false + } + } + + private fun PsiElement.getOccurrenceContainer(): PsiElement? { + var result: PsiElement? = null + for ((place, parent) in parentsWithSelf zip parents) { + when { + parent is KtContainerNode && place !is KtBlockExpression && !parent.isBadContainerNode(place) -> result = parent + parent is KtClassBody, parent is KtFile -> return result + parent is KtBlockExpression -> result = parent + parent is KtWhenEntry && place !is KtBlockExpression -> result = parent + parent is KtDeclarationWithBody && parent.bodyExpression == place && place !is KtBlockExpression -> result = parent + } + } + + return null + } + + private fun showErrorHint(project: Project, editor: Editor?, message: String) { + CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE) + } + + fun doRefactoring( + project: Project, + editor: Editor?, + expressionToExtract: KtExpression?, + occurrencesToReplace: List?, + onNonInteractiveFinish: ((KtProperty) -> Unit)? + ) { + val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) } + ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) + val parent = expression.parent + + when { + parent is KtQualifiedExpression -> { + if (parent.receiverExpression != expression) { + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) + } + } + expression is KtStatementExpression -> + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) + parent is KtOperationExpression && parent.operationReference == expression -> + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) + } + + PsiTreeUtil.getNonStrictParentOfType(expression, + KtTypeReference::class.java, + KtConstructorCalleeExpression::class.java, + KtSuperExpression::class.java)?.let { + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")) + } + + val resolutionFacade = expression.getResolutionFacade() + val bindingContext = resolutionFacade.analyze(expression, BodyResolveMode.FULL) + val expressionType = bindingContext.getType(expression) //can be null or error type + val scope = expression.getResolutionScope(bindingContext, resolutionFacade) + val dataFlowInfo = bindingContext.getDataFlowInfo(expression) + + val bindingTrace = ObservableBindingTrace(BindingTraceContext()) + val typeNoExpectedType = expression.computeTypeInfoInContext(scope, expression, bindingTrace, dataFlowInfo).type + val noTypeInference = expressionType != null + && typeNoExpectedType != null + && !KotlinTypeChecker.DEFAULT.equalTypes(expressionType, typeNoExpectedType) + + if (expressionType == null && bindingContext.get(BindingContext.QUALIFIER, expression) != null) { + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.package.expression")) + } + + if (expressionType != null && KotlinBuiltIns.isUnit(expressionType)) { + return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.expression.has.unit.type")) + } + + val container = expression.getContainer() + ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")) + val occurrenceContainer = expression.getOccurrenceContainer() + ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")) + + val isInplaceAvailable = editor != null + && editor.settings.isVariableInplaceRenameEnabled + && !ApplicationManager.getApplication().isUnitTestMode + + val allOccurrences = occurrencesToReplace ?: expression.findOccurrences(occurrenceContainer) + + val callback = Pass { replaceChoice -> + val allReplaces = when (replaceChoice) { + OccurrencesChooser.ReplaceChoice.ALL -> allOccurrences + else -> listOf(expression) + } + val replaceOccurrence = expression.shouldReplaceOccurrence(bindingContext, container) || allReplaces.size > 1 + + val commonParent = PsiTreeUtil.findCommonParent(allReplaces) as KtElement + val commonContainer = commonParent.getContainer() as KtElement + val validator = NewDeclarationNameValidator( + commonContainer, + calculateAnchor(commonParent, commonContainer, allReplaces), + NewDeclarationNameValidator.Target.VARIABLES + ) + val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression, bindingContext, validator, "value") + val introduceVariableContext = IntroduceVariableContext( + expression, suggestedNames.iterator().next(), allReplaces, commonContainer, commonParent, + replaceOccurrence, noTypeInference, expressionType, bindingContext, resolutionFacade + ) + project.executeCommand(INTRODUCE_VARIABLE, null) { + runWriteAction { introduceVariableContext.runRefactoring() } + + val property = introduceVariableContext.propertyRef ?: return@executeCommand + + if (editor == null) { + onNonInteractiveFinish?.invoke(property) + return@executeCommand + } + + editor.caretModel.moveToOffset(property.textOffset) + editor.selectionModel.removeSelection() + + if (!isInplaceAvailable) return@executeCommand + + PsiDocumentManager.getInstance(project).commitDocument(editor.document) + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) + KotlinVariableInplaceIntroducer( + property, + introduceVariableContext.reference, + introduceVariableContext.references.toTypedArray(), + suggestedNames, + /*todo*/ false, + /*todo*/ false, + expressionType, + noTypeInference, + project, + editor + ).startInplaceIntroduceTemplate() + } + } + + if (isInplaceAvailable && occurrencesToReplace == null) { + OccurrencesChooser.simpleChooser(editor).showChooser(expression, allOccurrences, callback) } else { - anchor = commonContainer.getFirstChild(); - int startOffset = commonContainer.getTextRange().getEndOffset(); - for (KtExpression expr : allReplaces) { - int offset = expr.getTextRange().getStartOffset(); - if (offset < startOffset) startOffset = offset; - } - while (anchor != null && !anchor.getTextRange().contains(startOffset)) { - anchor = anchor.getNextSibling(); - } - if (anchor == null) return null; + callback.pass(OccurrencesChooser.ReplaceChoice.ALL) } - return anchor; } - private static List findOccurrences(PsiElement occurrenceContainer, @NotNull KtExpression originalExpression) { - return CollectionsKt.map( - KotlinPsiRangeKt.toRange(originalExpression).match(occurrenceContainer, KotlinPsiUnifier.DEFAULT), - new Function1() { - @Override - public KtExpression invoke(KotlinPsiRange.Match match) { - PsiElement candidate = ((KotlinPsiRange.ListRange) match.getRange()).getStartElement(); - if (candidate instanceof KtExpression) return (KtExpression) candidate; - if (candidate instanceof KtStringTemplateEntryWithExpression) - return ((KtStringTemplateEntryWithExpression) candidate).getExpression(); - - throw new AssertionError("Unexpected candidate element: " + candidate.getText()); - } - } - ); + override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext) { + try { + KotlinRefactoringUtil.selectExpression(editor, file) { doRefactoring(project, editor, it, null, null) } + } + catch (e: KotlinRefactoringUtil.IntroduceRefactoringException) { + showErrorHint(project, editor, e.message!!) + } } - private static boolean shouldReplaceOccurrence( - @NotNull KtExpression expression, - @NotNull BindingContext bindingContext, - @Nullable PsiElement container - ) { - return BindingContextUtilsKt.isUsedAsExpression(expression, bindingContext) || container != expression.getParent(); - } - - @Nullable - private static PsiElement getContainer(PsiElement place) { - if (place instanceof KtBlockExpression) { - return place; - } - while (place != null) { - PsiElement parent = place.getParent(); - if (parent instanceof KtContainerNode) { - if (!isBadContainerNode((KtContainerNode)parent, place)) { - return parent; - } - } - if (parent instanceof KtBlockExpression - || (parent instanceof KtWhenEntry && place == ((KtWhenEntry) parent).getExpression())) { - return parent; - } - if (parent instanceof KtDeclarationWithBody && ((KtDeclarationWithBody) parent).getBodyExpression() == place) { - return parent; - } - place = parent; - } - return null; - } - - private static boolean isBadContainerNode(KtContainerNode parent, PsiElement place) { - if (parent.getParent() instanceof KtIfExpression && - ((KtIfExpression)parent.getParent()).getCondition() == place) { - return true; - } - else if (parent.getParent() instanceof KtLoopExpression && - ((KtLoopExpression)parent.getParent()).getBody() != place) { - return true; - } - else if (parent.getParent() instanceof KtArrayAccessExpression) { - return true; - } - return false; - } - - @Nullable - private static PsiElement getOccurrenceContainer(PsiElement place) { - PsiElement result = null; - while (place != null) { - PsiElement parent = place.getParent(); - if (parent instanceof KtContainerNode) { - if (!(place instanceof KtBlockExpression) && !isBadContainerNode((KtContainerNode)parent, place)) { - result = parent; - } - } - else if (parent instanceof KtClassBody || parent instanceof KtFile) { - return result; - } - else if (parent instanceof KtBlockExpression) { - result = parent; - } - else if (parent instanceof KtWhenEntry) { - if (!(place instanceof KtBlockExpression)) { - result = parent; - } - } - else if (parent instanceof KtDeclarationWithBody) { - KtDeclarationWithBody function = (KtDeclarationWithBody)parent; - if (function.getBodyExpression() == place) { - if (!(place instanceof KtBlockExpression)) { - result = parent; - } - } - } - place = parent; - } - return null; - } - - private static void showErrorHint(Project project, Editor editor, String message) { - CodeInsightUtils.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE); - } - - @Override - public void invoke(@NotNull Project project, @NotNull PsiElement[] elements, DataContext dataContext) { + override fun invoke(project: Project, elements: Array, dataContext: DataContext) { //do nothing } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt index c36b039cf05..735e91e6e3e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt @@ -40,6 +40,7 @@ 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.util.Key +import com.intellij.openapi.util.Pass import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.LocalFileSystem @@ -744,3 +745,7 @@ fun replaceListPsiAndKeepDelimiters( return originalList } + +fun Pass(body: (T) -> Unit) = object: Pass() { + override fun pass(t: T) = body(t) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt index 4fed5289c26..5374f4b661d 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractExtractionTest.kt @@ -32,6 +32,7 @@ import com.intellij.refactoring.introduceField.ElementToWorkOn import com.intellij.refactoring.introduceParameter.AbstractJavaInplaceIntroducer import com.intellij.refactoring.introduceParameter.IntroduceParameterProcessor import com.intellij.refactoring.introduceParameter.Util +import com.intellij.refactoring.util.CommonRefactoringUtil import com.intellij.refactoring.util.occurrences.ExpressionOccurrenceManager import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase @@ -67,11 +68,11 @@ public abstract class AbstractExtractionTest() : KotlinLightCodeInsightFixtureTe doTest(path) { file -> file as KtFile - KotlinIntroduceVariableHandler().invoke( - fixture.getProject(), - fixture.getEditor(), + KotlinIntroduceVariableHandler.invoke( + fixture.project, + fixture.editor, file, - DataManager.getInstance().getDataContext(fixture.getEditor().getComponent()) + DataManager.getInstance().getDataContext(fixture.editor.component) ) } } @@ -342,6 +343,9 @@ public abstract class AbstractExtractionTest() : KotlinLightCodeInsightFixtureTe val message = e.messages.sorted().joinToString(" ").replace("\n", " ") KotlinTestUtils.assertEqualsToFile(conflictFile, message) } + catch(e: CommonRefactoringUtil.RefactoringErrorHintException) { + KotlinTestUtils.assertEqualsToFile(conflictFile, e.message!!) + } catch(e: RuntimeException) { // RuntimeException is thrown by IDEA code in CodeInsightUtils.java if (e.javaClass != RuntimeException::class.java) throw e KotlinTestUtils.assertEqualsToFile(conflictFile, e.message!!)