From 0c27c361c8a1ab7b7cb15bd42820ad9b5294668d Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Fri, 24 Oct 2025 20:20:47 +0200 Subject: [PATCH] [python] (PY-84251) UI/UX changes for uv and Hatch dialogs * adjust validated path field logic: remove 2 sec timer, remove the action icon, make a warning, add browse action + detect existing environment for uv selection based on the module path (show all existing virtual environments in the module root) (cherry picked from commit 0fb4cf1f77ebe799b4bc60f3a44974678e94bc5a) IJ-MR-180201 GitOrigin-RevId: ab218030527774cb9ec1a1bfee37b220e6b82928 --- .../messages/PyBundle.properties | 3 +- .../jetbrains/python/sdk/add/v2/FileSystem.kt | 22 +++- .../python/sdk/add/v2/ToolValidator.kt | 13 +- .../python/sdk/add/v2/ValidatedPathField.kt | 113 +++--------------- .../conda/CondaExistingEnvironmentSelector.kt | 5 - .../v2/conda/CondaNewEnvironmentCreator.kt | 5 - .../sdk/add/v2/hatch/HatchUIComponents.kt | 50 ++++---- .../python/sdk/add/v2/hatch/HatchViewModel.kt | 2 +- .../jetbrains/python/sdk/add/v2/uiUtils.kt | 3 +- .../v2/uv/UvExistingEnvironmentSelector.kt | 29 +++-- 10 files changed, 94 insertions(+), 151 deletions(-) diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index fa2be8f19ebe..64bcece6c638 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -568,7 +568,7 @@ sdk.create.custom.venv.install.fix.title=Install {0} {1} sdk.create.custom.venv.run.error.message=Error Running {0} sdk.create.custom.venv.progress.title.detect.executable=Detect executable sdk.create.custom.existing.env.title=Environment: -sdk.create.custom.existing.error.no.interpreters.to.select=No interpreters were found. Please select one manually using the Browse button. +sdk.create.custom.existing.error.no.interpreters.to.select=No interpreters were found. Select manually using the Browse button. sdk.create.custom.hatch.environment=Environment: sdk.create.custom.hatch.environment.loading=Loading environments\u2026 sdk.create.custom.hatch.environment.exists=Environment already exists @@ -1209,6 +1209,7 @@ python.add.sdk.panel.name.system.interpreter=System interpreter python.add.sdk.panel.name.virtualenv.environment=Virtualenv environment python.add.sdk.panel.name.poetry.environment=Poetry Environment python.add.sdk.wait.for.validation=Wait for the executable validation +python.add.sdk.press.enter.to.validate=Press Enter to validate python.add.sdk.version=Version: {0} python.add.sdk.already.contains.python.with.version=Already contains Python with version {0} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt index 444cf9a15064..4c1316e1f6b7 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/FileSystem.kt @@ -13,12 +13,7 @@ import com.intellij.platform.eel.EelApi import com.intellij.platform.eel.provider.asNioPath import com.intellij.platform.eel.provider.localEel import com.intellij.platform.eel.where -import com.intellij.python.community.execService.Args -import com.intellij.python.community.execService.BinOnEel -import com.intellij.python.community.execService.BinOnTarget -import com.intellij.python.community.execService.BinaryToExec -import com.intellij.python.community.execService.ExecService -import com.intellij.python.community.execService.execGetStdout +import com.intellij.python.community.execService.* import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.intellij.python.community.services.internal.impl.VanillaPythonWithPythonInfoImpl import com.intellij.python.community.services.shared.VanillaPythonWithPythonInfo @@ -48,6 +43,7 @@ import java.nio.file.InvalidPathException import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.exists +import kotlin.io.path.isDirectory private val LOG: Logger = fileLogger() @@ -64,6 +60,7 @@ sealed interface FileSystem

