mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
[Kotlin] Implement multi-dollar interpolation support for paste handler
KTIJ-30945 GitOrigin-RevId: 9e6d34ffc8af9733c151d81ed32e2c1adacd6c2c
This commit is contained in:
committed by
intellij-monorepo-bot
parent
8c164f9d79
commit
fedf6094a8
@@ -0,0 +1,256 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.codeinsights.impl.base
|
||||
|
||||
import com.intellij.openapi.util.text.StringUtil
|
||||
import org.jetbrains.kotlin.psi.KtBlockStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtEscapeStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtLiteralStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtPsiFactory
|
||||
import org.jetbrains.kotlin.psi.KtSimpleNameStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
|
||||
import org.jetbrains.kotlin.psi.KtStringTemplateEntryWithExpression
|
||||
import org.jetbrains.kotlin.psi.KtStringTemplateExpression
|
||||
|
||||
val dollarLiteralExpressions: Array<String> = arrayOf(
|
||||
"'$'", "\"$\""
|
||||
)
|
||||
|
||||
private object PrefixedStringTemplateUtils
|
||||
|
||||
/**
|
||||
* Convert this string template to a new one with the specified interpolation prefix length.
|
||||
* The content of the template does not change.
|
||||
* If the template already has the required prefix, then it is returned as is.
|
||||
* Otherwise, a new template will be created with all its interpolation entries converted to the new prefix.
|
||||
* The function is not recursive — it doesn't update nested template expressions.
|
||||
* In case the passed template contains syntax errors and cannot be recreated with a different prefix, it will be returned unchanged.
|
||||
*/
|
||||
fun KtStringTemplateExpression.changeInterpolationPrefix(
|
||||
newPrefixLength: Int,
|
||||
isSourceSingleQuoted: Boolean,
|
||||
isDestinationSingleQuoted: Boolean,
|
||||
onEntryUpdate: (StringTemplateEntryUpdateInfo) -> Unit = {},
|
||||
): KtStringTemplateExpression {
|
||||
require(newPrefixLength >= 0) { "Unexpected string template prefix length: $newPrefixLength" }
|
||||
if (this.interpolationPrefix?.textLength == newPrefixLength) return this
|
||||
val factory = KtPsiFactory(project)
|
||||
val contentText = buildString {
|
||||
for ((index, entry) in entries.withIndex()) {
|
||||
val newPrefixLength = maxOf(newPrefixLength, 1)
|
||||
val newEntries: List<EntryUpdateDiff> = when (entry) {
|
||||
// prefix length 0 means non-prefixed string — its entries start with a single $
|
||||
is KtStringTemplateEntryWithExpression -> {
|
||||
val newEntry = entry.changePrefixLength(newPrefixLength).unescapeIfPossible(newPrefixLength)
|
||||
listOf(entry.asOneToOneDiff(newEntry.text))
|
||||
}
|
||||
is KtLiteralStringTemplateEntry -> {
|
||||
entry.escapeIfNecessary(newPrefixLength, isSourceSingleQuoted, isDestinationSingleQuoted)
|
||||
}
|
||||
is KtEscapeStringTemplateEntry -> {
|
||||
listOf(entry.unescapeIfPossible(newPrefixLength)).map { unescapedEntry ->
|
||||
entry.asOneToOneDiff(unescapedEntry.text)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
listOf(entry.asOneToOneDiff(entry.text))
|
||||
}
|
||||
}
|
||||
val newText = newEntries.joinToString(separator = "") { it.newText }
|
||||
append(newText)
|
||||
if (newText != entry.text) {
|
||||
onEntryUpdate(StringTemplateEntryUpdateInfo(index, entry, newEntries))
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (newPrefixLength == 0) {
|
||||
if (isDestinationSingleQuoted) {
|
||||
factory.createStringTemplate(contentText)
|
||||
} else {
|
||||
// hack until KtPsiFactory has a triple-quoted template generation
|
||||
factory.createExpression("\"\"\"$contentText\"\"\"") as KtStringTemplateExpression
|
||||
}
|
||||
} else {
|
||||
factory.createMultiDollarStringTemplate(contentText, newPrefixLength, forceMultiQuoted = !isDestinationSingleQuoted)
|
||||
}
|
||||
}
|
||||
|
||||
class StringTemplateEntryUpdateInfo(
|
||||
val index: Int,
|
||||
val oldEntry: KtStringTemplateEntry,
|
||||
val diffs: List<EntryUpdateDiff>,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return """
|
||||
"${oldEntry.text}"
|
||||
${diffs.joinToString("\n")}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
class EntryUpdateDiff(
|
||||
val oldRange: IntRange,
|
||||
val oldText: String,
|
||||
val newText: String,
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return """$oldRange: "$oldText" -> "$newText""""
|
||||
}
|
||||
}
|
||||
|
||||
internal fun KtStringTemplateEntry.asOneToOneDiff(newText: String): EntryUpdateDiff {
|
||||
return EntryUpdateDiff(0..<textLength, text, newText)
|
||||
}
|
||||
|
||||
fun KtStringTemplateEntryWithExpression.changePrefixLength(prefixLength: Int): KtStringTemplateEntryWithExpression {
|
||||
require(prefixLength > 0) { "Unexpected string template prefix length: $prefixLength" }
|
||||
|
||||
val replacement = when (this) {
|
||||
is KtSimpleNameStringTemplateEntry -> changePrefixLength(prefixLength)
|
||||
is KtBlockStringTemplateEntry -> changePrefixLength(prefixLength)
|
||||
else -> this
|
||||
}
|
||||
|
||||
return replacement
|
||||
}
|
||||
|
||||
fun KtBlockStringTemplateEntry.changePrefixLength(prefixLength: Int): KtStringTemplateEntryWithExpression {
|
||||
require(prefixLength > 0) { "Unexpected string template entry prefix length: $prefixLength" }
|
||||
val ktPsiFactory = KtPsiFactory(project)
|
||||
val expression = this.expression
|
||||
val replacement = if (expression != null) {
|
||||
ktPsiFactory.createMultiDollarBlockStringTemplateEntry(
|
||||
expression,
|
||||
prefixLength = prefixLength,
|
||||
)
|
||||
} else {
|
||||
// In case of incomplete code with no KtExpression inside the block, create a replacement from scratch
|
||||
val prefix = "$".repeat(prefixLength)
|
||||
val incompleteExpression = ktPsiFactory.createExpression("$prefix\"$prefix{}\"")
|
||||
(incompleteExpression as KtStringTemplateExpression).entries.single() as KtBlockStringTemplateEntry
|
||||
}
|
||||
|
||||
return replacement
|
||||
}
|
||||
|
||||
fun KtSimpleNameStringTemplateEntry.changePrefixLength(prefixLength: Int): KtSimpleNameStringTemplateEntry {
|
||||
require(prefixLength > 0) { "Unexpected string template entry prefix length: $prefixLength" }
|
||||
val ktPsiFactory = KtPsiFactory(project)
|
||||
return ktPsiFactory.createMultiDollarSimpleNameStringTemplateEntry(
|
||||
expression?.text.orEmpty(),
|
||||
prefixLength = prefixLength,
|
||||
)
|
||||
}
|
||||
|
||||
fun KtLiteralStringTemplateEntry.escapeIfNecessary(
|
||||
newPrefixLength: Int,
|
||||
isSourceSingleQuoted: Boolean,
|
||||
isDestinationSingleQuoted: Boolean,
|
||||
): List<EntryUpdateDiff> {
|
||||
// relying on that $ literal PSI nodes are grouped and consist only of $ chars
|
||||
if (text.all { ch -> ch == '$' }) {
|
||||
return escapeDollarIfNecessary(newPrefixLength, isDestinationSingleQuoted)
|
||||
}
|
||||
if (!isSourceSingleQuoted && isDestinationSingleQuoted) return escapeSpecialCharacters()
|
||||
return listOf(this.asOneToOneDiff(text))
|
||||
}
|
||||
|
||||
/**
|
||||
* If the literal string template entry doesn't contain $ chars or is safe to use with the [newPrefixLength], returns it as is.
|
||||
* Otherwise, create a new literal entry with an escaped last $.
|
||||
*/
|
||||
private fun KtLiteralStringTemplateEntry.escapeDollarIfNecessary(
|
||||
newPrefixLength: Int,
|
||||
isDestinationSingleQuoted: Boolean,
|
||||
): List<EntryUpdateDiff> {
|
||||
val unchangedDiff = this.asOneToOneDiff(text)
|
||||
if (textLength < newPrefixLength) return listOf(unchangedDiff)
|
||||
val nextSibling = nextSibling as? KtLiteralStringTemplateEntry ?: return listOf(unchangedDiff)
|
||||
if (!nextSibling.canBeConsideredIdentifierOrBlock()) return listOf(unchangedDiff)
|
||||
|
||||
val ktPsiFactory = KtPsiFactory(project)
|
||||
val escapedDollar = if (isDestinationSingleQuoted) """\$""" else "${"$".repeat(newPrefixLength)}{'$'}"
|
||||
val beforeLast = text.dropLast(1)
|
||||
val escapedLastDollar = ktPsiFactory.createStringTemplate(escapedDollar).entries.singleOrNull() ?: return listOf(unchangedDiff)
|
||||
return listOfNotNull(
|
||||
beforeLast.takeIf { it.isNotEmpty() }?.let { EntryUpdateDiff(0..<textLength - 1, it, it) },
|
||||
EntryUpdateDiff(textLength - 1..textLength, text.last().toString(), escapedLastDollar.text.orEmpty()),
|
||||
)
|
||||
}
|
||||
|
||||
fun KtLiteralStringTemplateEntry.escapeSpecialCharacters(): List<EntryUpdateDiff> {
|
||||
val escaper = StringUtil.escaper(true, "\"")
|
||||
var from = 0
|
||||
var to = 0
|
||||
var nextChunkBuilder = StringBuilder()
|
||||
val diffs = mutableListOf<EntryUpdateDiff>()
|
||||
|
||||
fun dumpSimpleChunk() {
|
||||
if (from < to) {
|
||||
val simpleTextChunk = nextChunkBuilder.toString()
|
||||
nextChunkBuilder = StringBuilder()
|
||||
diffs.add(EntryUpdateDiff(from..<to, simpleTextChunk, simpleTextChunk))
|
||||
}
|
||||
}
|
||||
|
||||
for (char in text) {
|
||||
val oldCharAsString = char.toString()
|
||||
val escapedCharAsString = escaper.apply(oldCharAsString)
|
||||
if (oldCharAsString == escapedCharAsString) {
|
||||
to++
|
||||
nextChunkBuilder.append(escapedCharAsString)
|
||||
} else {
|
||||
dumpSimpleChunk()
|
||||
diffs.add(EntryUpdateDiff(to..<to + 1, oldCharAsString, escapedCharAsString))
|
||||
to++
|
||||
from = to
|
||||
}
|
||||
}
|
||||
dumpSimpleChunk()
|
||||
|
||||
return diffs
|
||||
}
|
||||
|
||||
fun KtStringTemplateEntry.unescapeIfPossible(newPrefixLength: Int): KtStringTemplateEntry {
|
||||
fun previousDollarsCount(): Int {
|
||||
if (this.prevSibling !is KtLiteralStringTemplateEntry) return 0
|
||||
return this.prevSibling.text.takeLastWhile { it == '$' }.length
|
||||
}
|
||||
|
||||
return when (this) {
|
||||
is KtEscapeStringTemplateEntry -> {
|
||||
if (this.unescapedValue != "$") return this
|
||||
if (previousDollarsCount() + 1 >= newPrefixLength
|
||||
&& (nextSibling as? KtLiteralStringTemplateEntry)?.canBeConsideredIdentifierOrBlock() == true
|
||||
) return this
|
||||
KtPsiFactory(project).createLiteralStringTemplateEntry("$")
|
||||
}
|
||||
is KtBlockStringTemplateEntry -> {
|
||||
val expression = this.expression ?: return this
|
||||
if (expression.text !in dollarLiteralExpressions) return this
|
||||
KtPsiFactory(project).createLiteralStringTemplateEntry("$")
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ```
|
||||
* Identifier
|
||||
* : (Letter | '_') (Letter | '_' | UnicodeDigit)*
|
||||
* | '`' ~([\r\n] | '`')+ '`'
|
||||
* ;
|
||||
* ```
|
||||
*
|
||||
* The function can give false positives in corner cases when a backtick after `$` has no matching closing backtick.
|
||||
* This tradeoff allows avoiding potentially complicated and error-prone text searches in the file.
|
||||
* The closing backtick is not limited by the same string template and can come from various places of the PSI tree.
|
||||
* E.g., in the following case there are two backticks in different expressions that would be part of one string without escaping.
|
||||
* ```
|
||||
* println("\$`"); println("`")
|
||||
* ```
|
||||
*/
|
||||
fun KtLiteralStringTemplateEntry.canBeConsideredIdentifierOrBlock(): Boolean {
|
||||
val firstChar = text.firstOrNull() ?: return false
|
||||
return firstChar.isLetter() || firstChar == '_' || firstChar == '{' || firstChar == '`'
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.codeinsights.impl.base
|
||||
|
||||
import com.intellij.openapi.util.text.LineTokenizer
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import org.jetbrains.kotlin.lexer.KotlinLexer
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.psi.KtStringTemplateEntry
|
||||
import kotlin.text.isNotEmpty
|
||||
|
||||
class TemplateTokenSequence(
|
||||
private val inputString: String,
|
||||
interpolationPrefixLength: Int,
|
||||
) : Sequence<TemplateChunk> {
|
||||
val templatePrefix = "$".repeat(interpolationPrefixLength)
|
||||
val entryPrefixLength = maxOf(templatePrefix.length, 1)
|
||||
val entryPrefix = "$".repeat(entryPrefixLength)
|
||||
|
||||
private fun String.guessIsTemplateEntryStart(): Boolean = when {
|
||||
startsWith("$entryPrefix{") -> {
|
||||
true
|
||||
}
|
||||
length > entryPrefixLength && substring(0..< entryPrefixLength).all { it == '$' } -> {
|
||||
val guessedIdentifier = substring(entryPrefixLength)
|
||||
val tokenType = KotlinLexer().apply { start(guessedIdentifier) }.tokenType
|
||||
tokenType == KtTokens.IDENTIFIER || tokenType == KtTokens.THIS_KEYWORD
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
|
||||
private fun findTemplateEntryEnd(input: String, from: Int): Int {
|
||||
val wrapped = """$templatePrefix"${input.substring(from)}""""
|
||||
val lexer = KotlinLexer().apply { start(wrapped) }.apply { advance() }
|
||||
|
||||
when (lexer.tokenType) {
|
||||
KtTokens.SHORT_TEMPLATE_ENTRY_START -> {
|
||||
lexer.advance()
|
||||
val tokenType = lexer.tokenType
|
||||
return if (tokenType == KtTokens.IDENTIFIER || tokenType == KtTokens.THIS_KEYWORD) {
|
||||
from + lexer.tokenEnd - 1
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
KtTokens.LONG_TEMPLATE_ENTRY_START -> {
|
||||
var depth = 0
|
||||
while (lexer.tokenType != null) {
|
||||
if (lexer.tokenType == KtTokens.LONG_TEMPLATE_ENTRY_START) {
|
||||
depth++
|
||||
} else if (lexer.tokenType == KtTokens.LONG_TEMPLATE_ENTRY_END) {
|
||||
depth--
|
||||
if (depth == 0) {
|
||||
return from + lexer.currentPosition.offset
|
||||
}
|
||||
}
|
||||
lexer.advance()
|
||||
}
|
||||
return -1
|
||||
}
|
||||
else -> return -1
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun SequenceScope<TemplateChunk>.yieldLiteral(chunk: String) {
|
||||
val splitLines = LineTokenizer.tokenize(chunk, false, false)
|
||||
for (i in splitLines.indices) {
|
||||
if (i != 0) {
|
||||
yield(NewLineChunk)
|
||||
}
|
||||
splitLines[i].takeIf { it.isNotEmpty() }?.let { yield(LiteralChunk(it)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun templateChunkIterator(): Iterator<TemplateChunk> {
|
||||
return if (inputString.isEmpty()) emptySequence<TemplateChunk>().iterator()
|
||||
else
|
||||
iterator {
|
||||
val likeStackTrace = isAStackTrace(inputString)
|
||||
var from = 0
|
||||
var to = 0
|
||||
while (to < inputString.length) {
|
||||
val c = inputString[to]
|
||||
if (c == '\\') {
|
||||
to += 1
|
||||
if (to < inputString.length) to += 1
|
||||
continue
|
||||
} else if (c == '$') {
|
||||
val substring = inputString.substring(to)
|
||||
val guessIsTemplateEntryStart = !likeStackTrace && substring.guessIsTemplateEntryStart()
|
||||
if (guessIsTemplateEntryStart) {
|
||||
if (from < to) yieldLiteral(inputString.substring(from until to))
|
||||
from = to
|
||||
to = findTemplateEntryEnd(inputString, from)
|
||||
if (to != -1) {
|
||||
yield(EntryChunk(inputString.substring(from until to)))
|
||||
} else {
|
||||
to = inputString.length
|
||||
yieldLiteral(inputString.substring(from until to))
|
||||
}
|
||||
from = to
|
||||
continue
|
||||
}
|
||||
}
|
||||
to++
|
||||
}
|
||||
if (from < to) {
|
||||
yieldLiteral(inputString.substring(from until to))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<TemplateChunk> = templateChunkIterator()
|
||||
|
||||
private fun isAStackTrace(string: String): Boolean =
|
||||
stacktracePlaceRegex.containsMatchIn(string)
|
||||
|
||||
private val stacktracePlaceRegex = Regex("\\(\\w+\\.\\w+:\\d+\\)")
|
||||
}
|
||||
|
||||
sealed class TemplateChunk
|
||||
data class LiteralChunk(val text: String) : TemplateChunk()
|
||||
data class EntryChunk(val text: String) : TemplateChunk()
|
||||
object NewLineChunk : TemplateChunk()
|
||||
|
||||
@TestOnly
|
||||
fun createTemplateSequenceTokenString(input: String, prefixLength: Int): String {
|
||||
return TemplateTokenSequence(input, prefixLength).map {
|
||||
when (it) {
|
||||
is LiteralChunk -> "LITERAL_CHUNK(${it.text})"
|
||||
is EntryChunk -> "ENTRY_CHUNK(${it.text})"
|
||||
is NewLineChunk -> "NEW_LINE()"
|
||||
}
|
||||
}.joinToString(separator = "")
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.k2.codeinsight.intentions.multiDollarStrings
|
||||
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.canBeConsideredIdentifierOrBlock
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.changePrefixLength
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.dollarLiteralExpressions
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.isSingleQuoted
|
||||
import org.jetbrains.kotlin.psi.psiUtil.plainContent
|
||||
@@ -10,9 +13,6 @@ private const val DEFAULT_INTERPOLATION_PREFIX_LENGTH: Int = 2
|
||||
private const val INTERPOLATION_PREFIX_LENGTH_THRESHOLD: Int = 5
|
||||
|
||||
private const val DOLLAR: String = "$"
|
||||
private val dollarLiteralExpressions: Array<String> = arrayOf(
|
||||
"'$'", "\"$\""
|
||||
)
|
||||
|
||||
/**
|
||||
* Convert a string to a multi-dollar string, choosing an appropriate prefix length based on the string's content.
|
||||
@@ -22,16 +22,14 @@ private val dollarLiteralExpressions: Array<String> = arrayOf(
|
||||
internal fun convertToMultiDollarString(element: KtStringTemplateExpression): KtStringTemplateExpression {
|
||||
require(element.interpolationPrefix == null) { "Can't convert the string which already has a prefix to multi-dollar string" }
|
||||
|
||||
val ktPsiFactory = KtPsiFactory(element.project)
|
||||
|
||||
val longestUnsafeDollarSequence = longestUnsafeDollarSequenceLength(element, threshold = INTERPOLATION_PREFIX_LENGTH_THRESHOLD)
|
||||
val prefixLength = if (longestUnsafeDollarSequence in DEFAULT_INTERPOLATION_PREFIX_LENGTH..< INTERPOLATION_PREFIX_LENGTH_THRESHOLD)
|
||||
longestUnsafeDollarSequence + 1 else DEFAULT_INTERPOLATION_PREFIX_LENGTH
|
||||
|
||||
replaceExpressionEntries(element, prefixLength, ktPsiFactory)
|
||||
replaceExpressionEntries(element, prefixLength)
|
||||
|
||||
val replaced = element.replace(
|
||||
ktPsiFactory.createMultiDollarStringTemplate(
|
||||
KtPsiFactory(element.project).createMultiDollarStringTemplate(
|
||||
content = element.plainContent,
|
||||
prefixLength = prefixLength,
|
||||
forceMultiQuoted = !element.isSingleQuoted(),
|
||||
@@ -127,41 +125,14 @@ private fun KtBlockStringTemplateEntry.isSimplifiableInterpolatedDollar(): Boole
|
||||
return this.expression?.text in dollarLiteralExpressions
|
||||
}
|
||||
|
||||
private fun replaceExpressionEntries(stringTemplate: KtStringTemplateExpression, prefixLength: Int, ktPsiFactory: KtPsiFactory) {
|
||||
private fun replaceExpressionEntries(stringTemplate: KtStringTemplateExpression, prefixLength: Int) {
|
||||
for (entry in stringTemplate.entries) {
|
||||
when (entry) {
|
||||
is KtSimpleNameStringTemplateEntry -> replaceSimpleNameEntry(entry, prefixLength, ktPsiFactory)
|
||||
is KtBlockStringTemplateEntry -> replaceBlockEntry(entry, prefixLength, ktPsiFactory)
|
||||
if (entry is KtStringTemplateEntryWithExpression) {
|
||||
entry.replace(entry.changePrefixLength(prefixLength))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replaceSimpleNameEntry(simpleNameEntry: KtSimpleNameStringTemplateEntry, prefixLength: Int, ktPsiFactory: KtPsiFactory) {
|
||||
simpleNameEntry.replace(
|
||||
ktPsiFactory.createMultiDollarSimpleNameStringTemplateEntry(
|
||||
simpleNameEntry.expression?.text.orEmpty(),
|
||||
prefixLength = prefixLength,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceBlockEntry(blockEntry: KtBlockStringTemplateEntry, prefixLength: Int, ktPsiFactory: KtPsiFactory) {
|
||||
val blockExpression = blockEntry.expression
|
||||
val replacement = if (blockExpression != null) {
|
||||
ktPsiFactory.createMultiDollarBlockStringTemplateEntry(
|
||||
blockExpression,
|
||||
prefixLength = prefixLength,
|
||||
)
|
||||
} else {
|
||||
// In case of incomplete code with no KtExpression inside the block, create a replacement from scratch
|
||||
val prefix = DOLLAR.repeat(prefixLength)
|
||||
val incompleteExpression = ktPsiFactory.createExpression("$prefix\"$prefix{}\"")
|
||||
(incompleteExpression as KtStringTemplateExpression).entries.single()
|
||||
}
|
||||
|
||||
blockEntry.replace(replacement)
|
||||
}
|
||||
|
||||
private fun KtEscapeStringTemplateEntry.isEscapedDollar(): Boolean = unescapedValue == DOLLAR
|
||||
|
||||
/**
|
||||
@@ -175,24 +146,3 @@ private fun KtStringTemplateEntry.isSafeToReplaceWithDollar(prefixLength: Int):
|
||||
val trailingDollarsLength = prevSibling.text.takeLastWhile { it.toString() == DOLLAR }.length
|
||||
return trailingDollarsLength + 1 < prefixLength
|
||||
}
|
||||
|
||||
/**
|
||||
* ```
|
||||
* Identifier
|
||||
* : (Letter | '_') (Letter | '_' | UnicodeDigit)*
|
||||
* | '`' ~([\r\n] | '`')+ '`'
|
||||
* ;
|
||||
* ```
|
||||
*
|
||||
* The function can give false positives in corner cases when a backtick after `$` has no matching closing backtick.
|
||||
* This tradeoff allows avoiding potentially complicated and error-prone text searches in the file.
|
||||
* The closing backtick is not limited by the same string template and can come from various places of the PSI tree.
|
||||
* E.g., in the following case there are two backticks in different expressions that would be part of one string without escaping.
|
||||
* ```
|
||||
* println("\$`"); println("`")
|
||||
* ```
|
||||
*/
|
||||
private fun KtLiteralStringTemplateEntry.canBeConsideredIdentifierOrBlock(): Boolean {
|
||||
val firstChar = text.firstOrNull() ?: return false
|
||||
return firstChar.isLetter() || firstChar == '_' || firstChar == '{' || firstChar == '`'
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@
|
||||
serviceImplementation="org.jetbrains.kotlin.idea.k2.codeinsight.K2NameValidatorProviderImpl"/>
|
||||
|
||||
<copyPastePostProcessor implementation="org.jetbrains.kotlin.idea.k2.codeinsight.copyPaste.KotlinCopyPasteReferenceProcessor"/>
|
||||
<copyPastePostProcessor implementation="org.jetbrains.kotlin.idea.k2.codeinsight.copyPaste.KotlinCopyPasteStringTemplatePostProcessor"/>
|
||||
|
||||
<registryKey key="kotlin.k2.allow.constant.computation.on.EDT" defaultValue="true" description="When enabled, error about analysis on EDT is disabled"/>
|
||||
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.k2.codeinsight.copyPaste
|
||||
|
||||
import com.intellij.codeInsight.editorActions.CopyPastePostProcessor
|
||||
import com.intellij.codeInsight.editorActions.TextBlockTransferableData
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.diagnostic.ControlFlowException
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.RangeMarker
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.Ref
|
||||
import com.intellij.openapi.util.TextRange
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.SmartPsiElementPointer
|
||||
import com.intellij.psi.createSmartPointer
|
||||
import com.intellij.psi.util.startOffset
|
||||
import org.jetbrains.kotlin.analysis.utils.printer.parentOfType
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.EntryUpdateDiff
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.changeInterpolationPrefix
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.isSingleQuoted
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.datatransfer.Transferable
|
||||
import java.awt.datatransfer.UnsupportedFlavorException
|
||||
import java.io.IOException
|
||||
|
||||
internal class KotlinStringTemplateTransferableData(
|
||||
val selectionDataForSelections: List<StringTemplateSelectionData>,
|
||||
) : TextBlockTransferableData {
|
||||
override fun getFlavor(): DataFlavor = dataFlavor
|
||||
|
||||
companion object {
|
||||
val dataFlavor: DataFlavor by lazy {
|
||||
val transferableDataJavaClass = KotlinStringTemplateTransferableData::class.java
|
||||
DataFlavor(
|
||||
DataFlavor.javaJVMLocalObjectMimeType + ";class=" + transferableDataJavaClass.name,
|
||||
transferableDataJavaClass.simpleName,
|
||||
transferableDataJavaClass.classLoader,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class StringTemplateSelectionData(
|
||||
val range: TextRange,
|
||||
val prefixLength: Int,
|
||||
val selectedText: String,
|
||||
val templateText: String,
|
||||
val startOffsetRelativeToTemplate: Int,
|
||||
val endOffsetRelativeToTemplate: Int,
|
||||
val isSingleQuoted: Boolean,
|
||||
)
|
||||
|
||||
internal val LOG = Logger.getInstance(KotlinCopyPasteStringTemplatePostProcessor::class.java)
|
||||
|
||||
/**
|
||||
* Copy-paste post processor for handling transfers between Kotlin strings.
|
||||
* Uses [Transferable] information to update pasted text with respect to different string quoting and interpolation prefixes.
|
||||
*/
|
||||
internal class KotlinCopyPasteStringTemplatePostProcessor : CopyPastePostProcessor<KotlinStringTemplateTransferableData>() {
|
||||
override fun requiresAllDocumentsToBeCommitted(editor: Editor, project: Project): Boolean = false
|
||||
|
||||
override fun collectTransferableData(
|
||||
file: PsiFile,
|
||||
editor: Editor,
|
||||
startOffsets: IntArray,
|
||||
endOffsets: IntArray
|
||||
): List<KotlinStringTemplateTransferableData?> {
|
||||
if (file !is KtFile) return emptyList()
|
||||
|
||||
val templateSelectionData = startOffsets.zip(endOffsets).mapNotNull { (start, end) ->
|
||||
createSelectionData(file, editor, start, end)
|
||||
}
|
||||
return listOf(KotlinStringTemplateTransferableData(templateSelectionData))
|
||||
}
|
||||
|
||||
// pass info for selections that belong to one string template
|
||||
private fun createSelectionData(file: KtFile, editor: Editor, start: Int, end: Int): StringTemplateSelectionData? {
|
||||
val range = TextRange(start, end)
|
||||
val startKtElement = file.findElementAt(start)?.parentOfType<KtStringTemplateEntry>(withSelf = true) ?: return null
|
||||
val endKtElement = file.findElementAt(end - 1)?.parentOfType<KtStringTemplateEntry>(withSelf = true) ?: return null
|
||||
val parentStringTemplate = startKtElement.parent as? KtStringTemplateExpression ?: return null
|
||||
if (endKtElement.parent !== parentStringTemplate) return null
|
||||
val selectedText = editor.document.getText(range)
|
||||
val parentTemplateText = parentStringTemplate.text
|
||||
val startOffsetRelative = start - parentStringTemplate.startOffset
|
||||
val endOffsetRelative = end - parentStringTemplate.startOffset
|
||||
|
||||
return StringTemplateSelectionData(
|
||||
range,
|
||||
parentStringTemplate.interpolationPrefix?.textLength ?: 0,
|
||||
selectedText,
|
||||
parentTemplateText,
|
||||
startOffsetRelative,
|
||||
endOffsetRelative,
|
||||
parentStringTemplate.isSingleQuoted(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun extractTransferableData(content: Transferable): List<KotlinStringTemplateTransferableData?> {
|
||||
if (content.isDataFlavorSupported(KotlinStringTemplateTransferableData.dataFlavor)) {
|
||||
try {
|
||||
return listOf(
|
||||
content.getTransferData(KotlinStringTemplateTransferableData.dataFlavor) as KotlinStringTemplateTransferableData
|
||||
)
|
||||
} catch (_: IOException) { // fall through if transfer data is unavailable after the isDataFlavorSupported check
|
||||
} catch (_: UnsupportedFlavorException) {
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* In cases of the same prefix length and the same quotes return the original text unchanged.
|
||||
* If only quotes change, use only preprocessor, though there are known poorly handled cases, such as `trimMargin`.
|
||||
* When the prefix lengths are different:
|
||||
* * update interpolation prefixes of the template and its entries
|
||||
* * unescape `$` that no longer require escaping (prefix length increased)
|
||||
* * escape `$` that now require escaping (prefix length decreased — certain old unescaped $ can be misinterpreted as entries)
|
||||
* * handle escaped characters when pasting between strings with different quote lengths
|
||||
*
|
||||
* Multi-caret cases are not supported — preprocessor results are kept as-is with no specific expectations.
|
||||
*/
|
||||
override fun processTransferableData(
|
||||
project: Project,
|
||||
editor: Editor,
|
||||
bounds: RangeMarker,
|
||||
caretOffset: Int,
|
||||
indented: Ref<in Boolean>,
|
||||
values: List<KotlinStringTemplateTransferableData?>
|
||||
) {
|
||||
val stringTemplateData = values.singleOrNull() ?: return
|
||||
val document = editor.document
|
||||
val targetFile = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) as? KtFile ?: return
|
||||
|
||||
val replacementResult = try {
|
||||
prepareReplacement(targetFile, bounds, stringTemplateData)
|
||||
} catch (e: Throwable) {
|
||||
if (e is ControlFlowException) throw e
|
||||
LOG.error(e)
|
||||
ReplacementResult.KeepPreprocessed
|
||||
}
|
||||
|
||||
val replacementText = when (replacementResult) {
|
||||
is ReplacementResult.KeepPreprocessed -> return
|
||||
is ReplacementResult.KeepOriginal -> replacementResult.originalText
|
||||
is ReplacementResult.ReplaceWith -> replacementResult.updatedText
|
||||
}
|
||||
|
||||
ApplicationManager.getApplication().runWriteAction {
|
||||
document.replaceString(bounds.startOffset, bounds.endOffset, replacementText)
|
||||
PsiDocumentManager.getInstance(project).commitDocument(document)
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ReplacementResult {
|
||||
/**
|
||||
* @see [org.jetbrains.kotlin.idea.editor.KotlinLiteralCopyPasteProcessor]
|
||||
*/
|
||||
object KeepPreprocessed : ReplacementResult()
|
||||
class KeepOriginal(val originalText: String) : ReplacementResult()
|
||||
class ReplaceWith(val updatedText: String) : ReplacementResult()
|
||||
}
|
||||
|
||||
private fun prepareReplacement(
|
||||
file: KtFile,
|
||||
bounds: RangeMarker,
|
||||
stringTemplateData: KotlinStringTemplateTransferableData
|
||||
): ReplacementResult {
|
||||
val startElement = file.findElementAt(bounds.startOffset)
|
||||
// Keep preprocessor results when pasting outside string templates
|
||||
val destinationStringTemplate = startElement?.parentOfType<KtStringTemplateExpression>(withSelf = true)
|
||||
?: return ReplacementResult.KeepPreprocessed
|
||||
// Don't try applying string-to-string pasting logic in multi-caret cases
|
||||
val selectionData = stringTemplateData.selectionDataForSelections.singleOrNull() ?: return ReplacementResult.KeepPreprocessed
|
||||
|
||||
val destinationStringPrefixLength = destinationStringTemplate.interpolationPrefix?.textLength ?: 0
|
||||
val originalStringPrefixLength = selectionData.prefixLength
|
||||
val quotesChanged = selectionData.isSingleQuoted != destinationStringTemplate.isSingleQuoted()
|
||||
|
||||
// return the original text, reverting preprocessor changes, when prefix lengths and quotes are the same
|
||||
// copy-pasting from "" to $"" or vise versa follows the same rule
|
||||
if (destinationStringPrefixLength == originalStringPrefixLength
|
||||
|| destinationStringPrefixLength + originalStringPrefixLength == 1
|
||||
) {
|
||||
if (quotesChanged) return ReplacementResult.KeepPreprocessed
|
||||
else return ReplacementResult.KeepOriginal(selectionData.selectedText)
|
||||
}
|
||||
|
||||
return prepareReplacementForDifferentPrefixes(file, selectionData, destinationStringTemplate)
|
||||
}
|
||||
|
||||
private fun prepareReplacementForDifferentPrefixes(
|
||||
file: KtFile,
|
||||
selectionData: StringTemplateSelectionData,
|
||||
destinationStringTemplate: KtStringTemplateExpression,
|
||||
): ReplacementResult.ReplaceWith {
|
||||
val originalTemplate = KtPsiFactory(file.project).createExpression(selectionData.templateText) as KtStringTemplateExpression
|
||||
|
||||
val nonContentAdjustment = calculateNonContentAdjustment(selectionData, destinationStringTemplate)
|
||||
var startOffsetAdjustment = nonContentAdjustment
|
||||
var endOffsetAdjustment = nonContentAdjustment
|
||||
|
||||
val replacedTemplate = originalTemplate.changeInterpolationPrefix(
|
||||
newPrefixLength = destinationStringTemplate.interpolationPrefix?.textLength ?: 0,
|
||||
isSourceSingleQuoted = originalTemplate.isSingleQuoted(),
|
||||
isDestinationSingleQuoted = destinationStringTemplate.isSingleQuoted(),
|
||||
) { updateInfo ->
|
||||
updateInfo.diffs.forEach { diff ->
|
||||
val adjustment = calculateSelectionAdjustmentsForEntryDiff(updateInfo.oldEntry, diff, selectionData)
|
||||
startOffsetAdjustment += adjustment.start
|
||||
endOffsetAdjustment += adjustment.end
|
||||
}
|
||||
}
|
||||
|
||||
val newText = replacedTemplate.text.substring(
|
||||
selectionData.startOffsetRelativeToTemplate + startOffsetAdjustment,
|
||||
selectionData.endOffsetRelativeToTemplate + endOffsetAdjustment
|
||||
)
|
||||
return ReplacementResult.ReplaceWith(newText)
|
||||
}
|
||||
|
||||
private fun calculateNonContentAdjustment(
|
||||
selectionData: StringTemplateSelectionData,
|
||||
destinationStringTemplate: KtStringTemplateExpression,
|
||||
): Int {
|
||||
val originalStringPrefixLength = selectionData.prefixLength
|
||||
val destinationStringPrefixLength = destinationStringTemplate.interpolationPrefix?.textLength ?: 0
|
||||
val prefixLengthDiff = destinationStringPrefixLength - originalStringPrefixLength
|
||||
val isSourceTemplateSingleQuoted = selectionData.isSingleQuoted
|
||||
val isDestinationTemplateSingleQuoted = destinationStringTemplate.isSingleQuoted()
|
||||
val quoteLengthDiff = when {
|
||||
isSourceTemplateSingleQuoted && !isDestinationTemplateSingleQuoted -> 2
|
||||
!isSourceTemplateSingleQuoted && isDestinationTemplateSingleQuoted -> -2
|
||||
else -> 0
|
||||
}
|
||||
|
||||
return prefixLengthDiff + quoteLengthDiff
|
||||
}
|
||||
|
||||
private class OffsetAdjustments(
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
) {
|
||||
override fun toString(): String = "(%+d,%+d)".format(start, end)
|
||||
}
|
||||
|
||||
private fun calculateSelectionAdjustmentsForEntryDiff(
|
||||
oldEntry: KtStringTemplateEntry,
|
||||
diff: EntryUpdateDiff,
|
||||
selectionData: StringTemplateSelectionData,
|
||||
): OffsetAdjustments {
|
||||
val range = diff.oldRange
|
||||
val diffStartOffset = oldEntry.startOffsetInParent + range.first
|
||||
val diffEndOffset = oldEntry.startOffsetInParent + range.last + 1
|
||||
val textLengthDiff = diff.newText.length - diff.oldText.length
|
||||
|
||||
return when {
|
||||
// the entire diff entry is to the left of the selection — add the whole text length difference to both borders
|
||||
diffEndOffset <= selectionData.startOffsetRelativeToTemplate -> {
|
||||
OffsetAdjustments(textLengthDiff, textLengthDiff)
|
||||
}
|
||||
// the entire diff entry is to the right of the selection — no changes needed
|
||||
diffStartOffset >= selectionData.endOffsetRelativeToTemplate -> OffsetAdjustments(0, 0)
|
||||
// one or both borders of the selection are inside the entry — adjust to include the whole replacement
|
||||
else -> {
|
||||
OffsetAdjustments(
|
||||
// for the selection start: get rid of a partial start offset if present
|
||||
-maxOf(selectionData.startOffsetRelativeToTemplate - diffStartOffset, 0),
|
||||
// for the selection end: remove a possible partial selection and add text length diff
|
||||
maxOf(diffEndOffset - selectionData.endOffsetRelativeToTemplate, 0) + textLengthDiff,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.fir.copyPaste
|
||||
|
||||
import org.jetbrains.kotlin.idea.conversion.copy.AbstractLiteralKotlinToKotlinCopyPasteTest
|
||||
|
||||
abstract class AbstractFirKotlinToKotlinMultiDollarStringsCopyPasteTest : AbstractLiteralKotlinToKotlinCopyPasteTest()
|
||||
@@ -0,0 +1,232 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
|
||||
package org.jetbrains.kotlin.idea.fir.copyPaste;
|
||||
|
||||
import com.intellij.testFramework.TestDataPath;
|
||||
import org.jetbrains.kotlin.idea.base.plugin.KotlinPluginMode;
|
||||
import org.jetbrains.kotlin.idea.base.test.TestRoot;
|
||||
import org.jetbrains.kotlin.idea.test.JUnit3RunnerWithInners;
|
||||
import org.jetbrains.kotlin.idea.test.KotlinTestUtils;
|
||||
import org.jetbrains.kotlin.test.TestMetadata;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* This class is generated by {@link org.jetbrains.kotlin.testGenerator.generator.TestGenerator}.
|
||||
* DO NOT MODIFY MANUALLY.
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
@TestRoot("fir/tests")
|
||||
@TestDataPath("$CONTENT_ROOT")
|
||||
@RunWith(JUnit3RunnerWithInners.class)
|
||||
@TestMetadata("../../idea/tests/testData/copyPaste/multiDollar")
|
||||
public class FirKotlinToKotlinMultiDollarStringsCopyPasteTestGenerated extends AbstractFirKotlinToKotlinMultiDollarStringsCopyPasteTest {
|
||||
@java.lang.Override
|
||||
@org.jetbrains.annotations.NotNull
|
||||
public final KotlinPluginMode getPluginMode() {
|
||||
return KotlinPluginMode.K2;
|
||||
}
|
||||
|
||||
private void runTest(String testDataFilePath) throws Exception {
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeDollarMultiQuoted.kt")
|
||||
public void testEscapeDollarMultiQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeDollarMultiQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeDollarSequenceMultiQuoted.kt")
|
||||
public void testEscapeDollarSequenceMultiQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeDollarSequenceMultiQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeDollarSequenceSingleQuoted.kt")
|
||||
public void testEscapeDollarSequenceSingleQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeDollarSequenceSingleQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeDollarSingleQuoted.kt")
|
||||
public void testEscapeDollarSingleQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeDollarSingleQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeNotIfNotBeforeIdentifierMultiQuoted.kt")
|
||||
public void testEscapeNotIfNotBeforeIdentifierMultiQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeNotIfNotBeforeIdentifierMultiQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapeNotIfNotBeforeIdentifierSingleQuoted.kt")
|
||||
public void testEscapeNotIfNotBeforeIdentifierSingleQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapeNotIfNotBeforeIdentifierSingleQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("EscapedCharsInSingleQuotedToMultiQuoted.kt")
|
||||
public void testEscapedCharsInSingleQuotedToMultiQuoted() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/EscapedCharsInSingleQuotedToMultiQuoted.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix2ToPrefix3EscapedLiterals.kt")
|
||||
public void testFromPrefix2ToPrefix3EscapedLiterals() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix2ToPrefix3EscapedLiterals.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix2ToPrefix3LongTemplateEntry.kt")
|
||||
public void testFromPrefix2ToPrefix3LongTemplateEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix2ToPrefix3LongTemplateEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix2ToPrefix3PlainText.kt")
|
||||
public void testFromPrefix2ToPrefix3PlainText() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix2ToPrefix3PlainText.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix2ToPrefix3ShortNameEntry.kt")
|
||||
public void testFromPrefix2ToPrefix3ShortNameEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix2ToPrefix3ShortNameEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix3ToPrefix2EscapedLiterals.kt")
|
||||
public void testFromPrefix3ToPrefix2EscapedLiterals() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix3ToPrefix2EscapedLiterals.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix3ToPrefix2LongTemplateEntry.kt")
|
||||
public void testFromPrefix3ToPrefix2LongTemplateEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix3ToPrefix2LongTemplateEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix3ToPrefix2PlainText.kt")
|
||||
public void testFromPrefix3ToPrefix2PlainText() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix3ToPrefix2PlainText.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefix3ToPrefix2ShortNameEntry.kt")
|
||||
public void testFromPrefix3ToPrefix2ShortNameEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefix3ToPrefix2ShortNameEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToPrefixedEscapedLiterals.kt")
|
||||
public void testFromPrefixedToPrefixedEscapedLiterals() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToPrefixedEscapedLiterals.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToPrefixedLongTemplateEntry.kt")
|
||||
public void testFromPrefixedToPrefixedLongTemplateEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToPrefixedLongTemplateEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToPrefixedPlainText.kt")
|
||||
public void testFromPrefixedToPrefixedPlainText() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToPrefixedPlainText.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToPrefixedShortNameEntry.kt")
|
||||
public void testFromPrefixedToPrefixedShortNameEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToPrefixedShortNameEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimpleDangerousDollar.kt")
|
||||
public void testFromPrefixedToSimpleDangerousDollar() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimpleDangerousDollar.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimpleDangerousDollarLongPrefix.kt")
|
||||
public void testFromPrefixedToSimpleDangerousDollarLongPrefix() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimpleDangerousDollarLongPrefix.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimpleEscapedLiterals.kt")
|
||||
public void testFromPrefixedToSimpleEscapedLiterals() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimpleEscapedLiterals.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimpleLongTemplateEntry.kt")
|
||||
public void testFromPrefixedToSimpleLongTemplateEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimpleLongTemplateEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimplePlainText.kt")
|
||||
public void testFromPrefixedToSimplePlainText() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimplePlainText.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromPrefixedToSimpleShortNameEntry.kt")
|
||||
public void testFromPrefixedToSimpleShortNameEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromPrefixedToSimpleShortNameEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromSimpleToPrefixedEscapedLiterals.kt")
|
||||
public void testFromSimpleToPrefixedEscapedLiterals() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromSimpleToPrefixedEscapedLiterals.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromSimpleToPrefixedLongTemplateEntry.kt")
|
||||
public void testFromSimpleToPrefixedLongTemplateEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromSimpleToPrefixedLongTemplateEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromSimpleToPrefixedPlainText.kt")
|
||||
public void testFromSimpleToPrefixedPlainText() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromSimpleToPrefixedPlainText.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("FromSimpleToPrefixedShortNameEntry.kt")
|
||||
public void testFromSimpleToPrefixedShortNameEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/FromSimpleToPrefixedShortNameEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("PrefixedRawToSimpleEscapeNonEntry.kt")
|
||||
public void testPrefixedRawToSimpleEscapeNonEntry() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/PrefixedRawToSimpleEscapeNonEntry.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarSingleQuoteInsideSelection.kt")
|
||||
public void testUnescapeDollarSingleQuoteInsideSelection() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarSingleQuoteInsideSelection.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarSingleQuoteSelectionAfter.kt")
|
||||
public void testUnescapeDollarSingleQuoteSelectionAfter() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarSingleQuoteSelectionAfter.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarSingleQuoteSelectionBefore.kt")
|
||||
public void testUnescapeDollarSingleQuoteSelectionBefore() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarSingleQuoteSelectionBefore.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarSingleSelectionEndBreaksEscaping.kt")
|
||||
public void testUnescapeDollarSingleSelectionEndBreaksEscaping() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarSingleSelectionEndBreaksEscaping.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarSingleSelectionStartBreaksEscaping.kt")
|
||||
public void testUnescapeDollarSingleSelectionStartBreaksEscaping() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarSingleSelectionStartBreaksEscaping.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarTripleQuoteInsideSelection.kt")
|
||||
public void testUnescapeDollarTripleQuoteInsideSelection() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarTripleQuoteInsideSelection.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarTripleQuoteSelectionAfter.kt")
|
||||
public void testUnescapeDollarTripleQuoteSelectionAfter() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarTripleQuoteSelectionAfter.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarTripleQuoteSelectionBefore.kt")
|
||||
public void testUnescapeDollarTripleQuoteSelectionBefore() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarTripleQuoteSelectionBefore.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarTripleQuoteSelectionEndBreaksEscaping.kt")
|
||||
public void testUnescapeDollarTripleQuoteSelectionEndBreaksEscaping() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarTripleQuoteSelectionEndBreaksEscaping.kt");
|
||||
}
|
||||
|
||||
@TestMetadata("UnescapeDollarTripleQuoteSelectionStartBreaksEscaping.kt")
|
||||
public void testUnescapeDollarTripleQuoteSelectionStartBreaksEscaping() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/multiDollar/UnescapeDollarTripleQuoteSelectionStartBreaksEscaping.kt");
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,11 @@ public class FirLiteralTextToKotlinCopyPasteTestGenerated extends AbstractFirLit
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("AlreadyPrefixed.txt")
|
||||
public void testAlreadyPrefixed() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/plainTextLiteral/AlreadyPrefixed.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("BrokenEntries.txt")
|
||||
public void testBrokenEntries() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/plainTextLiteral/BrokenEntries.txt");
|
||||
@@ -40,6 +45,11 @@ public class FirLiteralTextToKotlinCopyPasteTestGenerated extends AbstractFirLit
|
||||
runTest("../../idea/tests/testData/copyPaste/plainTextLiteral/CustomTrimIndent.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("IsolatedDollarsToPrefixedString.txt")
|
||||
public void testIsolatedDollarsToPrefixedString() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/plainTextLiteral/IsolatedDollarsToPrefixedString.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("MultiLine.txt")
|
||||
public void testMultiLine() throws Exception {
|
||||
runTest("../../idea/tests/testData/copyPaste/plainTextLiteral/MultiLine.txt");
|
||||
|
||||
@@ -30,6 +30,11 @@ public class LiteralTextToKotlinCopyPasteTestGenerated extends AbstractLiteralTe
|
||||
KotlinTestUtils.runTest(this::doTest, this, testDataFilePath);
|
||||
}
|
||||
|
||||
@TestMetadata("AlreadyPrefixed.txt")
|
||||
public void testAlreadyPrefixed() throws Exception {
|
||||
runTest("testData/copyPaste/plainTextLiteral/AlreadyPrefixed.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("BrokenEntries.txt")
|
||||
public void testBrokenEntries() throws Exception {
|
||||
runTest("testData/copyPaste/plainTextLiteral/BrokenEntries.txt");
|
||||
@@ -40,6 +45,11 @@ public class LiteralTextToKotlinCopyPasteTestGenerated extends AbstractLiteralTe
|
||||
runTest("testData/copyPaste/plainTextLiteral/CustomTrimIndent.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("IsolatedDollarsToPrefixedString.txt")
|
||||
public void testIsolatedDollarsToPrefixedString() throws Exception {
|
||||
runTest("testData/copyPaste/plainTextLiteral/IsolatedDollarsToPrefixedString.txt");
|
||||
}
|
||||
|
||||
@TestMetadata("MultiLine.txt")
|
||||
public void testMultiLine() throws Exception {
|
||||
runTest("testData/copyPaste/plainTextLiteral/MultiLine.txt");
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
package org.jetbrains.kotlin.idea.editor
|
||||
|
||||
import junit.framework.TestCase
|
||||
import org.jetbrains.kotlin.idea.codeinsights.impl.base.createTemplateSequenceTokenString
|
||||
import org.junit.Assert
|
||||
|
||||
|
||||
class TemplateTokenSequenceTest : TestCase() {
|
||||
fun doTest(input: String, expected: String) {
|
||||
val output = createTemplateSequenceTokenString(input)
|
||||
val output = createTemplateSequenceTokenString(input, 0)
|
||||
Assert.assertEquals("Unexpected template sequence output for $input: ", expected, output)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start Foo${'$'}Bar end"""
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Foo$Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start <caret> end"""
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start Foo$$${'$'}Bar end"""
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$$$"<selection>Foo$$$Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start <caret> end"""
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start Foo$$\$Bar end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$$$"<selection>Foo$$$Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start Foo\$Bar end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Foo$Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start Foo$ Bar end"""
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Foo$ Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = """start <caret> end"""
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start Foo$ Bar end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Foo$ Bar</selection>"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1,5 @@
|
||||
// KTIJ-31607
|
||||
val to = """
|
||||
|Foo\
|
||||
Bar
|
||||
""".trimMargin()
|
||||
@@ -0,0 +1 @@
|
||||
val from = "<selection>Foo\\\t\nBar</selection>"
|
||||
@@ -0,0 +1,4 @@
|
||||
// KTIJ-31607
|
||||
val to = """
|
||||
|<caret>
|
||||
""".trimMargin()
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$$"start Lorem Ipsum \n $ \\ \t end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum \n $ \\ \t</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$$"start Lorem $$${i} Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $${i} Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$$"start Lorem Ipsum end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$$"start Lorem $$$i Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $$i Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum \n $ \\ \t end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$$"<selection>Lorem Ipsum \n $ \\ \t</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $${i} Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$$"<selection>Lorem $$${i} Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$$"<selection>Lorem Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $$i Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$$"<selection>Lorem $$$i Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum \n \$ \\ \t end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum \n \$ \\ \t</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $${i} Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $${i} Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $$i Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $$i Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val target = "start df$foo Not\$Entry end"
|
||||
@@ -0,0 +1 @@
|
||||
val source = $$"as<selection>df$$foo Not$Entry</selection> $$bar"
|
||||
@@ -0,0 +1 @@
|
||||
val target = "start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val target = $$"start df$$foo Not$$\$Entry end"
|
||||
@@ -0,0 +1 @@
|
||||
val source = $$$$$"as<selection>df$$$$$foo Not$$$Entry</selection> $$$$$bar"
|
||||
@@ -0,0 +1 @@
|
||||
val target = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start Lorem Ipsum \n $ \\ \t end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum \n \$ \\ \t</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = "start Lorem ${i} Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $${i} Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start Lorem Ipsum end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"<selection>Lorem Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = "start Lorem $i Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = $$"<selection>Lorem $$i Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = "start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum \n $ \\ \t end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = "<selection>Lorem Ipsum \n \$ \\ \t</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $${i} Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = "<selection>Lorem ${i} Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start Lorem Ipsum end"
|
||||
@@ -0,0 +1 @@
|
||||
val from = "<selection>Lorem Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1 @@
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start Lorem $$i Ipsum end"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val from = "<selection>Lorem $i Ipsum</selection> Dolor"
|
||||
@@ -0,0 +1,2 @@
|
||||
val i = 1
|
||||
val to = $$"start <caret> end"
|
||||
@@ -0,0 +1 @@
|
||||
val to = "\${11}"
|
||||
@@ -0,0 +1 @@
|
||||
val from = $$"""<selection>${11}</selection>"""
|
||||
@@ -0,0 +1 @@
|
||||
val to = "<caret>"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user