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:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// File: foo/bar/myFile.kt
|
||||||
|
package foo.bar2
|
||||||
|
|
||||||
|
fun test() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// File: foo/bar/test.kt
|
||||||
|
package foo.bar2
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
// File: foo/bar/myFile.kt
|
||||||
|
package foo.bar2
|
||||||
|
|
||||||
|
<spot>class Test</spot> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun test() {
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -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>
|
||||||
@@ -952,6 +952,11 @@
|
|||||||
<category>Kotlin</category>
|
<category>Kotlin</category>
|
||||||
</intentionAction>
|
</intentionAction>
|
||||||
|
|
||||||
|
<intentionAction>
|
||||||
|
<className>org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention</className>
|
||||||
|
<category>Kotlin</category>
|
||||||
|
</intentionAction>
|
||||||
|
|
||||||
<intentionAction>
|
<intentionAction>
|
||||||
<className>org.jetbrains.kotlin.idea.intentions.ChangeVisibilityModifierIntention$Public</className>
|
<className>org.jetbrains.kotlin.idea.intentions.ChangeVisibilityModifierIntention$Public</className>
|
||||||
<category>Kotlin</category>
|
<category>Kotlin</category>
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ fun <T: Any> PsiElement.getAndRemoveCopyableUserData(key: Key<T>): T? {
|
|||||||
fun getOrCreateKotlinFile(fileName: String, targetDir: PsiDirectory): JetFile? =
|
fun getOrCreateKotlinFile(fileName: String, targetDir: PsiDirectory): JetFile? =
|
||||||
(targetDir.findFile(fileName) ?: createKotlinFile(fileName, targetDir)) as? JetFile
|
(targetDir.findFile(fileName) ?: createKotlinFile(fileName, targetDir)) as? JetFile
|
||||||
|
|
||||||
fun createKotlinFile(fileName: String, targetDir: PsiDirectory): JetFile {
|
fun createKotlinFile(fileName: String,
|
||||||
val packageName = targetDir.getPackage()?.getQualifiedName()
|
targetDir: PsiDirectory,
|
||||||
|
packageName: String? = targetDir.getPackage()?.qualifiedName): JetFile {
|
||||||
targetDir.checkCreateFile(fileName)
|
targetDir.checkCreateFile(fileName)
|
||||||
val file = PsiFileFactory.getInstance(targetDir.getProject()).createFileFromText(
|
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
|
return targetDir.add(file) as JetFile
|
||||||
|
|||||||
+106
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
fun bar() {
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class A {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun foo() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = 1
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
File A.kt already exists
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
fun bar() {
|
||||||
|
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class <caret>A {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun foo() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = 1
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
File A.kt already exists
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"mainFile": "source/test.kt",
|
||||||
|
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class A {
|
||||||
|
|
||||||
|
}
|
||||||
idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/after/source/test.kt
Vendored
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
fun foo() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = 1
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class <caret>A {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun foo() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = 1
|
||||||
idea/testData/multiFileIntentions/moveDeclarationToSeparateFile/moveClassToFile/moveClassToFile.test
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"mainFile": "source/test.kt",
|
||||||
|
"intentionClass": "org.jetbrains.kotlin.idea.refactoring.move.moveTopLevelDeclarations.MoveDeclarationToSeparateFileIntention"
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class A {
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
package p
|
||||||
|
|
||||||
|
class <caret>A {
|
||||||
|
|
||||||
|
}
|
||||||
+5
@@ -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.JsonObject
|
||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import com.intellij.codeInsight.TargetElementUtilBase
|
|
||||||
import com.intellij.codeInsight.intention.IntentionAction
|
import com.intellij.codeInsight.intention.IntentionAction
|
||||||
import com.intellij.openapi.editor.Document
|
|
||||||
import com.intellij.openapi.editor.EditorFactory
|
import com.intellij.openapi.editor.EditorFactory
|
||||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||||
import com.intellij.openapi.util.io.FileUtil
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
import com.intellij.psi.PsiDocumentManager
|
import com.intellij.psi.PsiDocumentManager
|
||||||
import com.intellij.psi.PsiManager
|
import com.intellij.psi.PsiManager
|
||||||
import com.intellij.refactoring.BaseRefactoringProcessor
|
import com.intellij.refactoring.util.CommonRefactoringUtil
|
||||||
import com.intellij.util.PathUtil
|
|
||||||
import junit.framework.TestCase
|
import junit.framework.TestCase
|
||||||
import org.jetbrains.kotlin.idea.jsonUtils.getNullableString
|
import org.jetbrains.kotlin.idea.jsonUtils.getNullableString
|
||||||
import org.jetbrains.kotlin.idea.jsonUtils.getString
|
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.ConfigLibraryUtil
|
||||||
import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase
|
import org.jetbrains.kotlin.idea.test.KotlinMultiFileTestCase
|
||||||
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
|
||||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||||
import org.jetbrains.kotlin.idea.util.application.runWriteAction
|
|
||||||
import org.jetbrains.kotlin.test.JetTestUtils
|
import org.jetbrains.kotlin.test.JetTestUtils
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -55,6 +50,7 @@ public abstract class AbstractMultiFileIntentionTest : KotlinMultiFileTestCase()
|
|||||||
|
|
||||||
doTest({ rootDir, rootAfter ->
|
doTest({ rootDir, rootAfter ->
|
||||||
val mainFile = rootDir.findFileByRelativePath(mainFilePath)!!
|
val mainFile = rootDir.findFileByRelativePath(mainFilePath)!!
|
||||||
|
val conflictFile = rootDir.findFileByRelativePath("$mainFilePath.conflicts")
|
||||||
val document = FileDocumentManager.getInstance().getDocument(mainFile)!!
|
val document = FileDocumentManager.getInstance().getDocument(mainFile)!!
|
||||||
val editor = EditorFactory.getInstance()!!.createEditor(document, getProject()!!)!!
|
val editor = EditorFactory.getInstance()!!.createEditor(document, getProject()!!)!!
|
||||||
editor.getCaretModel().moveToOffset(extractCaretOffset(document))
|
editor.getCaretModel().moveToOffset(extractCaretOffset(document))
|
||||||
@@ -72,6 +68,11 @@ public abstract class AbstractMultiFileIntentionTest : KotlinMultiFileTestCase()
|
|||||||
intentionAction.invoke(getProject(), editor, mainPsiFile)
|
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 {
|
finally {
|
||||||
PsiDocumentManager.getInstance(getProject()!!).commitAllDocuments()
|
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$"));
|
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")
|
@TestMetadata("moveFileToPackageMatchingDirectory/moveToDefaultDirectory/moveToDefaultDirectory.test")
|
||||||
public void testMoveFileToPackageMatchingDirectory_moveToDefaultDirectory_MoveToDefaultDirectory() throws Exception {
|
public void testMoveFileToPackageMatchingDirectory_moveToDefaultDirectory_MoveToDefaultDirectory() throws Exception {
|
||||||
String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveFileToPackageMatchingDirectory/moveToDefaultDirectory/moveToDefaultDirectory.test");
|
String fileName = JetTestUtils.navigationMetadata("idea/testData/multiFileIntentions/moveFileToPackageMatchingDirectory/moveToDefaultDirectory/moveToDefaultDirectory.test");
|
||||||
|
|||||||
Reference in New Issue
Block a user