[python] PY-79486: (WIP): show workspace members one level deeper.

Simplify frontend logic, show workspace members as children of their parent

GitOrigin-RevId: 551adc0869323cb1e8318334aa830dfd37ec2337
This commit is contained in:
Ilya.Kazakevich
2025-10-31 08:11:12 +00:00
committed by intellij-monorepo-bot
parent 3d1bb84425
commit ef0216df92
6 changed files with 108 additions and 102 deletions
@@ -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<ModuleName, Pair<ModuleCreateInfo, CreateSdkDTO>>,
val modulesDTO: ModulesDTO = ModulesDTO(modules.map { Pair(it.key, it.value.second) }.toMap()),
private val modules: Map<ModuleName, ModuleCreateInfo>,
) {
val modulesDTO: List<ModuleDTO>
init {
val children = HashMap<ModuleName, MutableSet<ModuleName>>()
// 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<ModuleName, Pair<ModuleCreateInfo, CreateSdkDTO>> = withBackgroundProgress(project, PySdkConfiguratorBundle.message("intellij.python.sdk.looking")) {
private suspend fun getModulesWithoutSDKCreateInfo(project: Project): Map<ModuleName, ModuleCreateInfo> = 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<ToolId, PyProjectSdkConfigurationExtension>): Pair<ModuleCreateInfo, CreateSdkDTO>? = // Save on module level
private suspend fun getModuleInfo(module: Module, configuratorsByTool: Map<ToolId, PyProjectSdkConfigurationExtension>): 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<ModuleCreateInfo, CreateSdkDTO.ConfigurableModule> {
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<Module, Module>()
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)) {
@@ -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))
}
}
}
@@ -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<ModuleName, CreateSdkDTO>)
data class ModuleDTO(
val name: ModuleName,
val createdByTool: ToolIdDTO,
val existingPyVersion: @NlsSafe String?,
val childModules: List<ModuleName>,
)
@Serializable
@JvmInline
value class ModulesDTO(val modules: List<ModuleDTO>)
@@ -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<ModuleName, CreateSdkDTO>,
moduleItems: PersistentList<ModuleDTO>,
icons: ImmutableMap<ToolIdDTO, IconKey>,
checked: SnapshotStateSet<String>,
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<ToolIdDTO, IconKey>,
) {
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)
}
}
@@ -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<ToolIdDTO, IconKey> = 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<ModuleName, CreateSdkDTO> = persistentMapOf(*modulesDTO.modules
.map { (moduleName, createSdkInfo) ->
Pair(moduleName, createSdkInfo)
val icons: PersistentMap<ToolIdDTO, IconKey> = 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<ModuleDTO> = modulesDTO.modules.toPersistentList()
val checked: SnapshotStateSet<ModuleName> = mutableStateSetOf()
private val children = mutableMapOf<ModuleName, MutableSet<ModuleName>>()
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<ModuleName, ImmutableSet<ModuleName>> = 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<ModuleName, ModuleName> = 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)
@@ -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"))