diff --git a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt index d94ce695085..cdb6d1067ed 100644 --- a/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt +++ b/generators/src/org/jetbrains/kotlin/generators/tests/GenerateTests.kt @@ -676,6 +676,7 @@ fun main(args: Array) { model("refactoring/introduceVariable", extension = "kt", testMethod = "doIntroduceVariableTest") model("refactoring/extractFunction", extension = "kt", testMethod = "doExtractFunctionTest") model("refactoring/introduceProperty", extension = "kt", testMethod = "doIntroducePropertyTest") + model("refactoring/introduceParameter", extension = "kt", testMethod = "doIntroduceParameterTest") } testClass(javaClass()) { diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/CallableRefactoring.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/CallableRefactoring.kt index a534f5e8ba1..affe8f5c896 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/CallableRefactoring.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/CallableRefactoring.kt @@ -110,7 +110,7 @@ public abstract class CallableRefactoring( protected abstract fun performRefactoring(descriptorsForChange: Collection) - public fun run() { + public fun run(): Boolean { fun buttonPressed(code: Int, dialogButtons: List, button: String): Boolean { return code == dialogButtons indexOf button && button in dialogButtons } @@ -134,7 +134,7 @@ public abstract class CallableRefactoring( if (kind == SYNTHESIZED) { LOG.error("Change signature refactoring should not be called for synthesized member " + callableDescriptor) - return + return false } val closestModifiableDescriptors = getClosestModifiableDescriptors() @@ -144,12 +144,12 @@ public abstract class CallableRefactoring( ?: Collections.singletonList(callableDescriptor) if (ApplicationManager.getApplication()!!.isUnitTestMode()) { performRefactoring(deepestSuperDeclarations) - return + return true } if (closestModifiableDescriptors.size() == 1 && deepestSuperDeclarations == closestModifiableDescriptors) { performRefactoring(closestModifiableDescriptors) - return + return true } val isSingleFunctionSelected = closestModifiableDescriptors.size() == 1 @@ -165,8 +165,10 @@ public abstract class CallableRefactoring( } else -> { //do nothing + return false } } + return true } } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringBundle.properties b/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringBundle.properties index 558f5c4fa4a..f90d10d3703 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringBundle.properties +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringBundle.properties @@ -3,6 +3,7 @@ cannot.refactor.not.expression.to.extract=Cannot find an expression to extract expressions.title=Expressions introduce.variable=Introduce Variable introduce.property=Introduce Property +introduce.parameter=Introduce Parameter cannot.refactor.no.container=Cannot refactor in this place cannot.refactor.no.expression=Cannot perform refactoring without an expression cannot.refactor.expression.has.unit.type=Cannot introduce expression of unit type diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringSupportProvider.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringSupportProvider.java index 24e41da6bf7..3dd1281cfc4 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringSupportProvider.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/JetRefactoringSupportProvider.java @@ -24,6 +24,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.kotlin.idea.refactoring.changeSignature.JetChangeSignatureHandler; import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler; +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.introduceVariable.KotlinIntroduceVariableHandler; import org.jetbrains.kotlin.idea.refactoring.safeDelete.SafeDeletePackage; @@ -40,6 +41,12 @@ public class JetRefactoringSupportProvider extends RefactoringSupportProvider { return new KotlinIntroduceVariableHandler(); } + @Nullable + @Override + public RefactoringActionHandler getIntroduceParameterHandler() { + return new KotlinIntroduceParameterHandler(); + } + @NotNull public RefactoringActionHandler getIntroducePropertyHandler() { return new KotlinIntroducePropertyHandler(); diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetChangeSignature.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetChangeSignature.kt index 75dd8c38fd9..00317a077bc 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetChangeSignature.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetChangeSignature.kt @@ -56,8 +56,8 @@ public fun runChangeSignature(project: Project, configuration: JetChangeSignatureConfiguration, bindingContext: BindingContext, defaultValueContext: PsiElement, - commandName: String? = null) { - JetChangeSignature(project, functionDescriptor, configuration, bindingContext, defaultValueContext, commandName).run() + commandName: String? = null): Boolean { + return JetChangeSignature(project, functionDescriptor, configuration, bindingContext, defaultValueContext, commandName).run() } public class JetChangeSignature(project: Project, diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetMutableMethodDescriptor.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetMutableMethodDescriptor.kt index d489c882943..251169a7f1b 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetMutableMethodDescriptor.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/JetMutableMethodDescriptor.kt @@ -33,6 +33,10 @@ public class JetMutableMethodDescriptor(val original: JetMethodDescriptor): JetM parameters.add(parameter) } + public fun setParameter(index: Int, parameter: JetParameterInfo) { + parameters[index] = parameter + } + public fun removeParameter(index: Int) { parameters.remove(index) } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetFunctionCallUsage.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetFunctionCallUsage.java index a65c6522e68..b420e0ad97f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetFunctionCallUsage.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/changeSignature/usages/JetFunctionCallUsage.java @@ -156,8 +156,15 @@ public class JetFunctionCallUsage extends JetUsageInfo { ? psiFactory.createArgument(oldArgument.getArgumentExpression()) : oldArgument.asElement()); } - else if (parameterInfo.getDefaultValueForCall().isEmpty()) - newArgument.delete(); + // TODO: process default arguments in the middle + else if (parameterInfo.getDefaultValueForCall().isEmpty()) { + if (parameterInfo.getDefaultValueForParameter() != null) { + JetPsiUtil.deleteElementWithDelimiters(newArgument); + } + else { + newArgument.delete(); + } + } } List lambdaArguments = element.getFunctionLiteralArguments(); 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 new file mode 100644 index 00000000000..c640806dde9 --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinInplaceParameterIntroducer.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.impl.DocumentMarkupModel +import com.intellij.openapi.editor.markup.EffectType +import com.intellij.openapi.editor.markup.HighlighterTargetArea +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.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.refactoring.rename.inplace.InplaceRefactoring +import com.intellij.ui.DottedBorder +import com.intellij.ui.JBColor +import org.jetbrains.kotlin.idea.JetFileType +import org.jetbrains.kotlin.idea.caches.resolve.analyze +import org.jetbrains.kotlin.idea.refactoring.changeSignature.* +import org.jetbrains.kotlin.idea.refactoring.introduce.introduceVariable.KotlinInplaceVariableIntroducer +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.psi.JetExpression +import org.jetbrains.kotlin.psi.JetParameter +import org.jetbrains.kotlin.psi.JetPsiFactory +import org.jetbrains.kotlin.psi.JetPsiUtil +import org.jetbrains.kotlin.psi.psiUtil.getValueParameterList +import org.jetbrains.kotlin.psi.psiUtil.getValueParameters +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.types.JetType +import java.awt.BorderLayout +import java.awt.Color +import java.util.ArrayList +import java.util.LinkedHashSet +import javax.swing.BorderFactory +import javax.swing.JPanel +import javax.swing.border.EmptyBorder +import javax.swing.border.LineBorder +import kotlin.properties.Delegates + +public class KotlinInplaceParameterIntroducer( + val descriptor: IntroduceParameterDescriptor, + editor: Editor, + project: Project +): KotlinInplaceVariableIntroducer( + descriptor.addedParameter, + editor, + project, + INTRODUCE_PARAMETER, + JetExpression.EMPTY_ARRAY, + null, + false, + descriptor.addedParameter, + false, + true, + descriptor.parameterType, + false +) { + companion object { + private val LOG = Logger.getInstance(javaClass()) + } + + object PreviewDecorator { + protected val textAttributes: TextAttributes = with(TextAttributes()) { + setEffectType(EffectType.ROUNDED_BOX) + setEffectColor(JBColor.RED) + this + } + + fun applyToRange(range: TextRange, markupModel: MarkupModel) { + markupModel.addRangeHighlighter(range.getStartOffset(), + range.getEndOffset(), + 0, + textAttributes, + HighlighterTargetArea.EXACT_RANGE + ) + } + } + + private var previewer: EditorEx? = null + + private fun updatePreview(currentName: String?, currentType: String?) { + with (descriptor) { + var addedRange: TextRange? = null + val builder = StringBuilder() + + builder.append(callable.getName()) + + val parameters = callable.getValueParameters() + builder.append("(") + for (i in parameters.indices) { + val parameter = parameters[i] + + val parameterText = if (parameter == addedParameter){ + val parameterName = currentName ?: parameter.getName() + val parameterType = currentType ?: parameter.getTypeReference()!!.getText() + val modifier = if (valVar != JetValVar.None) "${valVar.name} " else "" + + "$modifier$parameterName: $parameterType" + } + else parameter.getText() + + builder.append(parameterText) + + val range = TextRange(builder.length() - parameterText.length(), builder.length()) + if (parameter == addedParameter) { + addedRange = range + } + + if (i < parameters.lastIndex) { + builder.append(", ") + } + } + builder.append(")") + + if (addedRange == null) { + LOG.error("Added parameter not found: ${JetPsiUtil.getElementTextWithContext(callable)}") + } + + val document = previewer!!.getDocument() + runWriteAction { document.setText(builder.toString()) } + + val markupModel = DocumentMarkupModel.forDocument(document, myProject, true) + markupModel.removeAllHighlighters() + addedRange?.let { PreviewDecorator.applyToRange(it, markupModel) } + } + revalidate() + } + + 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 + } + } + + 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) + } + } + } + myEditor.getDocument().addDocumentListener(myDocumentAdapter!!) + + return true + } + + override fun finish(success: Boolean) { + super.finish(success) + myDocumentAdapter?.let { myEditor.getDocument().removeDocumentListener(it) } + } + + override fun checkLocalScope(): PsiElement? { + return descriptor.callable + } + + override fun performRefactoring(): Boolean { + descriptor.performRefactoring() + return true + } + + override fun performCleanup() { + runWriteAction { JetPsiUtil.deleteElementWithDelimiters(descriptor.addedParameter) } + } + + override fun releaseResources() { + super.releaseResources() + previewer?.let { + EditorFactory.getInstance().releaseEditor(it) + previewer = null + } + } +} \ 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 new file mode 100644 index 00000000000..0bd0516bccf --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceParameter/KotlinIntroduceParameterHandler.kt @@ -0,0 +1,211 @@ +/* + * Copyright 2010-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.idea.refactoring.introduce.introduceParameter + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +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.core.refactoring.JetNameSuggester +import org.jetbrains.kotlin.idea.refactoring.JetNameValidatorImpl +import org.jetbrains.kotlin.idea.refactoring.JetRefactoringBundle +import org.jetbrains.kotlin.idea.refactoring.changeSignature.* +import org.jetbrains.kotlin.idea.refactoring.introduce.KotlinIntroduceHandlerBase +import org.jetbrains.kotlin.idea.refactoring.introduce.selectElementsWithTargetParent +import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHintByKey +import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers +import org.jetbrains.kotlin.idea.util.application.executeCommand +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.approximateWithResolvableType +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.getParentOfTypeAndBranch +import org.jetbrains.kotlin.psi.psiUtil.getStrictParentOfType +import org.jetbrains.kotlin.psi.psiUtil.getValueParameterList +import org.jetbrains.kotlin.psi.psiUtil.parents +import org.jetbrains.kotlin.resolve.BindingContext +import org.jetbrains.kotlin.resolve.scopes.JetScopeUtils +import org.jetbrains.kotlin.types.JetType +import kotlin.test.fail + +public data class IntroduceParameterDescriptor( + val originalExpression: JetExpression, + val callable: JetNamedDeclaration, + val callableDescriptor: FunctionDescriptor, + val addedParameter: JetParameter, + val parameterType: JetType +) { + val valVar: JetValVar + + init { + valVar = if (callable is JetClass) { + val modifierIsUnnecessary: (PsiElement) -> Boolean = { + when { + it.getParent() != callable.getBody() -> + false + it is JetClassInitializer -> + true + it is JetProperty && it.getInitializer()?.getTextRange()?.intersects(originalExpression.getTextRange()) ?: false -> + true + else -> + false + } + } + if (originalExpression.parents().any(modifierIsUnnecessary)) JetValVar.None else JetValVar.Val + } + else JetValVar.None + } +} + +fun IntroduceParameterDescriptor.performRefactoring() { + runWriteAction { + JetPsiUtil.deleteElementWithDelimiters(addedParameter) + + val config = object: JetChangeSignatureConfiguration { + override fun configure(originalDescriptor: JetMethodDescriptor, bindingContext: BindingContext): JetMethodDescriptor { + return originalDescriptor.modify { + val parameterInfo = JetParameterInfo(name = addedParameter.getName()!!, + type = parameterType, + defaultValueForParameter = JetPsiUtil.deparenthesize(originalExpression), + valOrVar = valVar) + parameterInfo.currentTypeText = addedParameter.getTypeReference()?.getText() ?: "Any" + addParameter(parameterInfo) + } + } + + override fun performSilently(affectedFunctions: Collection): Boolean = true + } + if (runChangeSignature(callable.getProject(), callableDescriptor, config, callable.analyze(), callable, INTRODUCE_PARAMETER)) { + originalExpression.replace(JetPsiFactory(callable).createSimpleName(addedParameter.getName()!!)) + } + } +} + +public class KotlinIntroduceParameterHandler: KotlinIntroduceHandlerBase() { + public fun invoke(project: Project, editor: Editor, expression: JetExpression, targetParent: JetNamedDeclaration) { + val psiFactory = JetPsiFactory(project) + + val parameterList = targetParent.getValueParameterList() + + val context = expression.analyze() + val descriptor = context[BindingContext.DECLARATION_TO_DESCRIPTOR, targetParent] + val functionDescriptor: FunctionDescriptor = + when (descriptor) { + is FunctionDescriptor -> descriptor : FunctionDescriptor + is ClassDescriptor -> descriptor.getUnsubstitutedPrimaryConstructor() + else -> null + } ?: throw AssertionError("Unexpected element type: ${JetPsiUtil.getElementTextWithContext(targetParent)}") + val expressionType = context[BindingContext.EXPRESSION_TYPE, expression] ?: KotlinBuiltIns.getInstance().getAnyType() + val parameterType = expressionType.approximateWithResolvableType(JetScopeUtils.getResolutionScope(targetParent, context), false) + + val validatorContainer = + when (targetParent) { + is JetFunction -> targetParent.getBodyExpression() + is JetClass -> targetParent.getBody() + else -> null + } ?: throw AssertionError("Body element is not found: ${JetPsiUtil.getElementTextWithContext(targetParent)}") + val nameValidator = JetNameValidatorImpl(validatorContainer, null, JetNameValidatorImpl.Target.PROPERTIES) + val suggestedNames = linkedSetOf(*JetNameSuggester.suggestNames(parameterType, nameValidator, "p")) + + project.executeCommand(INTRODUCE_PARAMETER) { + val renderedType = IdeDescriptorRenderers.SOURCE_CODE_SHORT_NAMES_IN_TYPES.renderType(parameterType) + val newParameter = psiFactory.createParameter("${suggestedNames.first()}: $renderedType") + + val addedParameter = runWriteAction { + val newParameterList = + if (parameterList == null) { + val klass = targetParent as? JetClass + val anchor = klass?.getTypeParameterList() ?: klass?.getNameIdentifier() + assert(anchor != null, "Invalid declaration: ${JetPsiUtil.getElementTextWithContext(targetParent)}") + + val constructor = targetParent.addAfter(psiFactory.createPrimaryConstructor(), anchor) as JetPrimaryConstructor + constructor.getValueParameterList()!! + } + else parameterList + + val lastParameter = newParameterList.getChildren().lastOrNull { it is JetParameter } as? JetParameter + if (lastParameter != null) { + val comma = newParameterList.addAfter(psiFactory.createComma(), lastParameter) + newParameterList.addAfter(newParameter, comma) as JetParameter + } + else { + val singleParameterList = psiFactory.createParameterList("(${newParameter.getText()})") + (newParameterList.replace(singleParameterList) as JetParameterList).getParameters().first() + } + } + + val introduceParameterDescriptor = + IntroduceParameterDescriptor(JetPsiUtil.deparenthesize(expression)!!, + targetParent, + functionDescriptor, + addedParameter, + parameterType) + if (editor.getSettings().isVariableInplaceRenameEnabled() && !ApplicationManager.getApplication().isUnitTestMode()) { + with(PsiDocumentManager.getInstance(project)) { + commitDocument(editor.getDocument()) + doPostponedOperationsAndUnblockDocument(editor.getDocument()) + } + + if (!KotlinInplaceParameterIntroducer(introduceParameterDescriptor, editor, project).startRefactoring(suggestedNames)) { + introduceParameterDescriptor.performRefactoring() + } + } + else { + introduceParameterDescriptor.performRefactoring() + } + } + } + + override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext?) { + if (file !is JetFile) return + selectElementsWithTargetParent( + operationName = INTRODUCE_PARAMETER, + editor = editor, + file = file, + getContainers = { elements, parent -> + parent.parents(withItself = false) + .filter { + ((it is JetClass && !it.isTrait() && it !is JetEnumEntry) || it is JetNamedFunction || it is JetSecondaryConstructor) && + ((it as JetNamedDeclaration).getValueParameterList() != null || it.getNameIdentifier() != null) + } + .toList() + }, + continuation = { elements, targetParent -> + val expression = ((elements.singleOrNull() as? JetBlockExpression)?.getStatements() ?: elements).singleOrNull() + if (expression is JetExpression) { + invoke(project, editor, expression, targetParent as JetNamedDeclaration) + } + else { + showErrorHintByKey(project, editor, "cannot.refactor.no.expression", INTRODUCE_PARAMETER) + } + } + ) + } + + override fun invoke(project: Project, elements: Array, dataContext: DataContext?) { + fail("$INTRODUCE_PARAMETER can only be invoked from editor") + } +} + +val INTRODUCE_PARAMETER: String = JetRefactoringBundle.message("introduce.property") \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinInplaceVariableIntroducer.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinInplaceVariableIntroducer.java index 5bdd5f2357a..f00d4662637 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinInplaceVariableIntroducer.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinInplaceVariableIntroducer.java @@ -26,6 +26,7 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.popup.Balloon; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Pass; import com.intellij.openapi.util.Ref; @@ -35,6 +36,7 @@ import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; import com.intellij.psi.search.SearchScope; import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; import com.intellij.ui.NonFocusableCheckBox; +import com.intellij.util.ui.PositionTracker; import kotlin.Function0; import kotlin.Function1; import kotlin.KotlinPackage; @@ -56,6 +58,9 @@ import java.util.LinkedHashSet; import java.util.List; public class KotlinInplaceVariableIntroducer extends InplaceVariableIntroducer { + public static final String TYPE_REFERENCE_VARIABLE_NAME = "TypeReferenceVariable"; + public static final String PRIMARY_VARIABLE_NAME = "PrimaryVariable"; + private static final Function0 TRUE = new Function0() { @Override public Boolean invoke() { @@ -329,10 +334,20 @@ public class KotlinInplaceVariableIntroducer e return nameSuggestions; } + protected void revalidate() { + getContentPanel().revalidate(); + if (myTarget != null) { + myBalloon.revalidate(new PositionTracker.Static(myTarget)); + } + } + protected void addTypeReferenceVariable(TemplateBuilderImpl builder) { JetTypeReference typeReference = myDeclaration.getTypeReference(); if (typeReference != null) { - builder.replaceElement(typeReference, SpecifyTypeExplicitlyAction.createTypeExpressionForTemplate(myExprType)); + builder.replaceElement(typeReference, + TYPE_REFERENCE_VARIABLE_NAME, + SpecifyTypeExplicitlyAction.createTypeExpressionForTemplate(myExprType), + false); } } diff --git a/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt b/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt new file mode 100644 index 00000000000..5986d9e0ce8 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt @@ -0,0 +1,9 @@ +class A(a: Int) { + init { + val t = 1 + 2 + } +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt.after b/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt.after new file mode 100644 index 00000000000..2e00511fb9f --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt.after @@ -0,0 +1,9 @@ +class A(a: Int, i: Int = 1 + 2) { + init { + val t = i + } +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt b/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt new file mode 100644 index 00000000000..9458f251914 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt @@ -0,0 +1,7 @@ +class A(a: Int) { + val x = 1 + 2 +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt.after b/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt.after new file mode 100644 index 00000000000..ff98be02321 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt.after @@ -0,0 +1,7 @@ +class A(a: Int, i: Int = 1 + 2) { + val x = i +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classNoParams.kt b/idea/testData/refactoring/introduceParameter/classNoParams.kt new file mode 100644 index 00000000000..1401c2487bb --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classNoParams.kt @@ -0,0 +1,7 @@ +class A { + val a = 1 + 2 +} + +fun test() { + A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classNoParams.kt.after b/idea/testData/refactoring/introduceParameter/classNoParams.kt.after new file mode 100644 index 00000000000..7df1beb5b62 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classNoParams.kt.after @@ -0,0 +1,7 @@ +class A(i: Int = 1 + 2) { + val a = i +} + +fun test() { + A() +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt b/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt new file mode 100644 index 00000000000..7b54067f1ee --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt @@ -0,0 +1,8 @@ +// TARGET: +class A(a: Int) { + fun foo() = 1 + 2 +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt.after b/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt.after new file mode 100644 index 00000000000..ac0cad2bf21 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt.after @@ -0,0 +1,8 @@ +// TARGET: +class A(a: Int, val i: Int = 1 + 2) { + fun foo() = i +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt b/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt new file mode 100644 index 00000000000..0df72335b9e --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt @@ -0,0 +1,8 @@ +fun foo(a: Int): Int { + val b = (a + 1) * 2 + return a + b +} + +fun test() { + foo(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt.after b/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt.after new file mode 100644 index 00000000000..f52bc3e9c59 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/defaultValueInParens.kt.after @@ -0,0 +1,8 @@ +fun foo(a: Int, i: Int = a + 1): Int { + val b = (i) * 2 + return a + b +} + +fun test() { + foo(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt b/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt new file mode 100644 index 00000000000..9daa9ac804f --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt @@ -0,0 +1,5 @@ +trait T + +fun foo() { + val a = object: T {} +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt.after b/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt.after new file mode 100644 index 00000000000..a7bc568087d --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt.after @@ -0,0 +1,5 @@ +trait T + +fun foo(t: T = object : T {}) { + val a = t +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt b/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt new file mode 100644 index 00000000000..d082324a3e1 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt @@ -0,0 +1,8 @@ +fun foo(a: Int): Int { + val b = (a + 1) * 2 + return a + b +} + +fun test() { + foo(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt.after b/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt.after new file mode 100644 index 00000000000..f52bc3e9c59 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt.after @@ -0,0 +1,8 @@ +fun foo(a: Int, i: Int = a + 1): Int { + val b = (i) * 2 + return a + b +} + +fun test() { + foo(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/propertyAccessor.kt b/idea/testData/refactoring/introduceParameter/propertyAccessor.kt new file mode 100644 index 00000000000..e3461ec3718 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/propertyAccessor.kt @@ -0,0 +1,4 @@ +var foo: Int = 1 + set(m: Int) { + val t = m + 1 + } \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/propertyAccessor.kt.conflicts b/idea/testData/refactoring/introduceParameter/propertyAccessor.kt.conflicts new file mode 100644 index 00000000000..dee11733077 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/propertyAccessor.kt.conflicts @@ -0,0 +1 @@ +Cannot refactor in this place \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt b/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt new file mode 100644 index 00000000000..a6a3a830f50 --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt @@ -0,0 +1,10 @@ +class A { + constructor(a: Int) { + val b = (a + 1) * 2 + val t = a + b + } +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt.after b/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt.after new file mode 100644 index 00000000000..644cae4acff --- /dev/null +++ b/idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt.after @@ -0,0 +1,10 @@ +class A { + constructor(a: Int, i: Int = a + 1) { + val b = (i) * 2 + val t = a + b + } +} + +fun test() { + A(1) +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractJetExtractionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractJetExtractionTest.kt index 32c8e64c076..1a57dfe923b 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractJetExtractionTest.kt +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/AbstractJetExtractionTest.kt @@ -25,16 +25,18 @@ import com.intellij.psi.util.PsiTreeUtil import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase +import org.jetbrains.kotlin.descriptors.ClassDescriptor +import org.jetbrains.kotlin.descriptors.FunctionDescriptor import org.jetbrains.kotlin.idea.JetLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.PluginTestCaseBase +import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor +import org.jetbrains.kotlin.idea.refactoring.JetRefactoringUtil import org.jetbrains.kotlin.idea.refactoring.introduce.extractFunction.ExtractKotlinFunctionHandler import org.jetbrains.kotlin.idea.refactoring.introduce.extractionEngine.* +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.introduceVariable.KotlinIntroduceVariableHandler -import org.jetbrains.kotlin.psi.JetDeclaration -import org.jetbrains.kotlin.psi.JetFile -import org.jetbrains.kotlin.psi.JetPackageDirective -import org.jetbrains.kotlin.psi.JetTreeVisitorVoid +import org.jetbrains.kotlin.psi.* import org.jetbrains.kotlin.renderer.DescriptorRenderer import org.jetbrains.kotlin.test.ConfigLibraryUtil import org.jetbrains.kotlin.test.InTextDirectivesUtils @@ -61,6 +63,22 @@ public abstract class AbstractJetExtractionTest() : JetLightCodeInsightFixtureTe } } + protected fun doIntroduceParameterTest(path: String) { + doTest(path) { file -> + with (KotlinIntroduceParameterHandler()) { + val target = file.findElementByComment("// TARGET:") as? JetNamedDeclaration + if (target != null) { + JetRefactoringUtil.selectExpression(fixture.getEditor(), file, true) { expression -> + invoke(fixture.getProject(), fixture.getEditor(), expression!!, target) + } + } + else { + invoke(fixture.getProject(), fixture.getEditor(), file, null) + } + } + } + } + protected fun doIntroducePropertyTest(path: String) { doTest(path) { file -> val extractionTarget = propertyTargets.single { diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/JetExtractionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/JetExtractionTestGenerated.java index 8de36ca42a3..67047cf3cd7 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/JetExtractionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/JetExtractionTestGenerated.java @@ -32,6 +32,7 @@ import java.util.regex.Pattern; JetExtractionTestGenerated.IntroduceVariable.class, JetExtractionTestGenerated.ExtractFunction.class, JetExtractionTestGenerated.IntroduceProperty.class, + JetExtractionTestGenerated.IntroduceParameter.class, }) @RunWith(JUnit3RunnerWithInners.class) public class JetExtractionTestGenerated extends AbstractJetExtractionTest { @@ -2181,4 +2182,67 @@ public class JetExtractionTestGenerated extends AbstractJetExtractionTest { doIntroducePropertyTest(fileName); } } + + @TestMetadata("idea/testData/refactoring/introduceParameter") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class IntroduceParameter extends AbstractJetExtractionTest { + public void testAllFilesPresentInIntroduceParameter() throws Exception { + JetTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/introduceParameter"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("classInAnonymousInitializer.kt") + public void testClassInAnonymousInitializer() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classInAnonymousInitializer.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("classInPropertyInitializer.kt") + public void testClassInPropertyInitializer() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classInPropertyInitializer.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("classNoParams.kt") + public void testClassNoParams() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classNoParams.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("classParameterInFunctionBody.kt") + public void testClassParameterInFunctionBody() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/classParameterInFunctionBody.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("defaultValueInParens.kt") + public void testDefaultValueInParens() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/defaultValueInParens.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("functionWithApproximatedType.kt") + public void testFunctionWithApproximatedType() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionWithApproximatedType.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("functionWithDefaultValue.kt") + public void testFunctionWithDefaultValue() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/functionWithDefaultValue.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("propertyAccessor.kt") + public void testPropertyAccessor() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/propertyAccessor.kt"); + doIntroduceParameterTest(fileName); + } + + @TestMetadata("secondaryConstructorWithDefaultValue.kt") + public void testSecondaryConstructorWithDefaultValue() throws Exception { + String fileName = JetTestUtils.navigationMetadata("idea/testData/refactoring/introduceParameter/secondaryConstructorWithDefaultValue.kt"); + doIntroduceParameterTest(fileName); + } + } }