diff --git a/ChangeLog.md b/ChangeLog.md
index 0ac76fd14e4..77985b7e256 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -258,6 +258,7 @@ Using 'this' as function argument in constructor of non-final class
- [`KT-7492`](https://youtrack.jetbrains.com/issue/KT-7492) Support "Create abstract function/property" inside an abstract class
- [`KT-10668`](https://youtrack.jetbrains.com/issue/KT-10668) Support "Create member/extension" corresponding to the extension receiver of enclosing function
- [`KT-12553`](https://youtrack.jetbrains.com/issue/KT-12553) Show versions in inspection about different version of Kotlin plugin in Maven and IDE plugin
+- [`KT-12489`](https://youtrack.jetbrains.com/issue/KT-12489) Implement intention to replace camel-case test function name with a space-separated one
- [`KT-12730`](https://youtrack.jetbrains.com/issue/KT-12730) Warn about using different versions of Kotlin Gradle plugin and bundled compiler
- [`KT-13173`](https://youtrack.jetbrains.com/issue/KT-13173) Handle more cases in "Add Const Modifier" Intention
- [`KT-12628`](https://youtrack.jetbrains.com/issue/KT-12628) Quickfix for `invoke` operator unsafe calls
diff --git a/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/after.kt.template b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/after.kt.template
new file mode 100644
index 00000000000..306191852a9
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/after.kt.template
@@ -0,0 +1,5 @@
+class MyTest {
+ @Test fun `test two plus two equals four`() {
+
+ }
+}
diff --git a/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/before.kt.template b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/before.kt.template
new file mode 100644
index 00000000000..8baeb555973
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/before.kt.template
@@ -0,0 +1,5 @@
+class MyTest {
+ @Test fun testTwoPlusTwoEqualsFour() {
+
+ }
+}
diff --git a/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/description.html b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/description.html
new file mode 100644
index 00000000000..de82b32bf35
--- /dev/null
+++ b/idea/resources/intentionDescriptions/ConvertCamelCaseTestFunctionToSpacedIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention changes camel-case name of the given test function to space-separated one
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index d95b99f4f54..e5e56249116 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -1336,6 +1336,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.intentions.ConvertCamelCaseTestFunctionToSpacedIntention
+ Kotlin
+
+
(
+ KtNamedFunction::class.java, "Replace camel-case name with spaces"
+) {
+ override fun applicabilityRange(element: KtNamedFunction): TextRange? {
+ val range = element.nameIdentifier?.textRange ?: return null
+
+ val name = element.name ?: return null
+ val newName = decamelize(name)
+ if (newName == name) return null
+
+ val lightMethod = element.toLightMethods().firstOrNull() ?: return null
+ if (!TestFrameworks.getInstance().isTestMethod(lightMethod)) return null
+
+ text = "Rename to $newName"
+
+ return range
+ }
+
+ enum class Case {
+ LOWER, UPPER, OTHER
+ }
+
+ private fun splitCamelName(name: String): List {
+ if (name === "") return emptyList()
+
+ val result = SmartList()
+ var previousCase = Case.OTHER
+ var from = 0
+ for (i in 0..name.length - 1) {
+ val c = name[i]
+ val currentCase = when {
+ Character.isUpperCase(c) -> Case.UPPER
+ Character.isLowerCase(c) -> Case.LOWER
+ else -> Case.OTHER
+ }
+
+ when {
+ i == name.lastIndex -> result += name.substring(from)
+ i > 0 && currentCase != previousCase && currentCase != Case.LOWER -> {
+ result += name.substring(from, i)
+ from = i
+ }
+ }
+
+ previousCase = currentCase
+ }
+
+ return result
+ }
+
+ private fun decamelize(name: String) = splitCamelName(name).joinToString(separator = " ") { it.decapitalizeSmart().trim() }.quoteIfNeeded()
+
+ private fun startRename(element: KtNamedFunction, newName: String) {
+ RenameProcessor(element.project, element, newName, false, false).run()
+ }
+
+ override fun startInWriteAction() = false
+
+ override fun applyTo(element: KtNamedFunction, editor: Editor?) {
+ val nameIdentifier = element.nameIdentifier ?: return
+ val oldName = element.name ?: return
+ val oldId = oldName.quoteIfNeeded()
+ val newId = decamelize(oldName)
+
+ if (editor != null) {
+ val builder = TemplateBuilderImpl(nameIdentifier)
+ builder.replaceElement(nameIdentifier, newId)
+ val template = runWriteAction { builder.buildInlineTemplate() }
+ TemplateManager.getInstance(element.project).startTemplate(
+ editor,
+ template,
+ object : TemplateEditingAdapter() {
+ private var chosenId: String = newId
+ private var range: TextRange? = null
+
+ override fun beforeTemplateFinished(state: TemplateState?, template: Template?) {
+ val varName = (template as? TemplateImpl)?.getVariableNameAt(0) ?: return
+ chosenId = state?.getVariableValue(varName)?.text?.quoteIfNeeded() ?: return
+ range = state?.getVariableRange(varName)
+ }
+
+ override fun templateFinished(template: Template?, brokenOff: Boolean) {
+ range?.let {
+ val doc = editor.document
+ runWriteAction { doc.replaceString(it.startOffset, it.endOffset, oldId) }
+ PsiDocumentManager.getInstance(element.project).commitDocument(doc)
+ }
+
+ if (!brokenOff && chosenId != oldId) {
+ startRename(element, chosenId)
+ }
+ }
+ }
+ )
+ }
+ else {
+ startRename(element, newId)
+ }
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/.intention b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/.intention
new file mode 100644
index 00000000000..7e37d249fa8
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/.intention
@@ -0,0 +1 @@
+org.jetbrains.kotlin.idea.intentions.ConvertCamelCaseTestFunctionToSpacedIntention
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/letters.kt_ignored b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/letters.kt_ignored
new file mode 100644
index 00000000000..205868cb33d
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/letters.kt_ignored
@@ -0,0 +1,12 @@
+// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
+// ERROR: Cannot access class 'java.lang.Class'. Check your module classpath for missing or conflicting dependencies
+// ERROR: This annotation is not applicable to target 'member function'
+import org.junit.Test
+
+class A {
+ @Test fun testTwoPlusTwoEqualsFour() {}
+}
+
+fun test() {
+ A().testTwoPlusTwoEqualsFour()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonFunction.kt b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonFunction.kt
new file mode 100644
index 00000000000..816ca69f2ca
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonFunction.kt
@@ -0,0 +1,4 @@
+// IS_APPLICABLE: false
+class A {
+ val foo = 1
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonLetters.kt_ignored b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonLetters.kt_ignored
new file mode 100644
index 00000000000..4cbf550fbb3
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonLetters.kt_ignored
@@ -0,0 +1,12 @@
+// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
+// ERROR: Cannot access class 'java.lang.Class'. Check your module classpath for missing or conflicting dependencies
+// ERROR: This annotation is not applicable to target 'member function'
+import org.junit.Test
+
+class A {
+ @Test fun `testTwo + Two==Four`() {}
+}
+
+fun test() {
+ A().`testTwo + Two==Four`()
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notAtIdentifier.kt b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notAtIdentifier.kt
new file mode 100644
index 00000000000..ecdd30125c1
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notAtIdentifier.kt
@@ -0,0 +1,4 @@
+// IS_APPLICABLE: false
+class A {
+ fun foo() {}
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notTestFunction.kt b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notTestFunction.kt
new file mode 100644
index 00000000000..58fe2091692
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notTestFunction.kt
@@ -0,0 +1,4 @@
+// IS_APPLICABLE: false
+class A {
+ fun foo() {}
+}
\ No newline at end of file
diff --git a/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/unchanged.kt_ignored b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/unchanged.kt_ignored
new file mode 100644
index 00000000000..7825ab491ff
--- /dev/null
+++ b/idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/unchanged.kt_ignored
@@ -0,0 +1,10 @@
+// CONFIGURE_LIBRARY: JUnit@lib/junit-4.12.jar
+// ERROR: Cannot access class 'java.lang.Class'. Check your module classpath for missing or conflicting dependencies
+// ERROR: This annotation is not applicable to target 'member function'
+// IS_APPLICABLE: false
+
+import org.junit.Test
+
+class A {
+ @Test fun `foo bar`() {}
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
index 88d89ae0ab9..95c80d0acda 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractIntentionTest.java
@@ -28,6 +28,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.util.CommonRefactoringUtil;
+import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.Convertor;
@@ -145,6 +146,8 @@ public abstract class AbstractIntentionTest extends KotlinCodeInsightTestCase {
ConfigLibraryUtil.configureKotlinRuntimeAndSdk(getModule(), PluginTestCaseBase.mockJdk());
}
+ ConfigLibraryUtil.configureLibrariesByDirective(getModule(), PlatformTestUtil.getCommunityPath(), fileText);
+
if (getFile() instanceof KtFile && !InTextDirectivesUtils.isDirectiveDefined(fileText, "// SKIP_ERRORS_BEFORE")) {
DirectiveBasedActionUtils.INSTANCE.checkForUnexpectedErrors((KtFile) getFile());
}
@@ -156,6 +159,7 @@ public abstract class AbstractIntentionTest extends KotlinCodeInsightTestCase {
}
}
finally {
+ ConfigLibraryUtil.unconfigureLibrariesByDirective(myModule, fileText);
if (isWithRuntime) {
ConfigLibraryUtil.unConfigureKotlinRuntimeAndSdk(getModule(), getTestProjectJdk());
}
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
index 5deb1937732..37c19b34522 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/IntentionTestGenerated.java
@@ -3509,6 +3509,33 @@ public class IntentionTestGenerated extends AbstractIntentionTest {
}
}
+ @TestMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced")
+ @TestDataPath("$PROJECT_ROOT")
+ @RunWith(JUnit3RunnerWithInners.class)
+ public static class ConvertCamelCaseTestFunctionToSpaced extends AbstractIntentionTest {
+ public void testAllFilesPresentInConvertCamelCaseTestFunctionToSpaced() throws Exception {
+ KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced"), Pattern.compile("^([\\w\\-_]+)\\.kt$"), true);
+ }
+
+ @TestMetadata("nonFunction.kt")
+ public void testNonFunction() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/nonFunction.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("notAtIdentifier.kt")
+ public void testNotAtIdentifier() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notAtIdentifier.kt");
+ doTest(fileName);
+ }
+
+ @TestMetadata("notTestFunction.kt")
+ public void testNotTestFunction() throws Exception {
+ String fileName = KotlinTestUtils.navigationMetadata("idea/testData/intentions/convertCamelCaseTestFunctionToSpaced/notTestFunction.kt");
+ doTest(fileName);
+ }
+ }
+
@TestMetadata("idea/testData/intentions/convertForEachToForLoop")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)