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
}
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 {
val file = createFile(text)
val declarations = file.getDeclarations()
@@ -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<Collection<String>>,
private val allReplaces: List<KtExpression>,
private val commonContainer: PsiElement,
private val commonParent: PsiElement,
private val replaceOccurrence: Boolean,
private val noTypeInference: Boolean,
private val expressionType: KotlinType?,
private val componentFunctions: List<FunctionDescriptor>,
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<KtExpression>()
@@ -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<KtExpression>
) {
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<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(
project: Project,
editor: Editor?,
@@ -374,7 +451,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() {
resolutionFacade: ResolutionFacade,
bindingContext: BindingContext,
occurrencesToReplace: List<KtExpression>?,
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<KtExpression>?,
onNonInteractiveFinish: ((KtProperty) -> Unit)?
onNonInteractiveFinish: ((KtDeclaration) -> Unit)?
) {
val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) }
?: 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")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)