Move Declarations: Add intention which moves top-level declaration to the separate file in the same directory and package

#KT-4936 Fixed
This commit is contained in:
Alexey Sedunov
2015-09-04 18:29:04 +03:00
parent b4410c625f
commit 33c93eb4a9
22 changed files with 248 additions and 10 deletions
@@ -0,0 +1,13 @@
// File: foo/bar/myFile.kt
package foo.bar2
fun test() {
}
// File: foo/bar/test.kt
package foo.bar2
class Test {
}
@@ -0,0 +1,10 @@
// File: foo/bar/myFile.kt
package foo.bar2
<spot>class Test</spot> {
}
fun test() {
}
@@ -0,0 +1,5 @@
<html>
<body>
This intention moves top-level class or object to the separate file in the same package and directory
</body>
</html>
+5
View File
@@ -952,6 +952,11 @@
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention</className>
<category>Kotlin</category>
</intentionAction>
<intentionAction>
<className>org.jetbrains.kotlin.idea.intentions.ChangeVisibilityModifierIntention$Public</className>
<category>Kotlin</category>
@@ -94,12 +94,12 @@ fun <T: Any> PsiElement.getAndRemoveCopyableUserData(key: Key<T>): 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
@@ -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>(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()
}
}
@@ -0,0 +1,5 @@
package source
fun bar() {
}
@@ -0,0 +1,11 @@
package p
class A {
}
fun foo() {
}
val x = 1
@@ -0,0 +1,5 @@
package source
fun bar() {
}
@@ -0,0 +1,11 @@
package p
class <caret>A {
}
fun foo() {
}
val x = 1
@@ -0,0 +1,4 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
}
@@ -0,0 +1,5 @@
package p
class A {
}
@@ -0,0 +1,7 @@
package p
fun foo() {
}
val x = 1
@@ -0,0 +1,11 @@
package p
class <caret>A {
}
fun foo() {
}
val x = 1
@@ -0,0 +1,4 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
}
@@ -0,0 +1,5 @@
package p
class A {
}
@@ -0,0 +1,5 @@
package p
class <caret>A {
}
@@ -0,0 +1,5 @@
{
"mainFile": "source/test.kt",
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention",
"isApplicable": "false"
}
@@ -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()
@@ -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");