Introduce Variable: Suggest choosing between single variable and

multi-declaration. Forbid multi-declaration for collections and arrays
 #KT-10242 Fixed
This commit is contained in:
Alexey Sedunov
2015-12-01 19:22:31 +03:00
parent c1c6d0ee3a
commit bcadfd4661
9 changed files with 138 additions and 61 deletions
@@ -24,7 +24,7 @@ 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.ui.popup.JBPopupFactory
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.TextRange
import com.intellij.psi.*
@@ -32,6 +32,7 @@ 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.ui.components.JBList
import com.intellij.util.SmartList
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
@@ -378,13 +379,29 @@ 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()
private fun KtExpression.chooseApplicableComponentFunctions(
haveOccurrencesToReplace: Boolean,
editor: Editor?,
callback: (List<FunctionDescriptor>) -> Unit
) {
if (haveOccurrencesToReplace) return callback(emptyList())
val functions = getApplicableComponentFunctions(this)
if (functions.size <= 1) return emptyList()
if (functions.size <= 1) return callback(emptyList())
return functions
if (ApplicationManager.getApplication().isUnitTestMode) return callback(functions)
if (editor == null) return callback(emptyList())
val list = JBList("Create single variable", "Create destructuring declaration")
JBPopupFactory.getInstance()
.createListPopupBuilder(list)
.setMovable(true)
.setResizable(false)
.setRequestFocus(true)
.setItemChoosenCallback { callback(if (list.selectedIndex == 0) emptyList() else functions) }
.createPopup()
.showInBestPositionFor(editor)
}
private fun executeMultiDeclarationTemplate(
@@ -517,67 +534,67 @@ 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 = 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, allReplaces, commonContainer, commonParent,
replaceOccurrence, noTypeInference, expressionType, componentFunctions, bindingContext, resolutionFacade
)
project.executeCommand(INTRODUCE_VARIABLE, null) {
runWriteAction { introduceVariableContext.runRefactoring() }
val property = introduceVariableContext.propertyRef ?: return@executeCommand
if (editor == null) {
onNonInteractiveFinish?.invoke(property)
return@executeCommand
physicalExpression.chooseApplicableComponentFunctions(replaceOccurrence, editor) { componentFunctions ->
val validator = NewDeclarationNameValidator(
commonContainer,
calculateAnchor(commonParent, commonContainer, allReplaces),
NewDeclarationNameValidator.Target.VARIABLES
)
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, allReplaces, commonContainer, commonParent,
replaceOccurrence, noTypeInference, expressionType, componentFunctions, bindingContext, resolutionFacade
)
project.executeCommand(INTRODUCE_VARIABLE, null) {
runWriteAction { introduceVariableContext.runRefactoring() }
editor.caretModel.moveToOffset(property.textOffset)
editor.selectionModel.removeSelection()
val property = introduceVariableContext.propertyRef ?: return@executeCommand
if (!isInplaceAvailable) return@executeCommand
PsiDocumentManager.getInstance(project).commitDocument(editor.document)
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
when (property) {
is KtProperty -> {
KotlinVariableInplaceIntroducer(
property,
introduceVariableContext.reference,
introduceVariableContext.references.toTypedArray(),
suggestedNames.single(),
/*todo*/ false,
/*todo*/ false,
expressionType,
noTypeInference,
project,
editor
).startInplaceIntroduceTemplate()
if (editor == null) {
onNonInteractiveFinish?.invoke(property)
return@executeCommand
}
is KtMultiDeclaration -> {
executeMultiDeclarationTemplate(project, editor, property, suggestedNames)
}
editor.caretModel.moveToOffset(property.textOffset)
editor.selectionModel.removeSelection()
else -> throw AssertionError("Unexpected declaration: ${property.getElementTextWithContext()}")
if (!isInplaceAvailable) return@executeCommand
PsiDocumentManager.getInstance(project).commitDocument(editor.document)
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(editor.document)
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()}")
}
}
}
}
@@ -16,19 +16,37 @@
package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable
import org.jetbrains.kotlin.builtins.PrimitiveType
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.imports.importableFqName
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.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.types.typeUtil.supertypes
import org.jetbrains.kotlin.util.isValidOperator
import org.jetbrains.kotlin.utils.addToStdlib.singletonList
fun getApplicableComponentFunctions(expression: KtExpression): List<FunctionDescriptor> {
val facade = expression.getResolutionFacade()
val scope = expression.getResolutionScope(facade.analyze(expression), facade)
val context = facade.analyze(expression)
val builtIns = facade.moduleDescriptor.builtIns
val forbiddenClasses = arrayListOf(builtIns.collection, builtIns.array)
PrimitiveType.values().mapTo(forbiddenClasses) { builtIns.getPrimitiveArrayClassDescriptor(it) }
context.getType(expression)?.let {
if ((it.singletonList() + it.supertypes()).any {
val fqName = it.constructor.declarationDescriptor?.importableFqName
forbiddenClasses.any { it.fqNameSafe == fqName }
}) return emptyList()
}
val scope = expression.getResolutionScope(context, facade)
val psiFactory = KtPsiFactory(expression)
@Suppress("UNCHECKED_CAST")
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
<selection>arrayOf("1", "2", "3")</selection>
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
val arrayOf = arrayOf("1", "2", "3")
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
<selection>intArrayOf(1, 2, 3)</selection>
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
val intArrayOf = intArrayOf(1, 2, 3)
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
<selection>listOf(1, 2, 3)</selection>
}
@@ -0,0 +1,4 @@
// WITH_RUNTIME
fun foo() {
val listOf = listOf(1, 2, 3)
}
@@ -516,6 +516,24 @@ public class ExtractionTestGenerated extends AbstractExtractionTest {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/introduceVariable/multiDeclarations"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("array.kt")
public void testArray() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/array.kt");
doIntroduceVariableTest(fileName);
}
@TestMetadata("intArray.kt")
public void testIntArray() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/intArray.kt");
doIntroduceVariableTest(fileName);
}
@TestMetadata("list.kt")
public void testList() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/list.kt");
doIntroduceVariableTest(fileName);
}
@TestMetadata("notOperators.kt")
public void testNotOperators() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/multiDeclarations/notOperators.kt");