From e7f3fd5d250bce404e925b22a61a9a74ac8be6f0 Mon Sep 17 00:00:00 2001 From: Konstantin Tskhovrebov Date: Thu, 6 Apr 2023 12:25:03 +0200 Subject: [PATCH] KTIJ-16167: Implemented new CreateMissedActualsFix with UI dialog for selecting source sets GitOrigin-RevId: 63c909bbacf2383228e7d01b3cbe99ebe4d44c42 --- .../messages/KotlinBundle.properties | 3 + .../kotlin/idea/core/packageUtils.kt | 6 +- .../OptionalExpectationInspection.kt | 62 ++- .../kotlin/idea/quickfix/QuickFixRegistrar.kt | 4 +- .../AbstractCreateDeclarationFix.kt | 119 ----- .../quickfix/expectactual/CreateActualFix.kt | 100 ----- .../expectactual/CreateExpectedFix.kt | 35 +- .../expectactual/CreateMissedActualsFix.kt | 413 ++++++++++++++++++ .../expectactual/ExpectActualUtils.kt | 89 ++++ 9 files changed, 559 insertions(+), 272 deletions(-) delete mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AbstractCreateDeclarationFix.kt delete mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt create mode 100644 plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateMissedActualsFix.kt diff --git a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties index 14a391d071e8..30fb59fad542 100644 --- a/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties +++ b/plugins/kotlin/base/resources/resources-en/messages/KotlinBundle.properties @@ -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: diff --git a/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/core/packageUtils.kt b/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/core/packageUtils.kt index c8c1756febc8..c0712d7a7c08 100644 --- a/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/core/packageUtils.kt +++ b/plugins/kotlin/core/src/org/jetbrains/kotlin/idea/core/packageUtils.kt @@ -111,7 +111,7 @@ private fun findLongestExistingPackage( private val kotlinSourceRootTypes: Set> = setOf(SourceKotlinRootType, TestSourceKotlinRootType) + JavaModuleSourceRootTypes.SOURCES -private class PureKotlinSourceFoldersHolder { +class PureKotlinSourceFoldersHolder { private val moduleMap = mutableMapOf?>() /*** @@ -143,7 +143,7 @@ private fun Module.findNonGeneratedKotlinSourceFolders(): Sequence it.jpsElement.getProperties(kotlinSourceRootTypes)?.isForGeneratedSources != true } -private fun Module.findExistingNonGeneratedKotlinSourceRootFiles( +fun Module.findExistingNonGeneratedKotlinSourceRootFiles( pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder ): List = findNonGeneratedKotlinSourceFolders().toExistingFiles(project, pureKotlinSourceFoldersHolder) @@ -157,7 +157,7 @@ private fun Sequence.toExistingFiles( sourceFolder.file?.takeIf { pureKotlinSourceFoldersHolder.hasPurePrefixInVirtualFile(project, it) } }.toList() -private fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List { +fun Module.findOrConfigureKotlinSourceRoots(pureKotlinSourceFoldersHolder: PureKotlinSourceFoldersHolder): List { val nonGeneratedSourceFolders = findNonGeneratedKotlinSourceFolders().toList() nonGeneratedSourceFolders.asSequence().toExistingFiles(project, pureKotlinSourceFoldersHolder).ifNotEmpty { return this } return listOfNotNull(createSourceRootDirectory(nonGeneratedSourceFolders)) diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/OptionalExpectationInspection.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/OptionalExpectationInspection.kt index d1c08a3fa86f..ce3bd5b0c583 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/OptionalExpectationInspection.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/inspections/OptionalExpectationInspection.kt @@ -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)) + ) }) } } diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt index 88fdb9d8bfbf..e4250627793d 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/QuickFixRegistrar.kt @@ -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)) diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AbstractCreateDeclarationFix.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AbstractCreateDeclarationFix.kt deleted file mode 100644 index ecef01586535..000000000000 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/AbstractCreateDeclarationFix.kt +++ /dev/null @@ -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( - declaration: D, - protected val module: Module, - protected val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D? -) : KotlinQuickFixAction(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, - ) - }) - } -} \ No newline at end of file diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt deleted file mode 100644 index 309bf917b7b3..000000000000 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateActualFix.kt +++ /dev/null @@ -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( - declaration: D, - actualModule: Module, - private val actualPlatform: TargetPlatform, - generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D? -) : AbstractCreateDeclarationFix(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(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(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) } -}) - diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt index a41ff855dc57..7d6cd5113d02 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateExpectedFix.kt @@ -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( declaration: D, targetExpectedClass: KtClassOrObject?, - commonModule: Module, - generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D? -) : AbstractCreateDeclarationFix(declaration, commonModule, generateIt) { + val module: Module, + val generateIt: KtPsiFactory.(Project, TypeAccessibilityChecker, D) -> D? +) : KotlinQuickFixAction(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( 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( 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 { val d = DiagnosticFactory.cast(diagnostic, Errors.ACTUAL_WITHOUT_EXPECT) @@ -84,13 +99,11 @@ sealed class CreateExpectedFix( ?: 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() } diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateMissedActualsFix.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateMissedActualsFix.kt new file mode 100644 index 000000000000..6572749274f4 --- /dev/null +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/CreateMissedActualsFix.kt @@ -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 +) : KotlinQuickFixAction(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.getSimpleSourceSetNames(): Map { + 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): List = + listOfNotNull(createIntentionAction(sameTypeDiagnostics)) + + private fun createIntentionAction(sameTypeDiagnostics: Collection): 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, + val simpleModuleNames: Map +) : 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() + 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 { + fun Module.getChildren(): List { + val implementing = implementingModules.toSet() + return implementing.filter { module -> + module.implementedModules.all { it !in implementing } + } + } + + fun sort(parent: Module, level: Int): List { + 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 { + 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): 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, + simpleModuleNames: Map +) { + 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) + } + } + } + } +} \ No newline at end of file diff --git a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt index bcf513c8dcc5..9009de4f282c 100644 --- a/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt +++ b/plugins/kotlin/idea/src/org/jetbrains/kotlin/idea/quickfix/expectactual/ExpectActualUtils.kt @@ -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 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()?.receiverTypeReference