Introduce Parameter: Duplicate search & replace

This commit is contained in:
Alexey Sedunov
2015-04-07 18:43:20 +03:00
parent 3bb86a63cc
commit 13776f5bbd
27 changed files with 281 additions and 34 deletions
@@ -36,6 +36,7 @@ import com.intellij.psi.PsiElement
import com.intellij.refactoring.rename.inplace.InplaceRefactoring import com.intellij.refactoring.rename.inplace.InplaceRefactoring
import com.intellij.ui.DottedBorder import com.intellij.ui.DottedBorder
import com.intellij.ui.JBColor import com.intellij.ui.JBColor
import com.intellij.ui.NonFocusableCheckBox
import org.jetbrains.kotlin.idea.JetFileType import org.jetbrains.kotlin.idea.JetFileType
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.refactoring.changeSignature.* import org.jetbrains.kotlin.idea.refactoring.changeSignature.*
@@ -49,11 +50,13 @@ import org.jetbrains.kotlin.psi.JetPsiFactory
import org.jetbrains.kotlin.psi.JetPsiUtil import org.jetbrains.kotlin.psi.JetPsiUtil
import org.jetbrains.kotlin.psi.psiUtil.getValueParameterList import org.jetbrains.kotlin.psi.psiUtil.getValueParameterList
import org.jetbrains.kotlin.psi.psiUtil.getValueParameters import org.jetbrains.kotlin.psi.psiUtil.getValueParameters
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.types.JetType import org.jetbrains.kotlin.types.JetType
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Color import java.awt.Color
import java.util.ArrayList import java.util.ArrayList
import java.util.Collections
import java.util.LinkedHashSet import java.util.LinkedHashSet
import javax.swing.BorderFactory import javax.swing.BorderFactory
import javax.swing.JPanel import javax.swing.JPanel
@@ -62,21 +65,21 @@ import javax.swing.border.LineBorder
import kotlin.properties.Delegates import kotlin.properties.Delegates
public class KotlinInplaceParameterIntroducer( public class KotlinInplaceParameterIntroducer(
val descriptor: IntroduceParameterDescriptor, val originalDescriptor: IntroduceParameterDescriptor,
editor: Editor, editor: Editor,
project: Project project: Project
): KotlinInplaceVariableIntroducer<JetParameter>( ): KotlinInplaceVariableIntroducer<JetParameter>(
descriptor.addedParameter, originalDescriptor.addedParameter,
editor, editor,
project, project,
INTRODUCE_PARAMETER, INTRODUCE_PARAMETER,
JetExpression.EMPTY_ARRAY, JetExpression.EMPTY_ARRAY,
null, null,
false, false,
descriptor.addedParameter, originalDescriptor.addedParameter,
false, false,
true, true,
descriptor.parameterType, originalDescriptor.parameterType,
false false
) { ) {
companion object { companion object {
@@ -112,6 +115,7 @@ public class KotlinInplaceParameterIntroducer(
} }
} }
private var descriptor = originalDescriptor
private var previewer: EditorEx? = null private var previewer: EditorEx? = null
private fun updatePreview(currentName: String?, currentType: String?) { private fun updatePreview(currentName: String?, currentType: String?) {
@@ -205,6 +209,29 @@ public class KotlinInplaceParameterIntroducer(
previewerPanel previewerPanel
} }
val occurrenceCount = descriptor.occurrencesToReplace.size()
if (occurrenceCount > 1) {
addPanelControl {
val replaceAllCheckBox = NonFocusableCheckBox("Replace all occurrences ($occurrenceCount)")
replaceAllCheckBox.setSelected(true)
replaceAllCheckBox.setMnemonic('R')
replaceAllCheckBox.addActionListener {
descriptor = descriptor.copy(
occurrencesToReplace = with(originalDescriptor) {
if (replaceAllCheckBox.isSelected()) {
occurrencesToReplace
}
else {
Collections.singletonList(originalOccurrence)
}
}
)
updatePreview(null, null)
}
replaceAllCheckBox
}
}
} }
private var myDocumentAdapter: DocumentAdapter? = null private var myDocumentAdapter: DocumentAdapter? = null
@@ -24,6 +24,7 @@ import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiReference
import com.intellij.psi.search.LocalSearchScope import com.intellij.psi.search.LocalSearchScope
import org.jetbrains.kotlin.builtins.KotlinBuiltIns import org.jetbrains.kotlin.builtins.KotlinBuiltIns
import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor
@@ -64,9 +65,13 @@ public data class IntroduceParameterDescriptor(
val callableDescriptor: FunctionDescriptor, val callableDescriptor: FunctionDescriptor,
val addedParameter: JetParameter, val addedParameter: JetParameter,
val parameterType: JetType, val parameterType: JetType,
val parametersToRemove: List<JetParameter> val parametersUsages: Map<JetParameter, List<PsiReference>>,
val occurrencesToReplace: List<JetExpression>
) { ) {
val originalOccurrence: JetExpression
get() = occurrencesToReplace.first { it.isAncestor(originalExpression) }
val valVar: JetValVar val valVar: JetValVar
val parametersToRemove: List<JetParameter>
init { init {
valVar = if (callable is JetClass) { valVar = if (callable is JetClass) {
@@ -82,9 +87,18 @@ public data class IntroduceParameterDescriptor(
false false
} }
} }
if (originalExpression.parents().any(modifierIsUnnecessary)) JetValVar.None else JetValVar.Val if (occurrencesToReplace.all { it.parents().any(modifierIsUnnecessary) }) JetValVar.None else JetValVar.Val
} }
else JetValVar.None else JetValVar.None
val occurrenceRanges = occurrencesToReplace.map { it.getTextRange() }
parametersToRemove = parametersUsages.entrySet()
.filter {
it.value.all { paramRef ->
occurrenceRanges.any { occurrenceRange -> occurrenceRange.contains(paramRef.getElement().getTextRange()) }
}
}
.map { it.key }
} }
} }
@@ -110,12 +124,15 @@ fun IntroduceParameterDescriptor.performRefactoring() {
override fun performSilently(affectedFunctions: Collection<PsiElement>): Boolean = true override fun performSilently(affectedFunctions: Collection<PsiElement>): Boolean = true
} }
if (runChangeSignature(callable.getProject(), callableDescriptor, config, callable.analyze(), callable, INTRODUCE_PARAMETER)) { if (runChangeSignature(callable.getProject(), callableDescriptor, config, callable.analyze(), callable, INTRODUCE_PARAMETER)) {
originalExpression.replace(JetPsiFactory(callable).createSimpleName(addedParameter.getName()!!)) val paramRef = JetPsiFactory(callable).createSimpleName(addedParameter.getName()!!)
occurrencesToReplace.forEach { it.replace(paramRef) }
} }
} }
} }
public class KotlinIntroduceParameterHandler: KotlinIntroduceHandlerBase() { public open class KotlinIntroduceParameterHandler: KotlinIntroduceHandlerBase() {
open fun configure(descriptor: IntroduceParameterDescriptor): IntroduceParameterDescriptor = descriptor
fun invoke(project: Project, editor: Editor, expression: JetExpression, targetParent: JetNamedDeclaration) { fun invoke(project: Project, editor: Editor, expression: JetExpression, targetParent: JetNamedDeclaration) {
val psiFactory = JetPsiFactory(project) val psiFactory = JetPsiFactory(project)
@@ -140,16 +157,28 @@ public class KotlinIntroduceParameterHandler: KotlinIntroduceHandlerBase() {
val nameValidator = JetNameValidatorImpl(body, null, JetNameValidatorImpl.Target.PROPERTIES) val nameValidator = JetNameValidatorImpl(body, null, JetNameValidatorImpl.Target.PROPERTIES)
val suggestedNames = linkedSetOf(*JetNameSuggester.suggestNames(parameterType, nameValidator, "p")) val suggestedNames = linkedSetOf(*JetNameSuggester.suggestNames(parameterType, nameValidator, "p"))
val parametersToRemove = (parameterList?.getParameters() ?: Collections.emptyList()).filter { val parametersUsages = targetParent.getValueParameters()
if (!it.hasValOrVarNode()) { .filter { !it.hasValOrVarNode() }
val usages = DefaultSearchHelper<JetParameter>() .map {
.newRequest(UsagesSearchTarget(element = it)) it to DefaultSearchHelper<JetParameter>()
.search() .newRequest(UsagesSearchTarget(element = it))
.toList() .search()
usages.isNotEmpty() && usages.all { expression.isAncestor(it.getElement()) } .toList()
} }
else false .filter { it.second.isNotEmpty() }
} .toMap()
val occurrencesToReplace = expression.toRange()
.match(body, JetPsiUnifier.DEFAULT)
.map {
val matchedElement = it.range.elements.singleOrNull()
when (matchedElement) {
is JetExpression -> matchedElement
is JetStringTemplateEntryWithExpression -> matchedElement.getExpression()
else -> null
} as? JetExpression
}
.filterNotNull()
project.executeCommand(INTRODUCE_PARAMETER) { project.executeCommand(INTRODUCE_PARAMETER) {
val renderedType = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(parameterType) val renderedType = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(parameterType)
@@ -179,12 +208,13 @@ public class KotlinIntroduceParameterHandler: KotlinIntroduceHandlerBase() {
} }
val introduceParameterDescriptor = val introduceParameterDescriptor =
IntroduceParameterDescriptor(JetPsiUtil.deparenthesize(expression)!!, configure(IntroduceParameterDescriptor(JetPsiUtil.deparenthesize(expression)!!,
targetParent, targetParent,
functionDescriptor, functionDescriptor,
addedParameter, addedParameter,
parameterType, parameterType,
parametersToRemove) parametersUsages,
occurrencesToReplace))
if (editor.getSettings().isVariableInplaceRenameEnabled() && !ApplicationManager.getApplication().isUnitTestMode()) { if (editor.getSettings().isVariableInplaceRenameEnabled() && !ApplicationManager.getApplication().isUnitTestMode()) {
with(PsiDocumentManager.getInstance(project)) { with(PsiDocumentManager.getInstance(project)) {
commitDocument(editor.getDocument()) commitDocument(editor.getDocument())
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String) {
val x = a + 1
fun foo() = (<selection>a + 1</selection>) * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) {
val x = i
fun foo() = i * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String) {
val x = <selection>a + 1</selection>
fun foo() = (a + 1) * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) {
val x = i
fun foo() = i * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String) {
val x = a + 1
fun foo() = (<selection>a + 1</selection>) * 2
}
fun test() {
val t = A(1, "2").a
}
@@ -0,0 +1,10 @@
// TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) {
val x = i
fun foo() = i * 2
}
fun test() {
val t = A(1, "2").a
}
@@ -0,0 +1,11 @@
// SINGLE_REPLACE
// TARGET:
class A(val a: Int, s: String) {
val x = a + 1
fun foo() = (<selection>a + 1</selection>) * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,11 @@
// SINGLE_REPLACE
// TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) {
val x = a + 1
fun foo() = i * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,11 @@
// SINGLE_REPLACE
// TARGET:
class A(val a: Int, s: String) {
val x = <selection>a + 1</selection>
fun foo() = (a + 1) * 2
}
fun test() {
A(1, "2")
}
@@ -0,0 +1,11 @@
// SINGLE_REPLACE
// TARGET:
class A(val a: Int, s: String, i: Int = a + 1) {
val x = i
fun foo() = (a + 1) * 2
}
fun test() {
A(1, "2")
}
@@ -1,6 +1,6 @@
// TARGET: // TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) { class A(val a: Int, s: String, val i: Int = a + 1) {
fun foo() = (i) * 2 fun foo() = i * 2
} }
fun test() { fun test() {
@@ -1,6 +1,6 @@
// TARGET: // TARGET:
class A(val a: Int, s: String, val i: Int = a + 1) { class A(val a: Int, s: String, val i: Int = a + 1) {
fun foo() = (i) * 2 fun foo() = i * 2
} }
fun test() { fun test() {
@@ -2,7 +2,7 @@
class A(val a: Int, s: String, val i: Int = a + 1) { class A(val a: Int, s: String, val i: Int = a + 1) {
val x = a + 2 val x = a + 2
fun foo() = (i) * 2 fun foo() = i * 2
} }
fun test() { fun test() {
@@ -1,5 +1,5 @@
fun foo(a: Int, i: Int = a + 1): Int { fun foo(a: Int, i: Int = a + 1): Int {
val b = (i) * 2 val b = i * 2
return a + b return a + b
} }
@@ -1,5 +1,5 @@
fun foo(s: String, i: Int = a + b): Int { fun foo(s: String, i: Int = a + b): Int {
return (i) * 2 return i * 2
} }
fun test() { fun test() {
@@ -0,0 +1,8 @@
fun foo(a: Int, s: String): Int {
val t = (a + 1) * 2
return <selection>a + 1</selection> - t
}
fun test() {
foo(1, "2")
}
@@ -0,0 +1,8 @@
fun foo(s: String, i: Int = a + 1): Int {
val t = i * 2
return i - t
}
fun test() {
foo("2")
}
@@ -0,0 +1,9 @@
// SINGLE_REPLACE
fun foo(a: Int, s: String): Int {
val t = (a + 1) * 2
return <selection>a + 1</selection> - t
}
fun test() {
foo(1, "2")
}
@@ -0,0 +1,9 @@
// SINGLE_REPLACE
fun foo(a: Int, s: String, i: Int = a + 1): Int {
val t = (a + 1) * 2
return i - t
}
fun test() {
foo(1, "2")
}
@@ -1,5 +1,5 @@
fun foo(s: String, i: Int = a + 1): Int { fun foo(s: String, i: Int = a + 1): Int {
return (i) * 2 return i * 2
} }
fun test() { fun test() {
@@ -1,5 +1,5 @@
fun foo(a: Int, s: String, i: Int = a + 1): Int { fun foo(a: Int, s: String, i: Int = a + 1): Int {
return (i) * a return i * a
} }
fun test() { fun test() {
@@ -1,5 +1,5 @@
fun foo(a: Int, i: Int = a + 1): Int { fun foo(a: Int, i: Int = a + 1): Int {
val b = (i) * 2 val b = i * 2
return a + b return a + b
} }
@@ -1,6 +1,6 @@
class A { class A {
constructor(a: Int, i: Int = a + 1) { constructor(a: Int, i: Int = a + 1) {
val b = (i) * 2 val b = i * 2
val t = a + b val t = a + b
} }
} }
@@ -33,6 +33,7 @@ import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.refactoring.JetRefactoringUtil import org.jetbrains.kotlin.idea.refactoring.JetRefactoringUtil
import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.* import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.*
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.IntroduceParameterDescriptor
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceParameterHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter.KotlinIntroduceParameterHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceProperty.KotlinIntroducePropertyHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceProperty.KotlinIntroducePropertyHandler
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinIntroduceVariableHandler
@@ -65,7 +66,16 @@ public abstract class AbstractJetExtractionTest() : JetLightCodeInsightFixtureTe
protected fun doIntroduceParameterTest(path: String) { protected fun doIntroduceParameterTest(path: String) {
doTest(path) { file -> doTest(path) { file ->
with (KotlinIntroduceParameterHandler()) { val handler = object: KotlinIntroduceParameterHandler() {
override fun configure(descriptor: IntroduceParameterDescriptor): IntroduceParameterDescriptor {
val fileText = file.getText()
val singleReplace = InTextDirectivesUtils.isDirectiveDefined(fileText, "// SINGLE_REPLACE")
return with (descriptor) {
copy(occurrencesToReplace = if (singleReplace) Collections.singletonList(originalOccurrence) else occurrencesToReplace)
}
}
}
with (handler) {
val target = file.findElementByComment("// TARGET:") as? JetNamedDeclaration val target = file.findElementByComment("// TARGET:") as? JetNamedDeclaration
if (target != null) { if (target != null) {
JetRefactoringUtil.selectExpression(fixture.getEditor(), file, true) { expression -> JetRefactoringUtil.selectExpression(fixture.getEditor(), file, true) { expression ->
@@ -2203,6 +2203,36 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest {
doIntroduceParameterTest(fileName); doIntroduceParameterTest(fileName);
} }
@TestMetadata("classMultipleUsages1.kt")
public void testClassMultipleUsages1() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classMultipleUsages1.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("classMultipleUsages2.kt")
public void testClassMultipleUsages2() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classMultipleUsages2.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("classMultipleUsagesOutsideScope.kt")
public void testClassMultipleUsagesOutsideScope() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classMultipleUsagesOutsideScope.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("classMultipleUsagesSingleReplace1.kt")
public void testClassMultipleUsagesSingleReplace1() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classMultipleUsagesSingleReplace1.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("classMultipleUsagesSingleReplace2.kt")
public void testClassMultipleUsagesSingleReplace2() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classMultipleUsagesSingleReplace2.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("classNoParams.kt") @TestMetadata("classNoParams.kt")
public void testClassNoParams() throws Exception { public void testClassNoParams() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classNoParams.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classNoParams.kt");
@@ -2245,6 +2275,18 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest {
doIntroduceParameterTest(fileName); doIntroduceParameterTest(fileName);
} }
@TestMetadata("functionMultipleUsages.kt")
public void testFunctionMultipleUsages() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionMultipleUsages.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("functionMultipleUsagesSingleReplace.kt")
public void testFunctionMultipleUsagesSingleReplace() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionMultipleUsagesSingleReplace.kt");
doIntroduceParameterTest(fileName);
}
@TestMetadata("functionUnusedParameter.kt") @TestMetadata("functionUnusedParameter.kt")
public void testFunctionUnusedParameter() throws Exception { public void testFunctionUnusedParameter() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionUnusedParameter.kt"); String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionUnusedParameter.kt");