Introduce Variable: Support destructuring declarations

#KT-6262
This commit is contained in:
Alexey Sedunov
2015-11-24 17:06:09 +03:00
parent 09f8aafb77
commit 7a40239e64
14 changed files with 297 additions and 44 deletions
@@ -199,6 +199,10 @@ public class KtPsiFactory(private val project: Project) {
return getter return getter
} }
public fun createMultiDeclaration(text: String): KtMultiDeclaration {
return (createFunction("fun foo() {$text}").bodyExpression as KtBlockExpression).statements.first() as KtMultiDeclaration
}
public fun <TDeclaration : KtDeclaration> createDeclaration(text: String): TDeclaration { public fun <TDeclaration : KtDeclaration> createDeclaration(text: String): TDeclaration {
val file = createFile(text) val file = createFile(text)
val declarations = file.getDeclarations() val declarations = file.getDeclarations()
@@ -16,30 +16,31 @@
package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable 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.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager 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.editor.Editor
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.*
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.HelpID import com.intellij.refactoring.HelpID
import com.intellij.refactoring.introduce.inplace.OccurrencesChooser import com.intellij.refactoring.introduce.inplace.OccurrencesChooser
import com.intellij.refactoring.util.CommonRefactoringUtil import com.intellij.refactoring.util.CommonRefactoringUtil
import com.intellij.util.SmartList import com.intellij.util.SmartList
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor import org.jetbrains.kotlin.descriptors.ValueParameterDescriptor
import org.jetbrains.kotlin.idea.analysis.analyzeInContext import org.jetbrains.kotlin.idea.analysis.analyzeInContext
import org.jetbrains.kotlin.idea.analysis.computeTypeInfoInContext import org.jetbrains.kotlin.idea.analysis.computeTypeInfoInContext
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.core.NewDeclarationNameValidator import org.jetbrains.kotlin.idea.core.*
import org.jetbrains.kotlin.idea.core.compareDescriptors
import org.jetbrains.kotlin.idea.core.moveInsideParenthesesAndReplaceWith
import org.jetbrains.kotlin.idea.core.refactoring.Pass import org.jetbrains.kotlin.idea.core.refactoring.Pass
import org.jetbrains.kotlin.idea.core.refactoring.chooseContainerElementIfNecessary import org.jetbrains.kotlin.idea.core.refactoring.chooseContainerElementIfNecessary
import org.jetbrains.kotlin.idea.core.refactoring.removeTemplateEntryBracesIfPossible 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.IdeDescriptorRenderers
import org.jetbrains.kotlin.idea.util.ShortenReferences import org.jetbrains.kotlin.idea.util.ShortenReferences
import org.jetbrains.kotlin.idea.util.application.executeCommand 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.application.runWriteAction
import org.jetbrains.kotlin.idea.util.getResolutionScope import org.jetbrains.kotlin.idea.util.getResolutionScope
import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiUnifier 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.KotlinType
import org.jetbrains.kotlin.types.checker.KotlinTypeChecker import org.jetbrains.kotlin.types.checker.KotlinTypeChecker
import org.jetbrains.kotlin.utils.addIfNotNull import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.kotlin.utils.addToStdlib.singletonList
import org.jetbrains.kotlin.utils.ifEmpty import org.jetbrains.kotlin.utils.ifEmpty
import org.jetbrains.kotlin.utils.sure import org.jetbrains.kotlin.utils.sure
import java.util.* import java.util.*
@@ -83,19 +86,20 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
private class IntroduceVariableContext( private class IntroduceVariableContext(
private val expression: KtExpression, private val expression: KtExpression,
private val nameSuggestion: String, private val nameSuggestions: List<Collection<String>>,
private val allReplaces: List<KtExpression>, private val allReplaces: List<KtExpression>,
private val commonContainer: PsiElement, private val commonContainer: PsiElement,
private val commonParent: PsiElement, private val commonParent: PsiElement,
private val replaceOccurrence: Boolean, private val replaceOccurrence: Boolean,
private val noTypeInference: Boolean, private val noTypeInference: Boolean,
private val expressionType: KotlinType?, private val expressionType: KotlinType?,
private val componentFunctions: List<FunctionDescriptor>,
private val bindingContext: BindingContext, private val bindingContext: BindingContext,
private val resolutionFacade: ResolutionFacade private val resolutionFacade: ResolutionFacade
) { ) {
private val psiFactory = KtPsiFactory(expression) private val psiFactory = KtPsiFactory(expression)
var propertyRef: KtProperty? = null var propertyRef: KtDeclaration? = null
var reference: KtExpression? = null var reference: KtExpression? = null
val references = ArrayList<KtExpression>() val references = ArrayList<KtExpression>()
@@ -106,7 +110,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
private fun replaceExpression(expressionToReplace: KtExpression, addToReferences: Boolean): KtExpression { private fun replaceExpression(expressionToReplace: KtExpression, addToReferences: Boolean): KtExpression {
val isActualExpression = expression == expressionToReplace val isActualExpression = expression == expressionToReplace
val replacement = psiFactory.createExpression(nameSuggestion) val replacement = psiFactory.createExpression(nameSuggestions.single().first())
val substringInfo = expressionToReplace.extractableSubstringInfo val substringInfo = expressionToReplace.extractableSubstringInfo
var result = when { var result = when {
expressionToReplace.isFunctionLiteralOutsideParentheses() -> { expressionToReplace.isFunctionLiteralOutsideParentheses() -> {
@@ -133,22 +137,31 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
commonParent: PsiElement, commonParent: PsiElement,
allReplaces: List<KtExpression> allReplaces: List<KtExpression>
) { ) {
val variableText = StringBuilder("val ").apply { val initializer = (expression as? KtParenthesizedExpression)?.expression ?: expression
append(nameSuggestion) var property: KtDeclaration = if (componentFunctions.isNotEmpty()) {
if (noTypeInference) { buildString {
val typeToRender = expressionType ?: resolutionFacade.moduleDescriptor.builtIns.anyType componentFunctions.indices.joinTo(this, prefix = "val (", postfix = ")") { nameSuggestions[it].first() }
append(": ").append(IdeDescriptorRenderers.SOURCE_CODE.renderType(typeToRender)) append(" = ")
} append(initializer.text)
append(" = ") }.let { psiFactory.createMultiDeclaration(it) }
}
append(((expression as? KtParenthesizedExpression)?.expression ?: expression).text) else {
}.toString() buildString {
var property = psiFactory.createProperty(variableText) 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 var anchor = calculateAnchor(commonParent, commonContainer, allReplaces) ?: return
val needBraces = commonContainer !is KtBlockExpression val needBraces = commonContainer !is KtBlockExpression
if (!needBraces) { if (!needBraces) {
property = commonContainer.addBefore(property, anchor) as KtProperty property = commonContainer.addBefore(property, anchor) as KtDeclaration
commonContainer.addBefore(psiFactory.createNewLine(), anchor) commonContainer.addBefore(psiFactory.createNewLine(), anchor)
} }
else { else {
@@ -365,6 +378,70 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE) CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE)
} }
private fun KtExpression.chooseApplicableComponentFunctions(haveOccurrencesToReplace: Boolean): List<FunctionDescriptor>? {
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<Collection<String>>) {
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<String> {
return LinkedHashSet<String>().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( fun doRefactoring(
project: Project, project: Project,
editor: Editor?, editor: Editor?,
@@ -374,7 +451,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
resolutionFacade: ResolutionFacade, resolutionFacade: ResolutionFacade,
bindingContext: BindingContext, bindingContext: BindingContext,
occurrencesToReplace: List<KtExpression>?, occurrencesToReplace: List<KtExpression>?,
onNonInteractiveFinish: ((KtProperty) -> Unit)? onNonInteractiveFinish: ((KtDeclaration) -> Unit)?
) { ) {
val substringInfo = expression.extractableSubstringInfo val substringInfo = expression.extractableSubstringInfo
val physicalExpression = expression.substringContextOrThis val physicalExpression = expression.substringContextOrThis
@@ -440,19 +517,27 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
commonContainer = container commonContainer = container
} }
val componentFunctions = physicalExpression.chooseApplicableComponentFunctions(replaceOccurrence) ?: return@Pass
val validator = NewDeclarationNameValidator( val validator = NewDeclarationNameValidator(
commonContainer, commonContainer,
calculateAnchor(commonParent, commonContainer, allReplaces), calculateAnchor(commonParent, commonContainer, allReplaces),
NewDeclarationNameValidator.Target.VARIABLES NewDeclarationNameValidator.Target.VARIABLES
) )
val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression, val suggestedNames = if (componentFunctions.isNotEmpty()) {
substringInfo?.type, val collectingValidator = CollectingNameValidator(filter = validator)
bindingContext, componentFunctions.map { suggestNamesForComponent(it, project, collectingValidator) }
validator, }
"value") else {
KotlinNameSuggester.suggestNamesByExpressionAndType(expression,
substringInfo?.type,
bindingContext,
validator,
"value").singletonList()
}
val introduceVariableContext = IntroduceVariableContext( val introduceVariableContext = IntroduceVariableContext(
expression, suggestedNames.iterator().next(), allReplaces, commonContainer, commonParent, expression, suggestedNames, allReplaces, commonContainer, commonParent,
replaceOccurrence, noTypeInference, expressionType, bindingContext, resolutionFacade replaceOccurrence, noTypeInference, expressionType, componentFunctions, bindingContext, resolutionFacade
) )
project.executeCommand(INTRODUCE_VARIABLE, null) { project.executeCommand(INTRODUCE_VARIABLE, null) {
runWriteAction { introduceVariableContext.runRefactoring() } runWriteAction { introduceVariableContext.runRefactoring() }
@@ -471,18 +556,29 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
PsiDocumentManager.getInstance(project).commitDocument(editor.document) PsiDocumentManager.getInstance(project).commitDocument(editor.document)
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document) PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
KotlinVariableInplaceIntroducer(
property, when (property) {
introduceVariableContext.reference, is KtProperty -> {
introduceVariableContext.references.toTypedArray(), KotlinVariableInplaceIntroducer(
suggestedNames, property,
/*todo*/ false, introduceVariableContext.reference,
/*todo*/ false, introduceVariableContext.references.toTypedArray(),
expressionType, suggestedNames.single(),
noTypeInference, /*todo*/ false,
project, /*todo*/ false,
editor expressionType,
).startInplaceIntroduceTemplate() 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?, editor: Editor?,
expressionToExtract: KtExpression?, expressionToExtract: KtExpression?,
occurrencesToReplace: List<KtExpression>?, occurrencesToReplace: List<KtExpression>?,
onNonInteractiveFinish: ((KtProperty) -> Unit)? onNonInteractiveFinish: ((KtDeclaration) -> Unit)?
) { ) {
val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) } val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) }
?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression"))
@@ -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<FunctionDescriptor> {
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<FunctionDescriptor>
}
@@ -0,0 +1,8 @@
class A
fun A.component1() = 1
fun A.component2() = 2
fun test() {
<selection>A()</selection>
}
@@ -0,0 +1,8 @@
class A
fun A.component1() = 1
fun A.component2() = 2
fun test() {
val a = A()
}
@@ -0,0 +1,7 @@
class A
operator fun A.component1() = 1
fun test() {
<selection>A()</selection>
}
@@ -0,0 +1,7 @@
class A
operator fun A.component1() = 1
fun test() {
val a = A()
}
@@ -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() {
<selection>A()</selection>
}
@@ -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()
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test() {
<selection>1 to 2</selection>
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test() {
val (i, i1) = 1 to 2
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun test() {
(<selection>1 to 2</selection>).first
}
@@ -0,0 +1,5 @@
// WITH_RUNTIME
fun test() {
val pair = 1 to 2
pair.first
}
@@ -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") @TestMetadata("idea/testData/refactoring/introduceVariable/stringTemplates")
@TestDataPath("$PROJECT_ROOT") @TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class) @RunWith(JUnit3RunnerWithInners.class)