diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index dffd95238db..aa30cb94ab4 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1189,6 +1189,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.ToRawStringLiteralIntention
+ Kotlin
+
+
(
+ KtStringTemplateExpression::class.java,
+ "To raw string literal"
+), LowPriorityAction {
+ override fun isApplicableTo(element: KtStringTemplateExpression): Boolean {
+ val text = element.text
+ if (text.startsWith("\"\"\"")) return false // already raw
+
+ val escapeEntries = element.entries.filterIsInstance()
+ for (entry in escapeEntries) {
+ val c = entry.unescapedValue.singleOrNull() ?: return false
+ if (Character.isISOControl(c) && c != '\n' && c != '\r') return false
+ }
+
+ val converted = convertContent(element)
+ return !converted.contains("\"\"\"") && !hasTrailingSpaces(converted)
+ }
+
+ override fun applyTo(element: KtStringTemplateExpression, editor: Editor?) {
+ val text = convertContent(element)
+ element.replace(KtPsiFactory(element).createExpression("\"\"\"" + text + "\"\"\""))
+ }
+
+ private fun convertContent(element: KtStringTemplateExpression): String {
+ val text = buildString {
+ val entries = element.entries
+ for ((index, entry) in entries.withIndex()) {
+ val value = entry.value()
+
+ if (value.endsWith("$") && index < entries.size - 1) {
+ val nextChar = entries[index + 1].value().first()
+ if (nextChar.isJavaIdentifierStart() || nextChar == '{') {
+ append("\${\"$\"}")
+ continue
+ }
+ }
+
+ append(value)
+ }
+ }
+ return StringUtilRt.convertLineSeparators(text, "\n")
+ }
+
+ private fun hasTrailingSpaces(text: String): Boolean {
+ var afterSpace = true
+ for (c in text) {
+ if ((c == '\n' || c == '\r') && afterSpace) return true
+ afterSpace = c == ' ' || c == '\t'
+ }
+ return false
+ }
+
+ private fun KtStringTemplateEntry.value() = if (this is KtEscapeStringTemplateEntry) this.unescapedValue else text
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/.intention b/idea/testData/intentions/toRawStringLiteral/.intention
new file mode 100644
index 00000000000..22fc2dca859
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ToRawStringLiteralIntention
diff --git a/idea/testData/intentions/toRawStringLiteral/3quotes.kt b/idea/testData/intentions/toRawStringLiteral/3quotes.kt
new file mode 100644
index 00000000000..cca27e059fb
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/3quotes.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = "\"\"\"Hello, world!\"\"\""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/alreadyRaw.kt b/idea/testData/intentions/toRawStringLiteral/alreadyRaw.kt
new file mode 100644
index 00000000000..f7431f4bd20
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/alreadyRaw.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = """ """
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/dollar.kt b/idea/testData/intentions/toRawStringLiteral/dollar.kt
new file mode 100644
index 00000000000..fb3c1f0b4c5
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/dollar.kt
@@ -0,0 +1 @@
+val v = "\$abc"
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/dollar.kt.after b/idea/testData/intentions/toRawStringLiteral/dollar.kt.after
new file mode 100644
index 00000000000..ac3f220ea40
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/dollar.kt.after
@@ -0,0 +1 @@
+val v = """${"$"}abc"""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/dollar2.kt b/idea/testData/intentions/toRawStringLiteral/dollar2.kt
new file mode 100644
index 00000000000..a656702a171
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/dollar2.kt
@@ -0,0 +1 @@
+val v = " $\u0041 $ \${} $"
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/dollar2.kt.after b/idea/testData/intentions/toRawStringLiteral/dollar2.kt.after
new file mode 100644
index 00000000000..baa9a2cfe1b
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/dollar2.kt.after
@@ -0,0 +1 @@
+val v = """ ${"$"}A $ ${"$"}{} $"""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt b/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt
new file mode 100644
index 00000000000..b5a16d90355
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt
@@ -0,0 +1 @@
+val v = "\"Hello, world!\" \\ \'a\'"
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt.after b/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt.after
new file mode 100644
index 00000000000..83e675cfcb9
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt.after
@@ -0,0 +1 @@
+val v = """"Hello, world!" \ 'a'"""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/simple.kt b/idea/testData/intentions/toRawStringLiteral/simple.kt
new file mode 100644
index 00000000000..3a17c96f351
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/simple.kt
@@ -0,0 +1 @@
+val v = "\"\\Hello,\nworld!\\\""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/simple.kt.after b/idea/testData/intentions/toRawStringLiteral/simple.kt.after
new file mode 100644
index 00000000000..755d4a19631
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/simple.kt.after
@@ -0,0 +1,2 @@
+val v = """"\Hello,
+world!\""""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/specialChar.kt b/idea/testData/intentions/toRawStringLiteral/specialChar.kt
new file mode 100644
index 00000000000..1580f7c1a04
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/specialChar.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = "\u0000"
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/tabCharacter.kt b/idea/testData/intentions/toRawStringLiteral/tabCharacter.kt
new file mode 100644
index 00000000000..0fe023e9f78
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/tabCharacter.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = "\t"
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/trailingSpace.kt b/idea/testData/intentions/toRawStringLiteral/trailingSpace.kt
new file mode 100644
index 00000000000..71a2782e9ee
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/trailingSpace.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = "\"\\Hello, \nworld!\\\""
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt b/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt
new file mode 100644
index 00000000000..317c635b870
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt
@@ -0,0 +1 @@
+val v = "Hello, world!\n "
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt.after b/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt.after
new file mode 100644
index 00000000000..1cbc6e4d32f
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt.after
@@ -0,0 +1,2 @@
+val v = """Hello, world!
+ """
\ No newline at end of file
diff --git a/idea/testData/intentions/toRawStringLiteral/trailingSpace3.kt b/idea/testData/intentions/toRawStringLiteral/trailingSpace3.kt
new file mode 100644
index 00000000000..b796e8731c6
--- /dev/null
+++ b/idea/testData/intentions/toRawStringLiteral/trailingSpace3.kt
@@ -0,0 +1,2 @@
+// IS_APPLICABLE: false
+val v = "\n Hello, world!"
\ No newline at end of file
diff --git a/idea/testData/quickfix/autoImports/delegateNoOperator.test b/idea/testData/quickfix/autoImports/delegateNoOperator.test
index 9af6d8f94b8..6fc3097634e 100644
--- a/idea/testData/quickfix/autoImports/delegateNoOperator.test
+++ b/idea/testData/quickfix/autoImports/delegateNoOperator.test
@@ -1,6 +1,7 @@
// FILE: first.before.kt
// "Import" "false"
// ERROR: Missing 'getValue(Nothing?, KProperty<*>)' method on delegate of type 'String'
+// ACTION: To raw string literal
package b
diff --git a/idea/testData/quickfix/createFromUsage/createClass/callExpression/callWithExtraArgs.kt b/idea/testData/quickfix/createFromUsage/createClass/callExpression/callWithExtraArgs.kt
index e022d12783e..85930497230 100644
--- a/idea/testData/quickfix/createFromUsage/createClass/callExpression/callWithExtraArgs.kt
+++ b/idea/testData/quickfix/createFromUsage/createClass/callExpression/callWithExtraArgs.kt
@@ -3,6 +3,7 @@
// ACTION: Add parameter to constructor 'Foo'
// ACTION: Create secondary constructor
// ERROR: Too many arguments for public constructor Foo(a: Int) defined in Foo
+// ACTION: To raw string literal
class Foo(a: Int)
diff --git a/idea/testData/quickfix/typeMismatch/addArrayOfTypeForJavaAnnotation.before.Main.kt b/idea/testData/quickfix/typeMismatch/addArrayOfTypeForJavaAnnotation.before.Main.kt
index 3499c0c081f..0e195a4e94b 100644
--- a/idea/testData/quickfix/typeMismatch/addArrayOfTypeForJavaAnnotation.before.Main.kt
+++ b/idea/testData/quickfix/typeMismatch/addArrayOfTypeForJavaAnnotation.before.Main.kt
@@ -3,5 +3,6 @@
// ACTION: Create test
// ACTION: Make internal
// ACTION: Make private
+// ACTION: To raw string literal
@ArrAnn("123") class My
\ No newline at end of file
diff --git a/idea/testData/quickfix/typeMismatch/parameterTypeMismatch/changeFunctionParameterType4.kt b/idea/testData/quickfix/typeMismatch/parameterTypeMismatch/changeFunctionParameterType4.kt
index bbc9aa20f83..2a86e92fd0d 100644
--- a/idea/testData/quickfix/typeMismatch/parameterTypeMismatch/changeFunctionParameterType4.kt
+++ b/idea/testData/quickfix/typeMismatch/parameterTypeMismatch/changeFunctionParameterType4.kt
@@ -1,4 +1,5 @@
// "Change parameter 'z' type of function 'foo' to '(Int) -> String'" "false"
+// ACTION: To raw string literal
fun foo(y: Int = 0, z: (Int) -> String = {""}) {
foo {
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index 6679a195e7e..6a38a1c2f28 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -9231,6 +9231,81 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/toRawStringLiteral")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ToRawStringLiteral extends AbstractIntentionTest {
+ @TestMetadata("3quotes.kt")
+ public void test3quotes() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/3quotes.kt");
+ doTest(fileName);
+ }
+
+ public void testAllFilesPresentInToRawStringLiteral() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/toRawStringLiteral"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("alreadyRaw.kt")
+ public void testAlreadyRaw() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/alreadyRaw.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("dollar.kt")
+ public void testDollar() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/dollar.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("dollar2.kt")
+ public void testDollar2() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/dollar2.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("quotesAndSlashes.kt")
+ public void testQuotesAndSlashes() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/quotesAndSlashes.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("simple.kt")
+ public void testSimple() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/simple.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("specialChar.kt")
+ public void testSpecialChar() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/specialChar.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("tabCharacter.kt")
+ public void testTabCharacter() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/tabCharacter.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("trailingSpace.kt")
+ public void testTrailingSpace() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/trailingSpace.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("trailingSpace2.kt")
+ public void testTrailingSpace2() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/trailingSpace2.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("trailingSpace3.kt")
+ public void testTrailingSpace3() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/toRawStringLiteral/trailingSpace3.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/usePropertyAccessSyntax")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)