diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt index 5df97b4d69f..6dd32570aa5 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt @@ -199,6 +199,10 @@ public class KtPsiFactory(private val project: Project) { return getter } + public fun createMultiDeclaration(text: String): KtMultiDeclaration { + return (createFunction("fun foo() {$text}").bodyExpression as KtBlockExpression).statements.first() as KtMultiDeclaration + } + public fun createDeclaration(text: String): TDeclaration { val file = createFile(text) val declarations = file.getDeclarations() 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 1f18c594852..1e72e202b4b 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 @@ -16,30 +16,31 @@ package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.codeInsight.template.* import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.command.impl.FinishMarkAction +import com.intellij.openapi.command.impl.StartMarkAction import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Key import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.* 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 com.intellij.util.SmartList import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor import org.jetbrains.kotlin.idea.analysis.analyzeInContext 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.compareDescriptors -import org.jetbrains.kotlin.idea.core.moveInsideParenthesesAndReplaceWith +import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.idea.core.* import org.jetbrains.kotlin.idea.core.refactoring.Pass import org.jetbrains.kotlin.idea.core.refactoring.chooseContainerElementIfNecessary import org.jetbrains.kotlin.idea.core.refactoring.removeTemplateEntryBracesIfPossible @@ -51,6 +52,7 @@ 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.executeWriteCommand import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.idea.util.getResolutionScope import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier @@ -68,6 +70,7 @@ import org.jetbrains.kotlin.resolve.source.getPsi import org.jetbrains.kotlin.types.KotlinType import org.jetbrains.kotlin.types.checker.KotlinTypeChecker import org.jetbrains.kotlin.utils.addIfNotNull +import org.jetbrains.kotlin.utils.addToStdlib.singletonList import org.jetbrains.kotlin.utils.ifEmpty import org.jetbrains.kotlin.utils.sure import java.util.* @@ -83,19 +86,20 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { private class IntroduceVariableContext( private val expression: KtExpression, - private val nameSuggestion: String, + private val nameSuggestions: List>, 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 componentFunctions: List, private val bindingContext: BindingContext, private val resolutionFacade: ResolutionFacade ) { private val psiFactory = KtPsiFactory(expression) - var propertyRef: KtProperty? = null + var propertyRef: KtDeclaration? = null var reference: KtExpression? = null val references = ArrayList() @@ -106,7 +110,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { private fun replaceExpression(expressionToReplace: KtExpression, addToReferences: Boolean): KtExpression { val isActualExpression = expression == expressionToReplace - val replacement = psiFactory.createExpression(nameSuggestion) + val replacement = psiFactory.createExpression(nameSuggestions.single().first()) val substringInfo = expressionToReplace.extractableSubstringInfo var result = when { expressionToReplace.isFunctionLiteralOutsideParentheses() -> { @@ -133,22 +137,31 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { commonParent: PsiElement, allReplaces: List ) { - val variableText = StringBuilder("val ").apply { - append(nameSuggestion) - if (noTypeInference) { - val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType - append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender)) - } - append(" = ") - - append(((expression as? KtParenthesizedExpression)?.expression ?: expression).text) - }.toString() - var property = psiFactory.createProperty(variableText) + val initializer = (expression as? KtParenthesizedExpression)?.expression ?: expression + var property: KtDeclaration = if (componentFunctions.isNotEmpty()) { + buildString { + componentFunctions.indices.joinTo(this, prefix = "val (", postfix = ")") { nameSuggestions[it].first() } + append(" = ") + append(initializer.text) + }.let { psiFactory.createMultiDeclaration(it) } + } + else { + buildString { + append("val ") + append(nameSuggestions.single().first()) + if (noTypeInference) { + val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType + append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender)) + } + append(" = ") + append(initializer.text) + }.let { psiFactory.createProperty(it) } + } var anchor = calculateAnchor(commonParent, commonContainer, allReplaces) ?: return val needBraces = commonContainer !is KtBlockExpression if (!needBraces) { - property = commonContainer.addBefore(property, anchor) as KtProperty + property = commonContainer.addBefore(property, anchor) as KtDeclaration commonContainer.addBefore(psiFactory.createNewLine(), anchor) } else { @@ -365,6 +378,70 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE) } + private fun KtExpression.chooseApplicableComponentFunctions(haveOccurrencesToReplace: Boolean): List? { + if (haveOccurrencesToReplace) return emptyList() + + val functions = getApplicableComponentFunctions(this) + if (functions.size <= 1) return emptyList() + + return functions + } + + private fun executeMultiDeclarationTemplate( + project: Project, + editor: Editor, + declaration: KtMultiDeclaration, + suggestedNames: List>) { + StartMarkAction.canStart(project)?.let { return } + + val builder = TemplateBuilderImpl(declaration) + for ((index, entry) in declaration.entries.withIndex()) { + val templateExpression = object : Expression() { + private val lookupItems = suggestedNames[index].map { LookupElementBuilder.create(it) }.toTypedArray() + + override fun calculateQuickResult(context: ExpressionContext?) = TextResult(suggestedNames[index].first()) + + override fun calculateResult(context: ExpressionContext?) = calculateQuickResult(context) + + override fun calculateLookupItems(context: ExpressionContext?) = lookupItems + } + builder.replaceElement(entry, templateExpression) + } + + val startMarkAction = StartMarkAction.start(editor, project, INTRODUCE_VARIABLE) + editor.caretModel.moveToOffset(declaration.startOffset) + + project.executeWriteCommand(INTRODUCE_VARIABLE) { + TemplateManager.getInstance(project).startTemplate( + editor, + builder.buildInlineTemplate(), + object: TemplateEditingAdapter() { + private fun finishMarkAction() { + FinishMarkAction.finish(project, editor, startMarkAction) + } + + override fun templateFinished(template: Template?, brokenOff: Boolean) { + finishMarkAction() + } + + override fun templateCancelled(template: Template?) { + finishMarkAction() + } + } + ) + } + } + + private fun suggestNamesForComponent(descriptor: FunctionDescriptor, project: Project, validator: (String) -> Boolean): Set { + return LinkedHashSet().apply { + descriptor.returnType?.let { addAll(KotlinNameSuggester.suggestNamesByType(it, validator)) } + + val componentName = (DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor) as? PsiNamedElement)?.name + ?: descriptor.name.asString() + add(KotlinNameSuggester.suggestNameByName(componentName, validator)) + } + } + fun doRefactoring( project: Project, editor: Editor?, @@ -374,7 +451,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { resolutionFacade: ResolutionFacade, bindingContext: BindingContext, occurrencesToReplace: List?, - onNonInteractiveFinish: ((KtProperty) -> Unit)? + onNonInteractiveFinish: ((KtDeclaration) -> Unit)? ) { val substringInfo = expression.extractableSubstringInfo val physicalExpression = expression.substringContextOrThis @@ -440,19 +517,27 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { commonContainer = container } + val componentFunctions = physicalExpression.chooseApplicableComponentFunctions(replaceOccurrence) ?: return@Pass + val validator = NewDeclarationNameValidator( commonContainer, calculateAnchor(commonParent, commonContainer, allReplaces), NewDeclarationNameValidator.Target.VARIABLES ) - val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression, - substringInfo?.type, - bindingContext, - validator, - "value") + val suggestedNames = if (componentFunctions.isNotEmpty()) { + val collectingValidator = CollectingNameValidator(filter = validator) + componentFunctions.map { suggestNamesForComponent(it, project, collectingValidator) } + } + else { + KotlinNameSuggester.suggestNamesByExpressionAndType(expression, + substringInfo?.type, + bindingContext, + validator, + "value").singletonList() + } val introduceVariableContext = IntroduceVariableContext( - expression, suggestedNames.iterator().next(), allReplaces, commonContainer, commonParent, - replaceOccurrence, noTypeInference, expressionType, bindingContext, resolutionFacade + expression, suggestedNames, allReplaces, commonContainer, commonParent, + replaceOccurrence, noTypeInference, expressionType, componentFunctions, bindingContext, resolutionFacade ) project.executeCommand(INTRODUCE_VARIABLE, null) { runWriteAction { introduceVariableContext.runRefactoring() } @@ -471,18 +556,29 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { 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() + + when (property) { + is KtProperty -> { + KotlinVariableInplaceIntroducer( + property, + introduceVariableContext.reference, + introduceVariableContext.references.toTypedArray(), + suggestedNames.single(), + /*todo*/ false, + /*todo*/ false, + expressionType, + noTypeInference, + project, + editor + ).startInplaceIntroduceTemplate() + } + + is KtMultiDeclaration -> { + executeMultiDeclarationTemplate(project, editor, property, suggestedNames) + } + + else -> throw AssertionError("Unexpected declaration: ${property.getElementTextWithContext()}") + } } } @@ -568,7 +664,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { editor: Editor?, expressionToExtract: KtExpression?, occurrencesToReplace: List?, - onNonInteractiveFinish: ((KtProperty) -> Unit)? + onNonInteractiveFinish: ((KtDeclaration) -> Unit)? ) { val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) } ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/introduceVariableUtils.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/introduceVariableUtils.kt new file mode 100644 index 00000000000..ac6a2c26683 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/introduceVariableUtils.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable + +import org.jetbrains.kotlin.descriptors.FunctionDescriptor +import org.jetbrains.kotlin.idea.analysis.analyzeInContext +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.psi.KtExpression +import org.jetbrains.kotlin.psi.KtPsiFactory +import org.jetbrains.kotlin.psi.createExpressionByPattern +import org.jetbrains.kotlin.resolve.calls.callUtil.getResolvedCall +import org.jetbrains.kotlin.util.isValidOperator + +fun getApplicableComponentFunctions(expression: KtExpression): List { + val facade = expression.getResolutionFacade() + val scope = expression.getResolutionScope(facade.analyze(expression), facade) + + val psiFactory = KtPsiFactory(expression) + @Suppress("UNCHECKED_CAST") + return sequence(1) { it + 1 } + .map { + val componentCallExpr = psiFactory.createExpressionByPattern("$0.$1", expression, "component$it()") + val newContext = componentCallExpr.analyzeInContext(scope, expression) + componentCallExpr.getResolvedCall(newContext)?.resultingDescriptor as? FunctionDescriptor + } + .takeWhile { it != null && it.isValidOperator() } + .toList() as List +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt b/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt new file mode 100644 index 00000000000..611b19fc904 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt @@ -0,0 +1,8 @@ +class A + +fun A.component1() = 1 +fun A.component2() = 2 + +fun test() { + A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt.after b/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt.after new file mode 100644 index 00000000000..63325aa7f25 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt.after @@ -0,0 +1,8 @@ +class A + +fun A.component1() = 1 +fun A.component2() = 2 + +fun test() { + val a = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt b/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt new file mode 100644 index 00000000000..267a1c73168 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt @@ -0,0 +1,7 @@ +class A + +operator fun A.component1() = 1 + +fun test() { + A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt.after b/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt.after new file mode 100644 index 00000000000..17c817f58c7 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt.after @@ -0,0 +1,7 @@ +class A + +operator fun A.component1() = 1 + +fun test() { + val a = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt b/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt new file mode 100644 index 00000000000..8f814de0889 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt @@ -0,0 +1,12 @@ +class A + +operator fun A.component1() = 1 +operator fun A.component2() = 2 + +class B { + operator fun A.component3() = 3 +} + +fun test() { + A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt.after b/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt.after new file mode 100644 index 00000000000..ab1990e5815 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt.after @@ -0,0 +1,12 @@ +class A + +operator fun A.component1() = 1 +operator fun A.component2() = 2 + +class B { + operator fun A.component3() = 3 +} + +fun test() { + val (i, i1) = A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt b/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt new file mode 100644 index 00000000000..b43b2751f1e --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + 1 to 2 +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt.after b/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt.after new file mode 100644 index 00000000000..c351e88bd39 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt.after @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + val (i, i1) = 1 to 2 +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt b/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt new file mode 100644 index 00000000000..34acc5a6e80 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt @@ -0,0 +1,4 @@ +// WITH_RUNTIME +fun test() { + (1 to 2).first +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt.after b/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt.after new file mode 100644 index 00000000000..e31180605c1 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt.after @@ -0,0 +1,5 @@ +// WITH_RUNTIME +fun test() { + val pair = 1 to 2 + pair.first +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java index 98450553af3..28b1c460090 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java @@ -508,6 +508,45 @@ public class ExtractionTestGenerated extends AbstractExtractionTest { } } + @TestMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class MultiDeclarations extends AbstractExtractionTest { + public void testAllFilesPresentInMultiDeclarations() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/introduceVariable/multiDeclarations"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("notOperators.kt") + public void testNotOperators() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("singleComponent.kt") + public void testSingleComponent() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/singleComponent.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("unresolvedComponent3.kt") + public void testUnresolvedComponent3() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/unresolvedComponent3.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("unusedExpr.kt") + public void testUnusedExpr() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/unusedExpr.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("usedExpr.kt") + public void testUsedExpr() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/usedExpr.kt"); + doIntroduceVariableTest(fileName); + } + } + @TestMetadata("idea/testData/refactoring/introduceVariable/stringTemplates") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class)