diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt index 2fd7fbac14b..c92d8c70f2a 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt @@ -216,6 +216,11 @@ public class KtPsiFactory(private val project: Project) { return (createFunction("fun foo() {$text}").bodyExpression as KtBlockExpression).statements.first() as KtDestructuringDeclaration } + public fun createDestructuringParameter(text: String): KtDestructuringDeclaration { + val dummyFun = createFunction("fun foo() { for ($text in foo) {} }") + return ((dummyFun.bodyExpression as KtBlockExpression).statements.first() as KtForExpression).destructuringParameter!! + } + public fun createDeclaration(text: String): TDeclaration { val file = createFile(text) val declarations = file.getDeclarations() diff --git a/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt b/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt index c40fda801db..5c8c9c200cf 100644 --- a/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt +++ b/idea/src/org/jetbrains/kotlin/idea/intentions/IterateExpressionIntention.kt @@ -24,13 +24,19 @@ import com.intellij.psi.PsiDocumentManager import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.core.* +import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.chooseApplicableComponentFunctions +import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.suggestNamesForComponent import org.jetbrains.kotlin.idea.resolve.ideService import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +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.psi.* import org.jetbrains.kotlin.psi.psiUtil.siblings import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.utils.addToStdlib.singletonList public class IterateExpressionIntention : SelfTargetingIntention(KtExpression::class.java, "Iterate over collection"), HighPriorityAction { override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean { @@ -54,26 +60,46 @@ public class IterateExpressionIntention : SelfTargetingIntention(K return Data(type, elementType) } - override fun applyTo(element: KtExpression, editor: Editor) { - //TODO: multi-declaration (when?) + override fun startInWriteAction() = false + override fun applyTo(element: KtExpression, editor: Editor) { val elementType = data(element)!!.elementType val nameValidator = NewDeclarationNameValidator(element, element.siblings(), NewDeclarationNameValidator.Target.VARIABLES) val bindingContext = element.analyze(BodyResolveMode.PARTIAL) - val names = KotlinNameSuggester.suggestIterationVariableNames(element, elementType, bindingContext, nameValidator, "e") - var forExpression = KtPsiFactory(element).createExpressionByPattern("for($0 in $1) {\nx\n}", names.first(), element) as KtForExpression - forExpression = element.replaced(forExpression) + val project = element.project + val psiFactory = KtPsiFactory(project) - PsiDocumentManager.getInstance(forExpression.project).doPostponedOperationsAndUnblockDocument(editor.document) + val receiverExpression = psiFactory.createExpressionByPattern("$0.iterator().next()", element) + chooseApplicableComponentFunctions(element, editor, elementType, receiverExpression) { componentFunctions -> + project.executeWriteCommand(text) { + val names = if (componentFunctions.isNotEmpty()) { + val collectingValidator = CollectingNameValidator(filter = nameValidator) + componentFunctions.map { suggestNamesForComponent(it, project, collectingValidator) } + } + else { + KotlinNameSuggester.suggestIterationVariableNames(element, elementType, bindingContext, nameValidator, "e").singletonList() + } - val bodyPlaceholder = (forExpression.body as KtBlockExpression).statements.single() + val paramPattern = (names.singleOrNull()?.first() + ?: psiFactory.createDestructuringParameter(names.indices.joinToString(prefix = "(", postfix = ")") { "p$it" })) + var forExpression = psiFactory.createExpressionByPattern("for($0 in $1) {\nx\n}", paramPattern, element) as KtForExpression + forExpression = element.replaced(forExpression) - val templateBuilder = TemplateBuilderImpl(forExpression) - templateBuilder.replaceElement(forExpression.loopParameter!!, ChooseStringExpression(names)) - templateBuilder.replaceElement(bodyPlaceholder, ConstantNode(""), false) - templateBuilder.setEndVariableAfter(bodyPlaceholder) + PsiDocumentManager.getInstance(forExpression.project).doPostponedOperationsAndUnblockDocument(editor.document) - templateBuilder.run(editor, true) + val bodyPlaceholder = (forExpression.body as KtBlockExpression).statements.single() + val parameters = forExpression.loopParameter?.singletonList() ?: forExpression.destructuringParameter!!.entries + + val templateBuilder = TemplateBuilderImpl(forExpression) + for ((parameter, parameterNames) in (parameters zip names)) { + templateBuilder.replaceElement(parameter, ChooseStringExpression(parameterNames)) + } + templateBuilder.replaceElement(bodyPlaceholder, ConstantNode(""), false) + templateBuilder.setEndVariableAfter(bodyPlaceholder) + + templateBuilder.run(editor, true) + } + } } } \ No newline at end of file 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 b32b89336e7..e80092b9cab 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 @@ -24,16 +24,17 @@ 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.popup.JBPopupFactory import com.intellij.openapi.util.Key import com.intellij.openapi.util.TextRange -import com.intellij.psi.* +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.RefactoringActionHandler 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 @@ -41,7 +42,6 @@ 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.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.core.* import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention import org.jetbrains.kotlin.idea.refactoring.* @@ -379,29 +379,13 @@ object KotlinIntroduceVariableHandler : RefactoringActionHandler { CommonRefactoringUtil.showErrorHint(project, editor, message, INTRODUCE_VARIABLE, HelpID.INTRODUCE_VARIABLE) } - private fun KtExpression.chooseApplicableComponentFunctions( + private fun KtExpression.chooseApplicableComponentFunctionsForVariableDeclaration( haveOccurrencesToReplace: Boolean, editor: Editor?, callback: (List) -> Unit ) { if (haveOccurrencesToReplace) return callback(emptyList()) - - val functions = getApplicableComponentFunctions(this) - if (functions.size <= 1) return callback(emptyList()) - - 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) + return chooseApplicableComponentFunctions(this, editor, callback = callback) } private fun executeMultiDeclarationTemplate( @@ -454,18 +438,6 @@ object KotlinIntroduceVariableHandler : RefactoringActionHandler { } } - private fun suggestNamesForComponent(descriptor: FunctionDescriptor, project: Project, validator: (String) -> Boolean): Set { - return LinkedHashSet().apply { - val descriptorName = descriptor.name.asString() - val componentName = (DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor) as? PsiNamedElement)?.name - ?: descriptorName - if (componentName == descriptorName) { - descriptor.returnType?.let { addAll(KotlinNameSuggester.suggestNamesByType(it, validator)) } - } - add(KotlinNameSuggester.suggestNameByName(componentName, validator)) - } - } - fun doRefactoring( project: Project, editor: Editor?, @@ -553,7 +525,7 @@ object KotlinIntroduceVariableHandler : RefactoringActionHandler { commonContainer = container } - physicalExpression.chooseApplicableComponentFunctions(replaceOccurrence, editor) { componentFunctions -> + physicalExpression.chooseApplicableComponentFunctionsForVariableDeclaration(replaceOccurrence, editor) { componentFunctions -> val validator = NewDeclarationNameValidator( commonContainer, calculateAnchor(commonParent, commonContainer, allReplaces), 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 index d2a31d880e7..fca0f32d2af 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/introduceVariableUtils.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/introduceVariableUtils.kt @@ -16,10 +16,18 @@ package org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.psi.PsiNamedElement +import com.intellij.ui.components.JBList 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.codeInsight.DescriptorToSourceUtilsIde +import org.jetbrains.kotlin.idea.core.KotlinNameSuggester import org.jetbrains.kotlin.idea.imports.importableFqName import org.jetbrains.kotlin.idea.util.getResolutionScope import org.jetbrains.kotlin.psi.KtExpression @@ -27,35 +35,78 @@ 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.KotlinType import org.jetbrains.kotlin.types.typeUtil.supertypes import org.jetbrains.kotlin.util.isValidOperator import org.jetbrains.kotlin.utils.addToStdlib.singletonList +import java.util.* -fun getApplicableComponentFunctions(expression: KtExpression): List { - val facade = expression.getResolutionFacade() - val context = facade.analyze(expression) +private fun getApplicableComponentFunctions( + contextExpression: KtExpression, + receiverType: KotlinType?, + receiverExpression: KtExpression? +): List { + val facade = contextExpression.getResolutionFacade() + val context = facade.analyze(contextExpression) val builtIns = facade.moduleDescriptor.builtIns val forbiddenClasses = arrayListOf(builtIns.collection, builtIns.array) PrimitiveType.values().mapTo(forbiddenClasses) { builtIns.getPrimitiveArrayClassDescriptor(it) } - context.getType(expression)?.let { + (receiverType ?: context.getType(contextExpression))?.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 scope = contextExpression.getResolutionScope(context, facade) - val psiFactory = KtPsiFactory(expression) + val psiFactory = KtPsiFactory(contextExpression) @Suppress("UNCHECKED_CAST") return sequence(1) { it + 1 } .map { - val componentCallExpr = psiFactory.createExpressionByPattern("$0.$1", expression, "component$it()") - val newContext = componentCallExpr.analyzeInContext(scope, expression) + val componentCallExpr = psiFactory.createExpressionByPattern("$0.$1", receiverExpression ?: contextExpression, "component$it()") + val newContext = componentCallExpr.analyzeInContext(scope, contextExpression) componentCallExpr.getResolvedCall(newContext)?.resultingDescriptor as? FunctionDescriptor } .takeWhile { it != null && it.isValidOperator() } .toList() as List +} + +internal fun chooseApplicableComponentFunctions( + contextExpression: KtExpression, + editor: Editor?, + type: KotlinType? = null, + receiverExpression: KtExpression? = null, + callback: (List) -> Unit +) { + val functions = getApplicableComponentFunctions(contextExpression, type, receiverExpression) + if (functions.size <= 1) return callback(emptyList()) + + 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) +} + +internal fun suggestNamesForComponent(descriptor: FunctionDescriptor, project: Project, validator: (String) -> Boolean): Set { + return LinkedHashSet().apply { + val descriptorName = descriptor.name.asString() + val componentName = (DescriptorToSourceUtilsIde.getAnyDeclaration(project, descriptor) as? PsiNamedElement)?.name + ?: descriptorName + if (componentName == descriptorName) { + descriptor.returnType?.let { addAll(KotlinNameSuggester.suggestNamesByType(it, validator)) } + } + add(KotlinNameSuggester.suggestNameByName(componentName, validator)) + } } \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt b/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt new file mode 100644 index 00000000000..bada0019844 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt @@ -0,0 +1,10 @@ +// WITH_RUNTIME +class Foo(val id: Int, val name: String) + +operator fun Foo.component1() = id +operator fun Foo.component2() = name +operator fun Foo.component3() = "$name: $id" + +fun test() { + listOf(Foo(123, "def"), Foo(456, "abc")) +} \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt.after b/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt.after new file mode 100644 index 00000000000..50d4e72eb19 --- /dev/null +++ b/idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt.after @@ -0,0 +1,12 @@ +// WITH_RUNTIME +class Foo(val id: Int, val name: String) + +operator fun Foo.component1() = id +operator fun Foo.component2() = name +operator fun Foo.component3() = "$name: $id" + +fun test() { + for ((i, s, s1) in listOf(Foo(123, "def"), Foo(456, "abc"))) { + + } +} \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt b/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt new file mode 100644 index 00000000000..5f25379d02e --- /dev/null +++ b/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt @@ -0,0 +1,6 @@ +// WITH_RUNTIME +data class Foo(val id: Int, val name: String) + +fun test() { + listOf(Foo(123, "def"), Foo(456, "abc")) +} \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt.after b/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt.after new file mode 100644 index 00000000000..a942372f2cc --- /dev/null +++ b/idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt.after @@ -0,0 +1,8 @@ +// WITH_RUNTIME +data class Foo(val id: Int, val name: String) + +fun test() { + for ((id, name) in listOf(Foo(123, "def"), Foo(456, "abc"))) { + + } +} \ No newline at end of file diff --git a/idea/testData/intentions/iterateExpression/mapIterator.kt.after b/idea/testData/intentions/iterateExpression/mapIterator.kt.after index a200ba53ff1..e91fccff3f2 100644 --- a/idea/testData/intentions/iterateExpression/mapIterator.kt.after +++ b/idea/testData/intentions/iterateExpression/mapIterator.kt.after @@ -1,6 +1,6 @@ // WITH_RUNTIME fun test() { - for (entry in mapOf(1 to "1", 2 to "2")) { + for ((i, s) in mapOf(1 to "1", 2 to "2")) { } } \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java index ba75bec9888..576ab911963 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java @@ -5817,6 +5817,18 @@ public class IntentionTestGenerated extends AbstractIntentionTest { KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/iterateExpression"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true); } + @TestMetadata("collectionIteratorWithComponents.kt") + public void testCollectionIteratorWithComponents() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterateExpression/collectionIteratorWithComponents.kt"); + doTest(fileName); + } + + @TestMetadata("dataClassCollectionIterator.kt") + public void testDataClassCollectionIterator() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterateExpression/dataClassCollectionIterator.kt"); + doTest(fileName); + } + @TestMetadata("extensionIterator.kt") public void testExtensionIterator() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/iterateExpression/extensionIterator.kt");