Iterate Intention: Add support of destructuring declarations

This commit is contained in:
Alexey Sedunov
2015-12-17 21:30:14 +03:00
parent 1d3054e7a6
commit ae636a0d32
10 changed files with 158 additions and 56 deletions
@@ -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 <TDeclaration : KtDeclaration> createDeclaration(text: String): TDeclaration {
val file = createFile(text)
val declarations = file.getDeclarations()
@@ -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>(KtExpression::class.java, "Iterate over collection"), HighPriorityAction {
override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean {
@@ -54,26 +60,46 @@ public class IterateExpressionIntention : SelfTargetingIntention<KtExpression>(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)
}
}
}
}
@@ -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<FunctionDescriptor>) -> 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<String> {
return LinkedHashSet<String>().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),
@@ -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<FunctionDescriptor> {
val facade = expression.getResolutionFacade()
val context = facade.analyze(expression)
private fun getApplicableComponentFunctions(
contextExpression: KtExpression,
receiverType: KotlinType?,
receiverExpression: KtExpression?
): List<FunctionDescriptor> {
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<FunctionDescriptor>
}
internal fun chooseApplicableComponentFunctions(
contextExpression: KtExpression,
editor: Editor?,
type: KotlinType? = null,
receiverExpression: KtExpression? = null,
callback: (List<FunctionDescriptor>) -> 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<String> {
return LinkedHashSet<String>().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))
}
}
@@ -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"))<caret>
}
@@ -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"))) {
<caret>
}
}
@@ -0,0 +1,6 @@
// WITH_RUNTIME
data class Foo(val id: Int, val name: String)
fun test() {
listOf(Foo(123, "def"), Foo(456, "abc"))<caret>
}
@@ -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"))) {
<caret>
}
}
@@ -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")) {
<caret>
}
}
@@ -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");