diff --git a/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/after.kt.template b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/after.kt.template
new file mode 100644
index 00000000000..5bc18517590
--- /dev/null
+++ b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/after.kt.template
@@ -0,0 +1,13 @@
+// File: foo/bar/myFile.kt
+package foo.bar2
+
+fun test() {
+
+}
+
+// File: foo/bar/test.kt
+package foo.bar2
+
+class Test {
+
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/before.kt.template b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/before.kt.template
new file mode 100644
index 00000000000..f99c851a557
--- /dev/null
+++ b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/before.kt.template
@@ -0,0 +1,10 @@
+// File: foo/bar/myFile.kt
+package foo.bar2
+
+class Test {
+
+}
+
+fun test() {
+
+}
\ No newline at end of file
diff --git a/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/description.html b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/description.html
new file mode 100644
index 00000000000..9deb81e874b
--- /dev/null
+++ b/idea/resources/intentionDescriptions/MoveDeclarationToSeparateFileIntention/description.html
@@ -0,0 +1,5 @@
+
+
+This intention moves top-level class or object to the separate file in the same package and directory
+
+
\ No newline at end of file
diff --git a/idea/src/META-INF/plugin.xml b/idea/src/META-INF/plugin.xml
index 568f990e650..d4b380c325b 100644
--- a/idea/src/META-INF/plugin.xml
+++ b/idea/src/META-INF/plugin.xml
@@ -952,6 +952,11 @@
Kotlin
+
+ org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention
+ Kotlin
+
+
org.jetbrains.kotlin.idea.intentions.ChangeVisibilityModifierIntention$Public
Kotlin
diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt
index 25643b82358..06dff716317 100644
--- a/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt
+++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/jetRefactoringUtil.kt
@@ -94,12 +94,12 @@ fun PsiElement.getAndRemoveCopyableUserData(key: Key): T? {
fun getOrCreateKotlinFile(fileName: String, targetDir: PsiDirectory): JetFile? =
(targetDir.findFile(fileName) ?: createKotlinFile(fileName, targetDir)) as? JetFile
-fun createKotlinFile(fileName: String, targetDir: PsiDirectory): JetFile {
- val packageName = targetDir.getPackage()?.getQualifiedName()
-
+fun createKotlinFile(fileName: String,
+ targetDir: PsiDirectory,
+ packageName: String? = targetDir.getPackage()?.qualifiedName): JetFile {
targetDir.checkCreateFile(fileName)
val file = PsiFileFactory.getInstance(targetDir.getProject()).createFileFromText(
- fileName, JetFileType.INSTANCE, if (packageName != null && packageName.isNotEmpty()) "package $packageName \n\n" else ""
+ fileName, JetFileType.INSTANCE, if (!packageName.isNullOrBlank()) "package $packageName \n\n" else ""
)
return targetDir.add(file) as JetFile
diff --git a/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveDeclarationToSeparateFileIntention.kt b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveDeclarationToSeparateFileIntention.kt
new file mode 100644
index 00000000000..10338a8815a
--- /dev/null
+++ b/idea/src/org/jetbrains/kotlin/idea/refactoring/move/moveTopLevelDeclarations/MoveDeclarationToSeparateFileIntention.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2010-2015 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations
+
+import com.intellij.codeInsight.intention.LowPriorityAction
+import com.intellij.codeInsight.navigation.NavigationUtil
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.util.TextRange
+import com.intellij.refactoring.move.MoveCallback
+import com.intellij.refactoring.util.CommonRefactoringUtil
+import org.jetbrains.kotlin.idea.core.refactoring.createKotlinFile
+import org.jetbrains.kotlin.idea.intentions.JetSelfTargetingRangeIntention
+import org.jetbrains.kotlin.idea.quickfix.moveCaret
+import org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog
+import org.jetbrains.kotlin.lexer.JetTokens
+import org.jetbrains.kotlin.psi.JetClass
+import org.jetbrains.kotlin.psi.JetClassOrObject
+import org.jetbrains.kotlin.psi.JetFile
+import org.jetbrains.kotlin.psi.JetObjectDeclaration
+import org.jetbrains.kotlin.psi.psiUtil.endOffset
+import org.jetbrains.kotlin.psi.psiUtil.startOffset
+
+public class MoveDeclarationToSeparateFileIntention :
+ JetSelfTargetingRangeIntention(JetClassOrObject::class.java, "Move declaration to separate file"),
+ LowPriorityAction {
+ override fun applicabilityRange(element: JetClassOrObject): TextRange? {
+ if (element.name == null) return null
+ if (element.parent !is JetFile) return null
+ if (element.hasModifier(JetTokens.PRIVATE_KEYWORD)) return null
+ if (element.getContainingJetFile().declarations.size() == 1) return null
+
+ val keyword = when (element) {
+ is JetClass -> element.getClassOrInterfaceKeyword()
+ is JetObjectDeclaration -> element.getObjectKeyword()
+ else -> return null
+ }
+ val startOffset = keyword?.startOffset ?: return null
+ val endOffset = element.nameIdentifier?.endOffset ?: return null
+
+ text = "Move '${element.name}' to separate file"
+
+ return TextRange(startOffset, endOffset)
+ }
+
+ override fun applyTo(element: JetClassOrObject, editor: Editor) {
+ val file = element.getContainingJetFile()
+ val project = file.project
+ val originalOffset = editor.caretModel.offset - element.startOffset
+ val directory = file.containingDirectory ?: return
+ val packageName = file.packageFqName
+ val targetFileName = "${element.name}.kt"
+ val targetFile = directory.findFile(targetFileName)
+ if (targetFile != null) {
+ if (ApplicationManager.getApplication().isUnitTestMode) {
+ throw CommonRefactoringUtil.RefactoringErrorHintException("File $targetFileName already exists")
+ }
+
+ // If automatic move is not possible, fall back to full-fledged Move Declarations refactoring
+ ApplicationManager.getApplication().invokeLater {
+ MoveKotlinTopLevelDeclarationsDialog(project,
+ setOf(element),
+ packageName.asString(),
+ directory,
+ targetFile as? JetFile,
+ true,
+ true,
+ true,
+ null).show()
+ }
+ return
+ }
+ val moveTarget = DeferredJetFileKotlinMoveTarget(project, packageName) {
+ createKotlinFile(targetFileName, directory, packageName.asString())
+ }
+ val moveOptions = MoveKotlinTopLevelDeclarationsOptions(
+ elementsToMove = listOf(element),
+ moveTarget = moveTarget,
+ searchInCommentsAndStrings = false,
+ searchInNonCode = false,
+ updateInternalReferences = false,
+ moveCallback = MoveCallback {
+ val newFile = directory.findFile(targetFileName) as JetFile
+ val newDeclaration = newFile.declarations.first()
+ NavigationUtil.activateFileWithPsiElement(newFile)
+ FileEditorManager.getInstance(project).selectedTextEditor?.moveCaret(newDeclaration.startOffset + originalOffset)
+ }
+ )
+ MoveKotlinTopLevelDeclarationsProcessor(project, moveOptions).run()
+ }
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/A.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/A.kt
new file mode 100644
index 00000000000..dc9680b8905
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/A.kt
@@ -0,0 +1,5 @@
+package source
+
+fun bar() {
+
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt
new file mode 100644
index 00000000000..f9e96612f0b
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt
@@ -0,0 +1,11 @@
+package p
+
+class A {
+
+}
+
+fun foo() {
+
+}
+
+val x = 1
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt.conflicts b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt.conflicts
new file mode 100644
index 00000000000..7feef1fd376
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/after/source/test.kt.conflicts
@@ -0,0 +1 @@
+File A.kt already exists
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/A.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/A.kt
new file mode 100644
index 00000000000..dc9680b8905
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/A.kt
@@ -0,0 +1,5 @@
+package source
+
+fun bar() {
+
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt
new file mode 100644
index 00000000000..e2aa3e5e7a9
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt
@@ -0,0 +1,11 @@
+package p
+
+class A {
+
+}
+
+fun foo() {
+
+}
+
+val x = 1
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt.conflicts b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt.conflicts
new file mode 100644
index 00000000000..7feef1fd376
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/before/source/test.kt.conflicts
@@ -0,0 +1 @@
+File A.kt already exists
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test
new file mode 100644
index 00000000000..0a22c66d7f9
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test
@@ -0,0 +1,4 @@
+{
+ "mainFile": "source/test.kt",
+ "intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/A.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/A.kt
new file mode 100644
index 00000000000..0fcfafdcf05
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/A.kt
@@ -0,0 +1,5 @@
+package p
+
+class A {
+
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/test.kt
new file mode 100644
index 00000000000..bf0c26be2f6
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/test.kt
@@ -0,0 +1,7 @@
+package p
+
+fun foo() {
+
+}
+
+val x = 1
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/before/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/before/source/test.kt
new file mode 100644
index 00000000000..e2aa3e5e7a9
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/before/source/test.kt
@@ -0,0 +1,11 @@
+package p
+
+class A {
+
+}
+
+fun foo() {
+
+}
+
+val x = 1
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test
new file mode 100644
index 00000000000..0a22c66d7f9
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test
@@ -0,0 +1,4 @@
+{
+ "mainFile": "source/test.kt",
+ "intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/after/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/after/source/test.kt
new file mode 100644
index 00000000000..0fcfafdcf05
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/after/source/test.kt
@@ -0,0 +1,5 @@
+package p
+
+class A {
+
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/before/source/test.kt b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/before/source/test.kt
new file mode 100644
index 00000000000..d08430ecde4
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/before/source/test.kt
@@ -0,0 +1,5 @@
+package p
+
+class A {
+
+}
\ No newline at end of file
diff --git a/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test
new file mode 100644
index 00000000000..02d810e77b3
--- /dev/null
+++ b/idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test
@@ -0,0 +1,5 @@
+{
+ "mainFile": "source/test.kt",
+ "intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention",
+ "isApplicable": "false"
+}
\ No newline at end of file
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractMultiFileIntentionTest.kt b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractMultiFileIntentionTest.kt
index cfda30592f9..80623e57f18 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractMultiFileIntentionTest.kt
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/AbstractMultiFileIntentionTest.kt
@@ -18,25 +18,20 @@ package org.jetbrains.kotlin.idea.intentions
import com.google.gson.JsonObject
import com.google.gson.JsonParser
-import com.intellij.codeInsight.TargetElementUtilBase
import com.intellij.codeInsight.intention.IntentionAction
-import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiManager
-import com.intellij.refactoring.BaseRefactoringProcessor
-import com.intellij.util.PathUtil
+import com.intellij.refactoring.util.CommonRefactoringUtil
import junit.framework.TestCase
import org.jetbrains.kotlin.idea.jsonUtils.getNullableString
import org.jetbrains.kotlin.idea.jsonUtils.getString
-import org.jetbrains.kotlin.idea.refactoring.move.MoveAction
import org.jetbrains.kotlin.idea.test.ConfigLibraryUtil
import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
-import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.test.JetTestUtils
import org.junit.Assert
import java.io.File
@@ -55,6 +50,7 @@ public abstract class AbstractMultiFileIntentionTest : KotlinMultiFileTestCase()
doTest({ rootDir, rootAfter ->
val mainFile = rootDir.findFileByRelativePath(mainFilePath)!!
+ val conflictFile = rootDir.findFileByRelativePath("$mainFilePath.conflicts")
val document = FileDocumentManager.getInstance().getDocument(mainFile)!!
val editor = EditorFactory.getInstance()!!.createEditor(document, getProject()!!)!!
editor.getCaretModel().moveToOffset(extractCaretOffset(document))
@@ -72,6 +68,11 @@ public abstract class AbstractMultiFileIntentionTest : KotlinMultiFileTestCase()
intentionAction.invoke(getProject(), editor, mainPsiFile)
}
}
+
+ assert(conflictFile == null) { "Conflict file $conflictFile should not exist" }
+ }
+ catch (e: CommonRefactoringUtil.RefactoringErrorHintException) {
+ JetTestUtils.assertEqualsToFile(File(conflictFile!!.path), e.getMessage()!!)
}
finally {
PsiDocumentManager.getInstance(getProject()!!).commitAllDocuments()
diff --git a/idea/tests/org/jetbrains/kotlin/idea/intentions/MultiFileIntentionTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/intentions/MultiFileIntentionTestGenerated.java
index 8b27750abb7..8d9ee4a263e 100644
--- a/idea/tests/org/jetbrains/kotlin/idea/intentions/MultiFileIntentionTestGenerated.java
+++ b/idea/tests/org/jetbrains/kotlin/idea/intentions/MultiFileIntentionTestGenerated.java
@@ -35,6 +35,24 @@ public class MultiFileIntentionTestGenerated extends AbstractMultiFileIntentionT
JetTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/multiFileIntentions"), Pattern.compile("^(.+)\\.test$"));
}
+ @TestMetadata("moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test")
+ public void testMoveDeclarationToSeparateFile_moveClassToExistingFile_MoveClassToExistingFile() throws Exception {
+ String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToExistingFile/moveClassToExistingFile.test");
+ doTest(fileName);
+ }
+
+ @TestMetadata("moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test")
+ public void testMoveDeclarationToSeparateFile_moveClassToFile_MoveClassToFile() throws Exception {
+ String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test");
+ doTest(fileName);
+ }
+
+ @TestMetadata("moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test")
+ public void testMoveDeclarationToSeparateFile_moveSingleToFile_MoveSingleToFile() throws Exception {
+ String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveSingleToFile/moveSingleToFile.test");
+ doTest(fileName);
+ }
+
@TestMetadata("moveFileToPackageMatchingDirectory/moveToDefaultDirectory/moveToDefaultDirectory.test")
public void testMoveFileToPackageMatchingDirectory_moveToDefaultDirectory_MoveToDefaultDirectory() throws Exception {
String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveFileToPackageMatchingDirectory/moveToDefaultDirectory/moveToDefaultDirectory.test");