diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt index 50d6a8e82cb..5df97b4d69f 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/KtPsiFactory.kt @@ -55,15 +55,25 @@ public class KtPsiFactory(private val project: Project) { return (createExpression("a?.b") as KtSafeQualifiedExpression).getOperationTokenNode() } - public fun createExpression(text: String): KtExpression { + private fun doCreateExpression(text: String): KtExpression { //TODO: '\n' below if important - some strange code indenting problems appear without it val expression = createProperty("val x =\n$text").getInitializer() ?: error("Failed to create expression from text: '$text'") + return expression + } + + public fun createExpression(text: String): KtExpression { + val expression = doCreateExpression(text) assert(expression.getText() == text) { "Failed to create expression from text: '$text', resulting expression's text was: '${expression.getText()}'" } return expression } + public fun createExpressionIfPossible(text: String): KtExpression? { + val expression = doCreateExpression(text) + return if (expression.getText() == text) expression else null + } + public fun createClassLiteral(className: String): KtClassLiteralExpression = createExpression("$className::class") as KtClassLiteralExpression @@ -291,6 +301,8 @@ public class KtPsiFactory(private val project: Project) { return stringTemplateExpression.getEntries()[0] as KtStringTemplateEntryWithExpression } + public fun createStringTemplate(content: String) = createExpression("\"$content\"") as KtStringTemplateExpression + public fun createPackageDirective(fqName: FqName): KtPackageDirective { return createFile("package ${fqName.asString()}").getPackageDirective()!! } diff --git a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt index ddf33fe7354..e0b5338003d 100644 --- a/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt +++ b/compiler/frontend/src/org/jetbrains/kotlin/psi/psiUtil/psiUtils.kt @@ -138,6 +138,8 @@ public fun PsiElement.getNextSiblingIgnoringWhitespaceAndComments(): PsiElement? return siblings(withItself = false).filter { it !is PsiWhiteSpace && it !is PsiComment }.firstOrNull() } +inline public fun T.nextSiblingOfSameType() = PsiTreeUtil.getNextSiblingOfType(this, T::class.java) + public fun PsiElement?.isAncestor(element: PsiElement, strict: Boolean = false): Boolean { return PsiTreeUtil.isAncestor(this, element, strict) } diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java index e5aded5d0bf..342ad7b094f 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/KotlinRefactoringUtil.java @@ -45,8 +45,8 @@ import org.jetbrains.kotlin.descriptors.DeclarationDescriptor; import org.jetbrains.kotlin.descriptors.impl.LocalVariableDescriptor; import org.jetbrains.kotlin.idea.KotlinBundle; import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils; -import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils; import org.jetbrains.kotlin.idea.codeInsight.DescriptorToSourceUtilsIde; +import org.jetbrains.kotlin.idea.refactoring.introduce.IntroduceUtilKt; import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers; import org.jetbrains.kotlin.idea.util.string.StringUtilKt; import org.jetbrains.kotlin.psi.*; @@ -489,7 +489,7 @@ public class KotlinRefactoringUtil { private static KtExpression findExpression( @NotNull KtFile file, int startOffset, int endOffset, boolean failOnNoExpression ) throws IntroduceRefactoringException { - KtExpression element = CodeInsightUtils.findExpression(file, startOffset, endOffset); + KtExpression element = IntroduceUtilKt.findExpressionOrStringFragment(file, startOffset, endOffset); if (element == null) { //todo: if it's infix expression => add (), then commit document then return new created expression diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractableSubstringInfo.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractableSubstringInfo.kt new file mode 100644 index 00000000000..199d1db889d --- /dev/null +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractableSubstringInfo.kt @@ -0,0 +1,99 @@ +/* + * 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 + +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import org.jetbrains.kotlin.builtins.KotlinBuiltIns +import org.jetbrains.kotlin.idea.analysis.analyzeInContext +import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade +import org.jetbrains.kotlin.idea.util.getResolutionScope +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.endOffset +import org.jetbrains.kotlin.psi.psiUtil.nextSiblingOfSameType +import org.jetbrains.kotlin.psi.psiUtil.startOffset +import org.jetbrains.kotlin.resolve.DelegatingBindingTrace +import org.jetbrains.kotlin.resolve.constants.evaluate.ConstantExpressionEvaluator +import org.jetbrains.kotlin.resolve.lazy.BodyResolveMode +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.kotlin.types.TypeUtils + +class ExtractableSubstringInfo( + val startEntry: KtStringTemplateEntry, + val endEntry: KtStringTemplateEntry, + val prefix: String, + val suffix: String, + type: KotlinType? = null +) { + private fun guessLiteralType(literal: String): KotlinType { + val facade = template.getResolutionFacade() + val builtIns = facade.moduleDescriptor.builtIns + val stringType = builtIns.stringType + + if (startEntry != endEntry || startEntry !is KtLiteralStringTemplateEntry) return stringType + + val expr = KtPsiFactory(startEntry).createExpressionIfPossible(literal) ?: return stringType + + val context = facade.analyze(template, BodyResolveMode.PARTIAL) + val scope = template.getResolutionScope(context, facade) + + val tempContext = expr.analyzeInContext(scope, template) + val trace = DelegatingBindingTrace(tempContext, "Evaluate '$literal'") + val value = ConstantExpressionEvaluator(builtIns).evaluateExpression(expr, trace) + if (value == null || value.isError) return stringType + + return value.toConstantValue(TypeUtils.NO_EXPECTED_TYPE).type + } + + val template: KtStringTemplateExpression = startEntry.parent as KtStringTemplateExpression + + val content = with(entries.map { it.text }.joinToString(separator = "")) { substring(prefix.length, length - suffix.length) } + + val type = type ?: guessLiteralType(content) + + val contentRange: TextRange + get() = TextRange(startEntry.startOffset + prefix.length, endEntry.endOffset - suffix.length) + + val entries: Sequence + get() = sequence(startEntry) { if (it != endEntry) it.nextSiblingOfSameType() else null } + + fun createExpression(): KtExpression { + val quote = template.firstChild.text + val literalValue = if (KotlinBuiltIns.isString(type)) "$quote$content$quote" else content + return KtPsiFactory(startEntry).createExpression(literalValue).apply { extractableSubstringInfo = this@ExtractableSubstringInfo } + } + + fun copy(newTemplate: KtStringTemplateExpression): ExtractableSubstringInfo { + val oldEntries = template.entries + val newEntries = newTemplate.entries + val startIndex = oldEntries.indexOf(startEntry) + val endIndex = oldEntries.indexOf(endEntry) + if (startIndex < 0 || startIndex >= newEntries.size || endIndex < 0 || endIndex >= newEntries.size) { + throw AssertionError("Old template($startIndex..$endIndex): ${template.text}, new template: ${newTemplate.text}") + } + return ExtractableSubstringInfo(newEntries[startIndex], newEntries[endIndex], prefix, suffix, type) + } +} + +var KtExpression.extractableSubstringInfo: ExtractableSubstringInfo? by UserDataProperty(Key.create("EXTRACTED_SUBSTRING_INFO")) + +val KtExpression.substringContextOrThis: KtExpression + get() = extractableSubstringInfo?.template ?: this + +val PsiElement.substringContextOrThis: PsiElement + get() = (this as? KtExpression)?.extractableSubstringInfo?.template ?: this \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt index b8f2307c7f4..a40368b276e 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceUtil.kt @@ -20,17 +20,16 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.ScrollType import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils import org.jetbrains.kotlin.idea.core.refactoring.chooseContainerElementIfNecessary import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtil -import org.jetbrains.kotlin.psi.KtExpression -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType -import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType -import org.jetbrains.kotlin.psi.psiUtil.getOutermostParentContainedIn +import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange +import org.jetbrains.kotlin.psi.* +import org.jetbrains.kotlin.psi.psiUtil.* fun showErrorHint(project: Project, editor: Editor, message: String, title: String) { CodeInsightUtils.showErrorHint(project, editor, message, title, null) @@ -48,8 +47,9 @@ fun selectElementsWithTargetSibling( continuation: (elements: List, targetSibling: PsiElement) -> Unit ) { fun onSelectionComplete(elements: List, targetContainer: PsiElement) { - val parent = PsiTreeUtil.findCommonParent(elements) - ?: throw AssertionError("Should have at least one parent: ${elements.joinToString("\n")}") + val physicalElements = elements.map { it.substringContextOrThis } + val parent = PsiTreeUtil.findCommonParent(physicalElements) + ?: throw AssertionError("Should have at least one parent: ${physicalElements.joinToString("\n")}") if (parent == targetContainer) { continuation(elements, elements.first()) @@ -80,8 +80,9 @@ fun selectElementsWithTargetParent( } fun selectTargetContainer(elements: List) { - val parent = PsiTreeUtil.findCommonParent(elements) - ?: throw AssertionError("Should have at least one parent: ${elements.joinToString("\n")}") + val physicalElements = elements.map { it.substringContextOrThis } + val parent = PsiTreeUtil.findCommonParent(physicalElements) + ?: throw AssertionError("Should have at least one parent: ${physicalElements.joinToString("\n")}") val containers = getContainers(elements, parent) if (containers.isEmpty()) { @@ -147,3 +148,29 @@ fun PsiElement.findExpressionsByCopyableDataAndClearIt(key: Key): List< results.forEach { it.putCopyableUserData(key, null) } return results } + +fun findExpressionOrStringFragment(file: KtFile, startOffset: Int, endOffset: Int): KtExpression? { + CodeInsightUtils.findExpression(file, startOffset, endOffset)?.let { return it } + + val entry1 = file.findElementAt(startOffset)?.getNonStrictParentOfType() ?: return null + val entry2 = file.findElementAt(endOffset - 1)?.getNonStrictParentOfType() ?: return null + + if (entry1 == entry2 && entry1 is KtStringTemplateEntryWithExpression) return entry1.expression + + val stringTemplate = entry1.parent as? KtStringTemplateExpression ?: return null + if (entry2.parent != stringTemplate) return null + + val templateOffset = stringTemplate.startOffset + if (stringTemplate.getContentRange().equalsToRange(startOffset - templateOffset, endOffset - templateOffset)) return stringTemplate + + val prefixOffset = startOffset - entry1.startOffset + if (entry1 !is KtLiteralStringTemplateEntry && prefixOffset > 0) return null + + val suffixOffset = endOffset - entry2.startOffset + if (entry2 !is KtLiteralStringTemplateEntry && suffixOffset < entry2.textLength) return null + + val prefix = entry1.text.substring(0, prefixOffset) + val suffix = entry2.text.substring(suffixOffset) + + return ExtractableSubstringInfo(entry1, entry2, prefix, suffix).createExpression() +} \ No newline at end of file diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt index 148341563a7..e4cfdf65b19 100644 --- a/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt +++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/introduce/introduceVariable/KotlinIntroduceVariableHandler.kt @@ -21,6 +21,7 @@ import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Editor import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile @@ -45,10 +46,7 @@ import org.jetbrains.kotlin.idea.intentions.ConvertToBlockBodyIntention import org.jetbrains.kotlin.idea.intentions.RemoveCurlyBracesFromTemplateIntention import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringBundle import org.jetbrains.kotlin.idea.refactoring.KotlinRefactoringUtil -import org.jetbrains.kotlin.idea.refactoring.introduce.KotlinIntroduceHandlerBase -import org.jetbrains.kotlin.idea.refactoring.introduce.findElementByCopyableDataAndClearIt -import org.jetbrains.kotlin.idea.refactoring.introduce.findExpressionByCopyableDataAndClearIt -import org.jetbrains.kotlin.idea.refactoring.introduce.findExpressionsByCopyableDataAndClearIt +import org.jetbrains.kotlin.idea.refactoring.introduce.* import org.jetbrains.kotlin.idea.resolve.ResolutionFacade import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.ShortenReferences @@ -105,17 +103,34 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { return newContainer.findElementAt(offset)?.parentsWithSelf?.firstOrNull { (it as? KtExpression)?.text == text } } - private fun replaceExpression(replace: KtExpression, addToReferences: Boolean): KtExpression { - val isActualExpression = expression == replace + private fun replaceExpression(expressionToReplace: KtExpression, addToReferences: Boolean): KtExpression { + val isActualExpression = expression == expressionToReplace val replacement = psiFactory.createExpression(nameSuggestion) - var result = if (replace.isFunctionLiteralOutsideParentheses()) { - val functionLiteralArgument = replace.getStrictParentOfType()!! + val substringInfo = expressionToReplace.extractableSubstringInfo + var result = if (expressionToReplace.isFunctionLiteralOutsideParentheses()) { + val functionLiteralArgument = expressionToReplace.getStrictParentOfType()!! val newCallExpression = functionLiteralArgument.moveInsideParenthesesAndReplaceWith(replacement, bindingContext) newCallExpression.valueArguments.last().getArgumentExpression()!! } + else if (substringInfo != null) { + with(substringInfo) { + val parent = startEntry.parent + + psiFactory.createStringTemplate(prefix).entries.singleOrNull()?.let { parent.addBefore(it, startEntry) } + + val refEntry = psiFactory.createBlockStringTemplateEntry(replacement) + val addedRefEntry = parent.addBefore(refEntry, startEntry) as KtStringTemplateEntryWithExpression + + psiFactory.createStringTemplate(suffix).entries.singleOrNull()?.let { parent.addAfter(it, endEntry) } + + parent.deleteChildRange(startEntry, endEntry) + + addedRefEntry.expression!! + } + } else { - replace.replace(replacement) as KtExpression + expressionToReplace.replace(replacement) as KtExpression } val parent = result.parent @@ -279,7 +294,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { expression.putCopyableUserData(EXPRESSION_KEY, true) for (replace in allReplaces) { - replace.putCopyableUserData(REPLACE_KEY, true) + replace.substringContextOrThis.putCopyableUserData(REPLACE_KEY, true) } commonParent.putCopyableUserData(COMMON_PARENT_KEY, true) @@ -290,7 +305,12 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { val newExpression = newCommonContainer.findExpressionByCopyableDataAndClearIt(EXPRESSION_KEY) val newCommonParent = newCommonContainer.findElementByCopyableDataAndClearIt(COMMON_PARENT_KEY) - val newAllReplaces = newCommonContainer.findExpressionsByCopyableDataAndClearIt(REPLACE_KEY) + val newAllReplaces = (allReplaces zip newCommonContainer.findExpressionsByCopyableDataAndClearIt(REPLACE_KEY)).map { + val (originalReplace, newReplace) = it + originalReplace.extractableSubstringInfo?.let { + originalReplace.apply { extractableSubstringInfo = it.copy(newReplace as KtStringTemplateExpression) } + } ?: newReplace + } runRefactoring(newExpression, newCommonContainer, newCommonParent, newAllReplaces) } @@ -299,7 +319,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { private fun calculateAnchor(commonParent: PsiElement, commonContainer: PsiElement, allReplaces: List): PsiElement? { if (commonParent != commonContainer) return commonParent.parentsWithSelf.firstOrNull { it.parent == commonContainer } - val startOffset = allReplaces.fold(commonContainer.endOffset) { offset, expr -> Math.min(offset, expr.startOffset) } + val startOffset = allReplaces.fold(commonContainer.endOffset) { offset, expr -> Math.min(offset, expr.substringContextOrThis.startOffset) } return commonContainer.allChildren.lastOrNull { it.textRange.contains(startOffset) } ?: return null } @@ -376,38 +396,42 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { occurrencesToReplace: List?, onNonInteractiveFinish: ((KtProperty) -> Unit)? ) { - val parent = expression.parent + val substringInfo = expression.extractableSubstringInfo + val physicalExpression = expression.substringContextOrThis + + val parent = physicalExpression.parent when { parent is KtQualifiedExpression -> { - if (parent.receiverExpression != expression) { + if (parent.receiverExpression != physicalExpression) { return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) } } - expression is KtStatementExpression -> + physicalExpression is KtStatementExpression -> return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) - parent is KtOperationExpression && parent.operationReference == expression -> + parent is KtOperationExpression && parent.operationReference == physicalExpression -> return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) } - PsiTreeUtil.getNonStrictParentOfType(expression, + PsiTreeUtil.getNonStrictParentOfType(physicalExpression, KtTypeReference::class.java, KtConstructorCalleeExpression::class.java, KtSuperExpression::class.java)?.let { return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.container")) } - val expressionType = bindingContext.getType(expression) //can be null or error type - val scope = expression.getResolutionScope(bindingContext, resolutionFacade) - val dataFlowInfo = bindingContext.getDataFlowInfo(expression) + val expressionType = substringInfo?.type ?: bindingContext.getType(physicalExpression) //can be null or error type + val scope = physicalExpression.getResolutionScope(bindingContext, resolutionFacade) + val dataFlowInfo = bindingContext.getDataFlowInfo(physicalExpression) val bindingTrace = ObservableBindingTrace(BindingTraceContext()) - val typeNoExpectedType = expression.computeTypeInfoInContext(scope, expression, bindingTrace, dataFlowInfo).type + val typeNoExpectedType = substringInfo?.type + ?: physicalExpression.computeTypeInfoInContext(scope, physicalExpression, bindingTrace, dataFlowInfo).type val noTypeInference = expressionType != null && typeNoExpectedType != null && !KotlinTypeChecker.DEFAULT.equalTypes(expressionType, typeNoExpectedType) - if (expressionType == null && bindingContext.get(BindingContext.QUALIFIER, expression) != null) { + if (expressionType == null && bindingContext.get(BindingContext.QUALIFIER, physicalExpression) != null) { return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.package.expression")) } @@ -426,9 +450,11 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { OccurrencesChooser.ReplaceChoice.ALL -> allOccurrences else -> listOf(expression) } - val replaceOccurrence = expression.shouldReplaceOccurrence(bindingContext, container) || allReplaces.size > 1 + val replaceOccurrence = substringInfo != null + || expression.shouldReplaceOccurrence(bindingContext, container) + || allReplaces.size > 1 - val commonParent = PsiTreeUtil.findCommonParent(allReplaces) as KtElement + val commonParent = PsiTreeUtil.findCommonParent(allReplaces.map { it.substringContextOrThis }) as KtElement var commonContainer = commonParent.getContainer()!! if (commonContainer != container && container.isAncestor(commonContainer, true)) { commonContainer = container @@ -439,7 +465,11 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { calculateAnchor(commonParent, commonContainer, allReplaces), NewDeclarationNameValidator.Target.VARIABLES ) - val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression, null, bindingContext, validator, "value") + val suggestedNames = KotlinNameSuggester.suggestNamesByExpressionAndType(expression, + substringInfo?.type, + bindingContext, + validator, + "value") val introduceVariableContext = IntroduceVariableContext( expression, suggestedNames.iterator().next(), allReplaces, commonContainer, commonParent, replaceOccurrence, noTypeInference, expressionType, bindingContext, resolutionFacade @@ -477,7 +507,12 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { } if (isInplaceAvailable && occurrencesToReplace == null) { - OccurrencesChooser.simpleChooser(editor).showChooser(expression, allOccurrences, callback) + val chooser = object: OccurrencesChooser(editor) { + override fun getOccurrenceRange(occurrence: KtExpression): TextRange? { + return occurrence.extractableSubstringInfo?.contentRange ?: occurrence.textRange + } + } + chooser.showChooser(expression, allOccurrences, callback) } else { callback.pass(OccurrencesChooser.ReplaceChoice.ALL) @@ -494,13 +529,17 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { resolutionFacade: ResolutionFacade, originalContext: BindingContext ): List> { - val file = getContainingKtFile() + val physicalExpression = substringContextOrThis + val contentRange = extractableSubstringInfo?.contentRange - val references = collectDescendantsOfType() + val file = physicalExpression.getContainingKtFile() + + val references = physicalExpression + .collectDescendantsOfType { contentRange == null || contentRange.contains(it.textRange) } fun isResolvableNextTo(neighbour: KtExpression): Boolean { val scope = neighbour.getResolutionScope(originalContext, resolutionFacade) - val newContext = analyzeInContext(scope, neighbour) + val newContext = physicalExpression.analyzeInContext(scope, neighbour) val project = file.project return references.all { val originalDescriptor = originalContext[BindingContext.REFERENCE_TARGET, it] @@ -515,8 +554,8 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { } } - val firstContainer = getContainer() ?: return emptyList() - val firstOccurrenceContainer = getOccurrenceContainer() ?: return emptyList() + val firstContainer = physicalExpression.getContainer() ?: return emptyList() + val firstOccurrenceContainer = physicalExpression.getOccurrenceContainer() ?: return emptyList() val containers = SmartList(firstContainer) val occurrenceContainers = SmartList(firstOccurrenceContainer) @@ -554,8 +593,10 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { val expression = expressionToExtract?.let { KtPsiUtil.safeDeparenthesize(it) } ?: return showErrorHint(project, editor, KotlinRefactoringBundle.message("cannot.refactor.no.expression")) - val resolutionFacade = expression.getResolutionFacade() - val bindingContext = resolutionFacade.analyze(expression, BodyResolveMode.FULL) + val physicalExpression = expression.substringContextOrThis + + val resolutionFacade = physicalExpression.getResolutionFacade() + val bindingContext = resolutionFacade.analyze(physicalExpression, BodyResolveMode.FULL) fun runWithChosenContainers(container: KtElement, occurrenceContainer: KtElement) { doRefactoring(project, editor, expression, container, occurrenceContainer, resolutionFacade, bindingContext, occurrencesToReplace, onNonInteractiveFinish) @@ -580,6 +621,7 @@ object KotlinIntroduceVariableHandler : KotlinIntroduceHandlerBase() { override fun invoke(project: Project, editor: Editor, file: PsiFile, dataContext: DataContext) { if (file !is KtFile) return + try { KotlinRefactoringUtil.selectExpression(editor, file) { doRefactoring(project, editor, it, null, null) } } diff --git a/idea/src/org/jetbrains/kotlin/idea/util/psi/patternMatching/KotlinPsiUnifier.kt b/idea/src/org/jetbrains/kotlin/idea/util/psi/patternMatching/KotlinPsiUnifier.kt index 8a2eea70be8..0539b523418 100644 --- a/idea/src/org/jetbrains/kotlin/idea/util/psi/patternMatching/KotlinPsiUnifier.kt +++ b/idea/src/org/jetbrains/kotlin/idea/util/psi/patternMatching/KotlinPsiUnifier.kt @@ -23,6 +23,8 @@ import com.intellij.util.containers.ContainerUtil import com.intellij.util.containers.MultiMap import org.jetbrains.kotlin.descriptors.* import org.jetbrains.kotlin.idea.core.refactoring.getContextForContainingDeclarationBody +import org.jetbrains.kotlin.idea.refactoring.introduce.ExtractableSubstringInfo +import org.jetbrains.kotlin.idea.refactoring.introduce.extractableSubstringInfo import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers import org.jetbrains.kotlin.idea.util.psi.patternMatching.KotlinPsiRange.Empty import org.jetbrains.kotlin.idea.util.psi.patternMatching.UnificationResult.* @@ -60,7 +62,7 @@ public interface UnificationResult { override fun and(other: Status): Status = this }; - public abstract fun and(other: Status): Status + public abstract infix fun and(other: Status): Status } object Unmatched : UnificationResult { @@ -111,6 +113,7 @@ public class KotlinPsiUnifier( val declarationPatternsToTargets = MultiMap() val weakMatches = HashMap() var checkEquivalence: Boolean = false + var targetSubstringInfo: ExtractableSubstringInfo? = null private fun KotlinPsiRange.getBindingContext(): BindingContext { val element = (this as? KotlinPsiRange.ListRange)?.startElement as? KtElement @@ -702,7 +705,73 @@ public class KotlinPsiUnifier( return targetElementType != null && KotlinTypeChecker.DEFAULT.isSubtypeOf(targetElementType, parameter.expectedType) } + private fun doUnifyStringTemplateFragments(target: KtStringTemplateExpression, pattern: ExtractableSubstringInfo): Status { + val prefixLength = pattern.prefix.length + val suffixLength = pattern.suffix.length + val targetEntries = target.entries + val patternEntries = pattern.entries.toList() + for ((index, targetEntry) in targetEntries.withIndex()) { + if (index + patternEntries.size > targetEntries.size) return UNMATCHED + + val targetEntryText = targetEntry.text + + if (pattern.startEntry == pattern.endEntry && (prefixLength > 0 || suffixLength > 0)) { + if (targetEntry !is KtLiteralStringTemplateEntry) continue + + val patternText = with(pattern.startEntry.text) { substring(prefixLength, length - suffixLength) } + val i = targetEntryText.indexOf(patternText) + if (i < 0) continue + val targetPrefix = targetEntryText.substring(0, i) + val targetSuffix = targetEntryText.substring(i + patternText.length) + targetSubstringInfo = ExtractableSubstringInfo(targetEntry, targetEntry, targetPrefix, targetSuffix, pattern.type) + return MATCHED + } + + val matchStartByText = pattern.startEntry is KtLiteralStringTemplateEntry + val matchEndByText = pattern.endEntry is KtLiteralStringTemplateEntry + + val targetPrefix = if (matchStartByText) { + if (targetEntry !is KtLiteralStringTemplateEntry) continue + + val patternText = pattern.startEntry.text.substring(prefixLength) + if (!targetEntryText.endsWith(patternText)) continue + targetEntryText.substring(0, targetEntryText.length - patternText.length) + } + else "" + + val lastTargetEntry = targetEntries[index + patternEntries.lastIndex] + + val targetSuffix = if (matchEndByText) { + if (lastTargetEntry !is KtLiteralStringTemplateEntry) continue + + val patternText = with(pattern.endEntry.text) { substring(0, length - suffixLength) } + val lastTargetEntryText = lastTargetEntry.text + if (!lastTargetEntryText.startsWith(patternText)) continue + lastTargetEntryText.substring(patternText.length) + } + else "" + + val fromIndex = if (matchStartByText) 1 else 0 + val toIndex = if (matchEndByText) patternEntries.lastIndex - 1 else patternEntries.lastIndex + val status = (fromIndex..toIndex).fold(MATCHED) { s, patternEntryIndex -> + val targetEntryToUnify = targetEntries[index + patternEntryIndex] + val patternEntryToUnify = patternEntries[patternEntryIndex] + if (s != UNMATCHED) s and doUnify(targetEntryToUnify, patternEntryToUnify) else s + } + if (status == UNMATCHED) continue + targetSubstringInfo = ExtractableSubstringInfo(targetEntry, lastTargetEntry, targetPrefix, targetSuffix, pattern.type) + return status + } + + return UNMATCHED + } + fun doUnify(target: KotlinPsiRange, pattern: KotlinPsiRange): Status { + (pattern.elements.singleOrNull() as? KtExpression)?.extractableSubstringInfo?.let { + val targetTemplate = target.elements.singleOrNull() as? KtStringTemplateExpression ?: return UNMATCHED + return doUnifyStringTemplateFragments(targetTemplate, it) + } + val targetElements = target.elements val patternElements = pattern.elements if (targetElements.size() != patternElements.size()) return UNMATCHED @@ -828,8 +897,14 @@ public class KotlinPsiUnifier( when { substitution.size() != descriptorToParameter.size() -> Unmatched - status == MATCHED -> - if (weakMatches.isEmpty()) StronglyMatched(target, substitution) else WeaklyMatched(target, substitution, weakMatches) + status == MATCHED -> { + val targetRange = targetSubstringInfo?.createExpression()?.toRange() ?: target + if (weakMatches.isEmpty()) { + StronglyMatched(targetRange, substitution) + } else { + WeaklyMatched(targetRange, substitution, weakMatches) + } + } else -> Unmatched } diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt new file mode 100644 index 00000000000..8d3f78f7b2c --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt @@ -0,0 +1,3 @@ +fun foo(param: Int): String { + return "abc${param}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt.conflicts b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt.conflicts new file mode 100644 index 00000000000..0c9a536b18e --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt.conflicts @@ -0,0 +1 @@ +Cannot find an expression to introduce \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt new file mode 100644 index 00000000000..a7ef59f2d42 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt @@ -0,0 +1,3 @@ +fun foo(param: Int): String { + return "abc$param" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt.conflicts b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt.conflicts new file mode 100644 index 00000000000..0c9a536b18e --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt.conflicts @@ -0,0 +1 @@ +Cannot find an expression to introduce \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt new file mode 100644 index 00000000000..3cc3e6f3471 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt @@ -0,0 +1,3 @@ +fun foo(a: Int): String { + return "abc$a\ndef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt.conflicts b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt.conflicts new file mode 100644 index 00000000000..0c9a536b18e --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt.conflicts @@ -0,0 +1 @@ +Cannot find an expression to introduce \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt new file mode 100644 index 00000000000..1ec4ee42caf --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt @@ -0,0 +1,6 @@ +fun foo(param: Int): String { + val x = "xyfalsez" + val y = "xyFalsez" + val z = false + return "abfalsedef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt.after new file mode 100644 index 00000000000..39c2dc543c6 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt.after @@ -0,0 +1,7 @@ +fun foo(param: Int): String { + val b = false + val x = "xy${b}z" + val y = "xyFalsez" + val z = false + return "ab${b}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt new file mode 100644 index 00000000000..826c73d618f --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt @@ -0,0 +1,7 @@ +fun foo(param: Int): String { + val x = "a1234_" + val y = "-4123a" + val z = "+1243a" + val u = 123 + return "ab123def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt.after new file mode 100644 index 00000000000..7be885da6e4 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt.after @@ -0,0 +1,8 @@ +fun foo(param: Int): String { + val i = 123 + val x = "a${i}4_" + val y = "-4${i}a" + val z = "+1243a" + val u = 123 + return "ab${i}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt new file mode 100644 index 00000000000..7d6dde97219 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt @@ -0,0 +1,6 @@ +fun foo(param: Int): String { + val x = "atrue123" + val x = "aTRUE123" + val z = true + return "abtruedef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt.after new file mode 100644 index 00000000000..ba078c9b293 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt.after @@ -0,0 +1,7 @@ +fun foo(param: Int): String { + val b = true + val x = "a${b}123" + val x = "aTRUE123" + val z = true + return "ab${b}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt new file mode 100644 index 00000000000..8b10cb97d4c --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "abc$a" + val y = "abc${a}" + val z = "abc{$a}def" + return "abc$a" + "def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt.after new file mode 100644 index 00000000000..2e2d3540f7c --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val s = "abc$a" + val x = s + val y = s + val z = "abc{$a}def" + return s + "def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt new file mode 100644 index 00000000000..2a19834a5bc --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "-${a + 1}" + val y = "x${a + 1}y" + val z = "x${a - 1}y" + return "abc${a + 1}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt.after new file mode 100644 index 00000000000..920ba830a21 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val i = a + 1 + val x = "-$i" + val y = "x${i}y" + val z = "x${a - 1}y" + return "abc${i}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt new file mode 100644 index 00000000000..b3e5ea58080 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "-$a" + val y = "x${a}y" + val z = "x$ay" + return "abc${a}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt.after new file mode 100644 index 00000000000..60d242b3bb8 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val a1 = a + val x = "-$a1" + val y = "x${a1}y" + val z = "x$ay" + return "abc${a1}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt new file mode 100644 index 00000000000..e059fcf9732 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt @@ -0,0 +1,5 @@ +fun foo(a: Int): String { + val x = "+cd$a:${a + 1}efg" + val y = "+cd$a${a + 1}efg" + return "abcd$a:${a + 1}ef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt.after new file mode 100644 index 00000000000..3bc1c2c2842 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt.after @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val s = "cd$a:${a + 1}ef" + val x = "+${s}g" + val y = "+cd$a${a + 1}efg" + return "ab$s" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt new file mode 100644 index 00000000000..e81ce108d0d --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt @@ -0,0 +1,5 @@ +fun foo(a: Int): String { + val x = "_c$a:${a + 1}d_" + val y = "_$a:${a + 1}d_" + return "abc$a:${a + 1}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt.after new file mode 100644 index 00000000000..fc66f321bf2 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt.after @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val s = "c$a:${a + 1}d" + val x = "_${s}_" + val y = "_$a:${a + 1}d_" + return "ab${s}ef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt new file mode 100644 index 00000000000..f77d65ff3a2 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt @@ -0,0 +1,5 @@ +fun foo(a: Int): String { + val x = "_ab$a:${a + 1}cd__" + val y = "_a$a:${a + 1}cd__" + return "ab$a:${a + 1}cdef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt.after new file mode 100644 index 00000000000..58ec28df9cd --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt.after @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val s = "ab$a:${a + 1}cd" + val x = "_${s}__" + val y = "_a$a:${a + 1}cd__" + return "${s}ef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt new file mode 100644 index 00000000000..d4f1d34ed57 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt @@ -0,0 +1,9 @@ +fun foo(a: Int): String { + val x = """_c$a + :${a + 1}d_""" + val y = "_$a:${a + 1}d_" + val z = """_c$a:${a + 1}d_""" + val u = "_c$a\n:${a + 1}d_" + return """abc$a + :${a + 1}def""" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt.after new file mode 100644 index 00000000000..31a44acbe3f --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt.after @@ -0,0 +1,9 @@ +fun foo(a: Int): String { + val s = """c$a + :${a + 1}d""" + val x = """_${s}_""" + val y = "_$a:${a + 1}d_" + val z = """_c$a:${a + 1}d_""" + val u = "_c$a\n:${a + 1}d_" + return """ab${s}ef""" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt new file mode 100644 index 00000000000..9dc8211a168 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "xabc$a" + val y = "${a}abcx" + val z = "xacb$a" + return "abcdef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt.after new file mode 100644 index 00000000000..fb4778f7e07 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val s = "abc" + val x = "x$s$a" + val y = "${a}${s}x" + val z = "xacb$a" + return "${s}def" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt new file mode 100644 index 00000000000..091f98c39f1 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "xcd$a" + val y = "${a}cdx" + val z = "xcf$a" + return "abcdef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt.after new file mode 100644 index 00000000000..f83413680bb --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val s = "cd" + val x = "x$s$a" + val y = "${a}${s}x" + val z = "xcf$a" + return "ab${s}ef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt new file mode 100644 index 00000000000..53d3c4eff51 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt @@ -0,0 +1,6 @@ +fun foo(a: Int): String { + val x = "xdef$a" + val y = "${a}defx" + val z = "xddf$a" + return "abcdef" +} \ No newline at end of file diff --git a/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt.after b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt.after new file mode 100644 index 00000000000..ca6300ff380 --- /dev/null +++ b/idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt.after @@ -0,0 +1,7 @@ +fun foo(a: Int): String { + val s = "def" + val x = "x$s$a" + val y = "${a}${s}x" + val z = "xddf$a" + return "abc$s" +} \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java index 1f1abfd5543..2f0a0c80f16 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/refactoring/introduce/ExtractionTestGenerated.java @@ -507,6 +507,111 @@ public class ExtractionTestGenerated extends AbstractExtractionTest { doIntroduceVariableTest(fileName); } } + + @TestMetadata("idea/testData/refactoring/introduceVariable/stringTemplates") + @TestDataPath("$PROJECT_ROOT") + @RunWith(JUnit3RunnerWithInners.class) + public static class StringTemplates extends AbstractExtractionTest { + public void testAllFilesPresentInStringTemplates() throws Exception { + KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/refactoring/introduceVariable/stringTemplates"), Pattern.compile("^(.+)\\.kt$"), true); + } + + @TestMetadata("brokenEntryWithBlockExpr.kt") + public void testBrokenEntryWithBlockExpr() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithBlockExpr.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("brokenEntryWithExpr.kt") + public void testBrokenEntryWithExpr() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/brokenEntryWithExpr.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("brokenEscapeEntry.kt") + public void testBrokenEscapeEntry() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/brokenEscapeEntry.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("extractFalse.kt") + public void testExtractFalse() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/extractFalse.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("extractIntegerLiteral.kt") + public void testExtractIntegerLiteral() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/extractIntegerLiteral.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("extractTrue.kt") + public void testExtractTrue() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/extractTrue.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("fullContent.kt") + public void testFullContent() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/fullContent.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("fullEntryWithBlockExpr.kt") + public void testFullEntryWithBlockExpr() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithBlockExpr.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("fullEntryWithSimpleName.kt") + public void testFullEntryWithSimpleName() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/fullEntryWithSimpleName.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("multipleEntriesWithPrefix.kt") + public void testMultipleEntriesWithPrefix() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithPrefix.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("multipleEntriesWithSubstring.kt") + public void testMultipleEntriesWithSubstring() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSubstring.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("multipleEntriesWithSuffix.kt") + public void testMultipleEntriesWithSuffix() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/multipleEntriesWithSuffix.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("rawTemplateWithSubstring.kt") + public void testRawTemplateWithSubstring() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/rawTemplateWithSubstring.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("singleEntryPrefix.kt") + public void testSingleEntryPrefix() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/singleEntryPrefix.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("singleEntrySubstring.kt") + public void testSingleEntrySubstring() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySubstring.kt"); + doIntroduceVariableTest(fileName); + } + + @TestMetadata("singleEntrySuffix.kt") + public void testSingleEntrySuffix() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/introduceVariable/stringTemplates/singleEntrySuffix.kt"); + doIntroduceVariableTest(fileName); + } + } } @TestMetadata("idea/testData/refactoring/extractFunction")