{ val isBrowseable: Boolean fun parsePath(raw: String): PyResult

+ fun validateExecutable(path: P): PyResult suspend fun getSystemPythonFromSelection(pathToPython: P): PyResult> @@ -95,6 +92,14 @@ sealed interface FileSystem

{ PyResult.localizedError(e.localizedMessage) } + override fun validateExecutable(path: PathHolder.Eel): PyResult { + return when { + !path.path.exists() -> PyResult.localizedError(message("sdk.create.not.executable.does.not.exist.error")) + path.path.isDirectory() -> PyResult.localizedError(message("sdk.create.executable.directory.error")) + else -> PyResult.success(Unit) + } + } + override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult = withContext(Dispatchers.IO) { val validationResult = when { !homePath.path.isAbsolute -> PyResult.localizedError(message("python.sdk.new.error.no.absolute")) @@ -202,6 +207,11 @@ sealed interface FileSystem

{ return PyResult.success(PathHolder.Target(raw)) } + /** + * Currently, we don't validate executable on target because there is no API to check path existence and its type on target. + */ + override fun validateExecutable(path: PathHolder.Target): PyResult = PyResult.success(Unit) + override suspend fun validateVenv(homePath: PathHolder.Target): PyResult = withContext(Dispatchers.IO) { val pythonBinaryPath = resolvePythonBinary(homePath) diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ToolValidator.kt b/python/src/com/jetbrains/python/sdk/add/v2/ToolValidator.kt index 187335236f14..57db3a92bcd6 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ToolValidator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ToolValidator.kt @@ -16,9 +16,9 @@ class ToolValidator

( override val backProperty: ObservableMutableProperty?>, propertyGraph: PropertyGraph, val defaultPathSupplier: suspend () -> P?, - val pathValidator: suspend (P) -> PyResult = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) }, + val toolValidator: suspend (P) -> PyResult = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) }, ) : PathValidator> { - override val isDirtyValue: ObservableMutableProperty = propertyGraph.property(true) + override val isDirtyValue: ObservableMutableProperty = propertyGraph.property(false) override val isValidationInProgress: Boolean get() = validationJob.isActive @@ -44,8 +44,13 @@ class ToolValidator

( val path = fileSystem.parsePath(input).getOr { error -> return@withContext ValidatedPath.Executable

(null, error) } - val validationResult = pathValidator(path) - ValidatedPath.Executable(path, validationResult) + + fileSystem.validateExecutable(path).getOr { + return@withContext ValidatedPath.Executable(path, it) + } + + val toolValidationResult = toolValidator(path) + ValidatedPath.Executable(path, toolValidationResult) } withContext(Dispatchers.UI) { backProperty.set(exec) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt index a2d649f24854..cfe5dc2d13e4 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/ValidatedPathField.kt @@ -7,6 +7,7 @@ import com.intellij.execution.target.getTargetType import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CustomShortcutSet +import com.intellij.openapi.application.UI import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.observable.properties.ObservableMutableProperty @@ -36,30 +37,23 @@ import com.intellij.ui.dsl.builder.components.ValidationType import com.intellij.ui.dsl.builder.components.validationTooltip import com.intellij.util.asDisposable import com.jetbrains.python.PyBundle.message -import com.jetbrains.python.onFailure import com.jetbrains.python.onSuccess import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.jetbrains.annotations.Nls -import java.awt.AlphaComposite -import java.awt.Component -import java.awt.Graphics -import java.awt.Graphics2D import java.awt.event.ActionListener import java.awt.event.KeyEvent -import java.util.concurrent.atomic.AtomicLong import javax.swing.Icon import javax.swing.JTextField import javax.swing.KeyStroke import javax.swing.event.DocumentEvent import kotlin.concurrent.atomics.AtomicBoolean import kotlin.concurrent.atomics.ExperimentalAtomicApi -import kotlin.math.cos interface PathValidator> { val backProperty: ObservableMutableProperty @@ -72,7 +66,9 @@ interface PathValidator> { } } -private class ValidationSuccessExtension(val validationInfo: T) : ExtendableTextComponent.Extension { +private interface ValidationStatusExtension + +private class ValidationSuccessExtension(val validationInfo: T) : ExtendableTextComponent.Extension, ValidationStatusExtension { override fun getIcon(hovered: Boolean): Icon = AllIcons.General.GreenCheckmark override fun getTooltip(): @NlsContexts.Tooltip String? { val tooltip = when (validationInfo) { @@ -83,68 +79,13 @@ private class ValidationSuccessExtension(val validationInfo: T) : ExtendableT } } -private object ValidationErrorExtension : ExtendableTextComponent.Extension { - override fun getIcon(hovered: Boolean): Icon = AllIcons.Status.FailedInProgress -} - -private object ValidationInProgressExtension : ExtendableTextComponent.Extension { +private object ValidationInProgressExtension : ExtendableTextComponent.Extension, ValidationStatusExtension { override fun getIcon(hovered: Boolean): Icon = AnimatedIcon.Default() override fun getTooltip(): @NlsContexts.Tooltip String { return message("python.add.sdk.wait.for.validation") } } -private class DebounceCounterIcon(val icon: Icon, val period: Int) : Icon { - private val time: AtomicLong = AtomicLong(System.currentTimeMillis()) - - fun reset() { - time.set(System.currentTimeMillis()) - } - - override fun getIconWidth(): Int { - return icon.iconWidth - } - - override fun getIconHeight(): Int { - return icon.iconHeight - } - - override fun paintIcon(c: Component?, g: Graphics?, x: Int, y: Int) { - assert(period > 0) { "unexpected" } - val time = (System.currentTimeMillis() - this.time.get()) % period - val alpha = ((cos(2 * Math.PI * time / period) + 1) / 2).toFloat() - if (alpha > 0) { - if (alpha < 1 && g is Graphics2D) { - val g2d = g.create() as Graphics2D - try { - g2d.composite = AlphaComposite.SrcAtop.derive(alpha) - icon.paintIcon(c, g2d, x, y) - } - finally { - g2d.dispose() - } - } - else { - icon.paintIcon(c, g, x, y) - } - } - } -} - -private class AnimatedFadingIcon(private val icon: DebounceCounterIcon) : AnimatedIcon(50, icon) { - fun reset() { - icon.reset() - } - - companion object { - fun build(icon: Icon, period: Int = VALIDATION_DELAY * 2): AnimatedFadingIcon { - return AnimatedFadingIcon(DebounceCounterIcon(icon, period)) - } - } -} - -private const val VALIDATION_DELAY = 2000 - @OptIn(FlowPreview::class, ExperimentalAtomicApi::class) internal class ValidatedPathField>( val fileSystem: FileSystem

, @@ -170,23 +111,12 @@ internal class ValidatedPathField>( } override fun actionPerformed(e: AnActionEvent) { - with(textField as ExtendableTextComponent) { - if (extensions.contains(validationWaitExtension)) { - doValidate() - } - } + doValidate() } }.apply { registerCustomShortcutSet(CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)), this@ValidatedPathField) } - private val validationWaitIcon = AnimatedFadingIcon.build(AllIcons.Gutter.SuggestedRefactoringBulb) - private val validationWaitExtension: ExtendableTextComponent.Extension = ExtendableTextComponent.Extension.create( - validationWaitIcon, message("python.add.sdk.wait.for.validation") - ) { - validationAction.doValidate() - } - private val fieldAccessor = object : TextComponentAccessor { override fun getText(component: JTextField): @NlsSafe String { return component.text @@ -227,27 +157,22 @@ internal class ValidatedPathField>( pathValidator.isDirtyValue.afterChange(scope.asDisposable()) { isDirtyValue -> with(textField as ExtendableTextComponent) { - extensions.forEach { removeExtension(it) } + extensions + .filter { it is ValidationStatusExtension } + .forEach { removeExtension(it) } if (isDirtyValue) { if (pathValidator.isValidationInProgress) { isEnabled = false addExtension(ValidationInProgressExtension) } - else { - addExtension(validationWaitExtension) - } } else { editorMode.store(false) - browseFolderActionLister?.let { setButtonVisible(true) } isEnabled = true pathValidator.backProperty.get()?.validationResult?.let { validationResult -> validationResult - .onFailure { - addExtension(ValidationErrorExtension) - } .onSuccess { addExtension(ValidationSuccessExtension(it)) } @@ -261,27 +186,20 @@ internal class ValidatedPathField>( this.scope = scope registerPropertyCallbacks() - scope.launch { + scope.launch(Dispatchers.UI) { textInputFlow .debounce(50) // setText method is a combination of two calls - remove + insert, should count them as 1 .map { if (it == null) return@map null - if (editorMode.load()) { - validationWaitIcon.reset() - } - else if ((pathValidator.backProperty.get()?.pathHolder?.toString() ?: "") != it) { + if (!editorMode.load() && (pathValidator.backProperty.get()?.pathHolder?.toString() ?: "") != it) { editorMode.store(true) pathValidator.markDirty() } it } - .debounce(VALIDATION_DELAY.toLong()) - .collectLatest { - if (it == null) return@collectLatest - validationAction.doValidate() - } + .collect {} } } @@ -399,9 +317,12 @@ internal fun > Panel.validatablePath pyErrorMessage != null -> { ValidationInfo(pyErrorMessage) } - pathValidator.isDirtyValue.get() -> { + pathValidator.isValidationInProgress -> { ValidationInfo(message("python.add.sdk.wait.for.validation")) } + pathValidator.isDirtyValue.get() -> { + ValidationInfo(message("python.add.sdk.press.enter.to.validate")).asWarning() + } else -> null } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt index c501c7a7ed64..696ad1251dcb 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaExistingEnvironmentSelector.kt @@ -18,7 +18,6 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.util.ui.JBUI import com.jetbrains.python.PyBundle.message import com.jetbrains.python.Result -import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.ModuleOrProject @@ -130,10 +129,6 @@ internal class CondaExistingEnvironmentSelector

(model: PythonAdd scope = scope, ) condaExecutable.initialize(scope) - condaExecutable.displayLoaderWhen( - loading = model.condaViewModel.condaEnvironmentsLoading, - scope = scope, - ) } override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt index 90e2ac4188d4..f8bf7c253db5 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/conda/CondaNewEnvironmentCreator.kt @@ -10,7 +10,6 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.dsl.builder.bindText import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer import com.jetbrains.python.PyBundle.message -import com.jetbrains.python.errorProcessing.ErrorSink import com.jetbrains.python.errorProcessing.PyResult import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.psi.LanguageLevel @@ -56,10 +55,6 @@ internal class CondaNewEnvironmentCreator(model: PythonMutableTar override fun onShown(scope: CoroutineScope) { condaExecutable.initialize(scope) - condaExecutable.displayLoaderWhen( - loading = model.condaViewModel.condaEnvironmentsLoading, - scope = scope, - ) } override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt index 61c1da08fcb8..2a7bbb49ef4f 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchUIComponents.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.application.EDT import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.observable.util.transform import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.FixedComboBoxEditor import com.intellij.openapi.ui.ValidationInfo import com.intellij.openapi.ui.validation.DialogValidationRequestor import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED @@ -73,12 +74,10 @@ private class HatchEnvComboBoxListCellRenderer(val contentFlow: StateFlow { - icon = AllIcons.General.ShowWarning append(message("sdk.create.custom.hatch.error.no.environments.to.select"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES) } is Result.Success -> { if (result.result.isEmpty() || value == null) { - icon = AllIcons.General.ShowWarning append(message("sdk.create.custom.hatch.error.no.environments.to.select"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES) } else { @@ -98,14 +97,36 @@ private class HatchEnvComboBoxListCellRenderer(val contentFlow: StateFlow( + model: PythonAddInterpreterModel

, +) : ComboBox() { + init { + renderer = HatchEnvComboBoxListCellRenderer(model.hatchViewModel.availableEnvironments) + editor = FixedComboBoxEditor() + } + + @Synchronized + internal fun syncWithEnvs( + environmentsResult: PyResult>, + isFilterOnlyExisting: Boolean = false, + ) { + removeAllItems() + val environments = environmentsResult.getOr { return } + environments.filter { !isFilterOnlyExisting || it.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing }.forEach { + addItem(it) + } + } +} + + private fun

Panel.addEnvironmentComboBox( model: PythonAddInterpreterModel

, validationRequestor: DialogValidationRequestor, isGenerateNewMode: Boolean, -): ComboBox { +): HatchEnvironmentComboBox

{ val environmentAlreadyExists = AtomicBooleanProperty(false) - lateinit var environmentComboBox: ComboBox + val environmentComboBox = HatchEnvironmentComboBox(model) val hatchEnvironmentProperty = if (isGenerateNewMode) model.hatchViewModel.selectedEnvFromAvailable @@ -113,7 +134,7 @@ private fun

Panel.addEnvironmentComboBox( model.hatchViewModel.selectedEnvFromExisting row(message("sdk.create.custom.hatch.environment")) { - environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer(model.hatchViewModel.availableEnvironments)) + cell(environmentComboBox) .bindItem(hatchEnvironmentProperty) .validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(hatchEnvironmentProperty)) .validationInfo { component -> @@ -121,7 +142,7 @@ private fun

Panel.addEnvironmentComboBox( when { !component.isVisible || !component.isEnabled -> null component.item == null -> { - ValidationInfo(message("sdk.create.custom.hatch.error.no.environments.to.select")) + ValidationInfo("") } isGenerateNewMode && component.item?.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing -> { environmentAlreadyExists.set(true) @@ -134,7 +155,6 @@ private fun

Panel.addEnvironmentComboBox( .applyToComponent { preferredSize = JBUI.size(preferredSize) } - .component } row("") { @@ -171,7 +191,7 @@ private fun

Panel.addExecutableSelector( } internal data class HatchFormFields

( - val environmentComboBox: ComboBox, + val environmentComboBox: HatchEnvironmentComboBox

, val basePythonComboBox: PythonInterpreterComboBox

?, val validatedPathField: ValidatedPathField>, ) { @@ -213,7 +233,7 @@ internal fun

Panel.buildHatchFormFields( validationRequestor, installHatchActionLink ) - var environmentComboBox: ComboBox by lateinitVal() + var environmentComboBox: HatchEnvironmentComboBox

by lateinitVal() var basePythonComboBox: PythonInterpreterComboBox

? = null rowsRange { @@ -238,15 +258,3 @@ internal fun

Panel.buildHatchFormFields( return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath) } -@Synchronized -internal fun ComboBox.syncWithEnvs( - environmentsResult: PyResult>, - isFilterOnlyExisting: Boolean = false, -) { - removeAllItems() - val environments = environmentsResult.getOr { return } - environments.filter { !isFilterOnlyExisting || it.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing }.forEach { - addItem(it) - } -} - diff --git a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchViewModel.kt b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchViewModel.kt index fd4864130ebf..e768c9372ce9 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchViewModel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/hatch/HatchViewModel.kt @@ -56,7 +56,7 @@ class HatchViewModel

( hatchExecutable.afterChange { hatchExecutable -> if (hatchExecutable?.validationResult?.successOrNull == null) { - availableEnvironments.value = null + availableEnvironments.value = PyResult.success(emptyList()) return@afterChange } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt index 05a8e40214fe..6497bfaa7811 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -178,7 +178,6 @@ internal fun

SimpleColoredComponent.customizeForPythonInterpret return } interpreter == null -> { - icon = AllIcons.General.ShowWarning append(message("sdk.create.custom.existing.error.no.interpreters.to.select"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES) return } @@ -304,7 +303,7 @@ internal fun

Panel.pythonInterpreterComboBox( when { !it.isVisible -> null it.isLoading.get() -> ValidationInfo(message("python.add.sdk.panel.wait")) - selectedSdkProperty.get() == null -> ValidationInfo(message("sdk.create.custom.existing.error.no.interpreters.to.select")) + selectedSdkProperty.get() == null -> ValidationInfo("") else -> null } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt index 2e636e7f173e..6c99ee3166e7 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uv/UvExistingEnvironmentSelector.kt @@ -3,25 +3,30 @@ package com.jetbrains.python.sdk.add.v2.uv import com.intellij.openapi.module.Module import com.intellij.openapi.projectRoots.Sdk +import com.intellij.python.community.execService.python.validatePythonAndGetInfo import com.jetbrains.python.PyBundle -import com.jetbrains.python.PythonInfo import com.jetbrains.python.Result import com.jetbrains.python.errorProcessing.PyResult +import com.jetbrains.python.getOrNull import com.jetbrains.python.sdk.ModuleOrProject import com.jetbrains.python.sdk.add.v2.* import com.jetbrains.python.sdk.associatedModulePath import com.jetbrains.python.sdk.basePath +import com.jetbrains.python.sdk.impl.resolvePythonBinary import com.jetbrains.python.sdk.isAssociatedWithModule import com.jetbrains.python.sdk.legacy.PythonSdkUtil import com.jetbrains.python.sdk.uv.isUv import com.jetbrains.python.sdk.uv.setupExistingEnvAndSdk import com.jetbrains.python.statistics.InterpreterType -import com.jetbrains.python.statistics.version import com.jetbrains.python.venvReader.VirtualEnvReader import com.jetbrains.python.venvReader.tryResolvePath +import java.nio.file.Files import java.nio.file.Path +import java.util.stream.Collectors +import kotlin.io.path.exists import kotlin.io.path.pathString + internal class UvExistingEnvironmentSelector

(model: PythonMutableTargetAddInterpreterModel

, module: Module?) : CustomExistingEnvironmentSelector

("uv", model, module) { override val toolState: PathValidator> = model.uvViewModel.toolValidator @@ -58,14 +63,18 @@ internal class UvExistingEnvironmentSelector

(model: PythonMutabl } override suspend fun detectEnvironments(modulePath: Path): List> { - val existingEnvs = PythonSdkUtil.getAllSdks().filter { - it.isUv && (it.associatedModulePath == modulePath.pathString || it.associatedModulePath == null) - }.mapNotNull { env -> - env.homePath?.let { path -> - model.fileSystem.parsePath(path).successOrNull?.let { homePath -> - DetectedSelectableInterpreter(homePath, PythonInfo(env.version), false) - } - } + val rootFolders = Files.walk(modulePath, 1) + .filter(Files::isDirectory) + .collect(Collectors.toList()) + + val existingEnvs = rootFolders.mapNotNull { possibleVenvHome -> + val pythonBinaryPath = possibleVenvHome.resolvePythonBinary()?.takeIf { it.exists() } + ?: return@mapNotNull null + val pythonInfo = pythonBinaryPath.validatePythonAndGetInfo().getOrNull() + ?: return@mapNotNull null + + val pathHolder = PathHolder.Eel(pythonBinaryPath) as P + DetectedSelectableInterpreter(pathHolder, pythonInfo, false) } return existingEnvs }