Copy: Support top-level declarations

This commit is contained in:
Alexey Sedunov
2017-05-16 12:57:28 +03:00
parent 4c1c1a989a
commit 1072495001
45 changed files with 477 additions and 43 deletions
+1 -1
View File
@@ -398,7 +398,7 @@
language="kotlin" />
<refactoring.copyHandler
id="kotlinClass"
implementation="org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinClassesHandler"
implementation="org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinDeclarationsHandler"
order="first" />
<refactoring.copyHandler
id="kotlinFile"
@@ -40,7 +40,7 @@ import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.refactoring.Pass
import org.jetbrains.kotlin.idea.refactoring.hasIdentifiersOnly
import org.jetbrains.kotlin.name.FqNameUnsafe
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import java.awt.BorderLayout
import java.awt.Font
import javax.swing.JComponent
@@ -48,8 +48,8 @@ import javax.swing.JLabel
import javax.swing.JPanel
// Based on com.intellij.refactoring.copy.CopyClassDialog
class CopyKotlinClassDialog(
klass: KtClassOrObject,
class CopyKotlinDeclarationDialog(
declaration: KtNamedDeclaration,
private val defaultTargetDirectory: PsiDirectory?,
private val project: Project
) : DialogWrapper(project, true) {
@@ -63,13 +63,13 @@ class CopyKotlinClassDialog(
override fun reportBaseInTestSelectionInSource() = true
}
private val originalFile = klass.containingFile
private val originalFile = declaration.containingFile
var targetDirectory: MoveDestination? = null
private set
init {
informationLabel.text = RefactoringBundle.message("copy.class.copy.0.1", UsageViewUtil.getType(klass), UsageViewUtil.getLongName(klass))
informationLabel.text = RefactoringBundle.message("copy.class.copy.0.1", UsageViewUtil.getType(declaration), UsageViewUtil.getLongName(declaration))
informationLabel.font = informationLabel.font.deriveFont(Font.BOLD)
init()
@@ -80,7 +80,7 @@ class CopyKotlinClassDialog(
Pass { setErrorText(it, destinationComboBox) },
packageNameField.childComponent
)
classNameField.text = UsageViewUtil.getShortName(klass)
classNameField.text = UsageViewUtil.getShortName(declaration)
classNameField.selectAll()
}
@@ -115,7 +115,7 @@ class CopyKotlinClassDialog(
private val qualifiedName: String
get() = defaultTargetDirectory?.getPackage()?.qualifiedName ?: ""
val className: String?
val newName: String?
get() = classNameField.text
val openInEditor: Boolean
@@ -123,7 +123,7 @@ class CopyKotlinClassDialog(
private fun checkForErrors(): String? {
val packageName = packageNameField.text
val className = className
val newName = newName
val manager = PsiManager.getInstance(project)
@@ -131,7 +131,7 @@ class CopyKotlinClassDialog(
return RefactoringBundle.message("invalid.target.package.name.specified")
}
if (className.isNullOrEmpty()) {
if (newName.isNullOrEmpty()) {
return RefactoringBundle.message("no.class.name.specified")
}
@@ -143,7 +143,7 @@ class CopyKotlinClassDialog(
}
targetDirectory?.getTargetIfExists(defaultTargetDirectory)?.let {
val targetFileName = className + "." + originalFile.virtualFile.extension
val targetFileName = newName + "." + originalFile.virtualFile.extension
if (it.findFile(targetFileName) == originalFile) {
return "Can't copy class to the containing file"
}
@@ -173,6 +173,6 @@ class CopyKotlinClassDialog(
override fun getHelpId() = HelpID.COPY_CLASS
companion object {
@NonNls private val RECENTS_KEY = "CopyKotlinClassDialog.RECENTS_KEY"
@NonNls private val RECENTS_KEY = "CopyKotlinDeclarationDialog.RECENTS_KEY"
}
}
@@ -25,6 +25,7 @@ import com.intellij.openapi.util.Key
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiNamedElement
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog
import com.intellij.refactoring.copy.CopyHandlerDelegateBase
@@ -41,30 +42,31 @@ import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.idea.util.application.runReadAction
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
import org.jetbrains.kotlin.utils.ifEmpty
class CopyKotlinClassesHandler : CopyHandlerDelegateBase() {
class CopyKotlinDeclarationsHandler : CopyHandlerDelegateBase() {
companion object {
@set:TestOnly
var Project.newName: String? by UserDataProperty(Key.create("NEW_NAME"))
private fun PsiElement.getTopLevelClasses(): List<KtClassOrObject> {
val classOrFile = parentsWithSelf.firstOrNull { it is KtFile || (it is KtClassOrObject && it.isTopLevel()) }
return when (classOrFile) {
is KtFile -> classOrFile.declarations.filterIsInstance<KtClassOrObject>()
is KtClassOrObject -> listOf(classOrFile)
private fun PsiElement.getElementsToCopy(): List<PsiNamedElement> {
val declarationOrFile = parentsWithSelf.firstOrNull { it is KtFile || (it is KtNamedDeclaration && it.parent is KtFile) }
return when (declarationOrFile) {
is KtFile -> declarationOrFile.declarations.filterIsInstance<KtNamedDeclaration>().ifEmpty { listOf(declarationOrFile) }
is KtNamedDeclaration -> listOf(declarationOrFile)
else -> emptyList()
}
}
}
override fun canCopy(elements: Array<out PsiElement>, fromUpdate: Boolean): Boolean {
return elements.flatMap { it.getTopLevelClasses().ifEmpty { return false } }.distinctBy { it.containingFile }.size == 1
return elements.flatMap { it.getElementsToCopy().ifEmpty { return false } }.distinctBy { it.containingFile }.size == 1
}
enum class ExistingFilePolicy {
@@ -135,32 +137,32 @@ class CopyKotlinClassesHandler : CopyHandlerDelegateBase() {
}
override fun doCopy(elements: Array<out PsiElement>, defaultTargetDirectory: PsiDirectory?) {
val classesToCopy = elements.flatMap { it.getTopLevelClasses() }
if (classesToCopy.isEmpty()) return
val elementsToCopy = elements.flatMap { it.getElementsToCopy() }
if (elementsToCopy.isEmpty()) return
val singleClassToCopy = classesToCopy.singleOrNull()
val singleElementToCopy = elementsToCopy.singleOrNull()
val originalFile = classesToCopy.first().containingKtFile
val originalFile = elementsToCopy.first().containingFile as KtFile
val initialTargetDirectory = defaultTargetDirectory ?: originalFile.containingDirectory ?: return
val project = initialTargetDirectory.project
if (ProjectRootManager.getInstance(project).fileIndex.getSourceRootForFile(initialTargetDirectory.virtualFile) == null) return
val commandName = RefactoringBundle.message("copy.handler.copy.class")
val commandName = "Copy Declarations"
var openInEditor = false
var newName: String? = singleClassToCopy?.name ?: originalFile.name
var newName: String? = singleElementToCopy?.name ?: originalFile.name
var targetDirWrapper: AutocreatingPsiDirectoryWrapper = initialTargetDirectory.toDirectoryWrapper()
if (!ApplicationManager.getApplication().isUnitTestMode) {
if (singleClassToCopy != null) {
val dialog = CopyKotlinClassDialog(singleClassToCopy, initialTargetDirectory, project)
if (singleElementToCopy != null && singleElementToCopy is KtNamedDeclaration) {
val dialog = CopyKotlinDeclarationDialog(singleElementToCopy, initialTargetDirectory, project)
dialog.title = commandName
if (!dialog.showAndGet()) return
openInEditor = dialog.openInEditor
newName = dialog.className ?: singleClassToCopy.name
newName = dialog.newName ?: singleElementToCopy.name
targetDirWrapper = dialog.targetDirectory?.toDirectoryWrapper() ?: return
}
else {
@@ -175,7 +177,7 @@ class CopyKotlinClassesHandler : CopyHandlerDelegateBase() {
project.newName?.let { newName = it }
}
if (singleClassToCopy != null && newName.isNullOrEmpty()) return
if (singleElementToCopy != null && newName.isNullOrEmpty()) return
val internalUsages = runReadAction {
val targetPackageName = targetDirWrapper.getPackageName()
@@ -183,10 +185,10 @@ class CopyKotlinClassesHandler : CopyHandlerDelegateBase() {
ContainerInfo.Package(originalFile.packageFqName),
ContainerInfo.Package(FqName(targetPackageName))
)
classesToCopy.flatMap { classToCopy ->
classToCopy.getInternalReferencesToUpdateOnPackageNameChange(changeInfo).filter {
elementsToCopy.flatMap { elementToCopy ->
(elementToCopy as KtElement).getInternalReferencesToUpdateOnPackageNameChange(changeInfo).filter {
val referencedElement = (it as? MoveRenameUsageInfo)?.referencedElement
referencedElement == null || !classToCopy.isAncestor(referencedElement)
referencedElement == null || !elementToCopy.isAncestor(referencedElement)
}
}
}
@@ -199,23 +201,30 @@ class CopyKotlinClassesHandler : CopyHandlerDelegateBase() {
val targetDirectory = runWriteAction { targetDirWrapper.getOrCreateDirectory(initialTargetDirectory) }
val targetFileName = if (newName?.contains(".") ?: false) newName!! else newName + "." + originalFile.virtualFile.extension
val targetFile = getOrCreateTargetFile(originalFile, targetDirectory, targetFileName, commandName) ?: return@executeCommand
val oldToNewElementsMapping = HashMap<PsiElement, PsiElement>()
val newClasses = runWriteAction {
val newClasses = classesToCopy.map { targetFile.add(it.copy()) as KtClassOrObject }
val oldToNewElementsMapping = classesToCopy.zip(newClasses).toMap<PsiElement, PsiElement>()
val targetFile: KtFile
if (singleElementToCopy is KtFile) {
targetFile = runWriteAction { targetDirectory.copyFileFrom(targetFileName, singleElementToCopy) as KtFile }
}
else {
targetFile = getOrCreateTargetFile(originalFile, targetDirectory, targetFileName, commandName) ?: return@executeCommand
runWriteAction {
val newElements = elementsToCopy.map { targetFile.add(it.copy()) as KtNamedDeclaration }
elementsToCopy.zip(newElements).toMap(oldToNewElementsMapping)
}
}
for (newClass in newClasses) {
restoredInternalUsages += restoreInternalUsages(newClass, oldToNewElementsMapping, true)
runWriteAction {
for (newElement in oldToNewElementsMapping.values) {
restoredInternalUsages += restoreInternalUsages(newElement as KtElement, oldToNewElementsMapping, true)
postProcessMoveUsages(restoredInternalUsages, oldToNewElementsMapping)
}
performDelayedRefactoringRequests(project)
newClasses
}
newClasses.singleOrNull()?.let {
oldToNewElementsMapping.values.singleOrNull()?.let {
RenameProcessor(project, it, newName!!.quoteIfNeeded(), false, false).run()
}
@@ -0,0 +1,5 @@
package bar
fun test() {
class A
}
@@ -0,0 +1,5 @@
package foo
fun test() {
class A
}
@@ -0,0 +1,5 @@
package foo
fun test() {
class <caret>A
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,8 @@
package bar
fun a() {
fun b() {
}
b()
}
@@ -0,0 +1,8 @@
package foo
fun a() {
fun b() {
}
b()
}
@@ -0,0 +1,8 @@
package foo
fun a() {
fun <caret>b() {
}
b()
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,5 @@
package bar
fun a() {
val x = 1
}
@@ -0,0 +1,5 @@
package foo
fun a() {
val x = 1
}
@@ -0,0 +1,5 @@
package foo
fun a() {
val <caret>x = 1
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,11 @@
package bar
class A {
fun b() {
}
init {
b()
}
}
@@ -0,0 +1,11 @@
package foo
class A {
fun b() {
}
init {
b()
}
}
@@ -0,0 +1,11 @@
package foo
class A {
fun <caret>b() {
}
init {
b()
}
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,6 @@
package bar
class A {
val x = 1
val y = x
}
@@ -0,0 +1,6 @@
package foo
class A {
val x = 1
val y = x
}
@@ -0,0 +1,6 @@
package foo
class A {
val <caret>x = 1
val y = x
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,26 @@
package bar
import foo.X
class A {
init {
val a: A = A()
val x: X = X()
b()
c
}
}
fun b() {
val a: A = A()
val x: X = X()
b()
c
}
val c: Int get() {
val a: A = A()
val x: X = X()
b()
c
}
@@ -0,0 +1,33 @@
package foo
class A {
init {
val a: A = A()
val x: X = X()
b()
c
}
}
class X {
init {
val a: A = A()
val x: X = X()
b()
c
}
}
fun b() {
val a: A = A()
val x: X = X()
b()
c
}
val c: Int get() {
val a: A = A()
val x: X = X()
b()
c
}
@@ -0,0 +1,33 @@
package foo
class <caret>A {
init {
val a: A = A()
val x: X = X()
b()
c
}
}
class X {
init {
val a: A = A()
val x: X = X()
b()
c
}
}
fun <caret>b() {
val a: A = A()
val x: X = X()
b()
c
}
val <caret>c: Int get() {
val a: A = A()
val x: X = X()
b()
c
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,8 @@
package bar
import foo.B
fun a() {
a()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
fun a() {
a()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
fun <caret>a() {
a()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,8 @@
package bar
import foo.B
fun x() {
x()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
fun a() {
a()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
fun <caret>a() {
a()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,5 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar",
"newName": "x"
}
@@ -0,0 +1,9 @@
package bar
import foo.B
val a: Int get() {
a
val b: B = B()
return 0
}
@@ -0,0 +1,12 @@
package foo
val a: Int get() {
a
val b: B = B()
return 0
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,12 @@
package foo
val <caret>a: Int get() {
a
val b: B = B()
return 0
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,9 @@
package bar
import foo.B
val x: Int get() {
x
val b: B = B()
return 0
}
@@ -0,0 +1,12 @@
package foo
val a: Int get() {
a
val b: B = B()
return 0
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,12 @@
package foo
val <caret>a: Int get() {
a
val b: B = B()
return 0
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,5 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar",
"newName": "x"
}
@@ -27,7 +27,7 @@ import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDest
import org.jetbrains.kotlin.idea.jsonUtils.getNullableString
import org.jetbrains.kotlin.idea.jsonUtils.getString
import org.jetbrains.kotlin.idea.refactoring.AbstractMultifileRefactoringTest
import org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinClassesHandler.Companion.newName
import org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinDeclarationsHandler.Companion.newName
import org.jetbrains.kotlin.idea.refactoring.runRefactoringTest
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.utils.ifEmpty
@@ -66,6 +66,36 @@ public class CopyTestGenerated extends AbstractCopyTest {
doTest(fileName);
}
@TestMetadata("copyLocalClass/copyLocalClass.test")
public void testCopyLocalClass_CopyLocalClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyLocalClass/copyLocalClass.test");
doTest(fileName);
}
@TestMetadata("copyLocalFunction/copyLocalFunction.test")
public void testCopyLocalFunction_CopyLocalFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyLocalFunction/copyLocalFunction.test");
doTest(fileName);
}
@TestMetadata("copyLocalVariable/copyLocalVariable.test")
public void testCopyLocalVariable_CopyLocalVariable() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyLocalVariable/copyLocalVariable.test");
doTest(fileName);
}
@TestMetadata("copyMemberFunction/copyMemberFunction.test")
public void testCopyMemberFunction_CopyMemberFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMemberFunction/copyMemberFunction.test");
doTest(fileName);
}
@TestMetadata("copyMemberProperty/copyMemberProperty.test")
public void testCopyMemberProperty_CopyMemberProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMemberProperty/copyMemberProperty.test");
doTest(fileName);
}
@TestMetadata("copyMultiClassFile/copyMultiClassFile.test")
public void testCopyMultiClassFile_CopyMultiClassFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMultiClassFile/copyMultiClassFile.test");
@@ -84,6 +114,12 @@ public class CopyTestGenerated extends AbstractCopyTest {
doTest(fileName);
}
@TestMetadata("copyMultipleDeclarations/copyMultipleDeclarations.test")
public void testCopyMultipleDeclarations_CopyMultipleDeclarations() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyMultipleDeclarations/copyMultipleDeclarations.test");
doTest(fileName);
}
@TestMetadata("copyNestedClass/copyNestedClass.test")
public void testCopyNestedClass_CopyNestedClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyNestedClass/copyNestedClass.test");
@@ -101,4 +137,28 @@ public class CopyTestGenerated extends AbstractCopyTest {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copySingleClassFile/copySingleClassFile.test");
doTest(fileName);
}
@TestMetadata("copyTopLevelFunction/copyTopLevelFunction.test")
public void testCopyTopLevelFunction_CopyTopLevelFunction() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyTopLevelFunction/copyTopLevelFunction.test");
doTest(fileName);
}
@TestMetadata("copyTopLevelFunctionWithRename/copyTopLevelFunctionWithRename.test")
public void testCopyTopLevelFunctionWithRename_CopyTopLevelFunctionWithRename() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyTopLevelFunctionWithRename/copyTopLevelFunctionWithRename.test");
doTest(fileName);
}
@TestMetadata("copyTopLevelProperty/copyTopLevelProperty.test")
public void testCopyTopLevelProperty_CopyTopLevelProperty() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyTopLevelProperty/copyTopLevelProperty.test");
doTest(fileName);
}
@TestMetadata("copyTopLevelPropertyWithRename/copyTopLevelPropertyWithRename.test")
public void testCopyTopLevelPropertyWithRename_CopyTopLevelPropertyWithRename() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyTopLevelPropertyWithRename/copyTopLevelPropertyWithRename.test");
doTest(fileName);
}
}