Copy: Support class copying

#KT-8180 Fixed
 #KT-9054 Fixed
This commit is contained in:
Alexey Sedunov
2017-05-11 21:28:49 +03:00
parent 405a28648f
commit 92446df14a
43 changed files with 989 additions and 124 deletions
@@ -115,6 +115,7 @@ import org.jetbrains.kotlin.idea.parameterInfo.AbstractParameterInfoTest
import org.jetbrains.kotlin.idea.quickfix.AbstractQuickFixMultiFileTest
import org.jetbrains.kotlin.idea.quickfix.AbstractQuickFixTest
import org.jetbrains.kotlin.idea.refactoring.AbstractNameSuggestionProviderTest
import org.jetbrains.kotlin.idea.refactoring.copy.AbstractCopyTest
import org.jetbrains.kotlin.idea.refactoring.inline.AbstractInlineTest
import org.jetbrains.kotlin.idea.refactoring.introduce.AbstractExtractionTest
import org.jetbrains.kotlin.idea.refactoring.move.AbstractMoveTest
@@ -745,6 +746,10 @@ fun main(args: Array<String>) {
model("refactoring/move", extension = "test", singleClass = true)
}
testClass<AbstractCopyTest> {
model("refactoring/copy", extension = "test", singleClass = true)
}
testClass<AbstractMultiModuleMoveTest> {
model("refactoring/moveMultiModule", extension = "test", singleClass = true)
}
+8 -1
View File
@@ -396,7 +396,14 @@
<refactoring.moveInnerClassUsagesHandler
implementationClass="org.jetbrains.kotlin.idea.refactoring.move.MoveJavaInnerClassKotlinUsagesHandler"
language="kotlin" />
<refactoring.copyHandler implementation="org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinFileHandler" order="first" />
<refactoring.copyHandler
id="kotlinClass"
implementation="org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinClassHandler"
order="first" />
<refactoring.copyHandler
id="kotlinFile"
implementation="org.jetbrains.kotlin.idea.refactoring.copy.CopyKotlinFileHandler"
order="after kotlinClass" />
<refactoring.changeSignatureUsageProcessor
implementation="org.jetbrains.kotlin.idea.refactoring.changeSignature.KotlinChangeSignatureUsageProcessor"
order="after javaProcessor" />
@@ -0,0 +1,178 @@
/*
* Copyright 2010-2017 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.copy
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.JavaProjectRootsUtil
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.Messages
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiManager
import com.intellij.refactoring.HelpID
import com.intellij.refactoring.MoveDestination
import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.copy.CopyFilesOrDirectoriesDialog
import com.intellij.refactoring.move.moveClassesOrPackages.DestinationFolderComboBox
import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo
import com.intellij.ui.EditorTextField
import com.intellij.ui.RecentsManager
import com.intellij.ui.ReferenceEditorComboWithBrowseButton
import com.intellij.usageView.UsageViewUtil
import com.intellij.util.IncorrectOperationException
import com.intellij.util.ui.FormBuilder
import com.intellij.util.ui.UIUtil
import org.jetbrains.annotations.NonNls
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 java.awt.BorderLayout
import java.awt.Font
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
// Based on com.intellij.refactoring.copy.CopyClassDialog
class CopyKotlinClassDialog(
klass: KtClassOrObject,
private val defaultTargetDirectory: PsiDirectory?,
private val project: Project
) : DialogWrapper(project, true) {
private val informationLabel = JLabel()
private val classNameField = EditorTextField("")
private val packageLabel = JLabel()
private lateinit var packageNameField: ReferenceEditorComboWithBrowseButton
private val openInEditorCheckBox = CopyFilesOrDirectoriesDialog.createOpenInEditorCB()
private val destinationComboBox = object : DestinationFolderComboBox() {
override fun getTargetPackage() = packageNameField.text.trim()
override fun reportBaseInTestSelectionInSource() = true
}
private val originalFile = klass.containingFile
var targetDirectory: MoveDestination? = null
private set
init {
informationLabel.text = RefactoringBundle.message("copy.class.copy.0.1", UsageViewUtil.getType(klass), UsageViewUtil.getLongName(klass))
informationLabel.font = informationLabel.font.deriveFont(Font.BOLD)
init()
destinationComboBox.setData(
project,
defaultTargetDirectory,
Pass { setErrorText(it, destinationComboBox) },
packageNameField.childComponent
)
classNameField.text = UsageViewUtil.getShortName(klass)
classNameField.selectAll()
}
override fun getPreferredFocusedComponent() = classNameField
override fun createCenterPanel() = JPanel(BorderLayout())
override fun createNorthPanel(): JComponent? {
val qualifiedName = qualifiedName
packageNameField = PackageNameReferenceEditorCombo(qualifiedName, project, RECENTS_KEY, RefactoringBundle.message("choose.destination.package"))
packageNameField.setTextFieldPreferredWidth(Math.max(qualifiedName.length + 5, 40))
packageLabel.text = RefactoringBundle.message("destination.package")
packageLabel.labelFor = packageNameField
val label = JLabel(RefactoringBundle.message("target.destination.folder"))
val isMultipleSourceRoots = JavaProjectRootsUtil.getSuitableDestinationSourceRoots(project).size > 1
destinationComboBox.isVisible = isMultipleSourceRoots
label.isVisible = isMultipleSourceRoots
label.labelFor = destinationComboBox
val panel = JPanel(BorderLayout())
panel.add(openInEditorCheckBox, BorderLayout.EAST)
return FormBuilder.createFormBuilder()
.addComponent(informationLabel)
.addLabeledComponent(RefactoringBundle.message("copy.files.new.name.label"), classNameField, UIUtil.LARGE_VGAP)
.addLabeledComponent(packageLabel, packageNameField)
.addLabeledComponent(label, destinationComboBox)
.addComponent(panel)
.panel
}
private val qualifiedName: String
get() = defaultTargetDirectory?.getPackage()?.qualifiedName ?: ""
val className: String?
get() = classNameField.text
val openInEditor: Boolean
get() = openInEditorCheckBox.isSelected
private fun checkForErrors(): String? {
val packageName = packageNameField.text
val className = className
val manager = PsiManager.getInstance(project)
if (packageName.isNotEmpty() && !FqNameUnsafe(packageName).hasIdentifiersOnly()) {
return RefactoringBundle.message("invalid.target.package.name.specified")
}
if (className.isNullOrEmpty()) {
return RefactoringBundle.message("no.class.name.specified")
}
try {
targetDirectory = destinationComboBox.selectDirectory(PackageWrapper(manager, packageName), false)
}
catch (e: IncorrectOperationException) {
return e.message
}
targetDirectory?.getTargetIfExists(defaultTargetDirectory)?.let {
val targetFileName = className + "." + originalFile.virtualFile.extension
if (it.findFile(targetFileName) == originalFile) {
return "Can't copy class to the containing file"
}
}
return null
}
override fun doOKAction() {
val packageName = packageNameField.text
val errorString = checkForErrors()
if (errorString != null) {
if (errorString.isNotEmpty()) {
Messages.showMessageDialog(project, errorString, RefactoringBundle.message("error.title"), Messages.getErrorIcon())
}
classNameField.requestFocusInWindow()
return
}
RecentsManager.getInstance(project).registerRecentEntry(RECENTS_KEY, packageName)
CopyFilesOrDirectoriesDialog.saveOpenInEditorState(openInEditorCheckBox.isSelected)
super.doOKAction()
}
override fun getHelpId() = HelpID.COPY_CLASS
companion object {
@NonNls private val RECENTS_KEY = "CopyKotlinClassDialog.RECENTS_KEY"
}
}
@@ -0,0 +1,222 @@
/*
* Copyright 2010-2017 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.copy
import com.intellij.ide.util.EditorHelper
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Key
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.MoveDestination
import com.intellij.refactoring.RefactoringBundle
import com.intellij.refactoring.copy.CopyHandlerDelegateBase
import com.intellij.refactoring.rename.RenameProcessor
import com.intellij.refactoring.util.MoveRenameUsageInfo
import com.intellij.usageView.UsageInfo
import com.intellij.util.IncorrectOperationException
import org.jetbrains.annotations.TestOnly
import org.jetbrains.kotlin.idea.codeInsight.shorten.performDelayedRefactoringRequests
import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.core.quoteIfNeeded
import org.jetbrains.kotlin.idea.refactoring.createKotlinFile
import org.jetbrains.kotlin.idea.refactoring.move.*
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.KtFile
import org.jetbrains.kotlin.psi.UserDataProperty
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
import org.jetbrains.kotlin.psi.psiUtil.parentsWithSelf
class CopyKotlinClassHandler : CopyHandlerDelegateBase() {
companion object {
@set:TestOnly
var Project.newName: String? by UserDataProperty(Key.create("NEW_NAME"))
private fun PsiElement.getTopLevelClass(): KtClassOrObject? {
val classOrFile = parentsWithSelf.firstOrNull { it is KtFile || (it is KtClassOrObject && it.isTopLevel()) }
return when (classOrFile) {
is KtFile -> classOrFile.declarations.singleOrNull() as? KtClassOrObject
is KtClassOrObject -> classOrFile
else -> null
}
}
private fun getClassToCopy(elements: Array<out PsiElement>) = elements.singleOrNull()?.getTopLevelClass()
}
override fun canCopy(elements: Array<out PsiElement>, fromUpdate: Boolean) = getClassToCopy(elements) != null
enum class ExistingFilePolicy {
APPEND, OVERWRITE, SKIP
}
private fun getOrCreateTargetFile(
originalFile: KtFile,
targetDirectory: PsiDirectory,
targetFileName: String,
commandName: String
): KtFile? {
val existingFile = targetDirectory.findFile(targetFileName)
if (existingFile == originalFile) return null
if (existingFile != null) {
val policy = getFilePolicy(existingFile, targetFileName, targetDirectory, commandName)
when (policy) {
ExistingFilePolicy.APPEND -> {}
ExistingFilePolicy.OVERWRITE -> runWriteAction { existingFile.delete() }
ExistingFilePolicy.SKIP -> return null
}
}
return runWriteAction {
if (existingFile != null && existingFile.isValid) {
existingFile as KtFile
} else {
createKotlinFile(targetFileName, targetDirectory)
}
}
}
private fun getFilePolicy(
existingFile: PsiFile?,
targetFileName: String,
targetDirectory: PsiDirectory,
commandName: String
): ExistingFilePolicy {
val isUnitTestMode = ApplicationManager.getApplication().isUnitTestMode
return if (existingFile !is KtFile) {
if (isUnitTestMode) return ExistingFilePolicy.OVERWRITE
val answer = Messages.showOkCancelDialog(
"File $targetFileName already exists in ${targetDirectory.virtualFile.path}",
commandName,
"Overwrite",
"Cancel",
Messages.getQuestionIcon()
)
if (answer == Messages.OK) ExistingFilePolicy.OVERWRITE else ExistingFilePolicy.SKIP
}
else {
if (isUnitTestMode) return ExistingFilePolicy.APPEND
val answer = Messages.showYesNoCancelDialog(
"File $targetFileName already exists in ${targetDirectory.virtualFile.path}",
commandName,
"Append",
"Overwrite",
"Cancel",
Messages.getQuestionIcon()
)
when (answer) {
Messages.YES -> ExistingFilePolicy.APPEND
Messages.NO -> ExistingFilePolicy.OVERWRITE
else -> ExistingFilePolicy.SKIP
}
}
}
override fun doCopy(elements: Array<out PsiElement>, defaultTargetDirectory: PsiDirectory?) {
val classToCopy = getClassToCopy(elements) ?: return
val originalFile = classToCopy.containingKtFile
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")
var openInEditor = false
var newClassName: String? = classToCopy.name
var moveDestination: MoveDestination? = null
if (!ApplicationManager.getApplication().isUnitTestMode) {
val dialog = CopyKotlinClassDialog(classToCopy, initialTargetDirectory, project)
dialog.title = commandName
if (!dialog.showAndGet()) return
openInEditor = dialog.openInEditor
newClassName = dialog.className
moveDestination = dialog.targetDirectory ?: return
}
else {
project.newName?.let { newClassName = it }
}
if (newClassName.isNullOrEmpty()) return
val internalUsages = runReadAction {
val targetPackageName = moveDestination?.targetPackage?.qualifiedName
?: initialTargetDirectory.getPackage()?.qualifiedName
?: ""
val changeInfo = ContainerChangeInfo(
ContainerInfo.Package(originalFile.packageFqName),
ContainerInfo.Package(FqName(targetPackageName))
)
classToCopy
.getInternalReferencesToUpdateOnPackageNameChange(changeInfo)
.filter {
val referencedElement = (it as? MoveRenameUsageInfo)?.referencedElement
referencedElement == null || !classToCopy.isAncestor(referencedElement)
}
}
markInternalUsages(internalUsages)
val restoredInternalUsages = ArrayList<UsageInfo>()
project.executeCommand(commandName) {
try {
val targetDirectory = runWriteAction { moveDestination?.getTargetDirectory(initialTargetDirectory) ?: initialTargetDirectory }
val targetFileName = newClassName + "." + originalFile.virtualFile.extension
val targetFile = getOrCreateTargetFile(originalFile, targetDirectory, targetFileName, commandName) ?: return@executeCommand
val newClass = runWriteAction {
val newClass = targetFile.add(classToCopy.copy()) as KtClassOrObject
val oldToNewElementsMapping: Map<PsiElement, PsiElement> = mapOf(classToCopy to newClass)
restoredInternalUsages += restoreInternalUsages(newClass, oldToNewElementsMapping, true)
postProcessMoveUsages(restoredInternalUsages, oldToNewElementsMapping)
performDelayedRefactoringRequests(project)
newClass
}
RenameProcessor(project, newClass, newClassName!!.quoteIfNeeded(), false, false).run()
if (openInEditor) {
EditorHelper.openFilesInEditor(arrayOf(targetFile))
}
}
catch (e: IncorrectOperationException) {
Messages.showMessageDialog(project, e.message, RefactoringBundle.message("error.title"), Messages.getErrorIcon())
}
finally {
cleanUpInternalUsages(internalUsages + restoredInternalUsages)
}
}
}
override fun doClone(element: PsiElement) {
}
}
@@ -18,7 +18,6 @@ package org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations
import com.intellij.ide.util.EditorHelper
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
@@ -30,7 +29,6 @@ import com.intellij.refactoring.move.MoveCallback
import com.intellij.refactoring.move.MoveMultipleElementsViewDescriptor
import com.intellij.refactoring.move.moveClassesOrPackages.MoveClassHandler
import com.intellij.refactoring.rename.RenameUtil
import com.intellij.refactoring.util.MoveRenameUsageInfo
import com.intellij.refactoring.util.NonCodeUsageInfo
import com.intellij.refactoring.util.RefactoringUIUtil
import com.intellij.refactoring.util.TextOccurrencesUtil
@@ -51,7 +49,6 @@ import org.jetbrains.kotlin.idea.refactoring.move.*
import org.jetbrains.kotlin.idea.refactoring.move.moveFilesOrDirectories.MoveKotlinClassHandler
import org.jetbrains.kotlin.idea.search.projectScope
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
import org.jetbrains.kotlin.psi.psiUtil.getElementTextWithContext
import org.jetbrains.kotlin.psi.psiUtil.isAncestor
@@ -83,8 +80,6 @@ interface Mover : (KtNamedDeclaration, KtElement) -> KtNamedDeclaration {
}
}
internal var KtSimpleNameExpression.internalUsageInfo: UsageInfo? by CopyableUserDataProperty(Key.create("INTERNAL_USAGE_INFO"))
class MoveDeclarationsDescriptor @JvmOverloads constructor(
val project: Project,
val elementsToMove: Collection<KtNamedDeclaration>,
@@ -245,7 +240,7 @@ class MoveKotlinDeclarationsProcessor(
val (oldInternalUsages, externalUsages) = usages.partition { it is KotlinMoveUsage && it.isInternal }
val newInternalUsages = ArrayList<UsageInfo>()
oldInternalUsages.forEach { (it.element as? KtSimpleNameExpression)?.internalUsageInfo = it }
markInternalUsages(oldInternalUsages)
val usagesToProcess = ArrayList(externalUsages)
@@ -281,16 +276,7 @@ class MoveKotlinDeclarationsProcessor(
}
}
newDeclarations.forEach { newDeclaration ->
newInternalUsages += newDeclaration.collectDescendantsOfType<KtSimpleNameExpression>().mapNotNull {
val usageInfo = it.internalUsageInfo
if (usageInfo?.element != null) return@mapNotNull usageInfo
val referencedElement = (usageInfo as? MoveRenameUsageInfo)?.referencedElement ?: return@mapNotNull null
val newReferencedElement = mapToNewOrThis(referencedElement, oldToNewElementsMapping)
if (!newReferencedElement.isValid) return@mapNotNull null
(usageInfo as? KotlinMoveUsage)?.refresh(it, newReferencedElement)
}
}
newDeclarations.forEach { newInternalUsages += restoreInternalUsages(it, oldToNewElementsMapping) }
usagesToProcess += newInternalUsages
@@ -301,9 +287,7 @@ class MoveKotlinDeclarationsProcessor(
RefactoringUIUtil.processIncorrectOperation(myProject, e)
}
finally {
(newInternalUsages + oldInternalUsages).forEach {
(it.element as? KtSimpleNameExpression)?.internalUsageInfo = null
}
cleanUpInternalUsages(newInternalUsages + oldInternalUsages)
}
}
@@ -181,6 +181,31 @@ fun KtElement.processInternalReferencesToUpdateOnPackageNameChange(
}
}
internal var KtSimpleNameExpression.internalUsageInfo: UsageInfo? by CopyableUserDataProperty(Key.create("INTERNAL_USAGE_INFO"))
internal fun markInternalUsages(usages: Collection<UsageInfo>) {
usages.forEach { (it.element as? KtSimpleNameExpression)?.internalUsageInfo = it }
}
internal fun restoreInternalUsages(
scope: KtElement,
oldToNewElementsMapping: Map<PsiElement, PsiElement>,
forcedRestore: Boolean = false
): List<UsageInfo> {
return scope.collectDescendantsOfType<KtSimpleNameExpression>().mapNotNull {
val usageInfo = it.internalUsageInfo
if (!forcedRestore && usageInfo?.element != null) return@mapNotNull usageInfo
val referencedElement = (usageInfo as? MoveRenameUsageInfo)?.referencedElement ?: return@mapNotNull null
val newReferencedElement = mapToNewOrThis(referencedElement, oldToNewElementsMapping)
if (!newReferencedElement.isValid) return@mapNotNull null
(usageInfo as? KotlinMoveUsage)?.refresh(it, newReferencedElement)
}
}
internal fun cleanUpInternalUsages(usages: Collection<UsageInfo>) {
usages.forEach { (it.element as? KtSimpleNameExpression)?.internalUsageInfo = null }
}
class ImplicitCompanionAsDispatchReceiverUsageInfo(
callee: KtSimpleNameExpression,
val companionDescriptor: ClassDescriptor
@@ -0,0 +1,8 @@
package bar
import foo.B
class A {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class A {
val a: A = A()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class A {
val a: <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,12 @@
package bar
import foo.B
fun foo() {
}
class A {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class A {
val a: A = A()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,5 @@
package bar
fun foo() {
}
@@ -0,0 +1,11 @@
package foo
class <caret>A {
val a: 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
class A {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class A {
val a: A = A()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class <caret>A {
val a: 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,15 @@
package bar
class A {
fun test() {
A.testCompanion()
testCompanion()
Companion.testCompanion()
}
companion object {
fun testCompanion() {
}
}
}
@@ -0,0 +1,15 @@
package foo
class A {
fun test() {
A.testCompanion()
testCompanion()
Companion.testCompanion()
}
companion object {
fun testCompanion() {
}
}
}
@@ -0,0 +1,15 @@
package foo
class <caret>A {
fun test() {
A.testCompanion()
testCompanion()
Companion.testCompanion()
}
companion object {
fun testCompanion() {
}
}
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,8 @@
package bar
import foo.B
class X {
val a: X = X()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class A {
val a: A = A()
val b: B = B()
}
class B {
val a: A = A()
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
class <caret>A {
val a: 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,5 @@
package bar
class A {
class B
}
@@ -0,0 +1,5 @@
package foo
class A {
class B
}
@@ -0,0 +1,5 @@
package foo
class A {
class <caret>B
}
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,8 @@
package bar
import foo.B
object A {
val a: A = A
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
object A {
val a: A = A
val b: B = B()
}
class B {
val a: A = A
val b: B = B()
}
@@ -0,0 +1,11 @@
package foo
object <caret>A {
val a: 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,3 @@
package bar
class A
@@ -0,0 +1,3 @@
package foo
class A
@@ -0,0 +1,3 @@
package foo
class A
@@ -0,0 +1,4 @@
{
"mainFile": "foo/test.kt",
"targetPackage": "bar"
}
@@ -0,0 +1,146 @@
/*
* Copyright 2010-2017 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
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.intellij.codeInsight.TargetElementUtil
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.refactoring.BaseRefactoringProcessor
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.UsefulTestCase
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.refactoring.move.runMoveRefactoring
import org.jetbrains.kotlin.idea.refactoring.rename.loadTestConfiguration
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinLightProjectDescriptor
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.kotlin.idea.test.extractMultipleMarkerOffsets
import org.jetbrains.kotlin.test.KotlinTestUtils
import java.io.File
abstract class AbstractMultifileRefactoringTest : KotlinLightCodeInsightFixtureTestCase() {
interface RefactoringAction {
fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List<PsiElement>, config: JsonObject)
}
override fun getProjectDescriptor(): LightProjectDescriptor {
if (KotlinTestUtils.isAllFilesPresentTest(getTestName(false))) return super.getProjectDescriptor()
val testConfigurationFile = File(super.getTestDataPath(), fileName())
val config = loadTestConfiguration(testConfigurationFile)
val withRuntime = config["withRuntime"]?.asBoolean ?: false
if (withRuntime) {
return KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
}
return KotlinLightProjectDescriptor.INSTANCE
}
protected abstract fun runRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project)
protected fun doTest(path: String) {
val testFile = File(path)
val config = JsonParser().parse(FileUtil.loadFile(testFile, true)) as JsonObject
doTestCommittingDocuments(testFile) { rootDir ->
runRefactoring(path, config, rootDir, project)
}
}
protected fun getTestDirName(lowercaseFirstLetter : Boolean) : String {
val testName = getTestName(lowercaseFirstLetter)
val endIndex = testName.lastIndexOf('_')
if (endIndex < 0) return testName
return testName.substring(0, endIndex).replace('_', '/')
}
override fun getTestDataPath() = super.getTestDataPath() + "/" + getTestDirName(true)
protected fun doTestCommittingDocuments(testFile: File, action: (VirtualFile) -> Unit) {
val beforeVFile = myFixture.copyDirectoryToProject("before", "")
PsiDocumentManager.getInstance(myFixture.project).commitAllDocuments()
val afterDir = File(testFile.parentFile, "after")
val afterVFile = LocalFileSystem.getInstance().findFileByIoFile(afterDir)?.apply {
UsefulTestCase.refreshRecursively(this)
}
action(beforeVFile)
PsiDocumentManager.getInstance(project).commitAllDocuments()
FileDocumentManager.getInstance().saveAllDocuments()
PlatformTestUtil.assertDirectoriesEqual(afterVFile, beforeVFile)
}
}
fun runRefactoringTest(
path: String,
config: JsonObject,
rootDir: VirtualFile,
project: Project,
action: AbstractMultifileRefactoringTest.RefactoringAction
) {
val testDir = path.substring(0, path.lastIndexOf("/"))
val mainFilePath = config.getNullableString("mainFile") ?: config.getAsJsonArray("filesToMove").first().asString
val conflictFile = File(testDir + "/conflicts.txt")
val mainFile = rootDir.findFileByRelativePath(mainFilePath)!!
val mainPsiFile = PsiManager.getInstance(project).findFile(mainFile)!!
val document = FileDocumentManager.getInstance().getDocument(mainFile)!!
val editor = EditorFactory.getInstance()!!.createEditor(document, project)!!
val caretOffsets = document.extractMultipleMarkerOffsets(project)
val elementsAtCaret = caretOffsets.map {
TargetElementUtil.getInstance().findTargetElement(
editor,
TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED or TargetElementUtil.ELEMENT_NAME_ACCEPTED,
it
)!!
}
try {
action.runRefactoring(rootDir, mainPsiFile, elementsAtCaret, config)
assert(!conflictFile.exists())
}
catch(e: BaseRefactoringProcessor.ConflictsInTestsException) {
KotlinTestUtils.assertEqualsToFile(conflictFile, e.messages.distinct().sorted().joinToString("\n"))
BaseRefactoringProcessor.ConflictsInTestsException.setTestIgnore(true)
// Run refactoring again with ConflictsInTestsException suppressed
action.runRefactoring(rootDir, mainPsiFile, elementsAtCaret, config)
}
finally {
BaseRefactoringProcessor.ConflictsInTestsException.setTestIgnore(false)
EditorFactory.getInstance()!!.releaseEditor(editor)
}
}
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2017 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.copy
import com.google.gson.JsonObject
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.copy.CopyHandler
import com.intellij.refactoring.move.moveClassesOrPackages.MultipleRootsMoveDestination
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.CopyKotlinClassHandler.Companion.newName
import org.jetbrains.kotlin.idea.refactoring.runRefactoringTest
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.utils.ifEmpty
abstract class AbstractCopyTest : AbstractMultifileRefactoringTest(), AbstractMultifileRefactoringTest.RefactoringAction {
override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List<PsiElement>, config: JsonObject) {
val elementsToCopy = elementsAtCaret.ifEmpty { listOf(mainFile) }.toTypedArray()
assert(CopyHandler.canCopy(elementsToCopy))
val packageWrapper = PackageWrapper(mainFile.manager, config.getString("targetPackage"))
project.newName = config.getNullableString("newName")
val targetDirectory = runWriteAction { MultipleRootsMoveDestination(packageWrapper).getTargetDirectory(mainFile) }
CopyHandler.doCopy(elementsToCopy, targetDirectory)
}
override fun runRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) {
runRefactoringTest(path, config, rootDir, project, this)
}
}
@@ -0,0 +1,86 @@
/*
* Copyright 2010-2017 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.copy;
import com.intellij.testFramework.TestDataPath;
import org.jetbrains.kotlin.test.JUnit3RunnerWithInners;
import org.jetbrains.kotlin.test.KotlinTestUtils;
import org.jetbrains.kotlin.test.TargetBackend;
import org.jetbrains.kotlin.test.TestMetadata;
import org.junit.runner.RunWith;
import java.io.File;
import java.util.regex.Pattern;
/** This class is generated by {@link org.jetbrains.kotlin.generators.tests.TestsPackage}. DO NOT MODIFY MANUALLY */
@SuppressWarnings("all")
@TestMetadata("idea/testData/refactoring/copy")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public class CopyTestGenerated extends AbstractCopyTest {
public void testAllFilesPresentInCopy() throws Exception {
KotlinTestUtils.assertAllTestsPresentInSingleGeneratedClass(this.getClass(), new File("idea/testData/refactoring/copy"), Pattern.compile("^(.+)\\.test$"), TargetBackend.ANY);
}
@TestMetadata("copyClassCaretInside/copyClassCaretInside.test")
public void testCopyClassCaretInside_CopyClassCaretInside() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyClassCaretInside/copyClassCaretInside.test");
doTest(fileName);
}
@TestMetadata("copyClassToExistingFile/copyClassToExistingFile.test")
public void testCopyClassToExistingFile_CopyClassToExistingFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyClassToExistingFile/copyClassToExistingFile.test");
doTest(fileName);
}
@TestMetadata("copyClassToNewFile/copyClassToNewFile.test")
public void testCopyClassToNewFile_CopyClassToNewFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyClassToNewFile/copyClassToNewFile.test");
doTest(fileName);
}
@TestMetadata("copyClassWithCompanionRefs/copyClassWithCompanionRefs.test")
public void testCopyClassWithCompanionRefs_CopyClassWithCompanionRefs() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyClassWithCompanionRefs/copyClassWithCompanionRefs.test");
doTest(fileName);
}
@TestMetadata("copyClassWithRename/copyClassWithRename.test")
public void testCopyClassWithRename_CopyClassWithRename() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyClassWithRename/copyClassWithRename.test");
doTest(fileName);
}
@TestMetadata("copyNestedClass/copyNestedClass.test")
public void testCopyNestedClass_CopyNestedClass() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyNestedClass/copyNestedClass.test");
doTest(fileName);
}
@TestMetadata("copyObject/copyObject.test")
public void testCopyObject_CopyObject() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copyObject/copyObject.test");
doTest(fileName);
}
@TestMetadata("copySingleClassFile/copySingleClassFile.test")
public void testCopySingleClassFile_CopySingleClassFile() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("idea/testData/refactoring/copy/copySingleClassFile/copySingleClassFile.test");
doTest(fileName);
}
}
@@ -17,18 +17,11 @@
package org.jetbrains.kotlin.idea.refactoring.move
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.intellij.codeInsight.TargetElementUtil
import com.intellij.openapi.editor.EditorFactory
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.*
import com.intellij.refactoring.BaseRefactoringProcessor.ConflictsInTestsException
import com.intellij.refactoring.MoveDestination
import com.intellij.refactoring.PackageWrapper
import com.intellij.refactoring.move.MoveHandler
@@ -37,122 +30,33 @@ import com.intellij.refactoring.move.moveFilesOrDirectories.MoveFilesOrDirectori
import com.intellij.refactoring.move.moveInner.MoveInnerProcessor
import com.intellij.refactoring.move.moveMembers.MockMoveMembersOptions
import com.intellij.refactoring.move.moveMembers.MoveMembersProcessor
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.UsefulTestCase
import com.intellij.util.ActionRunner
import org.jetbrains.kotlin.idea.jsonUtils.getNullableString
import org.jetbrains.kotlin.idea.jsonUtils.getString
import org.jetbrains.kotlin.idea.refactoring.createKotlinFile
import org.jetbrains.kotlin.idea.refactoring.*
import org.jetbrains.kotlin.idea.refactoring.move.changePackage.KotlinChangePackageRefactoring
import org.jetbrains.kotlin.idea.refactoring.move.moveClassesOrPackages.KotlinAwareDelegatingMoveDestination
import org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.*
import org.jetbrains.kotlin.idea.refactoring.rename.loadTestConfiguration
import org.jetbrains.kotlin.idea.refactoring.toPsiDirectory
import org.jetbrains.kotlin.idea.refactoring.toPsiFile
import org.jetbrains.kotlin.idea.search.allScope
import org.jetbrains.kotlin.idea.search.projectScope
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.idea.test.*
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtNamedDeclaration
import org.jetbrains.kotlin.psi.psiUtil.getNonStrictParentOfType
import org.jetbrains.kotlin.test.KotlinTestUtils
import java.io.File
abstract class AbstractMoveTest : KotlinLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor(): LightProjectDescriptor {
if (KotlinTestUtils.isAllFilesPresentTest(getTestName(false))) return super.getProjectDescriptor()
val testConfigurationFile = File(super.getTestDataPath(), fileName())
val config = loadTestConfiguration(testConfigurationFile)
val withRuntime = config["withRuntime"]?.asBoolean ?: false
if (withRuntime) {
return KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
}
return KotlinLightProjectDescriptor.INSTANCE
}
protected fun doTest(path: String) {
val testFile = File(path)
val config = JsonParser().parse(FileUtil.loadFile(testFile, true)) as JsonObject
doTestCommittingDocuments(testFile) { rootDir ->
runMoveRefactoring(path, config, rootDir, project)
}
}
protected fun getTestDirName(lowercaseFirstLetter : Boolean) : String {
val testName = getTestName(lowercaseFirstLetter)
val endIndex = testName.lastIndexOf('_')
if (endIndex < 0) return testName
return testName.substring(0, endIndex).replace('_', '/')
}
override fun getTestDataPath() = super.getTestDataPath() + "/" + getTestDirName(true)
protected fun doTestCommittingDocuments(testFile: File, action: (VirtualFile) -> Unit) {
val beforeVFile = myFixture.copyDirectoryToProject("before", "")
PsiDocumentManager.getInstance(myFixture.project).commitAllDocuments()
val afterDir = File(testFile.parentFile, "after")
val afterVFile = LocalFileSystem.getInstance().findFileByIoFile(afterDir)?.apply {
UsefulTestCase.refreshRecursively(this)
}
action(beforeVFile)
PsiDocumentManager.getInstance(project).commitAllDocuments()
FileDocumentManager.getInstance().saveAllDocuments()
PlatformTestUtil.assertDirectoriesEqual(afterVFile, beforeVFile)
abstract class AbstractMoveTest : AbstractMultifileRefactoringTest() {
override fun runRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) {
runMoveRefactoring(path, config, rootDir, project)
}
}
fun runMoveRefactoring(path: String, config: JsonObject, rootDir: VirtualFile, project: Project) {
val action = MoveAction.valueOf(config.getString("type"))
val testDir = path.substring(0, path.lastIndexOf("/"))
val mainFilePath = config.getNullableString("mainFile") ?: config.getAsJsonArray("filesToMove").first().asString
val conflictFile = File(testDir + "/conflicts.txt")
val mainFile = rootDir.findFileByRelativePath(mainFilePath)!!
val mainPsiFile = PsiManager.getInstance(project).findFile(mainFile)!!
val document = FileDocumentManager.getInstance().getDocument(mainFile)!!
val editor = EditorFactory.getInstance()!!.createEditor(document, project)!!
val caretOffsets = document.extractMultipleMarkerOffsets(project)
val elementsAtCaret = caretOffsets.map {
TargetElementUtil.getInstance().findTargetElement(
editor,
TargetElementUtil.REFERENCED_ELEMENT_ACCEPTED or TargetElementUtil.ELEMENT_NAME_ACCEPTED,
it
)!!
}
try {
action.runRefactoring(rootDir, mainPsiFile, elementsAtCaret, config)
assert(!conflictFile.exists())
}
catch(e: ConflictsInTestsException) {
KotlinTestUtils.assertEqualsToFile(conflictFile, e.messages.distinct().sorted().joinToString("\n"))
ConflictsInTestsException.setTestIgnore(true)
// Run refactoring again with ConflictsInTestsException suppressed
action.runRefactoring(rootDir, mainPsiFile, elementsAtCaret, config)
}
finally {
ConflictsInTestsException.setTestIgnore(false)
EditorFactory.getInstance()!!.releaseEditor(editor)
}
runRefactoringTest(path, config, rootDir, project, MoveAction.valueOf(config.getString("type")))
}
enum class MoveAction {
enum class MoveAction : AbstractMultifileRefactoringTest.RefactoringAction {
MOVE_MEMBERS {
override fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List<PsiElement>, config: JsonObject) {
val members = elementsAtCaret.map { it.getNonStrictParentOfType<PsiMember>()!! }
@@ -387,6 +291,4 @@ enum class MoveAction {
MoveKotlinDeclarationsProcessor(descriptor).run()
}
};
abstract fun runRefactoring(rootDir: VirtualFile, mainFile: PsiFile, elementsAtCaret: List<PsiElement>, config: JsonObject)
}