mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
KTIJ-16167: Implemented new CreateMissedActualsFix with UI dialog for selecting source sets
GitOrigin-RevId: 63c909bbacf2383228e7d01b3cbe99ebe4d44c42
This commit is contained in:
committed by
intellij-monorepo-bot
parent
86fc992330
commit
e7f3fd5d25
@@ -168,6 +168,8 @@ fix.add.argument.name.step.choose.parameter.title=Choose Parameter Name
|
||||
|
||||
fix.create.expect.actual=Create expect / actual declaration
|
||||
fix.create.missing.actual.members=Add missing actual members
|
||||
fix.create.missing.actual.declarations=Add missing actual declarations
|
||||
fix.create.missing.actual.declarations.title=Add Missing Actual Declarations
|
||||
|
||||
fix.create.declaration.error=Cannot generate {0}: {1}
|
||||
fix.create.declaration.error.inaccessible.type=Inaccessible Type
|
||||
@@ -766,6 +768,7 @@ label.text.file.name=File &name:
|
||||
label.text.introduce.as=&Introduce as:\u0020
|
||||
label.text.name=&Name:
|
||||
label.text.package.name=Packa&ge name:
|
||||
label.text.source.sets=Source sets:
|
||||
label.text.receiver.type=Receiver &type:
|
||||
label.text.target.file.name=Target file name:
|
||||
label.text.to.file=To &file:
|
||||
|
||||
@@ -111,7 +111,7 @@ private fun findLongestExistingPackage(
|
||||
private val kotlinSourceRootTypes: Set<JpsModuleSourceRootType<JavaSourceRootProperties>> =
|
||||
setOf(SourceKotlinRootType, TestSourceKotlinRootType) + JavaModuleSourceRootTypes.SOURCES
|
||||
|
||||
private class PureKotlinSourceFoldersHolder {
|
||||
class PureKotlinSourceFoldersHolder {
|
||||
private val moduleMap = mutableMapOf<Module, Collection<String>?>()
|
||||
|
||||
/***
|
||||
@@ -143,7 +143,7 @@ private fun Module.findNonGeneratedKotlinSourceFolders(): Sequence<SourceFolder>
|
||||
it.jpsElement.getProperties(kotlinSourceRootTypes)?.isForGeneratedSources != true
|
||||
}
|
||||
|
||||
private fun Module.findExistingNonGeneratedKotlinSourceRootFiles(
|
||||
fun Module.findExistingNonGeneratedKotlinSourceRootFiles(
|
||||
pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder
|
||||
): List<VirtualFile> = findNonGeneratedKotlinSourceFolders().toExistingFiles(project, pureKotlinSourceFoldersHolder)
|
||||
|
||||
@@ -157,7 +157,7 @@ private fun Sequence<SourceFolder>.toExistingFiles(
|
||||
sourceFolder.file?.takeIf { pureKotlinSourceFoldersHolder.hasPurePrefixInVirtualFile(project, it) }
|
||||
}.toList()
|
||||
|
||||
private fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List<VirtualFile> {
|
||||
fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List<VirtualFile> {
|
||||
val nonGeneratedSourceFolders = findNonGeneratedKotlinSourceFolders().toList()
|
||||
nonGeneratedSourceFolders.asSequence().toExistingFiles(project, pureKotlinSourceFoldersHolder).ifNotEmpty { return this }
|
||||
return listOfNotNull(createSourceRootDirectory(nonGeneratedSourceFolders))
|
||||
|
||||
+25
-37
@@ -7,14 +7,21 @@ import com.intellij.codeInspection.LocalInspectionToolSession
|
||||
import com.intellij.codeInspection.ProblemsHolder
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import org.jetbrains.kotlin.analyzer.ModuleInfo
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.analyzer.moduleInfo
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
|
||||
import org.jetbrains.kotlin.idea.base.facet.implementingModules
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.PlatformModuleInfo
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.caches.project.implementingDescriptors
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.findModuleDescriptor
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.resolveToDescriptorIfAny
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateActualClassFix
|
||||
import org.jetbrains.kotlin.platform.isCommon
|
||||
import org.jetbrains.kotlin.platform.oldFashionedDescription
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateMissedActualsFix
|
||||
import org.jetbrains.kotlin.idea.util.actualsForExpected
|
||||
import org.jetbrains.kotlin.idea.util.projectStructure.module
|
||||
import org.jetbrains.kotlin.platform.isMultiPlatform
|
||||
import org.jetbrains.kotlin.psi.KtClass
|
||||
import org.jetbrains.kotlin.psi.KtClassOrObject
|
||||
import org.jetbrains.kotlin.psi.classOrObjectVisitor
|
||||
@@ -26,8 +33,6 @@ import org.jetbrains.kotlin.resolve.multiplatform.ExpectedActualResolver
|
||||
import org.jetbrains.kotlin.resolve.multiplatform.OptionalAnnotationUtil
|
||||
import org.jetbrains.kotlin.resolve.multiplatform.onlyFromThisModule
|
||||
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.inspections.AbstractKotlinInspection
|
||||
|
||||
class OptionalExpectationInspection : AbstractKotlinInspection() {
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||
return classOrObjectVisitor(fun(classOrObject: KtClassOrObject) {
|
||||
@@ -41,37 +46,20 @@ class OptionalExpectationInspection : AbstractKotlinInspection() {
|
||||
val implementingModules = classOrObject.findModuleDescriptor().implementingDescriptors
|
||||
if (implementingModules.isEmpty()) return
|
||||
|
||||
for (actualModuleDescriptor in implementingModules) {
|
||||
val compatibility = ExpectedActualResolver.findActualForExpected(
|
||||
descriptor, actualModuleDescriptor, onlyFromThisModule(actualModuleDescriptor)
|
||||
) ?: continue
|
||||
|
||||
if (!compatibility.allStrongIncompatibilities() &&
|
||||
(ExpectActualCompatibility.Compatible in compatibility ||
|
||||
!compatibility.values.flatMapTo(
|
||||
hashSetOf()
|
||||
) { it }.all { actual ->
|
||||
val expectedOnes = ExpectedActualResolver.findExpectedForActual(
|
||||
actual, onlyFromThisModule(descriptor.module)
|
||||
)
|
||||
expectedOnes != null && ExpectActualCompatibility.Compatible in expectedOnes.keys
|
||||
})
|
||||
) continue
|
||||
val platform = actualModuleDescriptor.platform ?: continue
|
||||
if (platform.isCommon()) continue
|
||||
|
||||
val displayedName = actualModuleDescriptor.getCapability(ModuleInfo.Capability)?.displayedName ?: ""
|
||||
val actualModule = (actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module ?: continue
|
||||
holder.registerProblem(
|
||||
classOrObject.nameIdentifier ?: classOrObject,
|
||||
KotlinBundle.message(
|
||||
"optionally.expected.annotation.has.no.actual.annotation.in.module.0.for.platform.1",
|
||||
displayedName,
|
||||
platform.oldFashionedDescription
|
||||
),
|
||||
IntentionWrapper(CreateActualClassFix(classOrObject, actualModule, platform))
|
||||
)
|
||||
}
|
||||
val actualizedLeafModules = descriptor.actualsForExpected()
|
||||
.flatMap { it.module.implementingDescriptors.plus(it.module) } //all actualized modules
|
||||
.filter { it.implementingDescriptors.isEmpty() } //only leafs
|
||||
.toSet()
|
||||
val notActualizedLeafModules = implementingModules
|
||||
.filter { it.implementingDescriptors.isEmpty() && it !in actualizedLeafModules }
|
||||
.mapNotNull { (it.moduleInfo as? ModuleSourceInfo)?.module }
|
||||
.toSet()
|
||||
if (notActualizedLeafModules.isEmpty()) return
|
||||
holder.registerProblem(
|
||||
classOrObject.nameIdentifier ?: classOrObject,
|
||||
KotlinBundle.message("fix.create.missing.actual.declarations"),
|
||||
IntentionWrapper(CreateMissedActualsFix(classOrObject, notActualizedLeafModules))
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateL
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByNamedArgumentActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.createFromUsage.createVariable.CreateParameterByRefActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.AddActualFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateActualFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateExpectedFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.expectactual.CreateMissedActualsFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExperimentalToRequiresOptInFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateExternalExtensionFix
|
||||
import org.jetbrains.kotlin.idea.quickfix.migration.MigrateTypeParameterListFix
|
||||
@@ -600,7 +600,7 @@ class QuickFixRegistrar : QuickFixContributor {
|
||||
|
||||
ACTUAL_WITHOUT_EXPECT.registerFactory(RemoveModifierFixBase.createRemoveModifierFromListOwnerPsiBasedFactory(ACTUAL_KEYWORD))
|
||||
ACTUAL_WITHOUT_EXPECT.registerFactory(CreateExpectedFix)
|
||||
NO_ACTUAL_FOR_EXPECT.registerFactory(CreateActualFix)
|
||||
NO_ACTUAL_FOR_EXPECT.registerFactory(CreateMissedActualsFix)
|
||||
NO_ACTUAL_CLASS_MEMBER_FOR_EXPECTED_CLASS.registerFactory(AddActualFix)
|
||||
|
||||
ACTUAL_MISSING.registerFactory(AddModifierFixFE10.createFactory(ACTUAL_KEYWORD))
|
||||
|
||||
-119
@@ -1,119 +0,0 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix.expectactual
|
||||
|
||||
import com.intellij.ide.util.EditorHelper
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.ThrowableComputable
|
||||
import com.intellij.util.SlowOperations
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.base.util.reformatted
|
||||
import org.jetbrains.kotlin.idea.core.ShortenReferences
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
|
||||
import org.jetbrains.kotlin.idea.quickfix.TypeAccessibilityChecker
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
|
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
|
||||
abstract class AbstractCreateDeclarationFix<D : KtNamedDeclaration>(
|
||||
declaration: D,
|
||||
protected val module: Module,
|
||||
protected val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
|
||||
) : KotlinQuickFixAction<D>(declaration) {
|
||||
|
||||
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
|
||||
|
||||
protected val elementType: String = element.getTypeDescription()
|
||||
|
||||
override fun startInWriteAction() = false
|
||||
|
||||
protected abstract fun findExistingFileToCreateDeclaration(
|
||||
originalFile: KtFile,
|
||||
originalDeclaration: KtNamedDeclaration
|
||||
): KtFile?
|
||||
|
||||
protected fun getOrCreateImplementationFile(): KtFile? {
|
||||
val declaration = element as? KtNamedDeclaration ?: return null
|
||||
val parent = declaration.parent
|
||||
return (parent as? KtFile)?.let { findExistingFileToCreateDeclaration(it, declaration) }
|
||||
?: createFileForDeclaration(module, declaration)
|
||||
}
|
||||
|
||||
protected fun doGenerate(
|
||||
project: Project,
|
||||
editor: Editor?,
|
||||
originalFile: KtFile,
|
||||
targetFile: KtFile,
|
||||
targetClass: KtClassOrObject?
|
||||
) {
|
||||
val factory = KtPsiFactory(project)
|
||||
val targetClassPointer = targetClass?.createSmartPointer()
|
||||
val targetFilePointer = targetFile.createSmartPointer()
|
||||
DumbService.getInstance(project).runWhenSmart(fun() {
|
||||
val generated = try {
|
||||
element?.let {
|
||||
SlowOperations.allowSlowOperations(ThrowableComputable {
|
||||
factory.generateIt(project, TypeAccessibilityChecker.create(project, module), it)
|
||||
})
|
||||
}
|
||||
} catch (e: KotlinTypeInaccessibleException) {
|
||||
if (editor != null) {
|
||||
showErrorHint(
|
||||
project, editor,
|
||||
escapeXml(KotlinBundle.message("fix.create.declaration.error", elementType, e.message)),
|
||||
KotlinBundle.message("fix.create.declaration.error.inaccessible.type")
|
||||
)
|
||||
}
|
||||
null
|
||||
} ?: return
|
||||
|
||||
val shortened = project.executeWriteCommand(KotlinBundle.message("fix.create.expect.actual"), null) {
|
||||
val resultTargetFile = targetFilePointer.element ?: return@executeWriteCommand null
|
||||
if (resultTargetFile.packageDirective?.fqName != originalFile.packageDirective?.fqName &&
|
||||
resultTargetFile.declarations.isEmpty()
|
||||
) {
|
||||
val packageDirective = originalFile.packageDirective
|
||||
if (packageDirective != null) {
|
||||
val oldPackageDirective = resultTargetFile.packageDirective
|
||||
val newPackageDirective = packageDirective.copy() as KtPackageDirective
|
||||
if (oldPackageDirective != null) {
|
||||
if (oldPackageDirective.text.isEmpty()) {
|
||||
resultTargetFile.addAfter(factory.createNewLine(2), resultTargetFile.importList ?: oldPackageDirective)
|
||||
}
|
||||
|
||||
oldPackageDirective.replace(newPackageDirective)
|
||||
} else {
|
||||
resultTargetFile.add(newPackageDirective)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resultTargetClass = targetClassPointer?.element
|
||||
val generatedDeclaration = when {
|
||||
resultTargetClass != null -> {
|
||||
if (generated is KtPrimaryConstructor && resultTargetClass is KtClass)
|
||||
resultTargetClass.createPrimaryConstructorIfAbsent().replace(generated)
|
||||
else
|
||||
resultTargetClass.addDeclaration(generated as KtNamedDeclaration)
|
||||
}
|
||||
else -> {
|
||||
resultTargetFile.add(factory.createNewLine(1))
|
||||
resultTargetFile.add(generated) as KtElement
|
||||
}
|
||||
}
|
||||
|
||||
ShortenReferences.DEFAULT.process(generatedDeclaration.reformatted() as KtElement)
|
||||
} ?: return
|
||||
|
||||
EditorHelper.openInEditor(shortened)?.caretModel?.moveToOffset(
|
||||
(shortened as? KtNamedDeclaration)?.nameIdentifier?.startOffset ?: shortened.startOffset,
|
||||
true,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
-100
@@ -1,100 +0,0 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix.expectactual
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.Project
|
||||
import org.jetbrains.kotlin.analyzer.ModuleInfo
|
||||
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
|
||||
import org.jetbrains.kotlin.idea.core.toDescriptor
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
|
||||
import org.jetbrains.kotlin.idea.quickfix.TypeAccessibilityChecker
|
||||
import org.jetbrains.kotlin.idea.util.actualsForExpected
|
||||
import org.jetbrains.kotlin.platform.TargetPlatform
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.hasExpectModifier
|
||||
|
||||
sealed class CreateActualFix<D : KtNamedDeclaration>(
|
||||
declaration: D,
|
||||
actualModule: Module,
|
||||
private val actualPlatform: TargetPlatform,
|
||||
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
|
||||
) : AbstractCreateDeclarationFix<D>(declaration, actualModule, generateIt) {
|
||||
|
||||
override fun getText() = KotlinBundle.message(
|
||||
"create.actual.0.for.module.1.2",
|
||||
elementType,
|
||||
module.name,
|
||||
actualPlatform.singleOrNull()?.platformName ?: actualPlatform
|
||||
)
|
||||
|
||||
final override fun invoke(project: Project, editor: Editor?, file: KtFile) {
|
||||
val actualFile = getOrCreateImplementationFile() ?: return
|
||||
doGenerate(project, editor, originalFile = file, targetFile = actualFile, targetClass = null)
|
||||
}
|
||||
|
||||
override fun findExistingFileToCreateDeclaration(
|
||||
originalFile: KtFile,
|
||||
originalDeclaration: KtNamedDeclaration
|
||||
): KtFile? {
|
||||
for (otherDeclaration in originalFile.declarations) {
|
||||
if (otherDeclaration === originalDeclaration) continue
|
||||
if (!otherDeclaration.hasExpectModifier()) continue
|
||||
val actualDeclaration = otherDeclaration.actualsForExpected(module).singleOrNull() ?: continue
|
||||
return actualDeclaration.containingKtFile
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object : KotlinSingleIntentionActionFactory() {
|
||||
override fun createAction(diagnostic: Diagnostic): IntentionAction? {
|
||||
val d = DiagnosticFactory.cast(diagnostic, Errors.NO_ACTUAL_FOR_EXPECT)
|
||||
val declaration = d.psiElement as? KtNamedDeclaration ?: return null
|
||||
val compatibility = d.c
|
||||
// For function we allow it, because overloads are possible
|
||||
if (compatibility.isNotEmpty() && declaration !is KtFunction) return null
|
||||
val actualModuleDescriptor = d.b
|
||||
val actualModule = (actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module ?: return null
|
||||
val actualPlatform = actualModuleDescriptor.platform ?: return null
|
||||
return when (declaration) {
|
||||
is KtClassOrObject -> CreateActualClassFix(declaration, actualModule, actualPlatform)
|
||||
is KtFunction, is KtProperty -> CreateActualCallableMemberFix(
|
||||
declaration as KtCallableDeclaration,
|
||||
actualModule,
|
||||
actualPlatform
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CreateActualClassFix(
|
||||
klass: KtClassOrObject,
|
||||
actualModule: Module,
|
||||
actualPlatform: TargetPlatform
|
||||
) : CreateActualFix<KtClassOrObject>(klass, actualModule, actualPlatform, block@{ project, checker, element ->
|
||||
checker.findAndApplyExistingClasses(element.collectDeclarationsForAddActualModifier().toList())
|
||||
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
|
||||
|
||||
generateClassOrObject(project, false, element, checker = checker)
|
||||
})
|
||||
|
||||
class CreateActualCallableMemberFix(
|
||||
declaration: KtCallableDeclaration,
|
||||
actualModule: Module,
|
||||
actualPlatform: TargetPlatform
|
||||
) : CreateActualFix<KtCallableDeclaration>(declaration, actualModule, actualPlatform, block@{ project, checker, element ->
|
||||
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
|
||||
|
||||
val descriptor = element.toDescriptor() as? CallableMemberDescriptor
|
||||
descriptor?.let { generateCallable(project, false, element, descriptor, checker = checker) }
|
||||
})
|
||||
|
||||
+24
-11
@@ -29,6 +29,7 @@ import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
|
||||
import org.jetbrains.kotlin.idea.util.liftToExpected
|
||||
import org.jetbrains.kotlin.idea.base.util.module
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
|
||||
@@ -39,9 +40,15 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs
|
||||
sealed class CreateExpectedFix<D : KtNamedDeclaration>(
|
||||
declaration: D,
|
||||
targetExpectedClass: KtClassOrObject?,
|
||||
commonModule: Module,
|
||||
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
|
||||
) : AbstractCreateDeclarationFix<D>(declaration, commonModule, generateIt) {
|
||||
val module: Module,
|
||||
val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
|
||||
) : KotlinQuickFixAction<D>(declaration) {
|
||||
|
||||
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
|
||||
|
||||
protected val elementType: String = element.getTypeDescription()
|
||||
|
||||
override fun startInWriteAction() = false
|
||||
|
||||
private val targetExpectedClassPointer = targetExpectedClass?.createSmartPointer()
|
||||
|
||||
@@ -50,10 +57,11 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
|
||||
final override fun invoke(project: Project, editor: Editor?, file: KtFile) {
|
||||
val targetExpectedClass = targetExpectedClassPointer?.element
|
||||
val expectedFile = targetExpectedClass?.containingKtFile ?: getOrCreateImplementationFile() ?: return
|
||||
doGenerate(project, editor, originalFile = file, targetFile = expectedFile, targetClass = targetExpectedClass)
|
||||
val declaration = element ?: return
|
||||
generateExpectOrActualInFile(project, editor, originalFile = file, targetFile = expectedFile, targetClass = targetExpectedClass, declaration, module, generateIt)
|
||||
}
|
||||
|
||||
override fun findExistingFileToCreateDeclaration(
|
||||
private fun findExistingFileToCreateDeclaration(
|
||||
originalFile: KtFile,
|
||||
originalDeclaration: KtNamedDeclaration
|
||||
): KtFile? {
|
||||
@@ -67,6 +75,13 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getOrCreateImplementationFile(): KtFile? {
|
||||
val declaration = element as? KtNamedDeclaration ?: return null
|
||||
val parent = declaration.parent
|
||||
return (parent as? KtFile)?.let { findExistingFileToCreateDeclaration(it, declaration) }
|
||||
?: createFileForDeclaration(module, declaration)
|
||||
}
|
||||
|
||||
companion object : KotlinIntentionActionsFactory() {
|
||||
override fun doCreateActions(diagnostic: Diagnostic): List<IntentionAction> {
|
||||
val d = DiagnosticFactory.cast(diagnostic, Errors.ACTUAL_WITHOUT_EXPECT)
|
||||
@@ -84,13 +99,11 @@ sealed class CreateExpectedFix<D : KtNamedDeclaration>(
|
||||
?: actualDeclaration.module?.implementedModules
|
||||
?: return emptyList()
|
||||
return when (actualDeclaration) {
|
||||
is KtClassOrObject -> expectedModules.map { CreateExpectedClassFix(actualDeclaration, expectedContainingClass, it) }
|
||||
is KtClassOrObject -> expectedModules.map {
|
||||
CreateExpectedClassFix(actualDeclaration, expectedContainingClass, it)
|
||||
}
|
||||
is KtProperty, is KtParameter, is KtFunction -> expectedModules.map {
|
||||
CreateExpectedCallableMemberFix(
|
||||
actualDeclaration as KtCallableDeclaration,
|
||||
expectedContainingClass,
|
||||
it
|
||||
)
|
||||
CreateExpectedCallableMemberFix(actualDeclaration as KtCallableDeclaration, expectedContainingClass, it)
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
+413
@@ -0,0 +1,413 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package org.jetbrains.kotlin.idea.quickfix.expectactual
|
||||
|
||||
import com.intellij.codeInsight.intention.IntentionAction
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.wizard.setMinimumWidthForAllRowLabels
|
||||
import com.intellij.openapi.application.runWriteAction
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.externalSystem.service.project.manage.SourceFolderManager
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.DialogWrapper
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.findOrCreateDirectory
|
||||
import com.intellij.psi.PsiDirectory
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.layout.ComponentPredicate
|
||||
import org.jetbrains.kotlin.analyzer.ModuleInfo
|
||||
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
|
||||
import org.jetbrains.kotlin.diagnostics.Diagnostic
|
||||
import org.jetbrains.kotlin.diagnostics.DiagnosticFactory
|
||||
import org.jetbrains.kotlin.diagnostics.Errors
|
||||
import org.jetbrains.kotlin.idea.base.facet.implementedModules
|
||||
import org.jetbrains.kotlin.idea.base.facet.implementingModules
|
||||
import org.jetbrains.kotlin.idea.base.facet.kotlinSourceRootType
|
||||
import org.jetbrains.kotlin.idea.base.facet.platform.platform
|
||||
import org.jetbrains.kotlin.idea.base.projectStructure.moduleInfo.ModuleSourceInfo
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.codeinsight.api.classic.quickfixes.KotlinQuickFixAction
|
||||
import org.jetbrains.kotlin.idea.core.PureKotlinSourceFoldersHolder
|
||||
import org.jetbrains.kotlin.idea.core.findExistingNonGeneratedKotlinSourceRootFiles
|
||||
import org.jetbrains.kotlin.idea.core.findOrConfigureKotlinSourceRoots
|
||||
import org.jetbrains.kotlin.idea.core.toDescriptor
|
||||
import org.jetbrains.kotlin.idea.quickfix.KotlinSingleIntentionActionFactory
|
||||
import org.jetbrains.kotlin.idea.refactoring.getOrCreateKotlinFile
|
||||
import org.jetbrains.kotlin.idea.util.application.isHeadlessEnvironment
|
||||
import org.jetbrains.kotlin.idea.util.application.isUnitTestMode
|
||||
import org.jetbrains.kotlin.idea.util.projectStructure.module
|
||||
import org.jetbrains.kotlin.idea.util.sourceRoot
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.platform.isMultiPlatform
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
import javax.swing.JComponent
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.name
|
||||
import kotlin.io.path.nameWithoutExtension
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class CreateMissedActualsFix(
|
||||
val declaration: KtNamedDeclaration,
|
||||
val notActualizedLeafModules: Collection<Module>
|
||||
) : KotlinQuickFixAction<KtNamedDeclaration>(declaration) {
|
||||
|
||||
override fun startInWriteAction(): Boolean = false
|
||||
|
||||
override fun getFamilyName(): String = KotlinBundle.message("fix.create.expect.actual")
|
||||
|
||||
override fun getText(): String = KotlinBundle.message("fix.create.missing.actual.declarations")
|
||||
|
||||
override fun invoke(project: Project, editor: Editor?, file: KtFile) {
|
||||
val moduleWithExpect = declaration.module ?: return
|
||||
val simpleModuleNames = moduleWithExpect.implementingModules.plus(moduleWithExpect).getSimpleSourceSetNames()
|
||||
|
||||
val testMode = isUnitTestMode() || isHeadlessEnvironment()
|
||||
|
||||
if (!testMode) {
|
||||
CreateMissedActualsDialog(
|
||||
project,
|
||||
editor,
|
||||
declaration,
|
||||
moduleWithExpect,
|
||||
notActualizedLeafModules,
|
||||
simpleModuleNames
|
||||
).show()
|
||||
} else {
|
||||
val defaultPath = declaration.containingKtFile.let { f ->
|
||||
f.virtualFilePath.removePrefix(f.sourceRoot?.path.orEmpty())
|
||||
}
|
||||
generateActualsForSelectedModules(
|
||||
project,
|
||||
editor,
|
||||
declaration,
|
||||
defaultPath,
|
||||
notActualizedLeafModules,
|
||||
simpleModuleNames
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//returns modules with associated names which are cleared from common prefix
|
||||
private fun Collection<Module>.getSimpleSourceSetNames(): Map<Module, String> {
|
||||
if (this.size < 2) return this.associateWith { it.name }
|
||||
val commonPrefix = this.map { it.name }.zipWithNext().map { (a, b) -> a.commonPrefixWith(b) }.minBy { it.length }
|
||||
val prefixToRemove = commonPrefix.substringBeforeLast(".", "").let { if (it.isEmpty()) it else "$it." }
|
||||
return this.associateWith { it.name.removePrefix(prefixToRemove) }
|
||||
}
|
||||
|
||||
companion object : KotlinSingleIntentionActionFactory() {
|
||||
override fun createAction(diagnostic: Diagnostic): IntentionAction? =
|
||||
createIntentionAction(listOf(diagnostic))
|
||||
|
||||
override fun doCreateActionsForAllProblems(sameTypeDiagnostics: Collection<Diagnostic>): List<IntentionAction> =
|
||||
listOfNotNull(createIntentionAction(sameTypeDiagnostics))
|
||||
|
||||
private fun createIntentionAction(sameTypeDiagnostics: Collection<Diagnostic>): IntentionAction? {
|
||||
val first = sameTypeDiagnostics.firstOrNull() ?: return null
|
||||
val declaration = first.psiElement as? KtNamedDeclaration ?: return null
|
||||
val diagnostics = sameTypeDiagnostics
|
||||
.filter { it.psiElement == declaration }
|
||||
.map { DiagnosticFactory.cast(it, Errors.NO_ACTUAL_FOR_EXPECT) }
|
||||
.filter {
|
||||
val compatibility = it.c
|
||||
// For function declarations we allow it, because overloads are possible
|
||||
compatibility.isEmpty() || declaration is KtFunction
|
||||
}
|
||||
val notActualizedLeafModules = diagnostics.mapNotNull {
|
||||
val actualModuleDescriptor = it.b
|
||||
(actualModuleDescriptor.getCapability(ModuleInfo.Capability) as? ModuleSourceInfo)?.module
|
||||
}
|
||||
if (notActualizedLeafModules.isEmpty()) return null
|
||||
return CreateMissedActualsFix(declaration, notActualizedLeafModules)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LOG = Logger.getInstance(CreateMissedActualsDialog::class.java)
|
||||
|
||||
private class CreateMissedActualsDialog(
|
||||
val project: Project,
|
||||
val editor: Editor?,
|
||||
val declaration: KtNamedDeclaration,
|
||||
val moduleWithExpect: Module,
|
||||
val notActualizedLeafModules: Collection<Module>,
|
||||
val simpleModuleNames: Map<Module, String>
|
||||
) : DialogWrapper(project, true) {
|
||||
|
||||
private val notActualizedModules = getNotActualizedModules()
|
||||
|
||||
private var filePathTextField: JBTextField? = null
|
||||
private val filePathProperty = AtomicProperty(
|
||||
declaration.containingKtFile.let { file ->
|
||||
file.virtualFilePath.removePrefix(file.sourceRoot?.path.orEmpty())
|
||||
}
|
||||
)
|
||||
|
||||
private val selectedModules = mutableListOf<Module>()
|
||||
private val selectedModulesListeners = mutableListOf<() -> Unit>()
|
||||
|
||||
init {
|
||||
title = KotlinBundle.message("fix.create.missing.actual.declarations.title")
|
||||
init()
|
||||
}
|
||||
|
||||
override fun doValidate(): ValidationInfo? {
|
||||
val filePath = getNewFilePathForModule(moduleWithExpect).nameWithoutExtension
|
||||
if (filePath.isBlank()) {
|
||||
return ValidationInfo(KotlinBundle.message("text.file.name.cannot.be.empty"), filePathTextField)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
override fun doOKAction() {
|
||||
super.doOKAction()
|
||||
generateActualsForSelectedModules(
|
||||
project,
|
||||
editor,
|
||||
declaration,
|
||||
filePathProperty.get(),
|
||||
selectedModules,
|
||||
simpleModuleNames
|
||||
)
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent = panel {
|
||||
val sortedModules = moduleWithExpect.sortChildrenByHMPP()
|
||||
|
||||
row(KotlinBundle.message("label.text.package.name")) {
|
||||
textField()
|
||||
.text(declaration.containingKtFile.packageFqName.asString())
|
||||
.align(AlignX.FILL)
|
||||
.enabled(false)
|
||||
}
|
||||
|
||||
row(KotlinBundle.message("label.text.file.name")) {
|
||||
textField()
|
||||
.align(AlignX.FILL)
|
||||
.bindText(filePathProperty)
|
||||
.also { filePathTextField = it.component }
|
||||
}
|
||||
|
||||
row {
|
||||
panel {
|
||||
row { label(KotlinBundle.message("label.text.source.sets")) }
|
||||
separator()
|
||||
sortedModules.forEach { item ->
|
||||
val selectionPredicate = ModuleCheckBoxPredicate(item.module)
|
||||
row {
|
||||
val checkbox = checkBox(" ".repeat(item.level))
|
||||
.onChanged { cb ->
|
||||
if (cb.isSelected) {
|
||||
selectedModules.add(item.module)
|
||||
} else {
|
||||
selectedModules.remove(item.module)
|
||||
}
|
||||
selectedModulesListeners.forEach { listener -> listener() }
|
||||
}
|
||||
icon(AllIcons.Actions.ModuleDirectory)
|
||||
label(simpleModuleNames.getOrDefault(item.module, item.module.name))
|
||||
label("")
|
||||
.bindText(filePathProperty.transform { File.separator + getNewFilePathForModule(item.module) })
|
||||
.enabled(false)
|
||||
.visibleIf(checkbox.selected)
|
||||
}.enabledIf(selectionPredicate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
withMinimumWidth(500)
|
||||
setMinimumWidthForAllRowLabels(90)
|
||||
}
|
||||
|
||||
private data class ModuleWithLevel(val module: Module, val level: Int)
|
||||
|
||||
private fun Module.sortChildrenByHMPP(): List<ModuleWithLevel> {
|
||||
fun Module.getChildren(): List<Module> {
|
||||
val implementing = implementingModules.toSet()
|
||||
return implementing.filter { module ->
|
||||
module.implementedModules.all { it !in implementing }
|
||||
}
|
||||
}
|
||||
|
||||
fun sort(parent: Module, level: Int): List<ModuleWithLevel> {
|
||||
val children = parent.getChildren().sortedBy { it.name }
|
||||
if (children.isEmpty()) return emptyList()
|
||||
return children.flatMap { listOf(ModuleWithLevel(it, level)).plus(sort(it, level + 1)) }
|
||||
}
|
||||
|
||||
//'distinct' filters duplicates in a ruby-like dependencies case
|
||||
return sort(this, 0).distinctBy { it.module.name }
|
||||
}
|
||||
|
||||
private fun getNotActualizedModules(): List<Module> {
|
||||
val allModules = moduleWithExpect.implementingModules
|
||||
|
||||
val allLeafModules = allModules.filter { module ->
|
||||
//`isMultiPlatform` is just for optimization
|
||||
!module.platform.isMultiPlatform() && module.implementingModules.isEmpty()
|
||||
}
|
||||
val actualizedLeafModules = allLeafModules.filter { module ->
|
||||
module !in notActualizedLeafModules
|
||||
}
|
||||
val allActualizedModules = actualizedLeafModules
|
||||
.flatMap { module -> module.implementedModules }
|
||||
.plus(actualizedLeafModules)
|
||||
|
||||
return allModules.filter { module -> module !in allActualizedModules }
|
||||
}
|
||||
|
||||
private fun getNewFilePathForModule(module: Module): Path {
|
||||
val simpleName = simpleModuleNames[module].orEmpty()
|
||||
val suffixToRemove = listOf("Main", "Test").firstOrNull { simpleName.endsWith(it) }.orEmpty()
|
||||
val modulePlatformName = simpleName.removeSuffix(suffixToRemove).takeIf { it.isNotBlank() }
|
||||
return Path(
|
||||
filePathProperty.get().removePrefix(File.separator).removeSuffix(".kt") +
|
||||
modulePlatformName?.let { ".$it" }.orEmpty() + ".kt"
|
||||
)
|
||||
}
|
||||
|
||||
//Observer for UI changes
|
||||
private inner class ModuleCheckBoxPredicate(val module: Module) : ComponentPredicate() {
|
||||
override fun addListener(listener: (Boolean) -> Unit) {
|
||||
val onChange: () -> Unit = { listener(invoke()) }
|
||||
selectedModulesListeners.add(onChange)
|
||||
}
|
||||
|
||||
override fun invoke(): Boolean {
|
||||
val shouldBeActualized = module in notActualizedModules
|
||||
val anyParentOrChildIsSelected = module.implementedModules.plus(module.implementingModules).any { it in selectedModules }
|
||||
return shouldBeActualized && !anyParentOrChildIsSelected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNewFilePathForModule(commonFilePath: String, module: Module, simpleModuleNames: Map<Module, String>): Path {
|
||||
val simpleName = simpleModuleNames[module].orEmpty()
|
||||
val suffixToRemove = listOf("Main", "Test").firstOrNull { simpleName.endsWith(it) }.orEmpty()
|
||||
val modulePlatformName = simpleName.removeSuffix(suffixToRemove).takeIf { it.isNotBlank() }
|
||||
return Path(
|
||||
commonFilePath.removePrefix(File.separator).removeSuffix(".kt") +
|
||||
modulePlatformName?.let { ".$it" }.orEmpty() + ".kt"
|
||||
)
|
||||
}
|
||||
|
||||
private fun getOrCreateKotlinFileForSpecificPackage(
|
||||
fileName: String,
|
||||
targetDir: PsiDirectory,
|
||||
declarationPackage: FqName
|
||||
): KtFile {
|
||||
var file = getOrCreateKotlinFile(fileName, targetDir, declarationPackage.asString())
|
||||
var fileIndex = 0
|
||||
|
||||
//if file exists and has other declarations with a different package, we have to generate a new file
|
||||
fun KtFile.needOtherFile() = declarations.isNotEmpty() && packageDirective?.fqName != declarationPackage
|
||||
|
||||
while (file.needOtherFile()) {
|
||||
fileIndex++
|
||||
val newName = with(fileName) {
|
||||
val (name, suffixAndExtension) = split(".", limit = 2)
|
||||
"$name$fileIndex.$suffixAndExtension"
|
||||
}
|
||||
file = getOrCreateKotlinFile(newName, targetDir, declarationPackage.asString())
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
private fun Module.selectExistingSourceRoot(
|
||||
pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder
|
||||
): VirtualFile? {
|
||||
val roots = findExistingNonGeneratedKotlinSourceRootFiles(pureKotlinSourceFoldersHolder).sortedBy { it.path }
|
||||
|
||||
if (roots.size < 2) return roots.firstOrNull()
|
||||
|
||||
val root = roots.firstOrNull {
|
||||
it.name.equals("kotlin", true)
|
||||
} ?: roots.first()
|
||||
LOG.warn("${this.name} contains more then one source roots. ${root.name} was selected.")
|
||||
return root
|
||||
}
|
||||
|
||||
private fun generateActualsForSelectedModules(
|
||||
project: Project,
|
||||
editor: Editor?,
|
||||
declaration: KtNamedDeclaration,
|
||||
commonFilePath: String,
|
||||
selectedModules: Collection<Module>,
|
||||
simpleModuleNames: Map<Module, String>
|
||||
) {
|
||||
val sourceFolderManager = SourceFolderManager.getInstance(project)
|
||||
val pureKotlinSourceFoldersHolder = PureKotlinSourceFoldersHolder()
|
||||
val moduleSourceRoots = selectedModules.associateWith { module ->
|
||||
module.selectExistingSourceRoot(pureKotlinSourceFoldersHolder) ?: run {
|
||||
val newRoot = module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder)
|
||||
.also { if (it.size > 1) LOG.warn("In ${module.name} were configured more then one source roots. ${it.first().name} was selected.") }
|
||||
.firstOrNull()
|
||||
if (newRoot == null) {
|
||||
LOG.warn("Can't configure new source root for ${module.name}")
|
||||
return@associateWith null
|
||||
}
|
||||
|
||||
val sourceRootType = module.kotlinSourceRootType
|
||||
if (sourceRootType == null) {
|
||||
LOG.warn("Can't configure new source root for ${module.name} because `kotlinSourceRootType == null`")
|
||||
return@associateWith null
|
||||
}
|
||||
|
||||
sourceFolderManager.addSourceFolder(module, newRoot.url, sourceRootType)
|
||||
newRoot
|
||||
}
|
||||
}
|
||||
sourceFolderManager.rescanAndUpdateSourceFolders()
|
||||
|
||||
runWriteAction {
|
||||
selectedModules.forEach { module ->
|
||||
val root = moduleSourceRoots[module] ?: return@forEach
|
||||
val moduleFile = getNewFilePathForModule(commonFilePath, module, simpleModuleNames)
|
||||
val dir: PsiDirectory = declaration.manager.findDirectory(
|
||||
root.findOrCreateDirectory(moduleFile.parent?.pathString.orEmpty())
|
||||
) ?: return@forEach
|
||||
|
||||
val file = getOrCreateKotlinFileForSpecificPackage(moduleFile.name, dir, declaration.containingKtFile.packageFqName)
|
||||
|
||||
if (declaration is KtCallableDeclaration) {
|
||||
generateExpectOrActualInFile(
|
||||
project,
|
||||
editor,
|
||||
declaration.containingKtFile,
|
||||
file,
|
||||
null,
|
||||
declaration,
|
||||
module
|
||||
) block@{ project, checker, element ->
|
||||
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
|
||||
val descriptor = element.toDescriptor() as? CallableMemberDescriptor
|
||||
|
||||
descriptor?.let { generateCallable(project, false, element, descriptor, checker = checker) }
|
||||
}
|
||||
} else if (declaration is KtClassOrObject) {
|
||||
generateExpectOrActualInFile(
|
||||
project,
|
||||
editor,
|
||||
declaration.containingKtFile,
|
||||
file,
|
||||
null,
|
||||
declaration,
|
||||
module
|
||||
) block@{ project, checker, element ->
|
||||
checker.findAndApplyExistingClasses(element.collectDeclarationsForAddActualModifier().toList())
|
||||
if (!checker.isCorrectAndHaveAccessibleModifiers(element, true)) return@block null
|
||||
|
||||
generateClassOrObject(project, false, element, checker = checker)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+89
@@ -2,12 +2,16 @@
|
||||
|
||||
package org.jetbrains.kotlin.idea.quickfix.expectactual
|
||||
|
||||
import com.intellij.ide.util.EditorHelper
|
||||
import com.intellij.openapi.application.runWriteAction
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.project.DumbService
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.ThrowableComputable
|
||||
import com.intellij.psi.JavaDirectoryService
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.util.SlowOperations
|
||||
import org.jetbrains.kotlin.descriptors.*
|
||||
import org.jetbrains.kotlin.descriptors.annotations.AnnotationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.annotations.Annotations
|
||||
@@ -16,9 +20,11 @@ import org.jetbrains.kotlin.idea.base.psi.isInlineOrValue
|
||||
import org.jetbrains.kotlin.idea.base.psi.mustHaveValOrVar
|
||||
import org.jetbrains.kotlin.idea.base.resources.KotlinBundle
|
||||
import org.jetbrains.kotlin.idea.base.util.names.FqNames
|
||||
import org.jetbrains.kotlin.idea.base.util.reformatted
|
||||
import org.jetbrains.kotlin.idea.caches.resolve.analyzeWithContent
|
||||
import org.jetbrains.kotlin.idea.codeInsight.shorten.addToBeShortenedDescendantsToWaitingSet
|
||||
import org.jetbrains.kotlin.idea.codeinsight.utils.findExistingEditor
|
||||
import org.jetbrains.kotlin.idea.core.ShortenReferences
|
||||
import org.jetbrains.kotlin.idea.core.findOrCreateDirectoryForPackage
|
||||
import org.jetbrains.kotlin.idea.core.getFqNameWithImplicitPrefix
|
||||
import org.jetbrains.kotlin.idea.core.overrideImplement.BodyType.EmptyOrTemplate
|
||||
@@ -34,13 +40,16 @@ import org.jetbrains.kotlin.idea.refactoring.fqName.fqName
|
||||
import org.jetbrains.kotlin.idea.refactoring.introduce.showErrorHint
|
||||
import org.jetbrains.kotlin.idea.refactoring.isInterfaceClass
|
||||
import org.jetbrains.kotlin.idea.util.IdeDescriptorRenderers
|
||||
import org.jetbrains.kotlin.idea.util.application.executeWriteCommand
|
||||
import org.jetbrains.kotlin.idea.util.isEffectivelyActual
|
||||
import org.jetbrains.kotlin.lexer.KtModifierKeywordToken
|
||||
import org.jetbrains.kotlin.lexer.KtTokens
|
||||
import org.jetbrains.kotlin.name.FqName
|
||||
import org.jetbrains.kotlin.psi.*
|
||||
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||
import org.jetbrains.kotlin.psi.psiUtil.createSmartPointer
|
||||
import org.jetbrains.kotlin.psi.psiUtil.hasActualModifier
|
||||
import org.jetbrains.kotlin.psi.psiUtil.startOffset
|
||||
import org.jetbrains.kotlin.resolve.BindingContext
|
||||
import org.jetbrains.kotlin.resolve.checkers.OptInNames
|
||||
import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameOrNull
|
||||
@@ -49,6 +58,80 @@ import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
|
||||
import org.jetbrains.kotlin.types.KotlinType
|
||||
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
|
||||
|
||||
fun <D : KtNamedDeclaration> generateExpectOrActualInFile(
|
||||
project: Project,
|
||||
editor: Editor?,
|
||||
originalFile: KtFile,
|
||||
targetFile: KtFile,
|
||||
targetClass: KtClassOrObject?,
|
||||
element: D,
|
||||
module: Module,
|
||||
generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D?
|
||||
) {
|
||||
val factory = KtPsiFactory(project)
|
||||
val targetClassPointer = targetClass?.createSmartPointer()
|
||||
val targetFilePointer = targetFile.createSmartPointer()
|
||||
DumbService.getInstance(project).runWhenSmart(fun() {
|
||||
val generated = try {
|
||||
SlowOperations.allowSlowOperations(ThrowableComputable {
|
||||
factory.generateIt(project, TypeAccessibilityChecker.create(project, module), element)
|
||||
})
|
||||
} catch (e: KotlinTypeInaccessibleException) {
|
||||
if (editor != null) {
|
||||
showErrorHint(
|
||||
project, editor,
|
||||
escapeXml(KotlinBundle.message("fix.create.declaration.error", element.getTypeDescription(), e.message)),
|
||||
KotlinBundle.message("fix.create.declaration.error.inaccessible.type")
|
||||
)
|
||||
}
|
||||
null
|
||||
} ?: return
|
||||
|
||||
val shortened = project.executeWriteCommand(KotlinBundle.message("fix.create.expect.actual"), null) {
|
||||
val resultTargetFile = targetFilePointer.element ?: return@executeWriteCommand null
|
||||
if (resultTargetFile.packageDirective?.fqName != originalFile.packageDirective?.fqName &&
|
||||
resultTargetFile.declarations.isEmpty()
|
||||
) {
|
||||
val packageDirective = originalFile.packageDirective
|
||||
if (packageDirective != null) {
|
||||
val oldPackageDirective = resultTargetFile.packageDirective
|
||||
val newPackageDirective = packageDirective.copy() as KtPackageDirective
|
||||
if (oldPackageDirective != null) {
|
||||
if (oldPackageDirective.text.isEmpty()) {
|
||||
resultTargetFile.addAfter(factory.createNewLine(2), resultTargetFile.importList ?: oldPackageDirective) //
|
||||
}
|
||||
oldPackageDirective.replace(newPackageDirective)
|
||||
} else {
|
||||
resultTargetFile.add(newPackageDirective)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val resultTargetClass = targetClassPointer?.element
|
||||
val generatedDeclaration = when {
|
||||
resultTargetClass != null -> {
|
||||
if (generated is KtPrimaryConstructor && resultTargetClass is KtClass)
|
||||
resultTargetClass.createPrimaryConstructorIfAbsent().replace(generated)
|
||||
else
|
||||
resultTargetClass.addDeclaration(generated as KtNamedDeclaration)
|
||||
}
|
||||
|
||||
else -> {
|
||||
resultTargetFile.add(factory.createNewLine(1))
|
||||
resultTargetFile.add(generated) as KtElement
|
||||
}
|
||||
}
|
||||
|
||||
ShortenReferences.DEFAULT.process(generatedDeclaration.reformatted() as KtElement)
|
||||
} ?: return
|
||||
|
||||
EditorHelper.openInEditor(shortened)?.caretModel?.moveToOffset(
|
||||
(shortened as? KtNamedDeclaration)?.nameIdentifier?.startOffset ?: shortened.startOffset,
|
||||
true,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fun createFileForDeclaration(module: Module, declaration: KtNamedDeclaration): KtFile? {
|
||||
val fileName = declaration.name ?: return null
|
||||
|
||||
@@ -90,6 +173,7 @@ fun KtPsiFactory.createClassHeaderCopyByText(originalClass: KtClassOrObject): Kt
|
||||
} else {
|
||||
createObject(text)
|
||||
}
|
||||
|
||||
is KtEnumEntry -> createEnumEntry(text)
|
||||
else -> createClass(text)
|
||||
}.apply {
|
||||
@@ -106,6 +190,7 @@ fun KtNamedDeclaration?.getTypeDescription(): String = when (this) {
|
||||
isAnnotation() -> KotlinBundle.message("text.annotation.class")
|
||||
else -> KotlinBundle.message("text.class")
|
||||
}
|
||||
|
||||
is KtProperty, is KtParameter -> KotlinBundle.message("text.property")
|
||||
is KtFunction -> KotlinBundle.message("text.function")
|
||||
else -> KotlinBundle.message("text.declaration")
|
||||
@@ -158,6 +243,7 @@ internal fun KtPsiFactory.generateClassOrObject(
|
||||
originalDeclaration,
|
||||
checker
|
||||
)
|
||||
|
||||
is KtFunction, is KtProperty -> checker.runInContext(existingFqNamesWithSuperTypes) {
|
||||
generateCallable(
|
||||
project,
|
||||
@@ -168,6 +254,7 @@ internal fun KtPsiFactory.generateClassOrObject(
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
else -> continue@declLoop
|
||||
}
|
||||
generatedClass.addDeclaration(generatedDeclaration)
|
||||
@@ -322,6 +409,7 @@ private fun repairAnnotationEntries(
|
||||
val typeReference = target.typeReference ?: return
|
||||
repairAnnotationEntries(typeReference, descriptor.type, checker)
|
||||
}
|
||||
|
||||
is TypeParameterDescriptor -> {
|
||||
if (target !is KtTypeParameter) return
|
||||
val extendsBound = target.extendsBound ?: return
|
||||
@@ -329,6 +417,7 @@ private fun repairAnnotationEntries(
|
||||
repairAnnotationEntries(extendsBound, upperBound, checker)
|
||||
}
|
||||
}
|
||||
|
||||
is CallableDescriptor -> {
|
||||
val extension = descriptor.extensionReceiverParameter
|
||||
val receiver = target.safeAs<KtCallableDeclaration>()?.receiverTypeReference
|
||||
|
||||
Reference in New Issue
Block a user