Move refactoring available on cut/paste declarations from an object to top-level

This commit is contained in:
Valentin Kipyatkov
2017-06-03 13:55:09 +03:00
parent b143b1be42
commit cfa442a42a
18 changed files with 250 additions and 23 deletions
@@ -29,8 +29,10 @@ import com.intellij.psi.PsiFile
import com.intellij.psi.PsiWhiteSpace
import com.intellij.psi.util.PsiModificationTracker
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptor
import org.jetbrains.kotlin.psi.KtClassBody
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.KtObjectDeclaration
import org.jetbrains.kotlin.psi.psiUtil.elementsInRange
import java.awt.datatransfer.Transferable
@@ -58,13 +60,20 @@ class MoveDeclarationsCopyPasteProcessor : CopyPastePostProcessor<MoveDeclaratio
if (startOffsets.size != 1) return emptyList()
val declarations = rangeToDeclarations(file, startOffsets[0], endOffsets[0])
if (declarations.isEmpty() || declarations.any { it.parent !is KtFile }) return emptyList()
if (declarations.isEmpty()) return emptyList()
val parent = declarations.map { it.parent }.distinct().singleOrNull() ?: return emptyList()
val sourceObjectFqName = when (parent) {
is KtFile -> null
is KtClassBody -> (parent.parent as? KtObjectDeclaration)?.fqName?.asString() ?: return emptyList()
else -> return emptyList()
}
if (declarations.any { it.name == null }) return emptyList()
val declarationNames = declarations.map { it.name!! }.toSet()
val stubTexts = declarations.map { MoveDeclarationsTransferableData.STUB_RENDERER.render(it.resolveToDescriptor()) }
return listOf(MoveDeclarationsTransferableData(file.virtualFile.url, stubTexts, declarationNames))
return listOf(MoveDeclarationsTransferableData(file.virtualFile.url, sourceObjectFqName, stubTexts, declarationNames))
}
override fun extractTransferableData(content: Transferable): List<MoveDeclarationsTransferableData> {
@@ -34,9 +34,12 @@ class MoveDeclarationsIntentionAction(
private val bounds: RangeMarker,
private val modificationCount: Long
) : BaseRefactoringIntentionAction(), HintAction {
private val isSingleDeclaration = processor.pastedDeclarations.size == 1
override fun startInWriteAction() = false
override fun getText() = "Update usages to reflect package name change"
override fun getText() = "Update usages to reflect declaration${if (isSingleDeclaration) "s" else ""} move"
override fun getFamilyName() = "Update usages on declarations cut/paste"
override fun isAvailable(project: Project, editor: Editor, element: PsiElement): Boolean {
@@ -29,16 +29,16 @@ import org.jetbrains.kotlin.idea.conversion.copy.range
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.getSourceRoot
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.endOffset
import org.jetbrains.kotlin.psi.psiUtil.findDescendantOfType
import org.jetbrains.kotlin.psi.psiUtil.startOffset
class MoveDeclarationsProcessor(
val project: Project,
private val sourcePsiFile: KtFile,
private val sourceContainer: KtDeclarationContainer,
private val targetPsiFile: KtFile,
private val pastedDeclarations: List<KtNamedDeclaration>,
val pastedDeclarations: List<KtNamedDeclaration>,
private val stubTexts: List<String>
) {
companion object {
@@ -57,22 +57,29 @@ class MoveDeclarationsProcessor(
val targetPsiFile = psiDocumentManager.getPsiFile(editor.document) as? KtFile ?: return null
if (targetPsiFile.virtualFile.getSourceRoot(project) == null) return null
val sourcePsiFile = PsiManager.getInstance(project).findFile(sourceFile) as? KtFile ?: return null
if (targetPsiFile == sourcePsiFile) return null
val sourceObject = data.sourceObjectFqName?.let { fqName ->
sourcePsiFile.findDescendantOfType<KtObjectDeclaration> { it.fqName?.asString() == fqName } ?: return null
}
val sourceContainer: KtDeclarationContainer = sourceObject ?: sourcePsiFile
if (targetPsiFile == sourceContainer) return null
val declarations = MoveDeclarationsCopyPasteProcessor.rangeToDeclarations(targetPsiFile, range.startOffset, range.endOffset)
if (declarations.isEmpty() || declarations.any { it.parent !is KtFile }) return null
if (sourcePsiFile.packageFqName == targetPsiFile.packageFqName) return null
if (sourceContainer == sourcePsiFile && sourcePsiFile.packageFqName == targetPsiFile.packageFqName) return null
// check that declarations were cut (not copied)
val filteredDeclarations = sourcePsiFile.declarations.filter { it.name in data.declarationNames }
val filteredDeclarations = sourceContainer.declarations.filter { it.name in data.declarationNames }
val stubs = data.stubTexts.toSet()
if (filteredDeclarations.any { MoveDeclarationsTransferableData.STUB_RENDERER.render(it.resolveToDescriptor()) in stubs }) return null
return MoveDeclarationsProcessor(project, sourcePsiFile, targetPsiFile, declarations, data.stubTexts)
return MoveDeclarationsProcessor(project, sourceContainer, targetPsiFile, declarations, data.stubTexts)
}
}
private val sourcePsiFile = (sourceContainer as KtElement).containingKtFile
private val psiDocumentManager = PsiDocumentManager.getInstance(project)
private val sourceDocument = psiDocumentManager.getDocument(sourcePsiFile)!!
@@ -111,12 +118,8 @@ class MoveDeclarationsProcessor(
)
val declarationUsages = declarationProcessor.findUsages().toList()
// val changeInfo = ContainerChangeInfo(ContainerInfo.Package(sourcePackageName), ContainerInfo.Package(targetPackageName))
// val internalUsages = file.getInternalReferencesToUpdateOnPackageNameChange(changeInfo)
project.executeWriteCommand(commandName, commandGroupId) {
// postProcessMoveUsages(internalUsages) //TODO?
project.runRefactoringAndKeepDelayedRequests { declarationProcessor.execute(declarationUsages) }
psiDocumentManager.doPostponedOperationsAndUnblockDocument(sourceDocument)
@@ -126,7 +129,12 @@ class MoveDeclarationsProcessor(
}
private fun insertStubDeclarations(): RangeMarker {
val insertionOffset = sourcePsiFile.declarations.firstOrNull()?.startOffset ?: sourcePsiFile.textLength
val insertionOffset = sourceContainer.declarations.firstOrNull()?.startOffset
?: when (sourceContainer) {
is KtFile -> sourceContainer.textLength
is KtObjectDeclaration -> sourceContainer.getBody()?.rBrace?.startOffset ?: sourceContainer.endOffset
else -> error("Unknown sourceContainer: $sourceContainer")
}
val textToInsert = "\n//start\n\n" + stubTexts.joinToString(separator = "\n") + "\n//end\n"
sourceDocument.insertString(insertionOffset, textToInsert)
return sourceDocument.createRangeMarker(TextRange(insertionOffset, insertionOffset + textToInsert.length))
@@ -20,9 +20,9 @@ import com.intellij.codeInsight.editorActions.TextBlockTransferableData
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
import java.awt.datatransfer.DataFlavor
//TODO: how to make it work inside same project only?
class MoveDeclarationsTransferableData(
val sourceFileUrl: String,
val sourceObjectFqName: String?,
val stubTexts: List<String>,
val declarationNames: Set<String>
) : TextBlockTransferableData {
@@ -0,0 +1,15 @@
// IS_AVAILABLE: false
fun foo() {
val v = object : Runnable {
override fun run() {
}
<selection>
fun bar() {
}
</selection>
}
}
<caret>
@@ -0,0 +1,24 @@
package source
class X {
companion object {
fun other() {
foo()
}
}
fun f() {
bar++
}
}
fun foo() {
X.other()
bar++
}
var bar = 1
@@ -0,0 +1,24 @@
package source
class X {
companion object {
<selection>
fun foo() {
other()
bar++
}
var bar = 1
</selection>
fun other() {
foo()
}
}
fun f() {
bar++
}
}
<caret>
@@ -0,0 +1,21 @@
package source
fun sourcePackFun(){}
object SourceObject {
fun other() {
foo()
}
}
fun foo() {
SourceObject.other()
sourcePackFun()
bar++
}
var bar = 1
@@ -0,0 +1,21 @@
package source
fun sourcePackFun(){}
object SourceObject {
<selection>
fun foo() {
other()
sourcePackFun()
bar++
}
var bar = 1
</selection>
fun other() {
foo()
}
}
<caret>
@@ -0,0 +1,8 @@
package source
import target.foo
fun f() {
foo()
SourceObject.other()
}
@@ -0,0 +1,6 @@
package source
fun f() {
SourceObject.foo()
SourceObject.other()
}
@@ -0,0 +1,13 @@
package source
import target.foo
fun sourcePackFun(){}
object SourceObject {
fun other() {
foo()
}
}
@@ -0,0 +1,22 @@
package source
import target.targetPackFun
fun sourcePackFun(){}
object SourceObject {
<selection>
fun foo() {
other()
sourcePackFun()
targetPackFun()
bar++
}
var bar = 1
</selection>
fun other() {
foo()
}
}
@@ -0,0 +1,16 @@
package target
import source.SourceObject
import source.sourcePackFun
fun targetPackFun(){}
fun foo() {
SourceObject.other()
sourcePackFun()
targetPackFun()
bar++
}
var bar = 1
@@ -0,0 +1,5 @@
package target
fun targetPackFun(){}
<caret>
@@ -0,0 +1,3 @@
package to
<caret>
@@ -22,8 +22,8 @@ import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiDocumentManager
import junit.framework.TestCase
import org.jetbrains.kotlin.idea.AbstractCopyPasteTest
import org.jetbrains.kotlin.idea.core.moveCaret
import org.jetbrains.kotlin.idea.refactoring.cutPaste.MoveDeclarationsEditorCookie
import org.jetbrains.kotlin.idea.refactoring.cutPaste.MoveDeclarationsIntentionAction
import org.jetbrains.kotlin.idea.refactoring.cutPaste.MoveDeclarationsProcessor
import org.jetbrains.kotlin.idea.test.PluginTestCaseBase
import org.jetbrains.kotlin.idea.test.dumpTextWithErrors
@@ -51,7 +51,9 @@ abstract class AbstractMoveOnCutPasteTest : AbstractCopyPasteTest() {
val dependencyPsiFile = configureByDependencyIfExists(dependencyFileName) as KtFile?
val sourcePsiFile = myFixture.configureByFile(sourceFileName) as KtFile
val useCopy = InTextDirectivesUtils.isDirectiveDefined(testFileText, COPY_DIRECTIVE)
val caretMarker = myFixture.editor.document.createRangeMarker(myFixture.caretOffset, myFixture.caretOffset)
myFixture.performEditorAction(if (useCopy) IdeActions.ACTION_COPY else IdeActions.ACTION_CUT)
myFixture.editor.moveCaret(caretMarker.startOffset)
PsiDocumentManager.getInstance(project).commitAllDocuments()
if (InTextDirectivesUtils.isDirectiveDefined(testFileText, OPTIMIZE_IMPORTS_AFTER_CUT_DIRECTIVE)) {
@@ -61,7 +63,8 @@ abstract class AbstractMoveOnCutPasteTest : AbstractCopyPasteTest() {
editor.putUserData(MoveDeclarationsEditorCookie.KEY, null) // because editor is reused
val targetFileName = sourceFileName.replace(".kt", ".to.kt")
val targetPsiFile = configureTargetFile(targetFileName)
val targetFileExists = File(testDataPath + File.separator + targetFileName).exists()
val targetPsiFile = if (targetFileExists) configureTargetFile(targetFileName) else null
performNotWriteEditorAction(IdeActions.ACTION_PASTE)
val shouldBeAvailable = InTextDirectivesUtils.getPrefixedBoolean(testFileText, IS_AVAILABLE_DIRECTIVE) ?: true
@@ -82,8 +85,10 @@ abstract class AbstractMoveOnCutPasteTest : AbstractCopyPasteTest() {
KotlinTestUtils.assertEqualsToFile(File(BASE_PATH, sourceFileName.replace(".kt", ".expected.kt")),
sourcePsiFile.dumpTextWithErrors())
KotlinTestUtils.assertEqualsToFile(File(BASE_PATH, targetFileName.replace(".kt", ".expected.kt")),
targetPsiFile.dumpTextWithErrors())
if (targetPsiFile != null) {
KotlinTestUtils.assertEqualsToFile(File(BASE_PATH, targetFileName.replace(".kt", ".expected.kt")),
targetPsiFile.dumpTextWithErrors())
}
}
}
}
@@ -48,6 +48,30 @@ public class MoveOnCutPasteTestGenerated extends AbstractMoveOnCutPasteTest {
doTest(fileName);
}
@TestMetadata("FromAnonymousObject.kt")
public void testFromAnonymousObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/moveDeclarations/FromAnonymousObject.kt");
doTest(fileName);
}
@TestMetadata("FromCompanionObjectToTopLevel.kt")
public void testFromCompanionObjectToTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/moveDeclarations/FromCompanionObjectToTopLevel.kt");
doTest(fileName);
}
@TestMetadata("FromObjectToSameFile.kt")
public void testFromObjectToSameFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/moveDeclarations/FromObjectToSameFile.kt");
doTest(fileName);
}
@TestMetadata("FromObjectToTopLevel.kt")
public void testFromObjectToTopLevel() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/moveDeclarations/FromObjectToTopLevel.kt");
doTest(fileName);
}
@TestMetadata("OptimizeImportsAfterCut.kt")
public void testOptimizeImportsAfterCut() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/copyPaste/moveDeclarations/OptimizeImportsAfterCut.kt");