diff --git a/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/after.kt.template b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/after.kt.template
new file mode 100644
index 00000000000..1efa896629b
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/after.kt.template
@@ -0,0 +1 @@
+a until b
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/before.kt.template b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/before.kt.template
new file mode 100644
index 00000000000..e2c2168b033
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/before.kt.template
@@ -0,0 +1 @@
+a..b-1
diff --git a/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/description.html b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/description.html
new file mode 100644
index 00000000000..f7fcd47e779
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceRangeToWithUntilIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention replaces calls to 'rangeTo' or the '..' operator with calls to 'until' and offsets the upper bound by 1.
+
+
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/after.kt.template b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/after.kt.template
new file mode 100644
index 00000000000..e2c2168b033
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/after.kt.template
@@ -0,0 +1 @@
+a..b-1
diff --git a/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/before.kt.template b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/before.kt.template
new file mode 100644
index 00000000000..1efa896629b
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/before.kt.template
@@ -0,0 +1 @@
+a until b
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/description.html b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/description.html
new file mode 100644
index 00000000000..227464b937e
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ReplaceUntilWithRangeToIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention replaces calls to 'until' with the '..' operator and subtracts one from the upper bound.
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index f2e95d85d40..d61728fd904 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1310,6 +1310,16 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.ReplaceRangeToWithUntilIntention
+ Kotlin
+
+
+
+ org.jetbrains.kotlin.idea.intentions.ReplaceUntilWithRangeToIntention
+ Kotlin
+
+
this.left to this.right
+ is KtDotQualifiedExpression -> this.receiverExpression to this.callExpression?.valueArguments?.singleOrNull()?.getArgumentExpression()
+ else -> null
+}
+
+private val REGEX_RANGE_TO = """kotlin.(Char|Byte|Short|Int|Long).rangeTo""".toRegex()
+
+class ReplaceRangeToWithUntilIntention : SelfTargetingIntention(
+ KtExpression::class.java,
+ "Replace with 'until' function call"
+) {
+
+ override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean {
+ if (element !is KtBinaryExpression && element !is KtDotQualifiedExpression) return false
+
+ val fqName = element.getCallableDescriptor()?.fqNameUnsafe?.asString() ?: return false
+ if (!fqName.matches(REGEX_RANGE_TO)) return false
+ return element.getArguments()?.second?.isMinusOne() == true
+ }
+
+ override fun applyTo(element: KtExpression, editor: Editor?) {
+ val args = element.getArguments() ?: return
+ element.replace(KtPsiFactory(element).createExpressionByPattern("$0 until $1",
+ args.first ?: return,
+ (args.second as? KtBinaryExpression)?.left ?: return))
+ }
+
+ private fun KtExpression.isMinusOne(): Boolean {
+ if (this !is KtBinaryExpression) return false
+ if (operationToken != KtTokens.MINUS) return false
+
+ val right = right as? KtConstantExpression ?: return false
+ val context = right.analyze(BodyResolveMode.PARTIAL)
+ val constantValue = ConstantExpressionEvaluator.getConstant(right, context)?.toConstantValue(right.getType(context) ?: return false)
+ val rightValue = (constantValue?.value as? Number)?.toInt() ?: return false
+ return rightValue == 1
+ }
+
+}
+
+class ReplaceUntilWithRangeToIntention : SelfTargetingIntention(KtExpression::class.java, "Replace with '..' operator") {
+ override fun isApplicableTo(element: KtExpression, caretOffset: Int): Boolean {
+ if (element !is KtBinaryExpression && element !is KtDotQualifiedExpression) return false
+ val fqName = element.getCallableDescriptor()?.fqNameUnsafe?.asString() ?: return false
+ return fqName == "kotlin.ranges.until"
+ }
+
+ override fun applyTo(element: KtExpression, editor: Editor?) {
+ val args = element.getArguments() ?: return
+ element.replace(KtPsiFactory(element).createExpressionByPattern("$0..$1 - 1", args.first ?: return, args.second ?: return))
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/.intention b/idea/testData/intentions/replaceRangeToWithUntil/.intention
new file mode 100644
index 00000000000..5faf679b8df
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ReplaceRangeToWithUntilIntention
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/closedRange.kt b/idea/testData/intentions/replaceRangeToWithUntil/closedRange.kt
new file mode 100644
index 00000000000..cd4ffc2c903
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/closedRange.kt
@@ -0,0 +1,6 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+
+fun foo(a: Float) {
+ 1f..a - 1
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/minusTwo.kt b/idea/testData/intentions/replaceRangeToWithUntil/minusTwo.kt
new file mode 100644
index 00000000000..fb48d5920e5
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/minusTwo.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+
+fun foo(a: Int) {
+ for (i in 0..a - 2) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/notMinusOne.kt b/idea/testData/intentions/replaceRangeToWithUntil/notMinusOne.kt
new file mode 100644
index 00000000000..3ece06af44f
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/notMinusOne.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+
+fun foo(a: Int) {
+ for (i in 0..a) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/operator.kt b/idea/testData/intentions/replaceRangeToWithUntil/operator.kt
new file mode 100644
index 00000000000..fb00b189ad2
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/operator.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0..a - 1) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/operator.kt.after b/idea/testData/intentions/replaceRangeToWithUntil/operator.kt.after
new file mode 100644
index 00000000000..6109f8b93bd
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/operator.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0 until a) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt b/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt
new file mode 100644
index 00000000000..135db06bf6b
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Long) {
+ for (i in 1L..a - 1L) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt.after b/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt.after
new file mode 100644
index 00000000000..7faae7b6a3b
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Long) {
+ for (i in 1L until a) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/plusOne.kt b/idea/testData/intentions/replaceRangeToWithUntil/plusOne.kt
new file mode 100644
index 00000000000..814ac76fb49
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/plusOne.kt
@@ -0,0 +1,8 @@
+// WITH_RUNTIME
+// IS_APPLICABLE: false
+
+fun foo(a: Int) {
+ for (i in 0..a + 1) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt b/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt
new file mode 100644
index 00000000000..72fab0da864
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0.rangeTo(a - 1)) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt.after b/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt.after
new file mode 100644
index 00000000000..6109f8b93bd
--- /dev/null
+++ b/idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0 until a) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceUntilWithRangeTo/.intention b/idea/testData/intentions/replaceUntilWithRangeTo/.intention
new file mode 100644
index 00000000000..bc5b75b9b88
--- /dev/null
+++ b/idea/testData/intentions/replaceUntilWithRangeTo/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ReplaceUntilWithRangeToIntention
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt b/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt
new file mode 100644
index 00000000000..6109f8b93bd
--- /dev/null
+++ b/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0 until a) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt.after b/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt.after
new file mode 100644
index 00000000000..fb00b189ad2
--- /dev/null
+++ b/idea/testData/intentions/replaceUntilWithRangeTo/simple.kt.after
@@ -0,0 +1,7 @@
+// WITH_RUNTIME
+
+fun foo(a: Int) {
+ for (i in 0..a - 1) {
+
+ }
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index 318e9d1eace..270fd38ab15 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -10070,6 +10070,57 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/replaceRangeToWithUntil")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ReplaceRangeToWithUntil extends AbstractIntentionTest {
+ public void testAllFilesPresentInReplaceRangeToWithUntil() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/replaceRangeToWithUntil"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("closedRange.kt")
+ public void testClosedRange() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/closedRange.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("minusTwo.kt")
+ public void testMinusTwo() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/minusTwo.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("notMinusOne.kt")
+ public void testNotMinusOne() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/notMinusOne.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("operator.kt")
+ public void testOperator() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/operator.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("operatorLong.kt")
+ public void testOperatorLong() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/operatorLong.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("plusOne.kt")
+ public void testPlusOne() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/plusOne.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("rangeTo.kt")
+ public void testRangeTo() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceRangeToWithUntil/rangeTo.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/replaceSubstringWithDropLast")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
@@ -10220,6 +10271,21 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/replaceUntilWithRangeTo")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ReplaceUntilWithRangeTo extends AbstractIntentionTest {
+ public void testAllFilesPresentInReplaceUntilWithRangeTo() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/replaceUntilWithRangeTo"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("simple.kt")
+ public void testSimple() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/replaceUntilWithRangeTo/simple.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/replaceWithOperatorAssignment")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)