Introduce Parameter: Reimplement in-place refactoring using AbstractInplaceIntroducer

This simplifies implementation and fixes bug with unnecessarily detailed command history available through undo/redo
This commit is contained in:
Alexey Sedunov
2015-05-29 21:26:39 +03:00
parent d475401108
commit 4a56b99cff
3 changed files with 183 additions and 187 deletions
@@ -19,11 +19,7 @@ package org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter
import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.RangeMarker
import com.intellij.openapi.editor.colors.EditorColors
import com.intellij.openapi.editor.event.DocumentAdapter
import com.intellij.openapi.editor.event.DocumentEvent
import com.intellij.openapi.editor.ex.EditorEx
import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.impl.DocumentMarkupModel
import com.intellij.openapi.editor.markup.EffectType import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.HighlighterTargetArea
@@ -31,50 +27,47 @@ import com.intellij.openapi.editor.markup.MarkupModel
import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement import com.intellij.psi.PsiAnchor
import com.intellij.ui.DottedBorder import com.intellij.psi.PsiFile
import com.intellij.refactoring.introduce.inplace.AbstractInplaceIntroducer
import com.intellij.ui.JBColor import com.intellij.ui.JBColor
import com.intellij.ui.NonFocusableCheckBox import com.intellij.ui.NonFocusableCheckBox
import com.intellij.util.ui.FormBuilder
import org.jetbrains.kotlin.idea.JetFileType import org.jetbrains.kotlin.idea.JetFileType
import org.jetbrains.kotlin.idea.core.replaced
import org.jetbrains.kotlin.idea.refactoring.changeSignature.JetValVar import org.jetbrains.kotlin.idea.refactoring.changeSignature.JetValVar
import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinInplaceVariableIntroducer import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinInplaceVariableIntroducer
import org.jetbrains.kotlin.idea.util.application.runWriteAction import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.util.psi.patternMatching.JetPsiRange
import org.jetbrains.kotlin.idea.util.psi.patternMatching.toRange
import org.jetbrains.kotlin.idea.util.supertypes import org.jetbrains.kotlin.idea.util.supertypes
import org.jetbrains.kotlin.psi.JetExpression import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.JetParameter import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.psi.JetParameterList
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.psi.psiUtil.getValueParameters
import org.jetbrains.kotlin.types.JetType import org.jetbrains.kotlin.types.JetType
import org.jetbrains.kotlin.utils.addToStdlib.singletonList
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.Collections
import java.util.LinkedHashSet import javax.swing.JCheckBox
import javax.swing.BorderFactory
import javax.swing.JPanel
import javax.swing.border.EmptyBorder
import javax.swing.border.LineBorder
public class KotlinInplaceParameterIntroducer( public class KotlinInplaceParameterIntroducer(
val originalDescriptor: IntroduceParameterDescriptor, val originalDescriptor: IntroduceParameterDescriptor,
val addedParameter: JetParameter,
val parameterType: JetType, val parameterType: JetType,
editor: Editor, val suggestedNames: Array<out String>,
project: Project project: Project,
): KotlinInplaceVariableIntroducer<JetParameter>( editor: Editor
addedParameter, ): AbstractInplaceIntroducer<JetParameter, JetExpression>(
editor,
project, project,
INTRODUCE_PARAMETER, editor,
JetExpression.EMPTY_ARRAY, originalDescriptor.originalRange.elements.single() as JetExpression,
null, null,
false, originalDescriptor.occurrencesToReplace
addedParameter, .map { it.elements.single() as JetExpression }
false, .filterNotNull()
true, .toTypedArray(),
parameterType, INTRODUCE_PARAMETER,
false JetFileType.INSTANCE
) { ) {
companion object { companion object {
private val LOG = Logger.getInstance(javaClass<KotlinInplaceParameterIntroducer>()) private val LOG = Logger.getInstance(javaClass<KotlinInplaceParameterIntroducer>())
@@ -110,9 +103,98 @@ public class KotlinInplaceParameterIntroducer(
} }
private var descriptor = originalDescriptor private var descriptor = originalDescriptor
private var previewer: EditorEx? = null private var replaceAllCheckBox: JCheckBox? = null
init {
val panel = with(FormBuilder.createFormBuilder()) {
addComponent(getPreviewComponent())
val defaultValueCheckBox = NonFocusableCheckBox("Introduce default value")
defaultValueCheckBox.setSelected(descriptor.withDefaultValue)
defaultValueCheckBox.setMnemonic('d')
defaultValueCheckBox.addActionListener {
descriptor = descriptor.copy(withDefaultValue = defaultValueCheckBox.isSelected())
updateTitle(getVariable())
}
addComponent(defaultValueCheckBox)
val occurrenceCount = descriptor.occurrencesToReplace.size()
if (occurrenceCount > 1) {
val replaceAllCheckBox = NonFocusableCheckBox("Replace all occurrences ($occurrenceCount)")
replaceAllCheckBox.setSelected(true)
replaceAllCheckBox.setMnemonic('R')
addComponent(replaceAllCheckBox)
this@KotlinInplaceParameterIntroducer.replaceAllCheckBox = replaceAllCheckBox
}
getPanel()
}
myWholePanel.setLayout(BorderLayout())
myWholePanel.add(panel, BorderLayout.CENTER)
}
override fun getActionName() = "IntroduceParameter"
override fun checkLocalScope() = descriptor.callable
override fun getVariable() = originalDescriptor.callable.getValueParameters().lastOrNull()
override fun suggestNames(replaceAll: Boolean, variable: JetParameter?) = suggestedNames
override fun createFieldToStartTemplateOn(replaceAll: Boolean, names: Array<out String>): JetParameter? {
return runWriteAction {
with(descriptor) {
val parameterList = callable.getValueParameterList()
?: (callable as JetClass).createPrimaryConstructorParameterListIfAbsent()
val parameter = JetPsiFactory(myProject).createParameter("${newParameterName}: ${newParameterTypeText}")
parameterList.addParameter(parameter)
}
}
}
override fun deleteTemplateField(psiField: JetParameter) {
if (psiField.isValid()) {
(psiField.getParent() as? JetParameterList)?.removeParameter(psiField)
}
}
override fun restoreExpression(
containingFile: PsiFile,
parameter: JetParameter,
marker: RangeMarker,
exprText: String
): JetExpression? {
if (!parameter.isValid()) return null
val refExpr = containingFile.findElementAt(marker.getStartOffset())?.getNonStrictParentOfType<JetSimpleNameExpression>()
?: return null
val refName = refExpr.getReferencedName()
if (refExpr.getReference()?.resolve() == parameter || parameter.getName() == refName || exprText == refName) {
return refExpr.replaced(JetPsiFactory(myProject).createExpression(exprText))
}
return null
}
override fun isReplaceAllOccurrences() = replaceAllCheckBox?.isSelected() ?: true
override fun setReplaceAllOccurrences(allOccurrences: Boolean) {
replaceAllCheckBox?.setSelected(allOccurrences)
}
override fun getComponent() = myWholePanel
override fun updateTitle(parameter: JetParameter?) = updateTitle(parameter, null)
override fun updateTitle(addedParameter: JetParameter?, currentName: String?) {
val templateState = TemplateManagerImpl.getTemplateState(myEditor)
if (templateState == null || templateState.getTemplate() == null) return
val currentType = templateState
.getVariableValue(KotlinInplaceVariableIntroducer.TYPE_REFERENCE_VARIABLE_NAME)
?.getText()
private fun updatePreview(currentName: String?, currentType: String?) {
with (descriptor) { with (descriptor) {
val rangesToRemove = ArrayList<TextRange>() val rangesToRemove = ArrayList<TextRange>()
var addedRange: TextRange? = null var addedRange: TextRange? = null
@@ -128,6 +210,7 @@ public class KotlinInplaceParameterIntroducer(
val parameterText = if (parameter == addedParameter){ val parameterText = if (parameter == addedParameter){
val parameterName = currentName ?: parameter.getName() val parameterName = currentName ?: parameter.getName()
val parameterType = currentType ?: parameter.getTypeReference()!!.getText() val parameterType = currentType ?: parameter.getTypeReference()!!.getText()
descriptor = descriptor.copy(newParameterName = parameterName, newParameterTypeText = parameterType)
val modifier = if (valVar != JetValVar.None) "${valVar.name} " else "" val modifier = if (valVar != JetValVar.None) "${valVar.name} " else ""
val defaultValue = if (withDefaultValue) " = ${newArgumentValue.getText()}" else "" val defaultValue = if (withDefaultValue) " = ${newArgumentValue.getText()}" else ""
@@ -155,7 +238,7 @@ public class KotlinInplaceParameterIntroducer(
LOG.error("Added parameter not found: ${callable.getElementTextWithContext()}") LOG.error("Added parameter not found: ${callable.getElementTextWithContext()}")
} }
val document = previewer!!.getDocument() val document = getPreviewEditor().getDocument()
runWriteAction { document.setText(builder.toString()) } runWriteAction { document.setText(builder.toString()) }
val markupModel = DocumentMarkupModel.forDocument(document, myProject, true) val markupModel = DocumentMarkupModel.forDocument(document, myProject, true)
@@ -166,145 +249,29 @@ public class KotlinInplaceParameterIntroducer(
revalidate() revalidate()
} }
override fun getAdvertisementActionId() = "IntroduceParameter" override fun saveSettings(variable: JetParameter?) {
override fun initPanelControls() {
addPanelControl {
val previewer = EditorFactory.getInstance().createEditor(EditorFactory.getInstance().createDocument(""),
myProject,
JetFileType.INSTANCE,
true) as EditorEx
this.previewer = previewer
previewer.setOneLineMode(true)
with(previewer.getSettings()) {
setAdditionalLinesCount(0)
setAdditionalColumnsCount(1)
setRightMarginShown(false)
setFoldingOutlineShown(false)
setLineNumbersShown(false)
setLineMarkerAreaShown(false)
setIndentGuidesShown(false)
setVirtualSpace(false)
setLineCursorWidth(1)
}
previewer.setHorizontalScrollbarVisible(false)
previewer.setVerticalScrollbarVisible(false)
previewer.setCaretEnabled(false)
val bg = previewer.getColorsScheme().getColor(EditorColors.CARET_ROW_COLOR)
previewer.setBackgroundColor(bg)
previewer.setBorder(BorderFactory.createCompoundBorder(DottedBorder(Color.gray), LineBorder(bg, 2)))
updatePreview(null, null)
val previewerPanel = JPanel(BorderLayout())
previewerPanel.add(previewer.getComponent(), BorderLayout.CENTER)
previewerPanel.setBorder(EmptyBorder(2, 2, 6, 2))
previewerPanel
}
addPanelControl {
val defaultValueCheckBox = NonFocusableCheckBox("Introduce default value")
defaultValueCheckBox.setSelected(descriptor.withDefaultValue)
defaultValueCheckBox.setMnemonic('d')
defaultValueCheckBox.addActionListener {
descriptor = descriptor.copy(withDefaultValue = defaultValueCheckBox.isSelected())
updatePreview(null, null)
}
defaultValueCheckBox
}
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 override fun performIntroduce() {
getDescriptorToRefactor(isReplaceAllOccurrences()).performRefactoring()
fun startRefactoring(suggestedNames: LinkedHashSet<String>): Boolean {
if (!performInplaceRefactoring(suggestedNames)) return false
myDocumentAdapter = object : DocumentAdapter() {
override fun documentChanged(e: DocumentEvent?) {
if (previewer == null) return
val templateState = TemplateManagerImpl.getTemplateState(myEditor)
if (templateState != null) {
val name = templateState.getVariableValue(KotlinInplaceVariableIntroducer.PRIMARY_VARIABLE_NAME)?.getText()
val typeRefText = templateState.getVariableValue(KotlinInplaceVariableIntroducer.TYPE_REFERENCE_VARIABLE_NAME)?.getText()
updatePreview(name, typeRefText)
descriptor = descriptor.copy(newParameterName = name ?: descriptor.newParameterName,
newParameterTypeText = typeRefText ?: descriptor.newParameterTypeText)
}
}
}
myEditor.getDocument().addDocumentListener(myDocumentAdapter!!)
return true
} }
override fun finish(success: Boolean) { private fun getDescriptorToRefactor(replaceAll: Boolean): IntroduceParameterDescriptor {
super.finish(success) val originalRange = getExpr().toRange()
myDocumentAdapter?.let { myEditor.getDocument().removeDocumentListener(it) } return descriptor.copy(
} originalRange = originalRange,
occurrencesToReplace = if (replaceAll) getOccurrences().map { it.toRange() } else originalRange.singletonList()
override fun checkLocalScope(): PsiElement? { )
return descriptor.callable
}
private fun removeAddedParameter() {
runWriteAction { (addedParameter.getParent() as JetParameterList).removeParameter(addedParameter) }
}
override fun performRefactoring(): Boolean {
removeAddedParameter()
descriptor.performRefactoring()
return true
}
override fun performCleanup() {
removeAddedParameter()
}
override fun releaseResources() {
super.releaseResources()
previewer?.let {
EditorFactory.getInstance().releaseEditor(it)
previewer = null
}
} }
fun switchToDialogUI() { fun switchToDialogUI() {
stopIntroduce() stopIntroduce(myEditor)
with (originalDescriptor) { KotlinIntroduceParameterDialog(myProject,
KotlinIntroduceParameterDialog(myProject, myEditor,
myEditor, getDescriptorToRefactor(true),
this, myNameSuggestions.toTypedArray(),
myNameSuggestions.copyToArray(), listOf(parameterType) + parameterType.supertypes(),
listOf(parameterType) + parameterType.supertypes(), KotlinIntroduceParameterHelper.Default).show()
KotlinIntroduceParameterHelper.Default).show()
}
} }
} }
@@ -25,11 +25,13 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile import com.intellij.psi.PsiFile
import com.intellij.psi.PsiReference import com.intellij.psi.PsiReference
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import com.intellij.refactoring.introduce.inplace.AbstractInplaceIntroducer
import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor
import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.analyze
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.idea.core.refactoring.JetNameSuggester import org.jetbrains.kotlin.idea.core.refactoring.JetNameSuggester
import org.jetbrains.kotlin.idea.core.refactoring.runRefactoringWithPostprocessing
import org.jetbrains.kotlin.idea.refactoring.JetNameValidatorImpl import org.jetbrains.kotlin.idea.refactoring.JetNameValidatorImpl
import org.jetbrains.kotlin.idea.refactoring.JetRefactoringBundle import org.jetbrains.kotlin.idea.refactoring.JetRefactoringBundle
import org.jetbrains.kotlin.idea.refactoring.changeSignature.* import org.jetbrains.kotlin.idea.refactoring.changeSignature.*
@@ -133,7 +135,10 @@ 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)) {
val project = callable.getProject();
val changeSignature = { runChangeSignature(project, callableDescriptor, config, callable.analyze(), callable, INTRODUCE_PARAMETER) }
changeSignature.runRefactoringWithPostprocessing(project, "refactoring.changeSignature") {
occurrencesToReplace.forEach { occurrenceReplacer(it) } occurrencesToReplace.forEach { occurrenceReplacer(it) }
} }
} }
@@ -208,10 +213,6 @@ public open class KotlinIntroduceParameterHandler(
val parametersUsages = findInternalParameterUsages(targetParent) val parametersUsages = findInternalParameterUsages(targetParent)
val psiFactory = JetPsiFactory(project)
val renderedType = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(replacementType)
val newParameter = psiFactory.createParameter("${suggestedNames.first()}: $renderedType")
val forbiddenRanges = val forbiddenRanges =
if (targetParent is JetClass) { if (targetParent is JetClass) {
targetParent.getDeclarations().filter { isObjectOrNonInnerClass(it) }.map { it.getTextRange() } targetParent.getDeclarations().filter { isObjectOrNonInnerClass(it) }.map { it.getTextRange() }
@@ -243,24 +244,16 @@ public open class KotlinIntroduceParameterHandler(
val isTestMode = ApplicationManager.getApplication().isUnitTestMode() val isTestMode = ApplicationManager.getApplication().isUnitTestMode()
val inplaceIsAvailable = editor.getSettings().isVariableInplaceRenameEnabled() && !isTestMode val inplaceIsAvailable = editor.getSettings().isVariableInplaceRenameEnabled() && !isTestMode
val addedParameter = if (inplaceIsAvailable) {
runWriteAction {
val parameterList = targetParent.getValueParameterList()
?: (targetParent as JetClass).createPrimaryConstructorParameterListIfAbsent()
parameterList.addParameter(newParameter)
}
}
else newParameter
val originalExpression = JetPsiUtil.safeDeparenthesize(expression) val originalExpression = JetPsiUtil.safeDeparenthesize(expression)
val psiFactory = JetPsiFactory(project)
val introduceParameterDescriptor = val introduceParameterDescriptor =
helper.configure( helper.configure(
IntroduceParameterDescriptor( IntroduceParameterDescriptor(
originalRange = originalExpression.toRange(), originalRange = originalExpression.toRange(),
callable = targetParent, callable = targetParent,
callableDescriptor = functionDescriptor, callableDescriptor = functionDescriptor,
newParameterName = addedParameter.getName()!!, newParameterName = suggestedNames.first(),
newParameterTypeText = renderedType, newParameterTypeText = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(replacementType),
newArgumentValue = originalExpression, newArgumentValue = originalExpression,
withDefaultValue = false, withDefaultValue = false,
parametersUsages = parametersUsages, parametersUsages = parametersUsages,
@@ -282,11 +275,11 @@ public open class KotlinIntroduceParameterHandler(
} }
val introducer = KotlinInplaceParameterIntroducer(introduceParameterDescriptor, val introducer = KotlinInplaceParameterIntroducer(introduceParameterDescriptor,
addedParameter,
replacementType, replacementType,
editor, suggestedNames.toTypedArray(),
project) project,
if (introducer.startRefactoring(suggestedNames)) return editor)
if (introducer.startInplaceIntroduceTemplate()) return
} }
KotlinIntroduceParameterDialog(project, KotlinIntroduceParameterDialog(project,
@@ -300,7 +293,7 @@ public open class KotlinIntroduceParameterHandler(
} }
override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) {
(KotlinInplaceVariableIntroducer.getActiveInstance(editor) as? KotlinInplaceParameterIntroducer)?.let { (AbstractInplaceIntroducer.getActiveIntroducer(editor) as? KotlinInplaceParameterIntroducer)?.let {
it.switchToDialogUI() it.switchToDialogUI()
return return
} }
@@ -79,6 +79,8 @@ import org.jetbrains.kotlin.descriptors.ClassKind
import com.intellij.lang.java.JavaLanguage import com.intellij.lang.java.JavaLanguage
import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils
import com.intellij.psi.* import com.intellij.psi.*
import com.intellij.refactoring.listeners.RefactoringEventData
import com.intellij.refactoring.listeners.RefactoringEventListener
import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde
import org.jetbrains.kotlin.idea.j2k.IdeaResolverForConverter import org.jetbrains.kotlin.idea.j2k.IdeaResolverForConverter
import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor
@@ -568,4 +570,38 @@ fun PsiExpression.j2k(): JetExpression? {
IdeaResolverForConverter) IdeaResolverForConverter)
val text = j2kConverter.elementsToKotlin(listOf(this)).results.single()?.text ?: return null //TODO: insert imports val text = j2kConverter.elementsToKotlin(listOf(this)).results.single()?.text ?: return null //TODO: insert imports
return JetPsiFactory(getProject()).createExpression(text) return JetPsiFactory(getProject()).createExpression(text)
}
public fun (() -> Any).runRefactoringWithPostprocessing(
project: Project,
targetRefactoringId: String,
finishAction: () -> Unit
) {
val connection = project.getMessageBus().connect()
connection.subscribe(RefactoringEventListener.REFACTORING_EVENT_TOPIC,
object: RefactoringEventListener {
override fun undoRefactoring(refactoringId: String) {
}
override fun refactoringStarted(refactoringId: String, beforeData: RefactoringEventData?) {
}
override fun conflictsDetected(refactoringId: String, conflictsData: RefactoringEventData) {
}
override fun refactoringDone(refactoringId: String, afterData: RefactoringEventData?) {
if (refactoringId == targetRefactoringId) {
try {
finishAction()
}
finally {
connection.disconnect()
}
}
}
})
this()
} }