diff --git a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt index 58145ea56094..3571974c0474 100644 --- a/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt +++ b/python/python-sdk-configurator/backend/src/impl/ModulesSdkConfigurator.kt @@ -16,9 +16,8 @@ import com.intellij.python.pyproject.model.api.SuggestedSdk import com.intellij.python.pyproject.model.api.suggestSdk import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.create import com.intellij.python.sdkConfigurator.backend.impl.ModulesSdkConfigurator.Companion.popModulesSDKConfigurator -import com.intellij.python.sdkConfigurator.common.impl.CreateSdkDTO +import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO import com.intellij.python.sdkConfigurator.common.impl.ModuleName -import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO import com.jetbrains.python.Result import com.jetbrains.python.sdk.configuration.CreateSdkInfo import com.jetbrains.python.sdk.configuration.CreateSdkInfoWithTool @@ -43,10 +42,43 @@ import kotlinx.coroutines.withContext */ internal class ModulesSdkConfigurator private constructor( private val project: Project, - private val modules: Map>, - val modulesDTO: ModulesDTO = ModulesDTO(modules.map { Pair(it.key, it.value.second) }.toMap()), + private val modules: Map, +) { + + val modulesDTO: List + + init { + val children = HashMap>() + + // Find parents + for ((moduleName, createInfo) in modules) { + children.putIfAbsent(moduleName, HashSet()) + when (createInfo) { + is ModuleCreateInfo.CreateSdkInfoWrapper -> Unit + is ModuleCreateInfo.SameAs -> { + children.getOrPut(createInfo.parentModuleName) { HashSet() }.add(moduleName) + } + } + } + //Map modules + modulesDTO = modules.mapNotNull { (moduleName, createInfo) -> + when (createInfo) { + is ModuleCreateInfo.CreateSdkInfoWrapper -> { + val version = when (val r = createInfo.createSdkInfo) { + is CreateSdkInfo.ExistingEnv -> r.version + is CreateSdkInfo.WillCreateEnv -> null + } + ModuleDTO(moduleName, createInfo.toolId.id, version, children[moduleName]!!.toList().sorted()) + } + is ModuleCreateInfo.SameAs -> null + } + } + .sortedBy { it.name } + .toList() + + + } - ) { companion object { /** * Create instance and save in [project] @@ -65,7 +97,7 @@ internal class ModulesSdkConfigurator private constructor( return instance } - private suspend fun getModulesWithoutSDKCreateInfo(project: Project): Map> = withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) { + private suspend fun getModulesWithoutSDKCreateInfo(project: Project): Map = withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) { val tools = PyProjectSdkConfigurationExtension.createMap() val limit = Semaphore(permits = Registry.intValue("intellij.python.sdkConfigurator.backend.sdk.parallel")) val now = System.currentTimeMillis() @@ -80,21 +112,19 @@ internal class ModulesSdkConfigurator private constructor( val result = resultDef.awaitAll().filterNotNull() logger.debug { "SDKs calculated in ${System.currentTimeMillis() - now}ms" } result.associate { (module, createInfoAndDTO) -> - val (createInfo, dto) = createInfoAndDTO - //module.putUserData(modulesKey, createInfo) - Pair(module.name, Pair(createInfo, dto)) + Pair(module.name, createInfoAndDTO) } } private val logger = fileLogger() private sealed interface ModuleCreateInfo { - data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo) : ModuleCreateInfo + data class CreateSdkInfoWrapper(val createSdkInfo: CreateSdkInfo, val toolId: ToolId) : ModuleCreateInfo data class SameAs(val parentModuleName: ModuleName) : ModuleCreateInfo } - private suspend fun getModuleInfo(module: Module, configuratorsByTool: Map): Pair? = // Save on module level + private suspend fun getModuleInfo(module: Module, configuratorsByTool: Map): ModuleCreateInfo? = // Save on module level when (val r = module.suggestSdk()) { is SuggestedSdk.PyProjectIndependent -> { val tools = r.preferTools.map { configuratorsByTool[it]!! } @@ -105,23 +135,15 @@ internal class ModulesSdkConfigurator private constructor( } } is SuggestedSdk.SameAs -> { - val createInfo = ModuleCreateInfo.SameAs(r.parentModule.name) - Pair(createInfo, createInfo.asDTO()) + ModuleCreateInfo.SameAs(r.parentModule.name) } null -> null } // No tools or not pyproject.toml at all? Use EP as a fallback ?: PyProjectSdkConfigurationExtension.findAllSortedForModule(module).firstOrNull()?.let { CreateSdkInfoWithTool(it.createSdkInfo, it.toolId).asDTO() } - private fun CreateSdkInfoWithTool.asDTO(): Pair { - val version = when (val r = createSdkInfo) { - is CreateSdkInfo.ExistingEnv -> r.version - is CreateSdkInfo.WillCreateEnv -> null - } - return Pair(ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo), CreateSdkDTO.ConfigurableModule(version, toolId.id)) - } + private fun CreateSdkInfoWithTool.asDTO(): ModuleCreateInfo = ModuleCreateInfo.CreateSdkInfoWrapper(createSdkInfo, toolId) - private fun ModuleCreateInfo.SameAs.asDTO(): CreateSdkDTO.SameAs = CreateSdkDTO.SameAs(parentModuleName) /** * Key used to store instance in project by [create] to be used by [popModulesSDKConfigurator] @@ -140,7 +162,7 @@ internal class ModulesSdkConfigurator private constructor( val modulesWithSameSdk = mutableMapOf() for (module in modulesOnly.map { modulesMap[it] ?: error("No module $it, caller broke the contract") }) { // TODO: Run in parallel withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.configuring.module", module.name)) { - val createInfo = (modules[module.name] ?: error("No create info for module $module, caller broke the contract")).first + val createInfo = (modules[module.name] ?: error("No create info for module $module, caller broke the contract")) when (createInfo) { is ModuleCreateInfo.CreateSdkInfoWrapper -> { when (val r = createInfo.createSdkInfo.sdkCreator(false)) { diff --git a/python/python-sdk-configurator/backend/src/impl/backendLib.kt b/python/python-sdk-configurator/backend/src/impl/backendLib.kt index d9a50e38f942..651ee4ca3020 100644 --- a/python/python-sdk-configurator/backend/src/impl/backendLib.kt +++ b/python/python-sdk-configurator/backend/src/impl/backendLib.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.components.Service.Level import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.platform.rpc.topics.sendToClient +import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO import com.intellij.python.sdkConfigurator.common.impl.SHOW_SDK_CONFIG_UI_TOPIC import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -32,9 +33,9 @@ internal suspend fun configureSdkAskingUser(project: Project) { askUserMutex.withLock { val moduleToSuggestedSdk = ModulesSdkConfigurator.create(project) val modulesDTO = moduleToSuggestedSdk.modulesDTO - if (modulesDTO.modules.isNotEmpty()) { + if (modulesDTO.isNotEmpty()) { // No need to send empty list - SHOW_SDK_CONFIG_UI_TOPIC.sendToClient(project, modulesDTO) + SHOW_SDK_CONFIG_UI_TOPIC.sendToClient(project, ModulesDTO(modulesDTO)) } } } diff --git a/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt b/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt index 6a9e33bf6c81..944204deb125 100644 --- a/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt +++ b/python/python-sdk-configurator/common/src/impl/moduleDTOs.kt @@ -6,25 +6,18 @@ import kotlinx.serialization.Serializable typealias ModuleName = @NlsSafe String typealias ToolIdDTO = @NlsSafe String // value classes aren't serializable by default -// Serializable DTO that reflects regular object is, unfortunately, recommended approach - -@Serializable -sealed interface CreateSdkDTO { - /** - * This module is part of workspace and parent is [parentModuleName] - */ - @Serializable - data class SameAs(val parentModuleName: ModuleName) : CreateSdkDTO - - /** - * [createdByTool] can create an SDK for this module (if [existingVersion] is not null, venv is already exists on disk) - */ - @Serializable - data class ConfigurableModule(val existingVersion: @NlsSafe String?, val createdByTool: ToolIdDTO) : CreateSdkDTO -} /** * Module and how do we create it */ @Serializable -data class ModulesDTO(val modules: Map) \ No newline at end of file +data class ModuleDTO( + val name: ModuleName, + val createdByTool: ToolIdDTO, + val existingPyVersion: @NlsSafe String?, + val childModules: List, +) + +@Serializable +@JvmInline +value class ModulesDTO(val modules: List) \ No newline at end of file diff --git a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt index c4f322475700..6b05c5628d83 100644 --- a/python/python-sdk-configurator/frontend/src/components/ModuleList.kt +++ b/python/python-sdk-configurator/frontend/src/components/ModuleList.kt @@ -9,13 +9,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.intellij.openapi.util.NlsSafe -import com.intellij.python.sdkConfigurator.common.impl.CreateSdkDTO +import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO import com.intellij.python.sdkConfigurator.common.impl.ModuleName import com.intellij.python.sdkConfigurator.common.impl.ToolIdDTO import com.intellij.python.sdkConfigurator.frontend.PySdkConfiguratorFrontendBundle import kotlinx.collections.immutable.ImmutableMap -import org.jetbrains.annotations.Nls +import kotlinx.collections.immutable.PersistentList import org.jetbrains.jewel.ui.Orientation import org.jetbrains.jewel.ui.component.* import org.jetbrains.jewel.ui.icon.IconKey @@ -23,15 +22,14 @@ import org.jetbrains.jewel.ui.icon.IconKey @Composable internal fun ModuleList( - moduleItems: ImmutableMap, + moduleItems: PersistentList, icons: ImmutableMap, checked: SnapshotStateSet, onCheck: (ModuleName) -> Unit, - topLabel: @Nls String, - projectStructureLabel: @Nls String, - environmentLabel: @Nls String, + topLabel: String, + projectStructureLabel: String, + environmentLabel: String, ) { - val moduleItems = remember { moduleItems.entries.sortedBy { it.key } } Box { VerticallyScrollableContainer { val colPadding = 2.dp @@ -46,9 +44,20 @@ internal fun ModuleList( Text(environmentLabel, modifier = modifier) } Divider(Orientation.Horizontal) - for ((moduleName, moduleInfo) in moduleItems) { + for (module in moduleItems) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = horizontalArrangement) { - Module(moduleName, moduleName in checked, onCheck, colPadding, moduleInfo, icons) + Module(module.name in checked, onCheck, colPadding, module, icons) + } // Module children + for (childModule in module.childModules) { + CheckboxRow( + childModule, + childModule in checked, + onCheckedChange = {}, + softWrap = false, + maxLines = 1, + enabled = false, + modifier = Modifier.padding(start = colPadding + 26.dp) // Checkbox width + ) } } } @@ -58,45 +67,33 @@ internal fun ModuleList( @Composable private fun RowScope.Module( - moduleName: @NlsSafe String, checked: Boolean, onCheck: (ModuleName) -> Unit, columnPadding: Dp, - moduleInfo: CreateSdkDTO, + moduleInfo: ModuleDTO, // For parent only icons: ImmutableMap, ) { val colModifier = Modifier.weight(1f) val newText = remember { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.new") } - val parent = when (moduleInfo) { - is CreateSdkDTO.ConfigurableModule -> null - is CreateSdkDTO.SameAs -> moduleInfo.parentModuleName - } - val elementToChange = parent ?: moduleName // TODO: move logic out of UI + val moduleName = moduleInfo.name CheckboxRow( moduleName, softWrap = false, maxLines = 1, checked = checked, onCheckedChange = { - onCheck(elementToChange) + onCheck(moduleName) }, modifier = colModifier ) + Row(colModifier.padding(start = columnPadding)) { - val text = when (moduleInfo) { - is CreateSdkDTO.ConfigurableModule -> { - val text = moduleInfo.existingVersion?.let { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) } - ?: newText - val icon = icons[moduleInfo.createdByTool] - if (icon != null) { - Icon(icon, text) - } - text - } - is CreateSdkDTO.SameAs -> { - PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.member", moduleInfo.parentModuleName) - } + val text = moduleInfo.existingPyVersion?.let { PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.workspace.existing", it) } + ?: newText + val icon = icons[moduleInfo.createdByTool] + if (icon != null) { + Icon(icon, text) } - Text(text, Modifier.clickable(onClick = { onCheck(elementToChange) }), maxLines = 1, softWrap = false) + Text(text, Modifier.clickable(onClick = { onCheck(moduleName) }), maxLines = 1, softWrap = false) } } diff --git a/python/python-sdk-configurator/frontend/src/frontendLib.kt b/python/python-sdk-configurator/frontend/src/frontendLib.kt index 1e220f845f77..5ac24f91e4fa 100644 --- a/python/python-sdk-configurator/frontend/src/frontendLib.kt +++ b/python/python-sdk-configurator/frontend/src/frontendLib.kt @@ -4,12 +4,11 @@ import androidx.compose.runtime.mutableStateSetOf import androidx.compose.runtime.snapshots.SnapshotStateSet import com.intellij.python.common.tools.ToolId import com.intellij.python.common.tools.getIcon -import com.intellij.python.sdkConfigurator.common.impl.CreateSdkDTO +import com.intellij.python.sdkConfigurator.common.impl.ModuleDTO import com.intellij.python.sdkConfigurator.common.impl.ModuleName import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO import com.intellij.python.sdkConfigurator.common.impl.ToolIdDTO -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.persistentMapOf +import kotlinx.collections.immutable.* import org.jetbrains.jewel.bridge.icon.fromPlatformIcon import org.jetbrains.jewel.ui.icon.IconKey import org.jetbrains.jewel.ui.icon.IntelliJIconKey @@ -19,35 +18,29 @@ import org.jetbrains.jewel.ui.icon.IntelliJIconKey * Result can be taken from [checked] */ internal class ModulesViewModel(modulesDTO: ModulesDTO) { - val icons: PersistentMap = persistentMapOf(*modulesDTO.modules.values.mapNotNull { - when (it) { - is CreateSdkDTO.ConfigurableModule -> it.createdByTool - is CreateSdkDTO.SameAs -> null - } - }.mapNotNull { toolId -> - val icon = getIcon(ToolId(toolId))?.let { IntelliJIconKey.fromPlatformIcon(it.first, it.second) } ?: return@mapNotNull null - Pair(toolId, icon) - }.toTypedArray()) - - val checkBoxItems: PersistentMap = persistentMapOf(*modulesDTO.modules - .map { (moduleName, createSdkInfo) -> - Pair(moduleName, createSdkInfo) + val icons: PersistentMap = persistentMapOf(*modulesDTO.modules + .mapNotNull { module -> + val toolId = module.createdByTool + val icon = getIcon(ToolId(toolId))?.let { IntelliJIconKey.fromPlatformIcon(it.first, it.second) } ?: return@mapNotNull null + Pair(toolId, icon) }.toTypedArray()) + + val checkBoxItems: PersistentList = modulesDTO.modules.toPersistentList() val checked: SnapshotStateSet = mutableStateSetOf() - private val children = mutableMapOf>() - init { - for ((child, createSdkDTO) in modulesDTO.modules) { - val parent = when (createSdkDTO) { - is CreateSdkDTO.ConfigurableModule -> continue - is CreateSdkDTO.SameAs -> createSdkDTO.parentModuleName - } - children.getOrPut(parent) { HashSet() }.add(child) - } - } + // parent -> children + private val children: ImmutableMap> = checkBoxItems.associate { + Pair(it.name, it.childModules.toPersistentSet()) + }.toPersistentMap() - fun clicked(what: ModuleName) { - val toChange = setOf(what) + children.getOrDefault(what, emptySet()) + // child -> parent + private val parents: ImmutableMap = children.flatMap { (parent, children) -> + children.map { Pair(it, parent) } + }.toMap().toImmutableMap() + + fun clicked(module: ModuleName) { + val what = parents[module] ?: module // Get parent if child module + val toChange = setOf(what) + children.getOrDefault(what, emptySet()) // Get children if parent val checkBoxSet = what !in checked if (checkBoxSet) { checked.addAll(toChange) diff --git a/python/python-sdk-configurator/frontend/src/swingBridge.kt b/python/python-sdk-configurator/frontend/src/swingBridge.kt index 607f94ff044d..2a646cfade57 100644 --- a/python/python-sdk-configurator/frontend/src/swingBridge.kt +++ b/python/python-sdk-configurator/frontend/src/swingBridge.kt @@ -41,7 +41,7 @@ private class MyDialog(project: Project, private val viewModel: ModulesViewModel ModuleList(viewModel.checkBoxItems, viewModel.icons, viewModel.checked, - viewModel::clicked, + viewModel::clicked, topLabel = message("python.sdk.configurator.frontend.choose.modules.text"), projectStructureLabel = message("python.sdk.configurator.frontend.choose.modules.project.structure"), environmentLabel = message("python.sdk.configurator.frontend.choose.modules.environment"))