From 4a56b99cffe2f62745d2050d1277d24dc9cfe0cf Mon Sep 17 00:00:00 2001 From: Alexey Sedunov Date: Fri, 29 May 2015 21:26:39 +0300 Subject: [PATCH] Introduce Parameter: Reimplement in-place refactoring using AbstractInplaceIntroducer This simplifies implementation and fixes bug with unnecessarily detailed command history available through undo/redo --- .../KotlinInplaceParameterIntroducer.kt | 299 ++++++++---------- .../KotlinIntroduceParameterHandler.kt | 35 +- .../idea/refactoring/jetRefactoringUtil.kt | 36 +++ 3 files changed, 183 insertions(+), 187 deletions(-) diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinInplaceParameterIntroducer.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinInplaceParameterIntroducer.kt index 1dfb331af2f..a4321902072 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinInplaceParameterIntroducer.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinInplaceParameterIntroducer.kt @@ -19,11 +19,7 @@ package org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter import com.intellij.codeInsight.template.impl.TemplateManagerImpl import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorFactory -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.RangeMarker import com.intellij.openapi.editor.impl.DocumentMarkupModel import com.intellij.openapi.editor.markup.EffectType 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.project.Project import com.intellij.openapi.util.TextRange -import com.intellij.psi.PsiElement -import com.intellij.ui.DottedBorder +import com.intellij.psi.PsiAnchor +import com.intellij.psi.PsiFile +import com.intellij.refactoring.introduce.inplace.AbstractInplaceIntroducer import com.intellij.ui.JBColor import com.intellij.ui.NonFocusableCheckBox +import com.intellij.util.ui.FormBuilder 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.introduce.introduceVariable.KotlinInplaceVariableIntroducer 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.psi.JetExpression -import org.jetbrains.kotlin.psi.JetParameter -import org.jetbrains.kotlin.psi.JetParameterList -import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext -import org.jetbrains.kotlin.psi.psiUtil.getValueParameters +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* import org.jetbrains.kotlin.types.JetType +import org.jetbrains.kotlin.utils.addToStdlib.singletonList import java.awt.BorderLayout import java.awt.Color import java.util.ArrayList import java.util.Collections -import java.util.LinkedHashSet -import javax.swing.BorderFactory -import javax.swing.JPanel -import javax.swing.border.EmptyBorder -import javax.swing.border.LineBorder +import javax.swing.JCheckBox public class KotlinInplaceParameterIntroducer( val originalDescriptor: IntroduceParameterDescriptor, - val addedParameter: JetParameter, val parameterType: JetType, - editor: Editor, - project: Project -): KotlinInplaceVariableIntroducer( - addedParameter, - editor, + val suggestedNames: Array, + project: Project, + editor: Editor +): AbstractInplaceIntroducer( project, - INTRODUCE_PARAMETER, - JetExpression.EMPTY_ARRAY, + editor, + originalDescriptor.originalRange.elements.single() as JetExpression, null, - false, - addedParameter, - false, - true, - parameterType, - false + originalDescriptor.occurrencesToReplace + .map { it.elements.single() as JetExpression } + .filterNotNull() + .toTypedArray(), + INTRODUCE_PARAMETER, + JetFileType.INSTANCE ) { companion object { private val LOG = Logger.getInstance(javaClass()) @@ -110,9 +103,98 @@ public class KotlinInplaceParameterIntroducer( } 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): 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() + ?: 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) { val rangesToRemove = ArrayList() var addedRange: TextRange? = null @@ -128,6 +210,7 @@ public class KotlinInplaceParameterIntroducer( val parameterText = if (parameter == addedParameter){ val parameterName = currentName ?: parameter.getName() val parameterType = currentType ?: parameter.getTypeReference()!!.getText() + descriptor = descriptor.copy(newParameterName = parameterName, newParameterTypeText = parameterType) val modifier = if (valVar != JetValVar.None) "${valVar.name} " else "" val defaultValue = if (withDefaultValue) " = ${newArgumentValue.getText()}" else "" @@ -155,7 +238,7 @@ public class KotlinInplaceParameterIntroducer( LOG.error("Added parameter not found: ${callable.getElementTextWithContext()}") } - val document = previewer!!.getDocument() + val document = getPreviewEditor().getDocument() runWriteAction { document.setText(builder.toString()) } val markupModel = DocumentMarkupModel.forDocument(document, myProject, true) @@ -166,145 +249,29 @@ public class KotlinInplaceParameterIntroducer( 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 - - fun startRefactoring(suggestedNames: LinkedHashSet): 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 performIntroduce() { + getDescriptorToRefactor(isReplaceAllOccurrences()).performRefactoring() } - override fun finish(success: Boolean) { - super.finish(success) - myDocumentAdapter?.let { myEditor.getDocument().removeDocumentListener(it) } - } - - 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 - } + private fun getDescriptorToRefactor(replaceAll: Boolean): IntroduceParameterDescriptor { + val originalRange = getExpr().toRange() + return descriptor.copy( + originalRange = originalRange, + occurrencesToReplace = if (replaceAll) getOccurrences().map { it.toRange() } else originalRange.singletonList() + ) } fun switchToDialogUI() { - stopIntroduce() - with (originalDescriptor) { - KotlinIntroduceParameterDialog(myProject, - myEditor, - this, - myNameSuggestions.copyToArray(), - listOf(parameterType) + parameterType.supertypes(), - KotlinIntroduceParameterHelper.Default).show() - } + stopIntroduce(myEditor) + KotlinIntroduceParameterDialog(myProject, + myEditor, + getDescriptorToRefactor(true), + myNameSuggestions.toTypedArray(), + listOf(parameterType) + parameterType.supertypes(), + KotlinIntroduceParameterHelper.Default).show() } } \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinIntroduceParameterHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinIntroduceParameterHandler.kt index 4cea56fa39c..90e74d08dfb 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinIntroduceParameterHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinIntroduceParameterHandler.kt @@ -25,11 +25,13 @@ import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiReference import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.introduce.inplace.AbstractInplaceIntroducer import org.jetbrains.kotlin.descriptors.ClassDescriptor import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.caches.resolve.analyze import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor 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.JetRefactoringBundle import org.jetbrains.kotlin.idea.refactoring.changeSignature.* @@ -133,7 +135,10 @@ fun IntroduceParameterDescriptor.performRefactoring() { override fun performSilently(affectedFunctions: Collection): 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) } } } @@ -208,10 +213,6 @@ public open class KotlinIntroduceParameterHandler( 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 = if (targetParent is JetClass) { targetParent.getDeclarations().filter { isObjectOrNonInnerClass(it) }.map { it.getTextRange() } @@ -243,24 +244,16 @@ public open class KotlinIntroduceParameterHandler( val isTestMode = ApplicationManager.getApplication().isUnitTestMode() 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 psiFactory = JetPsiFactory(project) val introduceParameterDescriptor = helper.configure( IntroduceParameterDescriptor( originalRange = originalExpression.toRange(), callable = targetParent, callableDescriptor = functionDescriptor, - newParameterName = addedParameter.getName()!!, - newParameterTypeText = renderedType, + newParameterName = suggestedNames.first(), + newParameterTypeText = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(replacementType), newArgumentValue = originalExpression, withDefaultValue = false, parametersUsages = parametersUsages, @@ -282,11 +275,11 @@ public open class KotlinIntroduceParameterHandler( } val introducer = KotlinInplaceParameterIntroducer(introduceParameterDescriptor, - addedParameter, replacementType, - editor, - project) - if (introducer.startRefactoring(suggestedNames)) return + suggestedNames.toTypedArray(), + project, + editor) + if (introducer.startInplaceIntroduceTemplate()) return } KotlinIntroduceParameterDialog(project, @@ -300,7 +293,7 @@ public open class KotlinIntroduceParameterHandler( } 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() return } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt index 103e5145405..56f03c0a868 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt @@ -79,6 +79,8 @@ import org.jetbrains.kotlin.descriptors.ClassKind import com.intellij.lang.java.JavaLanguage import com.intellij.codeInsight.daemon.impl.quickfix.CreateFromUsageUtils 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.j2k.IdeaResolverForConverter import org.jetbrains.kotlin.idea.j2k.J2kPostProcessor @@ -568,4 +570,38 @@ fun PsiExpression.j2k(): JetExpression? { IdeaResolverForConverter) val text = j2kConverter.elementsToKotlin(listOf(this)).results.single()?.text ?: return null //TODO: insert imports 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() } \ No newline at end of file