Iterate Intention: Add support of destructuring declarations
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-35
@@ -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),
|
||||
|
||||
+59
-8
@@ -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))
|
||||
}
|
||||
}
|
||||
+10
@@ -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>
|
||||
}
|
||||
+12
@@ -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>
|
||||
}
|
||||
+8
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user