Create from Usage: Support target directory selection for "Create class"

#KT-22152 Fixed
This commit is contained in:
Alexey Sedunov
2018-02-08 18:43:09 +03:00
parent 695a2a4098
commit 84fed8071c
2 changed files with 91 additions and 12 deletions
@@ -203,7 +203,7 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
}
}
fun build() {
fun build(onFinish: () -> Unit = {}) {
try {
assert(config.currentEditor != null) { "Can't run build() without editor" }
if (finished) throw IllegalStateException("Current builder has already finished")
@@ -211,6 +211,7 @@ class CallableBuilder(val config: CallableBuilderConfiguration) {
}
finally {
finished = true
onFinish()
}
}
@@ -18,32 +18,44 @@ package org.jetbrains.kotlin.idea.quickfix.createFromUsage.createClass
import com.intellij.codeInsight.intention.HighPriorityAction
import com.intellij.codeInsight.intention.LowPriorityAction
import com.intellij.codeInsight.intention.impl.CreateClassDialog
import com.intellij.ide.util.DirectoryChooserUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiComment
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiPackage
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.psi.*
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.idea.codeInsight.CodeInsightUtils
import org.jetbrains.kotlin.idea.core.getPackage
import org.jetbrains.kotlin.idea.core.quoteIfNeeded
import org.jetbrains.kotlin.idea.quickfix.IntentionActionPriority
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.CreateFromUsageFixBase
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.callableBuilder.*
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createClass.ClassKind.*
import org.jetbrains.kotlin.idea.refactoring.SeparateFileWrapper
import org.jetbrains.kotlin.idea.refactoring.canRefactor
import org.jetbrains.kotlin.idea.refactoring.chooseContainerElementIfNecessary
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
import org.jetbrains.kotlin.idea.references.KtSimpleNameReference
import org.jetbrains.kotlin.idea.references.mainReference
import org.jetbrains.kotlin.idea.util.application.executeCommand
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
import org.jetbrains.kotlin.idea.util.application.runWriteAction
import org.jetbrains.kotlin.idea.util.projectStructure.module
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.psi.KtClass
import org.jetbrains.kotlin.psi.KtElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.kotlin.psi.psiUtil.allChildren
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.psi.psiUtil.getQualifiedElementSelector
import org.jetbrains.kotlin.utils.SmartList
import java.util.*
import com.intellij.codeInsight.daemon.impl.quickfix.ClassKind as IdeaClassKind
enum class ClassKind(val keyword: String, val description: String) {
PLAIN_CLASS("class", "class"),
@@ -55,6 +67,8 @@ enum class ClassKind(val keyword: String, val description: String) {
DEFAULT("", "") // Used as a placeholder and must be replaced with one of the kinds above
}
fun ClassKind.toIdeaClassKind() = IdeaClassKind { this@toIdeaClassKind.description.capitalize() }
val ClassKind.actionPriority: IntentionActionPriority
get() = if (this == ANNOTATION_CLASS) IntentionActionPriority.LOW else IntentionActionPriority.NORMAL
@@ -102,10 +116,17 @@ open class CreateClassFromUsageFix<E : KtElement> protected constructor (
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
if (editor == null) return
val applicableParents = if (classInfo.kind == ClassKind.INTERFACE)
classInfo.applicableParents.filter { it != element?.containingClass() }
else
classInfo.applicableParents
val applicableParents = SmartList<PsiElement>().also { parents ->
if (classInfo.kind == ClassKind.INTERFACE)
classInfo.applicableParents.filterTo(parents) { it != element?.containingClass() }
else
parents += classInfo.applicableParents
if (classInfo.kind != ClassKind.ENUM_ENTRY) {
parents += SeparateFileWrapper(PsiManager.getInstance(project))
}
}
if (ApplicationManager.getApplication().isUnitTestMode) {
val targetParent = applicableParents.firstOrNull {
@@ -154,7 +175,45 @@ open class CreateClassFromUsageFix<E : KtElement> protected constructor (
return targetFile
}
private fun doInvoke(selectedParent: PsiElement, editor: Editor, file: KtFile) {
private fun doInvoke(selectedParent: PsiElement, editor: Editor, file: KtFile, startCommand: Boolean = true) {
val className = classInfo.name
if (selectedParent is SeparateFileWrapper) {
if (ApplicationManager.getApplication().isUnitTestMode) {
return doInvoke(file, editor, file)
}
val ideaClassKind = classInfo.kind.toIdeaClassKind()
val defaultPackageFqName = file.packageFqName
val dialog = object : CreateClassDialog(
file.project,
"Create ${ideaClassKind.description.capitalize()}",
className,
defaultPackageFqName.asString(),
ideaClassKind,
false,
file.module
) {
override fun reportBaseInSourceSelectionInTest() = true
}
dialog.show()
if (dialog.exitCode != DialogWrapper.OK_EXIT_CODE) return
val targetDirectory = dialog.targetDirectory ?: return
val fileName = "$className.${KotlinFileType.EXTENSION}"
val packageFqName = targetDirectory.getPackage()?.qualifiedName?.let { FqName(it).quoteIfNeeded() }
file.project.executeWriteCommand(text) {
val targetFile = getOrCreateKotlinFile(fileName, targetDirectory, (packageFqName ?: defaultPackageFqName).asString())
if (targetFile != null) {
doInvoke(targetFile, editor, file, false)
}
}
return
}
val element = element ?: return
runWriteAction {
with(classInfo) {
val targetParent =
@@ -166,14 +225,33 @@ open class CreateClassFromUsageFix<E : KtElement> protected constructor (
val constructorInfo = ClassWithPrimaryConstructorInfo(classInfo, expectedTypeInfo)
val builder = CallableBuilderConfiguration(
Collections.singletonList(constructorInfo),
element as KtElement,
element,
file,
editor,
false,
kind == PLAIN_CLASS || kind == INTERFACE
).createBuilder()
builder.placement = CallablePlacement.NoReceiver(targetParent)
file.project.executeCommand(text) { builder.build() }
fun buildClass() {
builder.build {
if (targetParent !is KtFile || targetParent == file) return@build
val targetPackageFqName = targetParent.packageFqName
if (targetPackageFqName == file.packageFqName) return@build
val reference = (element.getQualifiedElementSelector() as? KtSimpleNameExpression)?.mainReference ?: return@build
reference.bindToFqName(
targetPackageFqName.child(Name.identifier(className)),
KtSimpleNameReference.ShorteningMode.FORCED_SHORTENING
)
}
}
if (startCommand) {
file.project.executeCommand(text, command = ::buildClass)
}
else {
buildClass()
}
}
}
}