[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
This commit is contained in:
Vitaly Legchilkin
2025-10-27 21:38:43 +00:00
committed by intellij-monorepo-bot
parent d6e1c5df15
commit 0c27c361c8
10 changed files with 94 additions and 151 deletions
@@ -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}
@@ -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<P : PathHolder> {
val isBrowseable: Boolean
fun parsePath(raw: String): PyResult<P>
fun validateExecutable(path: P): PyResult<Unit>
suspend fun getSystemPythonFromSelection(pathToPython: P): PyResult<DetectedSelectableInterpreter<P>>
@@ -95,6 +92,14 @@ sealed interface FileSystem<P : PathHolder> {
PyResult.localizedError(e.localizedMessage)
}
override fun validateExecutable(path: PathHolder.Eel): PyResult<Unit> {
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<Unit> = 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<P : PathHolder> {
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<Unit> = PyResult.success(Unit)
override suspend fun validateVenv(homePath: PathHolder.Target): PyResult<Unit> = withContext(Dispatchers.IO) {
val pythonBinaryPath = resolvePythonBinary(homePath)
@@ -16,9 +16,9 @@ class ToolValidator<P : PathHolder>(
override val backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
propertyGraph: PropertyGraph,
val defaultPathSupplier: suspend () -> P?,
val pathValidator: suspend (P) -> PyResult<Version> = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) },
val toolValidator: suspend (P) -> PyResult<Version> = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) },
) : PathValidator<Version, P, ValidatedPath.Executable<P>> {
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(true)
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(false)
override val isValidationInProgress: Boolean
get() = validationJob.isActive
@@ -44,8 +44,13 @@ class ToolValidator<P : PathHolder>(
val path = fileSystem.parsePath(input).getOr { error ->
return@withContext ValidatedPath.Executable<P>(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) }
@@ -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<T, P : PathHolder, VP : ValidatedPath<T, P>> {
val backProperty: ObservableMutableProperty<VP?>
@@ -72,7 +66,9 @@ interface PathValidator<T, P : PathHolder, VP : ValidatedPath<T, P>> {
}
}
private class ValidationSuccessExtension<T>(val validationInfo: T) : ExtendableTextComponent.Extension {
private interface ValidationStatusExtension
private class ValidationSuccessExtension<T>(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<T>(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<T, P : PathHolder, VP : ValidatedPath<T, P>>(
val fileSystem: FileSystem<P>,
@@ -170,23 +111,12 @@ internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
}
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<JTextField> {
override fun getText(component: JTextField): @NlsSafe String {
return component.text
@@ -227,27 +157,22 @@ internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
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<T, P : PathHolder, VP : ValidatedPath<T, P>>(
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 <T, P : PathHolder, VP : ValidatedPath<T, P>> 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
}
}
@@ -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<P : PathHolder>(model: PythonAdd
scope = scope,
)
condaExecutable.initialize(scope)
condaExecutable.displayLoaderWhen(
loading = model.condaViewModel.condaEnvironmentsLoading,
scope = scope,
)
}
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
@@ -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<P: PathHolder>(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<Sdk> {
@@ -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<PyResu
append(message("sdk.create.custom.hatch.environment.loading"), SimpleTextAttributes.GRAYED_ITALIC_ATTRIBUTES)
}
is Result.Failure -> {
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<PyResu
}
}
internal class HatchEnvironmentComboBox<P : PathHolder>(
model: PythonAddInterpreterModel<P>,
) : ComboBox<HatchVirtualEnvironment?>() {
init {
renderer = HatchEnvComboBoxListCellRenderer(model.hatchViewModel.availableEnvironments)
editor = FixedComboBoxEditor()
}
@Synchronized
internal fun syncWithEnvs(
environmentsResult: PyResult<List<HatchVirtualEnvironment>>,
isFilterOnlyExisting: Boolean = false,
) {
removeAllItems()
val environments = environmentsResult.getOr { return }
environments.filter { !isFilterOnlyExisting || it.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing }.forEach {
addItem(it)
}
}
}
private fun <P : PathHolder> Panel.addEnvironmentComboBox(
model: PythonAddInterpreterModel<P>,
validationRequestor: DialogValidationRequestor,
isGenerateNewMode: Boolean,
): ComboBox<HatchVirtualEnvironment> {
): HatchEnvironmentComboBox<P> {
val environmentAlreadyExists = AtomicBooleanProperty(false)
lateinit var environmentComboBox: ComboBox<HatchVirtualEnvironment>
val environmentComboBox = HatchEnvironmentComboBox(model)
val hatchEnvironmentProperty = if (isGenerateNewMode)
model.hatchViewModel.selectedEnvFromAvailable
@@ -113,7 +134,7 @@ private fun <P : PathHolder> 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 <P : PathHolder> 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 <P : PathHolder> Panel.addEnvironmentComboBox(
.applyToComponent {
preferredSize = JBUI.size(preferredSize)
}
.component
}
row("") {
@@ -171,7 +191,7 @@ private fun <P : PathHolder> Panel.addExecutableSelector(
}
internal data class HatchFormFields<P : PathHolder>(
val environmentComboBox: ComboBox<HatchVirtualEnvironment>,
val environmentComboBox: HatchEnvironmentComboBox<P>,
val basePythonComboBox: PythonInterpreterComboBox<P>?,
val validatedPathField: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>,
) {
@@ -213,7 +233,7 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
validationRequestor,
installHatchActionLink
)
var environmentComboBox: ComboBox<HatchVirtualEnvironment> by lateinitVal()
var environmentComboBox: HatchEnvironmentComboBox<P> by lateinitVal()
var basePythonComboBox: PythonInterpreterComboBox<P>? = null
rowsRange {
@@ -238,15 +258,3 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath)
}
@Synchronized
internal fun ComboBox<HatchVirtualEnvironment>.syncWithEnvs(
environmentsResult: PyResult<List<HatchVirtualEnvironment>>,
isFilterOnlyExisting: Boolean = false,
) {
removeAllItems()
val environments = environmentsResult.getOr { return }
environments.filter { !isFilterOnlyExisting || it.pythonVirtualEnvironment is PythonVirtualEnvironment.Existing }.forEach {
addItem(it)
}
}
@@ -56,7 +56,7 @@ class HatchViewModel<P : PathHolder>(
hatchExecutable.afterChange { hatchExecutable ->
if (hatchExecutable?.validationResult?.successOrNull == null) {
availableEnvironments.value = null
availableEnvironments.value = PyResult.success(emptyList())
return@afterChange
}
@@ -178,7 +178,6 @@ internal fun <P : PathHolder> 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 <P : PathHolder> 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
}
}
@@ -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<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?)
: CustomExistingEnvironmentSelector<P>("uv", model, module) {
override val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>> = model.uvViewModel.toolValidator
@@ -58,14 +63,18 @@ internal class UvExistingEnvironmentSelector<P : PathHolder>(model: PythonMutabl
}
override suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>> {
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
}