PY-79486: (WIP) Fix backend call to detect modules, show icon for pythons.

It is not possible to use topics to access the backend. Only RPC API must be used.
Moreover, registry settings require restart.

GitOrigin-RevId: 965ed41d17350ab0b24b36eb53edf23fba95c4af
This commit is contained in:
Ilya.Kazakevich
2025-11-24 16:35:26 +00:00
committed by intellij-monorepo-bot
parent 13adc74931
commit 7a97e9f027
20 changed files with 169 additions and 85 deletions
@@ -16,10 +16,6 @@
<rpc.backend.remoteApiProvider implementation="com.intellij.python.sdkConfigurator.backend.impl.rpcBridge.ApiProvider"/>
</extensions>
<extensions defaultExtensionNs="com.intellij">
<platform.rpc.projectRemoteTopicListener
implementation="com.intellij.python.sdkConfigurator.backend.impl.rpcBridge.BackendTopicListener"/>
</extensions>
<actions>
<action class="com.intellij.python.sdkConfigurator.backend.impl.platformBridge.ConfigureSDKAction" id="ConfigureSDKAction"/>
</actions>
@@ -30,9 +30,20 @@ import kotlinx.coroutines.withContext
private val askUserMutex = Mutex()
// TODO: DOC
internal fun configureSdkAskingUser(project: Project) {
/**
* Same as [configureSdkAutomatically] but in a separate coroutine
*/
internal fun configureSdkAskingUserBg(project: Project) {
project.service<MyService>().scope.launch(Dispatchers.Default) {
configureSdkAskingUser(project)
}
}
/**
* Ask user for list of modules and configure them
*/
internal suspend fun configureSdkAskingUser(project: Project) {
withContext(Dispatchers.Default) {
askUserMutex.withLock {
val moduleToSuggestedSdk = getModulesWithoutSDK(project)
if (moduleToSuggestedSdk.modules.isNotEmpty()) {
@@ -4,6 +4,7 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUserBg
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
internal class ConfigureSDKAction : AnAction() {
@@ -13,7 +14,7 @@ internal class ConfigureSDKAction : AnAction() {
if (!enableSDKAutoConfigurator) {
return
}
configureSdkAskingUser(project)
configureSdkAskingUserBg(project)
}
override fun update(e: AnActionEvent) {
@@ -3,9 +3,8 @@ package com.intellij.python.sdkConfigurator.backend.impl.platformBridge
import com.intellij.openapi.extensions.ExtensionNotApplicableException
import com.intellij.openapi.project.Project
import com.intellij.python.pyproject.model.api.ModelRebuiltListener
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUserBg
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
import kotlinx.coroutines.sync.Mutex
internal class ModelRebuiltListenerImpl : ModelRebuiltListener {
init {
@@ -15,7 +14,7 @@ internal class ModelRebuiltListenerImpl : ModelRebuiltListener {
}
override fun modelRebuilt(project: Project) {
configureSdkAskingUser(project)
configureSdkAskingUserBg(project)
}
}
@@ -1,15 +0,0 @@
package com.intellij.python.sdkConfigurator.backend.impl.rpcBridge
import com.intellij.openapi.project.Project
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
import com.intellij.platform.rpc.topics.ProjectRemoteTopicListener
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
import com.intellij.python.sdkConfigurator.common.impl.DETECT_SDK_FOR_MODULES
internal class BackendTopicListener : ProjectRemoteTopicListener<Unit> {
override val topic: ProjectRemoteTopic<Unit> = DETECT_SDK_FOR_MODULES
override fun handleEvent(project: Project, event: Unit) {
configureSdkAskingUser(project)
}
}
@@ -2,6 +2,7 @@ package com.intellij.python.sdkConfigurator.backend.impl.rpcBridge
import com.intellij.platform.project.ProjectId
import com.intellij.platform.project.findProject
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAskingUser
import com.intellij.python.sdkConfigurator.backend.impl.configureSdkAutomatically
import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi
@@ -10,4 +11,8 @@ internal object SdkConfiguratorApiImpl : SdkConfiguratorBackEndApi {
override suspend fun configureSdkAutomatically(projectId: ProjectId, onlyModules: Set<ModuleName>) {
configureSdkAutomatically(projectId.findProject(), onlyModules)
}
override suspend fun configureAskingUser(projectId: ProjectId) {
configureSdkAskingUser(projectId.findProject())
}
}
@@ -1,5 +1,5 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<registryKey defaultValue="false" description="Configure SDK for modules which lack thereof in automatic manner" key="intellij.python.sdkConfigurator.auto" restartRequired="false"/>
<registryKey defaultValue="false" description="Configure SDK for modules which lack thereof in automatic manner" key="intellij.python.sdkConfigurator.auto" restartRequired="true"/>
</extensions>
</idea-plugin>
@@ -1,9 +1,11 @@
package com.intellij.python.sdkConfigurator.common.impl
import com.intellij.platform.project.ProjectId
import com.intellij.platform.rpc.RemoteApiProviderService
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
import fleet.rpc.RemoteApi
import fleet.rpc.Rpc
import fleet.rpc.remoteApiDescriptor
import kotlinx.serialization.Serializable
/**
@@ -12,9 +14,14 @@ import kotlinx.serialization.Serializable
@Rpc
interface SdkConfiguratorBackEndApi : RemoteApi<Unit> {
/***
* Configure SDK for all modules in [projectId] if their names in [onlyModules]
* Configure SDK for all modules in [projectId] if their names in [onlyModules] unconditionally
*/
suspend fun configureSdkAutomatically(projectId: ProjectId, onlyModules: Set<ModuleName>)
/**
* Ask user about modules, then call [configureSdkAutomatically]
*/
suspend fun configureAskingUser(projectId: ProjectId)
}
typealias ModuleName = String
@@ -29,4 +36,9 @@ val SHOW_SDK_CONFIG_UI_TOPIC: ProjectRemoteTopic<ModulesDTO> = ProjectRemoteTopi
*/
@Serializable
data class ModulesDTO(val modules: Map<ModuleName, ModuleName?>) {
}
}
/**
* [SdkConfiguratorBackEndApi] instance
*/
suspend fun SdkConfiguratorBackEndApi(): SdkConfiguratorBackEndApi = RemoteApiProviderService.resolve(remoteApiDescriptor<SdkConfiguratorBackEndApi>())
@@ -1,10 +0,0 @@
package com.intellij.python.sdkConfigurator.common.impl
import com.intellij.platform.rpc.topics.ProjectRemoteTopic
import kotlinx.serialization.builtins.serializer
/**
* Backend listens for this topic to start SDK detection process
*/
val DETECT_SDK_FOR_MODULES: ProjectRemoteTopic<Unit> = ProjectRemoteTopic("PySDKConfigurationDetectSDKTopic", Unit.serializer())
@@ -2,8 +2,8 @@ package com.intellij.python.sdkConfigurator.common
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.rpc.topics.sendToClient
import com.intellij.python.sdkConfigurator.common.impl.DETECT_SDK_FOR_MODULES
import com.intellij.platform.project.projectId
import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi
/**
@@ -11,14 +11,10 @@ import com.intellij.python.sdkConfigurator.common.impl.DETECT_SDK_FOR_MODULES
*/
val enableSDKAutoConfigurator: Boolean get() = Registry.`is`("intellij.python.sdkConfigurator.auto")
/**
* Check if [enableSDKAutoConfigurator] (return `false` if not) and start detecting process.
* it might ask user for list of modules and then detect them
* it might ask user for list of modules and then detect SDK for them
*/
fun detectSdkForModulesIn(project: Project): Boolean {
if (!enableSDKAutoConfigurator) {
return false
}
DETECT_SDK_FOR_MODULES.sendToClient(project, Unit)
return true
suspend fun detectSdkForModulesIn(project: Project) {
SdkConfiguratorBackEndApi().configureAskingUser(project.projectId())
}
@@ -1 +1,4 @@
python.sdk.configurator.frontend.choose.modules.title=Choose Modules to Configure SDK For
python.sdk.configurator.frontend.choose.modules.title=Configure Modules Environment
python.sdk.configurator.frontend.choose.modules.text=Pick the ones you want to use to run code and set up environment for them
python.sdk.configurator.frontend.choose.modules.project.structure=Project Structure
python.sdk.configurator.frontend.choose.modules.environment=Environment
@@ -1,17 +1,18 @@
package com.intellij.python.sdkConfigurator.frontend.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateSet
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.intellij.python.sdkConfigurator.frontend.ModuleInfo
import kotlinx.collections.immutable.ImmutableMap
import org.jetbrains.jewel.ui.component.Checkbox
import org.jetbrains.annotations.Nls
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.CheckboxRow
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.component.VerticallyScrollableContainer
@@ -21,23 +22,42 @@ internal fun ModuleList(
moduleItems: ImmutableMap<String, ModuleInfo>,
checked: SnapshotStateSet<String>,
onCheckChange: (String, Boolean) -> Unit,
topLabel: @Nls String,
projectStructureLabel: @Nls String,
environmentLabel: @Nls String,
) {
VerticallyScrollableContainer {
Column(Modifier.width(500.dp)) {
for ((moduleName, moduleInfo) in moduleItems) {
val (parent, pythons) = moduleInfo
Row(Modifier.padding(2.dp), verticalAlignment = Alignment.CenterVertically) {
val checked = moduleName in checked
val elementToChange = parent ?: moduleName
Checkbox(
checked = checked,
onCheckedChange = {
onCheckChange(elementToChange, it)
},
)
Text(moduleName, Modifier.padding(start = 1.dp).clickable(true, onClick = { onCheckChange(elementToChange, !checked) }))
Spacer(Modifier.weight(1f))
PythonsDropDown(pythons, Modifier.width(200.dp))
val ts = JewelTheme.defaultTextStyle
val padding = 2.dp
val longestItemChars = remember { (listOf(projectStructureLabel) + moduleItems.keys).maxBy { it.length } }
val leftColumnMinSize = measureText(longestItemChars, ts)
val spaceBetweenCols = 16.dp
Column(Modifier.padding(padding), verticalArrangement = Arrangement.spacedBy(padding)) {
Text(topLabel, Modifier.padding(bottom = 10.dp))
Row(Modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) {
Text(projectStructureLabel, Modifier.width(leftColumnMinSize + spaceBetweenCols + padding + 26.dp)) //~ checkbox size
Text(environmentLabel, textAlign = TextAlign.End)
}
val moduleItems = remember { moduleItems.entries.sortedBy { it.key } }
VerticallyScrollableContainer {
Column(Modifier, horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(padding)) {
for ((moduleName, moduleInfo) in moduleItems) {
val (parent, pythons) = moduleInfo
Row(Modifier.padding(padding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(spaceBetweenCols)) {
val checked = moduleName in checked
val elementToChange = parent ?: moduleName // TODO: move logic out of UI
CheckboxRow(
moduleName,
checked = checked,
onCheckedChange = {
onCheckChange(elementToChange, it)
},
textStyle = ts,
textModifier = Modifier.width(leftColumnMinSize)
)
if (parent == null) {
PythonsDropDown(pythons)
}
}
}
}
}
@@ -1,13 +1,40 @@
package com.intellij.python.sdkConfigurator.frontend.components
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.ListComboBox
import org.jetbrains.jewel.ui.component.SimpleListItem
import org.jetbrains.jewel.ui.icons.AllIconsKeys.Language.Python
@OptIn(ExperimentalJewelApi::class)
@Composable
internal fun PythonsDropDown(pythons: ImmutableList<String>, modifier: Modifier = Modifier) {
internal fun PythonsDropDown(pythons: ImmutableList<String>, grayedOut: Boolean = false, modifier: Modifier = Modifier) {
val ts = JewelTheme.defaultTextStyle
val longestPython = remember { pythons.maxBy { it.length } }
val padding = 100.dp
val width = measureText(longestPython, ts) + padding // Padding
var i by remember { mutableIntStateOf(0) }
ListComboBox(pythons, i, { i = it }, modifier)
ListComboBox(
items = pythons,
selectedIndex = i,
modifier = modifier.widthIn(min = width, max = width + padding),
onSelectedItemChange = { i = it },
itemKeys = { index, _ -> index },
itemContent = { item, isSelected, isActive ->
SimpleListItem(
text = item,
selected = isSelected,
active = isActive,
icon = Python,
colorFilter = if (grayedOut) ColorFilter.tint(Color.Gray) else null,
)
},
)
}
@@ -0,0 +1,15 @@
package com.intellij.python.sdkConfigurator.frontend.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Dp
@Composable
internal fun measureText(text: String, textStyle: TextStyle): Dp {
val textMeasurer = rememberTextMeasurer()
return with(LocalDensity.current) {
textMeasurer.measure(text, textStyle).size.width.toDp()
}
}
@@ -1,3 +1,6 @@
/**
* Front-end implementation, do not access directly
*/
@ApiStatus.Internal
package com.intellij.python.sdkConfigurator.frontend;
@@ -9,6 +9,7 @@ import com.intellij.platform.rpc.topics.ProjectRemoteTopic
import com.intellij.platform.rpc.topics.ProjectRemoteTopicListener
import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO
import com.intellij.python.sdkConfigurator.common.impl.SHOW_SDK_CONFIG_UI_TOPIC
import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi
import com.intellij.python.sdkConfigurator.frontend.askUser
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@@ -22,7 +23,7 @@ internal class FrontendTopicListener : ProjectRemoteTopicListener<ModulesDTO> {
// Ask user to choose modules, then ask backend to configure it
askUser(project, event) { modulesChosenByUser ->
scope.launch {
getBackendApi().configureSdkAutomatically(project.projectId(), modulesChosenByUser)
SdkConfiguratorBackEndApi().configureSdkAutomatically(project.projectId(), modulesChosenByUser)
}
}
}
@@ -1,9 +0,0 @@
package com.intellij.python.sdkConfigurator.frontend.rpcBridge
import com.intellij.platform.rpc.RemoteApiProviderService
import com.intellij.python.sdkConfigurator.common.impl.SdkConfiguratorBackEndApi
import fleet.rpc.remoteApiDescriptor
internal suspend fun getBackendApi() = RemoteApiProviderService.resolve(remoteApiDescriptor<SdkConfiguratorBackEndApi>())
@@ -5,6 +5,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.python.sdkConfigurator.common.impl.ModuleName
import com.intellij.python.sdkConfigurator.common.impl.ModulesDTO
import com.intellij.python.sdkConfigurator.frontend.PySdkConfiguratorFrontendBundle.message
import com.intellij.python.sdkConfigurator.frontend.components.ModuleList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -28,7 +29,8 @@ internal suspend fun askUser(project: Project, modules: ModulesDTO, onResult: (S
private class MyDialog(project: Project, private val viewModel: ModulesViewModel) : DialogWrapper(project) {
init {
title = PySdkConfiguratorFrontendBundle.message("python.sdk.configurator.frontend.choose.modules.title")
title = message("python.sdk.configurator.frontend.choose.modules.title")
isResizable = false
init()
}
@@ -36,7 +38,10 @@ private class MyDialog(project: Project, private val viewModel: ModulesViewModel
override fun createCenterPanel(): JComponent {
enableNewSwingCompositing()
return compose(focusOnClickInside = true, content = {
ModuleList(viewModel.checkBoxItems, viewModel.checked, viewModel::clicked)
ModuleList(viewModel.checkBoxItems, viewModel.checked, 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"))
})
}
}
@@ -58,7 +58,7 @@ import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.intellij.python.sdkConfigurator.common.PublicApiKt.detectSdkForModulesIn;
import static com.jetbrains.python.inspections.PyInterpreterInspectionExKt.detectSdkForModulesForJvmIn;
import static com.jetbrains.python.inspections.PyInterpreterInspectionExKt.findAllSortedForModuleForJvm;
@@ -343,7 +343,7 @@ public final class PyInterpreterInspection extends PyInspection {
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
if (!detectSdkForModulesIn(project)) {
if (!detectSdkForModulesForJvmIn(project)) {
PyProjectSdkConfiguration.INSTANCE.configureSdkUsingCreateSdkInfo(myModule, myCreateSdkInfo);
}
}
@@ -1,13 +1,37 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.inspections
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.project.Project
import com.intellij.python.sdkConfigurator.common.detectSdkForModulesIn
import com.intellij.python.sdkConfigurator.common.enableSDKAutoConfigurator
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import com.jetbrains.python.sdk.configuration.CreateSdkInfo
import com.jetbrains.python.sdk.configuration.PyProjectSdkConfigurationExtension
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
/**
* To be used by [PyInterpreterInspection] only
*/
@ApiStatus.Internal
internal fun detectSdkForModulesForJvmIn(project: Project): Boolean {
if (!enableSDKAutoConfigurator) {
return false
}
project.service<MyService>().scope.launch {
detectSdkForModulesIn(project)
}
return true
}
@Service(Service.Level.PROJECT)
private class MyService(val scope: CoroutineScope)
/**
* To be used by [PyInterpreterInspection] only
*/