Introduce Variable: Support extraction of string template fragments

#KT-2089 Fixed
This commit is contained in:
Alexey Sedunov
2015-11-16 16:06:01 +03:00
parent 7dbcceee79
commit f4b4b023a3
40 changed files with 592 additions and 48 deletions
@@ -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()!!
}
@@ -138,6 +138,8 @@ public fun PsiElement.getNextSiblingIgnoringWhitespaceAndComments(): PsiElement?
return siblings(withItself = false).filter { it !is PsiWhiteSpace && it !is PsiComment }.firstOrNull()
}
inline public fun <reified T : PsiElement> T.nextSiblingOfSameType() = PsiTreeUtil.getNextSiblingOfType(this, T::class.java)
public fun PsiElement?.isAncestor(element: PsiElement, strict: Boolean = false): Boolean {
return PsiTreeUtil.isAncestor(this, element, strict)
}
@@ -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
@@ -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<KtStringTemplateEntry>
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
@@ -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<PsiElement>, targetSibling: PsiElement) -> Unit
) {
fun onSelectionComplete(elements: List<PsiElement>, 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<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")}")
val containers = getContainers(elements, parent)
if (containers.isEmpty()) {
@@ -147,3 +148,29 @@ fun PsiElement.findExpressionsByCopyableDataAndClearIt(key: Key<Boolean>): 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<KtStringTemplateEntry>() ?: return null
val entry2 = file.findElementAt(endOffset - 1)?.getNonStrictParentOfType<KtStringTemplateEntry>() ?: 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()
}
@@ -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<KtFunctionLiteralArgument>()!!
val substringInfo = expressionToReplace.extractableSubstringInfo
var result = if (expressionToReplace.isFunctionLiteralOutsideParentheses()) {
val functionLiteralArgument = expressionToReplace.getStrictParentOfType<KtFunctionLiteralArgument>()!!
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<KtExpression>): 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<KtExpression>?,
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<KtExpression>(editor).showChooser(expression, allOccurrences, callback)
val chooser = object: OccurrencesChooser<KtExpression>(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<Pair<KtElement, KtElement>> {
val file = getContainingKtFile()
val physicalExpression = substringContextOrThis
val contentRange = extractableSubstringInfo?.contentRange
val references = collectDescendantsOfType<KtReferenceExpression>()
val file = physicalExpression.getContainingKtFile()
val references = physicalExpression
.collectDescendantsOfType<KtReferenceExpression> { 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) }
}
@@ -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<DeclarationDescriptor, DeclarationDescriptor>()
val weakMatches = HashMap<KtElement, KtElement>()
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
}
@@ -0,0 +1,3 @@
fun foo(param: Int): String {
return "<selection>abc${pa</selection>ram}def"
}
@@ -0,0 +1 @@
Cannot find an expression to introduce
@@ -0,0 +1,3 @@
fun foo(param: Int): String {
return "<selection>abc$pa</selection>ram"
}
@@ -0,0 +1 @@
Cannot find an expression to introduce
@@ -0,0 +1,3 @@
fun foo(a: Int): String {
return "<selection>abc$a\</selection>ndef"
}
@@ -0,0 +1 @@
Cannot find an expression to introduce
@@ -0,0 +1,6 @@
fun foo(param: Int): String {
val x = "xyfalsez"
val y = "xyFalsez"
val z = false
return "ab<selection>false</selection>def"
}
@@ -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"
}
@@ -0,0 +1,7 @@
fun foo(param: Int): String {
val x = "a1234_"
val y = "-4123a"
val z = "+1243a"
val u = 123
return "ab<selection>123</selection>def"
}
@@ -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"
}
@@ -0,0 +1,6 @@
fun foo(param: Int): String {
val x = "atrue123"
val x = "aTRUE123"
val z = true
return "ab<selection>true</selection>def"
}
@@ -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"
}
@@ -0,0 +1,6 @@
fun foo(a: Int): String {
val x = "abc$a"
val y = "abc${a}"
val z = "abc{$a}def"
return "<selection>abc$a</selection>" + "def"
}
@@ -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"
}
@@ -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<selection>${a + 1}</selection>def"
}
@@ -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"
}
@@ -0,0 +1,6 @@
fun foo(a: Int): String {
val x = "-$a"
val y = "x${a}y"
val z = "x$ay"
return "abc<selection>${a}</selection>def"
}
@@ -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"
}
@@ -0,0 +1,5 @@
fun foo(a: Int): String {
val x = "+cd$a:${a + 1}efg"
val y = "+cd$a${a + 1}efg"
return "ab<selection>cd$a:${a + 1}ef</selection>"
}
@@ -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"
}
@@ -0,0 +1,5 @@
fun foo(a: Int): String {
val x = "_c$a:${a + 1}d_"
val y = "_$a:${a + 1}d_"
return "ab<selection>c$a:${a + 1}d</selection>ef"
}
@@ -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"
}
@@ -0,0 +1,5 @@
fun foo(a: Int): String {
val x = "_ab$a:${a + 1}cd__"
val y = "_a$a:${a + 1}cd__"
return "<selection>ab$a:${a + 1}cd</selection>ef"
}
@@ -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"
}
@@ -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 """ab<selection>c$a
:${a + 1}d</selection>ef"""
}
@@ -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"""
}
@@ -0,0 +1,6 @@
fun foo(a: Int): String {
val x = "xabc$a"
val y = "${a}abcx"
val z = "xacb$a"
return "<selection>abc</selection>def"
}
@@ -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"
}
@@ -0,0 +1,6 @@
fun foo(a: Int): String {
val x = "xcd$a"
val y = "${a}cdx"
val z = "xcf$a"
return "ab<selection>cd</selection>ef"
}
@@ -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"
}
@@ -0,0 +1,6 @@
fun foo(a: Int): String {
val x = "xdef$a"
val y = "${a}defx"
val z = "xddf$a"
return "abc<selection>def</selection>"
}
@@ -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"
}
@@ -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")