diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index da161da61548..c0ae2eae69a6 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -626,6 +626,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of + diff --git a/python/python-sdk/src/com/jetbrains/python/sdk/Sdks.kt b/python/python-sdk/src/com/jetbrains/python/sdk/Sdks.kt index ce69781f5b65..9f953ba277df 100644 --- a/python/python-sdk/src/com/jetbrains/python/sdk/Sdks.kt +++ b/python/python-sdk/src/com/jetbrains/python/sdk/Sdks.kt @@ -21,7 +21,7 @@ import java.net.URL import java.nio.charset.StandardCharsets -private val LOG: Logger = logger() +val LOG: Logger = logger() /** diff --git a/python/src/com/jetbrains/python/sdk/AddInterpreterActions.kt b/python/src/com/jetbrains/python/sdk/AddInterpreterActions.kt index 94cc1e681f79..62fb43e9310e 100644 --- a/python/src/com/jetbrains/python/sdk/AddInterpreterActions.kt +++ b/python/src/com/jetbrains/python/sdk/AddInterpreterActions.kt @@ -15,6 +15,7 @@ import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.registry.Registry import com.jetbrains.python.PyBundle import com.jetbrains.python.configuration.PyConfigurableInterpreterList import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory @@ -22,6 +23,7 @@ import com.jetbrains.python.run.allowCreationTargetOfThisType import com.jetbrains.python.sdk.add.PyAddSdkDialog import com.jetbrains.python.sdk.add.collector.PythonNewInterpreterAddedCollector import com.jetbrains.python.sdk.add.target.PyAddTargetBasedSdkDialog +import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterDialog import com.jetbrains.python.target.PythonLanguageRuntimeType import java.util.function.Consumer @@ -54,8 +56,12 @@ private class AddLocalInterpreterAction(private val project: Project, private val onSdkCreated: Consumer) : AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware { override fun actionPerformed(e: AnActionEvent) { - val model = PyConfigurableInterpreterList.getInstance(project).model + if (Registry.`is`("python.unified.interpreter.configuration")) { + PythonAddLocalInterpreterDialog(project).show() + return + } + val model = PyConfigurableInterpreterList.getInstance(project).model PyAddTargetBasedSdkDialog.show( project, module, diff --git a/python/src/com/jetbrains/python/sdk/PySdkExt.kt b/python/src/com/jetbrains/python/sdk/PySdkExt.kt index 7cd65e3b38e3..cefa6d13a3d3 100644 --- a/python/src/com/jetbrains/python/sdk/PySdkExt.kt +++ b/python/src/com/jetbrains/python/sdk/PySdkExt.kt @@ -67,7 +67,7 @@ import java.nio.file.Paths import kotlin.io.path.div import kotlin.io.path.pathString -private data class TargetAndPath( +internal data class TargetAndPath( val target: TargetEnvironmentConfiguration?, val path: FullPathOnTarget?, ) @@ -127,19 +127,24 @@ fun detectSystemWideSdks( { it.homePath }).reversed()) } -private fun PythonSdkFlavor<*>.detectSdks( - module: Module?, - context: UserDataHolder, - targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?, - existingPaths: HashSet, -): List = +private fun PythonSdkFlavor<*>.detectSdks(module: Module?, + context: UserDataHolder, + targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?, + existingPaths: HashSet): List = + detectSdkPaths(module, context, targetModuleSitsOn, existingPaths) + .map { createDetectedSdk(it, targetModuleSitsOn?.asTargetConfig, this) } + + +internal fun PythonSdkFlavor<*>.detectSdkPaths(module: Module?, + context: UserDataHolder, + targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?, + existingPaths: HashSet): List = suggestLocalHomePaths(module, context) .mapNotNull { // If module sits on target, this target maps its path. if (targetModuleSitsOn == null) it.pathString else targetModuleSitsOn.getTargetPathIfLocalPathIsOnTarget(it) } .filter { TargetAndPath(targetModuleSitsOn?.asTargetConfig, it) !in existingPaths } - .map { createDetectedSdk(it, targetModuleSitsOn?.asTargetConfig, this) } fun resetSystemWideSdksDetectors() { PythonSdkFlavor.getApplicableFlavors(false).forEach(PythonSdkFlavor<*>::dropCaches) @@ -365,7 +370,7 @@ fun getInnerVirtualEnvRoot(sdk: Sdk): VirtualFile? { } } -private fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? { +internal fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): String? { // please don't forget to update com.jetbrains.python.inspections.PyInterpreterInspection.Visitor#getSuitableSdkFix // after changing this method @@ -385,7 +390,7 @@ private fun suggestAssociatedSdkName(sdkHome: String, associatedPath: String?): return "$baseSdkName ($associatedName)" } -private val Sdk.isSystemWide: Boolean +internal val Sdk.isSystemWide: Boolean get() = !PythonSdkUtil.isRemote(this) && !PythonSdkUtil.isVirtualEnv( this) && !PythonSdkUtil.isCondaVirtualEnv(this) diff --git a/python/src/com/jetbrains/python/sdk/PySdks.kt b/python/src/com/jetbrains/python/sdk/PySdks.kt index 11578cf2575a..69fee786b7fe 100644 --- a/python/src/com/jetbrains/python/sdk/PySdks.kt +++ b/python/src/com/jetbrains/python/sdk/PySdks.kt @@ -65,6 +65,19 @@ private suspend fun detectSystemWideSdksSuspended(module: Module?, { it.homePath }).reversed()) } +private suspend fun detectSystemWideInterpreters(module: Module?, + existingSdks: List, + target: TargetEnvironmentConfiguration? = null, + context: UserDataHolder): List { + if (module != null && module.isDisposed) return emptyList() + val effectiveTarget = target ?: module?.let { PythonInterpreterTargetEnvironmentFactory.getTargetModuleResidesOn(it) }?.asTargetConfig + val baseDirFromContext = context.getUserData(BASE_DIR) + return service().getOrDetectSdks(effectiveTarget, baseDirFromContext) + .filter { detectedSdk -> existingSdks.none(detectedSdk::isSameAs) } + .sortedWith(compareBy({ it.guessedLanguageLevel }, + { it.homePath }).reversed()) +} + private fun Sdk.isSameAs(another: Sdk): Boolean = targetEnvConfiguration == another.targetEnvConfiguration && homePath == another.homePath @@ -127,7 +140,7 @@ private fun tryFindBaseSdksOnTarget(targetEnvironmentConfiguration: TargetEnviro private val PYTHON_INTERPRETER_NAME_UNIX_PATTERN = Pattern.compile("python\\d(\\.\\d+)") -private fun Path.tryFindPythonBinaries(): List = +internal fun Path.tryFindPythonBinaries(): List = runCatching { Files.list(this).filter(Path::looksLikePythonBinary).collect(Collectors.toList()) }.getOrElse { emptyList() } private fun Path.looksLikePythonBinary(): Boolean = diff --git a/python/src/com/jetbrains/python/sdk/add/target/PyAddTargetBasedSdkDialog.kt b/python/src/com/jetbrains/python/sdk/add/target/PyAddTargetBasedSdkDialog.kt index 7a7dcc8c2346..00365005a444 100644 --- a/python/src/com/jetbrains/python/sdk/add/target/PyAddTargetBasedSdkDialog.kt +++ b/python/src/com/jetbrains/python/sdk/add/target/PyAddTargetBasedSdkDialog.kt @@ -29,7 +29,7 @@ class PyAddTargetBasedSdkDialog private constructor(private val project: Project centerPanel = PyAddTargetBasedSdkPanel(project, module, existingSdks, targetEnvironmentConfiguration?.let { { it } }, config = PythonLanguageRuntimeConfiguration(), introspectable = null).apply { - Disposer.register(disposable, this) + Disposer.register(disposable, this) } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt index 03bfb3bec8ba..c0bedbd645bf 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt @@ -1,11 +1,9 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2 -import com.intellij.execution.target.FullPathOnTarget import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.observable.util.notEqualsTo import com.intellij.openapi.projectRoots.Sdk @@ -16,24 +14,18 @@ import com.intellij.ui.dsl.builder.bindItem import com.intellij.ui.layout.predicate import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.add.WslContext import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import com.jetbrains.python.statistics.InterpreterCreationMode -import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce -import kotlin.time.Duration.Companion.seconds @OptIn(FlowPreview::class) -class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { +class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) { private lateinit var envComboBox: ComboBox - private val selectedEnvironment = propertyGraph.property(null) private val lastLoadedConda = propertyGraph.property("") - private val loadingCondaEnvironments = MutableStateFlow(value = false) override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { @@ -41,16 +33,16 @@ class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) validationRequestor, message("sdk.create.conda.executable.path"), message("sdk.create.conda.missing.text"), - createInstallCondaFix(presenter)) - .displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext) + createInstallCondaFix(model)) + .displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext) row(message("sdk.create.custom.env.creation.type")) { - val condaEnvironmentsLoaded = loadingCondaEnvironments.predicate(presenter.scope) { !it } + val condaEnvironmentsLoaded = model.condaEnvironmentsLoading.predicate(model.scope) { !it } envComboBox = comboBox(emptyList(), CondaEnvComboBoxListCellRenderer()) - .bindItem(selectedEnvironment) - .displayLoaderWhen(loadingCondaEnvironments, makeTemporaryEditable = true, - scope = presenter.scope, uiContext = presenter.uiContext) + .bindItem(state.selectedCondaEnv) + .displayLoaderWhen(model.condaEnvironmentsLoading, makeTemporaryEditable = true, + scope = model.scope, uiContext = model.uiContext) .component link(message("sdk.create.custom.conda.refresh.envs"), action = { onReloadCondaEnvironments() }) @@ -60,73 +52,67 @@ class CondaExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) } private fun onReloadCondaEnvironments() { - val modalityState = ModalityState.current().asContextElement() - state.scope.launch(Dispatchers.EDT + modalityState) { - reloadCondaEnvironments(presenter.condaExecutableOnTarget) - } - } - - private suspend fun reloadCondaEnvironments(condaExecutableOnTarget: FullPathOnTarget) { - try { - loadingCondaEnvironments.value = true - val commandExecutor = presenter.createExecutor() - val environments = PyCondaEnv.getEnvs(commandExecutor, condaExecutableOnTarget) - envComboBox.removeAllItems() - val envs = environments.getOrLogException(LOG) ?: emptyList() - selectedEnvironment.set(envs.firstOrNull()) - envs.forEach(envComboBox::addItem) - lastLoadedConda.set(state.condaExecutable.get()) - } - finally { - loadingCondaEnvironments.value = false + model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) { + model.condaEnvironmentsLoading.value = true + model.detectCondaEnvironments() + model.condaEnvironmentsLoading.value = false } } override fun onShown() { - val modalityState = ModalityState.current().asContextElement() - state.scope.launch(start = CoroutineStart.UNDISPATCHED) { - presenter.currentCondaExecutableFlow - .debounce(1.seconds) - .collectLatest { condaExecutablePath -> - withContext(Dispatchers.EDT + modalityState) { - val pathOnTarget = condaExecutablePath?.let { presenter.getPathOnTarget(it) } - if (pathOnTarget != null) { - reloadCondaEnvironments(pathOnTarget) - } - else { - loadingCondaEnvironments.value = false - } - } - } - } - - state.scope.launch(start = CoroutineStart.UNDISPATCHED) { - presenter.currentCondaExecutableFlow.collectLatest { - loadingCondaEnvironments.value = true + model.scope.launch(start = CoroutineStart.UNDISPATCHED) { + model.condaEnvironments.collectLatest { environments -> + envComboBox.removeAllItems() + environments.forEach(envComboBox::addItem) } } - state.scope.launch(start = CoroutineStart.UNDISPATCHED) { - presenter.detectingCondaExecutable.collectLatest { isDetecting -> - if (isDetecting) loadingCondaEnvironments.value = true - } - } + + //model.scope.launch(start = CoroutineStart.UNDISPATCHED) { + // presenter.currentCondaExecutableFlow + // .debounce(1.seconds) + // .collectLatest { condaExecutablePath -> + // withContext(Dispatchers.EDT + modalityState) { + // val pathOnTarget = condaExecutablePath?.let { presenter.getPathOnTarget(it) } + // if (pathOnTarget != null) { + // reloadCondaEnvironments(pathOnTarget) + // } + // else { + // loadingCondaEnvironments.value = false + // } + // } + // } + //} + + //state.scope.launch(start = CoroutineStart.UNDISPATCHED) { + // presenter.currentCondaExecutableFlow.collectLatest { + // loadingCondaEnvironments.value = true + // } + //} + // + //state.scope.launch(start = CoroutineStart.UNDISPATCHED) { + // presenter.detectingCondaExecutable.collectLatest { isDetecting -> + // if (isDetecting) loadingCondaEnvironments.value = true + // } + //} } override fun getOrCreateSdk(): Sdk { - return presenter.selectCondaEnvironment(selectedEnvironment.get()!!.envIdentity) + return model.selectCondaEnvironment(state.selectedCondaEnv.get()!!.envIdentity) } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() - val identity = selectedEnvironment.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() + val identity = model.state.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv val selectedConda = if (identity?.isBase == true) InterpreterType.BASE_CONDA else InterpreterType.CONDAVENV return InterpreterStatisticsInfo(selectedConda, statisticsTarget, false, false, true, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, InterpreterCreationMode.CUSTOM) } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt index 87bab778f207..bc13157828d1 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt @@ -20,9 +20,8 @@ import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType import java.io.File -class CondaNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { +class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { - private val envName = propertyGraph.property("") private lateinit var pythonVersion: ObservableMutableProperty private lateinit var versionComboBox: ComboBox @@ -36,34 +35,36 @@ class CondaNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Pyt } row(message("sdk.create.custom.conda.env.name")) { textField() - .bindText(envName) + .bindText(model.state.newCondaEnvName) } - executableSelector(state.condaExecutable, + executableSelector(model.state.condaExecutable, validationRequestor, message("sdk.create.conda.executable.path"), message("sdk.create.conda.missing.text"), - createInstallCondaFix(presenter)) - .displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext) + createInstallCondaFix(model)) + .displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext) } } override fun onShown() { - envName.set(state.projectPath.get().substringAfterLast(File.separator)) + model.state.newCondaEnvName.set(model.projectPath.get().substringAfterLast(File.separator)) } override fun getOrCreateSdk(): Sdk? { - return presenter.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), envName.get())) + return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get())) } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() // todo fix for wsl return InterpreterStatisticsInfo(InterpreterType.CONDAVENV, statisticsTarget, false, false, false, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, // todo fix for wsl InterpreterCreationMode.CUSTOM) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt index 0d0450b78e97..f99ff1673e6c 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt @@ -2,90 +2,89 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.ide.util.PropertiesComponent -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.asContextElement import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.validation.DialogValidationRequestor import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.intellij.util.text.nullize import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.add.WslContext -import com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable import com.jetbrains.python.sdk.pipenv.pipEnvPath import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress import com.jetbrains.python.statistics.InterpreterCreationMode -import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class PipEnvNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { - private val executable = propertyGraph.property(UNKNOWN_EXECUTABLE) - private val basePythonVersion = propertyGraph.property(initial = null) - private lateinit var pipEnvPathField: TextFieldWithBrowseButton - private lateinit var basePythonComboBox: ComboBox +class PipEnvNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { + + private lateinit var basePythonComboBox: PythonInterpreterComboBox override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { row(message("sdk.create.custom.base.python")) { - basePythonComboBox = pythonInterpreterComboBox(basePythonVersion, - presenter, - presenter.basePythonSdksFlow, - presenter::addBasePythonInterpreter) + basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter, + model, + model::addInterpreter, + model.interpreterLoading) .align(Align.FILL) .component } - pipEnvPathField = executableSelector(executable, - validationRequestor, - message("sdk.create.custom.pipenv.path"), - message("sdk.create.custom.pipenv.missing.text")).component + executableSelector(model.state.pipenvExecutable, + validationRequestor, + message("sdk.create.custom.pipenv.path"), + message("sdk.create.custom.pipenv.missing.text")).component } } override fun onShown() { - val savedPath = PropertiesComponent.getInstance().pipEnvPath - if (savedPath != null) { - executable.set(savedPath) - } - else { - val modalityState = ModalityState.current().asContextElement() - state.scope.launch(Dispatchers.IO) { - val detectedExecutable = detectPipEnvExecutable() - withContext(Dispatchers.EDT + modalityState) { - detectedExecutable?.let { executable.set(it.path) } - } - } - } + basePythonComboBox.setItems(model.baseInterpreters) + + //val savedPath = PropertiesComponent.getInstance().pipEnvPath + //if (savedPath != null) { + // model.state.pipenvExecutable.set(savedPath) + //} + //else { + // val modalityState = ModalityState.current().asContextElement() + // model.scope.launch(Dispatchers.IO) { + // val detectedExecutable = detectPipEnvExecutable() + // withContext(Dispatchers.EDT + modalityState) { + // detectedExecutable?.let { model.state.pipenvExecutable.set(it.path) } + // } + // } + //} } override fun getOrCreateSdk(): Sdk? { - PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize() - val baseSdk = installBaseSdk(basePythonVersion.get()!!, state.allSdks.get()) ?: return null - val newSdk = setupPipEnvSdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(), - baseSdk.homePath, false)!! + if (model is PythonLocalAddInterpreterModel) { + PropertiesComponent.getInstance().pipEnvPath = model.state.pipenvExecutable.get().nullize() + } + + // todo think about better error handling + val selectedBasePython = model.state.baseInterpreter.get()!! + val homePath = model.installPythonIfNeeded(selectedBasePython) + + val newSdk = setupPipEnvSdkUnderProgress(null, null, + model.baseSdks, + model.projectPath.get(), + homePath, false)!! SdkConfigurationUtil.addSdk(newSdk) return newSdk } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() // todo fix for wsl return InterpreterStatisticsInfo(InterpreterType.PIPENV, statisticsTarget, false, false, false, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, // todo fix for wsl InterpreterCreationMode.CUSTOM) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt index a56463f2d4ab..511c831d5715 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt @@ -2,46 +2,35 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.ide.util.PropertiesComponent -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.asContextElement import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil -import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.validation.DialogValidationRequestor import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.intellij.util.text.nullize import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.add.WslContext -import com.jetbrains.python.sdk.poetry.detectPoetryExecutable import com.jetbrains.python.sdk.poetry.poetryPath import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress import com.jetbrains.python.statistics.InterpreterCreationMode -import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { +class PoetryNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { + + private lateinit var basePythonComboBox: PythonInterpreterComboBox - val executable = propertyGraph.property(UNKNOWN_EXECUTABLE) - private val basePythonVersion = propertyGraph.property(initial = null) - private lateinit var basePythonComboBox: ComboBox override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { row(message("sdk.create.custom.base.python")) { - basePythonComboBox = pythonInterpreterComboBox(basePythonVersion, - presenter, - presenter.basePythonSdksFlow, - presenter::addBasePythonInterpreter) - .align(Align.FILL) - .component + basePythonComboBox = pythonInterpreterComboBox(model.state.baseInterpreter, + model, + model::addInterpreter, + model.interpreterLoading) + .align(Align.FILL) + .component } - executableSelector(executable, + executableSelector(model.state.poetryExecutable, validationRequestor, message("sdk.create.custom.poetry.path"), message("sdk.create.custom.poetry.missing.text")).component @@ -49,39 +38,51 @@ class PoetryNewEnvironmentCreator(presenter: PythonAddInterpreterPresenter) : Py } override fun onShown() { - val savedPath = PropertiesComponent.getInstance().poetryPath - if (savedPath != null) { - executable.set(savedPath) - } - else { - val modalityState = ModalityState.current().asContextElement() - state.scope.launch(Dispatchers.IO) { - val poetryExecutable = detectPoetryExecutable() - withContext(Dispatchers.EDT + modalityState) { - poetryExecutable?.let { executable.set(it.path) } - } - } - } + basePythonComboBox.setItems(model.baseInterpreters) + + //val savedPath = PropertiesComponent.getInstance().poetryPath + //if (savedPath != null) { + // model.state.poetryExecutable.set(savedPath) + //} + //else { + // val modalityState = ModalityState.current().asContextElement() + // model.scope.launch(Dispatchers.IO) { + // val poetryExecutable = detectPoetryExecutable() + // withContext(Dispatchers.EDT + modalityState) { + // poetryExecutable?.let { model.state.poetryExecutable.set(it.path) } + // } + // } + //} } override fun getOrCreateSdk(): Sdk? { - PropertiesComponent.getInstance().poetryPath = executable.get().nullize() - val baseSdk = installBaseSdk(basePythonVersion.get()!!, state.allSdks.get()) ?: return null - val newSdk = setupPoetrySdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(), - baseSdk.homePath, false)!! + if (model is PythonLocalAddInterpreterModel) { + PropertiesComponent.getInstance().poetryPath = model.state.poetryExecutable.get().nullize() + } + + val selectedBasePython = model.state.baseInterpreter.get()!! + val homePath = model.installPythonIfNeeded(selectedBasePython) + + val newSdk = setupPoetrySdkUnderProgress(null, + null, + model.baseSdks, + model.projectPath.get(), + homePath, false)!! SdkConfigurationUtil.addSdk(newSdk) return newSdk } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() // todo fix for wsl return InterpreterStatisticsInfo(InterpreterType.POETRY, statisticsTarget, false, false, false, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, // todo fix for wsl InterpreterCreationMode.CUSTOM) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt index 21b357641fee..72555f34159e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -1,56 +1,129 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2 +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.equalsTo import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.validation.DialogValidationRequestor +import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED +import com.intellij.openapi.ui.validation.and import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bind +import com.intellij.ui.dsl.builder.bindItem +import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets.LOCAL_MACHINE +import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.awt.Component -class PythonAddCustomInterpreter(presenter: PythonAddInterpreterPresenter) { +class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel) { - //private lateinit var targetSelector: ComboBox + private val propertyGraph = model.propertyGraph + private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW) + private val _createNew = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.CREATE_NEW) + private val _selectExisting = propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.SELECT_EXISTING) + private val newInterpreterManager = propertyGraph.property(VIRTUALENV) + private val existingInterpreterManager = propertyGraph.property(PYTHON) - private val targets = mapOf( - LOCAL_MACHINE to PythonLocalEnvironmentCreator(presenter), + private lateinit var component: Component + + private val newInterpreterCreators = mapOf( + VIRTUALENV to PythonNewVirtualenvCreator(model), + CONDA to CondaNewEnvironmentCreator(model), + PIPENV to PipEnvNewEnvironmentCreator(model), + POETRY to PoetryNewEnvironmentCreator(model), ) + private val existingInterpreterSelectors = mapOf( + PYTHON to PythonExistingEnvironmentSelector(model), + CONDA to CondaExistingEnvironmentSelector(model), + ) + + private val currentSdkManager: PythonAddEnvironment + get() { + return if (_selectExisting.get()) existingInterpreterSelectors[existingInterpreterManager.get()]!! + else newInterpreterCreators[newInterpreterManager.get()]!! + } + + fun buildPanel(outerPanel: Panel, validationRequestor: DialogValidationRequestor) { + with(model) { + navigator.selectionMethod = selectionMethod + navigator.newEnvManager = newInterpreterManager + navigator.existingEnvManager = existingInterpreterManager + } + + // todo delete this. testing busy state + //existingInterpreterManager.afterChange { + // model.scope.launch { + // model.interpreterLoading.value = true + // delay(5000) + // model.interpreterLoading.value = false + // } + //} + with(outerPanel) { + buttonsGroup { + row(message("sdk.create.custom.env.creation.type")) { + val newRadio = radioButton(message("sdk.create.custom.generate.new"), PythonInterpreterSelectionMethod.CREATE_NEW).onChanged { + selectionMethod.set( + if (it.isSelected) PythonInterpreterSelectionMethod.CREATE_NEW else PythonInterpreterSelectionMethod.SELECT_EXISTING) + }.component - // todo uncomment for all available targets - //row(message("sdk.create.custom.develop.on")) { - // targetSelector = comboBox(targets.keys, PythonEnvironmentComboBoxRenderer()) - // .widthGroup("env_aligned") - // .component - //} - //targets.forEach { target -> - // rowsRange { - // target.value.buildPanel(this) - // }.visibleIf(targetSelector.selectedValueMatches { it == target.key }) - //} + val existingRadio = radioButton(message("sdk.create.custom.select.existing"), PythonInterpreterSelectionMethod.SELECT_EXISTING).component + selectionMethod.afterChange { + newRadio.isSelected = it == PythonInterpreterSelectionMethod.CREATE_NEW + existingRadio.isSelected = it == PythonInterpreterSelectionMethod.SELECT_EXISTING + } + } + }.bind({ selectionMethod.get() }, { selectionMethod.set(it) }) - rowsRange { - targets[LOCAL_MACHINE]!!.buildPanel(this, validationRequestor) + row(message("sdk.create.custom.type")) { + comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer()) + .bindItem(newInterpreterManager) + .widthGroup("env_aligned") + .visibleIf(_createNew) + + comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer()) + .bindItem(existingInterpreterManager) + .widthGroup("env_aligned") + .visibleIf(_selectExisting) } + + newInterpreterCreators.forEach { (type, creator) -> + rowsRange { + creator.buildOptions(this, + validationRequestor + and WHEN_PROPERTY_CHANGED(selectionMethod) + and WHEN_PROPERTY_CHANGED(newInterpreterManager)) + }.visibleIf(_createNew and newInterpreterManager.equalsTo(type)) + } + + existingInterpreterSelectors.forEach { (type, selector) -> + rowsRange { + selector.buildOptions(this, + validationRequestor + and WHEN_PROPERTY_CHANGED(selectionMethod) + and WHEN_PROPERTY_CHANGED(existingInterpreterManager)) + }.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type)) + } + } } + fun onShown() { - targets.values.forEach(PythonLocalEnvironmentCreator::onShown) + newInterpreterCreators.values.forEach(PythonAddEnvironment::onShown) + existingInterpreterSelectors.values.forEach(PythonAddEnvironment::onShown) } - fun getSdk(): Sdk? { - // todo uncomment for all available targets - //return targets[targetSelector.selectedItem]!!.getSdk() - return targets[LOCAL_MACHINE]!!.getSdk() - } + fun getSdk(): Sdk? = currentSdkManager.getOrCreateSdk() fun createStatisticsInfo(): InterpreterStatisticsInfo { - // todo uncomment for all available targets - //return targets[targetSelector.selectedItem]!!.createStatisticsInfo() - return targets[LOCAL_MACHINE]!!.createStatisticsInfo() + return currentSdkManager.createStatisticsInfo(PythonInterpreterCreationTargets.LOCAL_MACHINE) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt index fcfce2067490..8a7144d5eee4 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterPresenter.kt @@ -34,7 +34,7 @@ import java.nio.file.InvalidPathException import java.nio.file.Path import kotlin.coroutines.CoroutineContext -private val emptyContext: UserDataHolder by lazy { UserDataHolderBase() } +val emptyContext: UserDataHolder by lazy { UserDataHolderBase() } internal fun PythonAddInterpreterPresenter.tryGetVirtualFile(pathOnTarget: FullPathOnTarget): VirtualFile? { val mapper = targetEnvironmentConfiguration?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) } @@ -61,7 +61,10 @@ internal fun PythonAddInterpreterPresenter.setupVirtualenv(venvPath: Path, proje * @param state is the model for this presented in Model-View-Presenter pattern */ @OptIn(ExperimentalCoroutinesApi::class) -class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val uiContext: CoroutineContext) { +open class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val uiContext: CoroutineContext) { + + lateinit var controller: PythonAddInterpreterModel + val scope: CoroutineScope get() = state.scope @@ -234,7 +237,7 @@ class PythonAddInterpreterPresenter(val state: PythonAddInterpreterState, val ui data class ProjectPathWithContext(val projectPath: String, val context: ProjectLocationContext) companion object { - private val LOG = logger() + val LOG = logger() private fun MutableStateFlow>.addDetectedSdk(targetPath: String, targetEnvironmentConfiguration: TargetEnvironmentConfiguration?) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt index 007677f17af1..dc8635c4e230 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt @@ -8,12 +8,12 @@ import com.intellij.openapi.projectRoots.Sdk import kotlinx.coroutines.CoroutineScope class PythonAddInterpreterState( - val propertyGraph: PropertyGraph, + val propertyGraph: PropertyGraph, // todo move to presenter val projectPath: ObservableProperty, val scope: CoroutineScope, - val basePythonSdks: ObservableMutableProperty>, - val allExistingSdks: ObservableMutableProperty>, - val installableSdks: ObservableMutableProperty>, + val basePythonSdks: ObservableMutableProperty>, // todo replace with flow, local properties for every creator + val allExistingSdks: ObservableMutableProperty>, // todo merge with allSdks, replace with flow and local properties + val installableSdks: ObservableMutableProperty>, // todo not needed val selectedVenv: ObservableMutableProperty, val condaExecutable: ObservableMutableProperty, ) { diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt index 80d13dd37910..3178db1fb02a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt @@ -13,10 +13,7 @@ import com.intellij.openapi.observable.util.or import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.TopGap -import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.* import com.jetbrains.python.PyBundle.message import com.jetbrains.python.configuration.PyConfigurableInterpreterList import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo @@ -49,18 +46,9 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty, private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV) private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA) private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM) - - private val allExistingSdks = propertyGraph.property>(emptyList()) - private val basePythonSdks = propertyGraph.property>(emptyList()) - private val installableSdks = propertyGraph.property>(emptyList()) - private val pythonBaseVersion = propertyGraph.property(null) - private val selectedVenv = propertyGraph.property(null) - - private val condaExecutable = propertyGraph.property("") private var venvHint = propertyGraph.property("") - private lateinit var pythonBaseVersionComboBox: ComboBox - + private lateinit var pythonBaseVersionComboBox: PythonInterpreterComboBox private var initialized = false private fun updateVenvLocationHint() { @@ -69,23 +57,18 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty, else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint")) } - val state = PythonAddInterpreterState(propertyGraph, - projectPath, - service().coroutineScope, - basePythonSdks, - allExistingSdks, - installableSdks, - selectedVenv, - condaExecutable) - - private lateinit var presenter: PythonAddInterpreterPresenter private lateinit var custom: PythonAddCustomInterpreter - + private lateinit var model: PythonMutableTargetAddInterpreterModel fun buildPanel(outerPanel: Panel) { - presenter = PythonAddInterpreterPresenter(state, uiContext = Dispatchers.EDT + ModalityState.current().asContextElement()) - presenter.navigator.selectionMode = selectedMode - custom = PythonAddCustomInterpreter(presenter) + //presenter = PythonAddInterpreterPresenter(state, uiContext = Dispatchers.EDT + ModalityState.current().asContextElement()) + model = PythonLocalAddInterpreterModel(service().coroutineScope, + Dispatchers.EDT + ModalityState.current().asContextElement(), projectPath) + model.navigator.selectionMode = selectedMode + //presenter.controller = model + + custom = PythonAddCustomInterpreter(model) + val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode) @@ -98,27 +81,26 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty, } row(message("sdk.create.python.version")) { - pythonBaseVersionComboBox = pythonInterpreterComboBox(pythonBaseVersion, - presenter, - presenter.basePythonSdksFlow, - presenter::addBasePythonInterpreter) + pythonBaseVersionComboBox = pythonInterpreterComboBox(model.state.baseInterpreter, + model, + model::addInterpreter, + model.interpreterLoading) .align(AlignX.FILL) .component }.visibleIf(_projectVenv) rowsRange { - executableSelector(state.condaExecutable, + executableSelector(model.state.condaExecutable, validationRequestor, message("sdk.create.conda.executable.path"), message("sdk.create.conda.missing.text"), - createInstallCondaFix(presenter)) - .displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext) + createInstallCondaFix(model)) + //.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext) }.visibleIf(_baseConda) - row("") { comment("").bindText(venvHint) - }.visibleIf(_projectVenv or (_baseConda and state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE))) + }.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.notEqualsTo(UNKNOWN_EXECUTABLE))) rowsRange { custom.buildPanel(this, validationRequestor) @@ -133,28 +115,27 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty, if (!initialized) { initialized = true val modalityState = ModalityState.current().asContextElement() - state.scope.launch(Dispatchers.EDT + modalityState) { - val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList() - val allValidSdks = withContext(Dispatchers.IO) { - ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks) - } - allExistingSdks.set(allValidSdks) - installableSdks.set(getSdksToInstall()) + model.scope.launch(Dispatchers.EDT + modalityState) { + model.initialize() + pythonBaseVersionComboBox.setItems(model.baseInterpreters) + custom.onShown() + updateVenvLocationHint() } - custom.onShown() - presenter.navigator.restoreLastState(onlyAllowedSelectionModes = allowedInterpreterTypes) + // todo don't forget allowedEnvs + model.navigator.restoreLastState() } } fun getSdk(): Sdk? { - presenter.navigator.saveLastState() + model.navigator.saveLastState() return when (selectedMode.get()) { - PROJECT_VENV -> presenter.setupVirtualenv(Path.of(projectPath.get(), ".venv"), - projectPath.get(), - pythonBaseVersion.get()!!) - BASE_CONDA -> presenter.selectCondaEnvironment(presenter.baseConda!!.envIdentity) + PROJECT_VENV -> model.setupVirtualenv(Path.of(projectPath.get(), ".venv"), // todo just keep venv path, all the rest is in the model + projectPath.get(), + //pythonBaseVersion.get()!!) + model.state.baseInterpreter.get()!!) + BASE_CONDA -> model.selectCondaEnvironment(model.state.baseCondaEnv.get()!!.envIdentity) CUSTOM -> custom.getSdk() } } @@ -166,14 +147,16 @@ class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty, false, false, false, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, InterpreterCreationMode.SIMPLE) BASE_CONDA -> InterpreterStatisticsInfo(InterpreterType.BASE_CONDA, InterpreterTarget.LOCAL, false, false, true, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, InterpreterCreationMode.SIMPLE) CUSTOM -> custom.createStatisticsInfo() } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt index b94c82b5448c..265f9657892a 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt @@ -7,38 +7,45 @@ import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.Panel import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.add.WslContext import com.jetbrains.python.statistics.InterpreterCreationMode -import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType -class PythonExistingEnvironmentSelector(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { +class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) { + + private lateinit var comboBox: PythonInterpreterComboBox override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) { with(panel) { row(message("sdk.create.custom.python.path")) { - pythonInterpreterComboBox(presenter.state.selectedVenv, - presenter, - presenter.allSdksFlow, - presenter::addPythonInterpreter) + comboBox = pythonInterpreterComboBox(model.state.selectedInterpreter, + model, + model::addInterpreter, + model.interpreterLoading) .align(Align.FILL) + .component } } } + override fun onShown() { + comboBox.setItems(model.allInterpreters) + } + override fun getOrCreateSdk(): Sdk { - val selectedSdk = state.selectedVenv.get() ?: error("Unknown sdk selected") - return setupSdkIfDetected(selectedSdk, state.allSdks.get()) + // todo error handling, nullability issues + return setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!! } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() // todo fix for wsl return InterpreterStatisticsInfo(InterpreterType.REGULAR, statisticsTarget, false, false, true, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, // todo fix for wsl InterpreterCreationMode.CUSTOM) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt index 7f3487d260eb..c70b91dec60e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt @@ -23,17 +23,19 @@ class PythonLocalEnvironmentCreator(val presenter: PythonAddInterpreterPresenter private val newInterpreterManager = propertyGraph.property(VIRTUALENV) private val existingInterpreterManager = propertyGraph.property(PYTHON) - private val newInterpreterCreators = mapOf( - VIRTUALENV to PythonNewVirtualenvCreator(presenter), - CONDA to CondaNewEnvironmentCreator(presenter), - PIPENV to PipEnvNewEnvironmentCreator(presenter), - POETRY to PoetryNewEnvironmentCreator(presenter), - ) + private val newInterpreterCreators = emptyMap() + //private val newInterpreterCreators = mapOf( + // VIRTUALENV to PythonNewVirtualenvCreator(presenter), + // CONDA to CondaNewEnvironmentCreator(presenter), + // PIPENV to PipEnvNewEnvironmentCreator(presenter), + // POETRY to PoetryNewEnvironmentCreator(presenter), + //) - private val existingInterpreterSelectors = mapOf( - PYTHON to PythonExistingEnvironmentSelector(presenter), - CONDA to CondaExistingEnvironmentSelector(presenter), - ) + private val existingInterpreterSelectors = emptyMap() + //private val existingInterpreterSelectors = mapOf( + // PYTHON to PythonExistingEnvironmentSelector(presenter), + // CONDA to CondaExistingEnvironmentSelector(presenter), + //) private val currentSdkManager: PythonAddEnvironment get() { @@ -68,33 +70,33 @@ class PythonLocalEnvironmentCreator(val presenter: PythonAddInterpreterPresenter row(message("sdk.create.custom.type")) { comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer()) - .bindItem(newInterpreterManager) + //.bindItem(newInterpreterManager) .widthGroup("env_aligned") .visibleIf(_createNew) comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer()) - .bindItem(existingInterpreterManager) + //.bindItem(existingInterpreterManager) .widthGroup("env_aligned") .visibleIf(_selectExisting) } - newInterpreterCreators.forEach { (type, creator) -> - rowsRange { - creator.buildOptions(this, - validationRequestor - and WHEN_PROPERTY_CHANGED(selectionMethod) - and WHEN_PROPERTY_CHANGED(newInterpreterManager)) - }.visibleIf(_createNew and newInterpreterManager.equalsTo(type)) - } - - existingInterpreterSelectors.forEach { (type, selector) -> - rowsRange { - selector.buildOptions(this, - validationRequestor - and WHEN_PROPERTY_CHANGED(selectionMethod) - and WHEN_PROPERTY_CHANGED(existingInterpreterManager)) - }.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type)) - } + //newInterpreterCreators.forEach { (type, creator) -> + // rowsRange { + // creator.buildOptions(this, + // validationRequestor + // and WHEN_PROPERTY_CHANGED(selectionMethod) + // and WHEN_PROPERTY_CHANGED(newInterpreterManager)) + // }.visibleIf(_createNew and newInterpreterManager.equalsTo(type)) + //} + // + //existingInterpreterSelectors.forEach { (type, selector) -> + // rowsRange { + // selector.buildOptions(this, + // validationRequestor + // and WHEN_PROPERTY_CHANGED(selectionMethod) + // and WHEN_PROPERTY_CHANGED(existingInterpreterManager)) + // }.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type)) + //} } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt index 19de70e4c8f0..be4e82e29c59 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt @@ -5,59 +5,47 @@ import com.intellij.execution.wsl.WslPath.Companion.parseWindowsUncPath import com.intellij.openapi.application.EDT import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.diagnostic.getOrLogException import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.TextFieldWithBrowseButton import com.intellij.openapi.ui.validation.DialogValidationRequestor import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED import com.intellij.openapi.ui.validation.and import com.intellij.openapi.util.SystemInfo -import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.OSAgnosticPathUtil import com.intellij.ui.components.ActionLink import com.intellij.ui.dsl.builder.* import com.intellij.ui.dsl.builder.components.validationTooltip -import com.intellij.util.PathUtil import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.jetbrains.python.PyBundle.message import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector -import com.jetbrains.python.sdk.PySdkSettings -import com.jetbrains.python.sdk.add.LocalContext -import com.jetbrains.python.sdk.add.ProjectLocationContext -import com.jetbrains.python.sdk.add.WslContext import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING +import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON import com.jetbrains.python.statistics.InterpreterCreationMode -import com.jetbrains.python.statistics.InterpreterTarget import com.jetbrains.python.statistics.InterpreterType -import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.nio.file.InvalidPathException import java.nio.file.Path import java.nio.file.Paths import kotlin.io.path.exists -class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : PythonAddEnvironment(presenter) { - private val location = propertyGraph.property("") - private val inheritSitePackages = propertyGraph.property(false) - private val makeAvailable = propertyGraph.property(false) +class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) { + private lateinit var versionComboBox: PythonInterpreterComboBox + private val locationValidationFailed = propertyGraph.property(false) private val locationValidationMessage = propertyGraph.property("Current location already exists") - private val basePythonVersion = propertyGraph.property(initial = null) - private lateinit var versionComboBox: ComboBox private var locationModified = false private var suggestedVenvName: String = "" private var suggestedLocation: Path = Path.of("") + private val pythonInVenvPath: Path get() { return when { - SystemInfo.isWindows && presenter.projectLocationContext !is WslContext -> Paths.get("Scripts", "python.exe") + SystemInfo.isWindows -> Paths.get("Scripts", "python.exe") + //SystemInfo.isWindows && presenter.projectLocationContext !is WslContext -> Paths.get("Scripts", "python.exe") else -> Paths.get("bin", "python") } } @@ -66,33 +54,33 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt val firstFixLink = ActionLink(message("sdk.create.custom.venv.use.different.venv.link", ".venv1")) { PythonNewProjectWizardCollector.logSuggestedVenvDirFixUsed() val newPath = suggestedLocation.resolve(suggestedVenvName) - location.set(newPath.toString()) + model.state.venvPath.set(newPath.toString()) } val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) { PythonNewProjectWizardCollector.logExistingVenvFixUsed() - val sdkPath = Paths.get(location.get()).resolve(pythonInVenvPath).toString() - if (!presenter.state.allSdks.get().any { it.homePath == sdkPath }) { - presenter.addPythonInterpreter(sdkPath) - } - presenter.state.selectedVenvPath.set(sdkPath) - presenter.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PythonSupportedEnvironmentManagers.PYTHON) + val sdkPath = Paths.get(model.state.venvPath.get()).resolve(pythonInVenvPath).toString() + + val interpreter = model.findInterpreter(sdkPath) ?: model.addInterpreter(sdkPath) + + model.state.selectedInterpreter.set(interpreter) + model.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PYTHON) } with(panel) { row(message("sdk.create.custom.base.python")) { - versionComboBox = pythonInterpreterComboBox(basePythonVersion, - presenter, - presenter.basePythonSdksFlow, - presenter::addBasePythonInterpreter) - .align(Align.FILL) - .component + versionComboBox = pythonInterpreterComboBox(model.state.baseInterpreter, + model, + model::addInterpreter, + model.interpreterLoading) + .align(Align.FILL) + .component } row(message("sdk.create.custom.location")) { textFieldWithBrowseButton(message("sdk.create.custom.venv.location.browse.title"), fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()) - .bindText(location) + .bindText(model.state.venvPath) .whenTextChangedFromUi { locationModified = true } - .validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(location)) + .validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.venvPath)) .cellValidation { textField -> addInputRule("") { val pathExists = textField.isVisible && textField.doesPathExist() @@ -129,38 +117,65 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt row("") { checkBox(message("sdk.create.custom.inherit.packages")) - .bindSelected(inheritSitePackages) + .bindSelected(model.state.inheritSitePackages) } row("") { checkBox(message("sdk.create.custom.make.available")) - .bindSelected(makeAvailable) + .bindSelected(model.state.makeAvailable) } } - state.scope.launch(start = CoroutineStart.UNDISPATCHED) { - presenter.projectWithContextFlow.collectLatest { (projectPath, projectLocationContext) -> - withContext(presenter.uiContext) { - if (!locationModified) { - val suggestedVirtualEnvPath = runCatching { - suggestVirtualEnvPath(projectPath, projectLocationContext) - }.getOrLogException(LOG) - location.set(suggestedVirtualEnvPath.orEmpty()) - } - } + + model.projectPath.afterChange { + if (!locationModified) { + val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue + model.state.venvPath.set(suggestedVirtualEnvPath) } } + + // todo venv path suggestion from controller + //model.scope.launch(start = CoroutineStart.UNDISPATCHED) { + // presenter.projectWithContextFlow.collectLatest { (projectPath, projectLocationContext) -> + // withContext(presenter.uiContext) { + // if (!locationModified) { + // val suggestedVirtualEnvPath = runCatching { + // suggestVirtualEnvPath(projectPath, projectLocationContext) + // }.getOrLogException(LOG) + // location.set(suggestedVirtualEnvPath.orEmpty()) + // } + // } + // } + //} } override fun onShown() { val modalityState = ModalityState.current().asContextElement() - state.scope.launch(Dispatchers.EDT + modalityState) { - val basePath = suggestVirtualEnvPath(state.projectPath.get(), presenter.projectLocationContext) - location.set(basePath) - } - } + model.scope.launch(Dispatchers.EDT + modalityState) { - private suspend fun suggestVirtualEnvPath(projectPath: String, projectLocationContext: ProjectLocationContext): String = - projectLocationContext.suggestVirtualEnvPath(projectPath) + val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue + model.state.venvPath.set(suggestedVirtualEnvPath) + + //val projectBasePath = state.projectPath.get() + + //val basePath = if (model is PythonLocalAddInterpreterModel) + // withContext(Dispatchers.IO) { + // FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath)) + // } + //else { + // "" + // // todo fix for wsl and targets + // //val suggestedVirtualEnvName = PathUtil.getFileName(projectBasePath) + // //val userHome = presenter.projectLocationContext.fetchUserHomeDirectory() + // //userHome?.resolve(DEFAULT_VIRTUALENVS_DIR)?.resolve(suggestedVirtualEnvName)?.toString().orEmpty() + //} + // + //model.state.venvPath.set(basePath) + } + + versionComboBox.setItems(model.baseInterpreters) + + + } private fun suggestVenvName(currentName: String): String { val digitSuffix = currentName.takeLastWhile { it.isDigit() } @@ -170,7 +185,8 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt } override fun getOrCreateSdk(): Sdk? { - return presenter.setupVirtualenv(Path.of(location.get()), state.projectPath.get(), basePythonVersion.get()!!) + // todo remove project path, or move to controller + return model.setupVirtualenv((Path.of(model.state.venvPath.get())), model.projectPath.get(), model.state.baseInterpreter.get()!!) } companion object { @@ -184,22 +200,6 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt */ private const val DEFAULT_VIRTUALENVS_DIR = ".virtualenvs" - private suspend fun ProjectLocationContext.suggestVirtualEnvPath(projectBasePath: String?): String = - if (this is LocalContext) - withContext(Dispatchers.IO) { - FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectBasePath)) - } - else suggestVirtualEnvPathGeneral(projectBasePath) - - - /** - * The simplest case of [PySdkSettings.getPreferredVirtualEnvBasePath] implemented. - */ - private suspend fun ProjectLocationContext.suggestVirtualEnvPathGeneral(projectBasePath: String?): String { - val suggestedVirtualEnvName = projectBasePath?.let { PathUtil.getFileName(it) } ?: "venv" - val userHome = fetchUserHomeDirectory() - return userHome?.resolve(DEFAULT_VIRTUALENVS_DIR)?.resolve(suggestedVirtualEnvName)?.toString().orEmpty() - } /** * Checks if [this] field's text points to an existing file or directory. Calls [Path.exists] from EDT with care to avoid freezing the @@ -238,13 +238,15 @@ class PythonNewVirtualenvCreator(presenter: PythonAddInterpreterPresenter) : Pyt } override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo { - val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + //val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField() + val statisticsTarget = target.toStatisticsField() // todo fix for wsl return InterpreterStatisticsInfo(InterpreterType.VIRTUALENV, statisticsTarget, - inheritSitePackages.get(), - makeAvailable.get(), + model.state.inheritSitePackages.get(), + model.state.makeAvailable.get(), false, - presenter.projectLocationContext is WslContext, + //presenter.projectLocationContext is WslContext, + false, // todo fix for wsl InterpreterCreationMode.CUSTOM) } } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt index 6b439eced23e..5c7a3e618152 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonSdkComboBoxWithBrowseButtonEditor.kt @@ -1,8 +1,7 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2 import com.intellij.icons.AllIcons -import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.observable.util.addMouseHoverListener import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.ui.ComboBox @@ -13,13 +12,7 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.gridLayout.UnscaledGaps import com.intellij.ui.hover.HoverListener -import com.intellij.util.text.nullize import com.jetbrains.python.PyBundle.message -import com.jetbrains.python.sdk.PythonSdkType -import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.awt.Component import java.awt.Cursor import java.awt.event.ActionListener @@ -30,8 +23,8 @@ import javax.swing.ComboBoxEditor import javax.swing.JComponent import javax.swing.JLabel -class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox, - val presenter: PythonAddInterpreterPresenter, +class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox, + val controller: PythonAddInterpreterModel, onPathSelected: (String) -> Unit) : ComboBoxEditor { private val component = SimpleColoredComponent() private val panel: JComponent @@ -79,22 +72,32 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox, } }) + val browseAction = controller.createBrowseAction() + addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent?) { if (!isBusy) { - val currentBaseSdkVirtualFile = (_item as? Sdk)?.let { sdk -> - val currentBaseSdkPathOnTarget = sdk.homePath.nullize(nullizeSpaces = true) - currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) } - } - FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, - null, - currentBaseSdkVirtualFile) { file -> - val nioPath = file?.toNioPath() ?: return@chooseFile - val targetPath = presenter.getPathOnTarget(nioPath) - comboBox.setPathToSelectAfterModelUpdate(targetPath) - onPathSelected(targetPath) - } + // todo add interpreter to allSdks + browseAction()?.let { onPathSelected(it) } + + + + //onPathSelected(selectedInterpreter) + + //val currentBaseSdkVirtualFile = (_item as? Sdk)?.let { sdk -> + // val currentBaseSdkPathOnTarget = sdk.homePath.nullize(nullizeSpaces = true) + // currentBaseSdkPathOnTarget?.let { presenter.tryGetVirtualFile(it) } + //} + // + //FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, + // null, + // currentBaseSdkVirtualFile) { file -> + // val nioPath = file?.toNioPath() ?: return@chooseFile + // val targetPath = presenter.getPathOnTarget(nioPath) + // comboBox.setPathToSelectAfterModelUpdate(targetPath) + // onPathSelected(targetPath) + //} } } }) @@ -105,13 +108,6 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox, } panel.border = null - presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) { - presenter.detectingSdks.collectLatest { - withContext(presenter.uiContext) { - setBusy(it) - } - } - } } @@ -119,14 +115,22 @@ class PythonSdkComboBoxWithBrowseButtonEditor(val comboBox: ComboBox, if (_item == anObject) return _item = anObject component.clear() - if (anObject is Sdk) component.customizeForPythonSdk(anObject) + if (anObject is PythonSelectableInterpreter) component.customizeForPythonInterpreter(anObject) } - private fun setBusy(busy: Boolean) { + fun setBusy(busy: Boolean) { isBusy = busy iconLabel.icon = if (isBusy) AnimatedIcon.Default.INSTANCE else AllIcons.General.OpenDisk component.isEnabled = !isBusy comboBox.isEnabled = !isBusy + if (busy) { + component.clear() + component.append("Loading interpeterers") + } + else { + component.clear() + if (item is PythonSelectableInterpreter) component.customizeForPythonInterpreter(item as PythonSelectableInterpreter) + } } override fun getEditorComponent(): Component = panel diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt index 574fc84b5318..a175fb0d17a7 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/common.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -1,6 +1,7 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.sdk.add.v2 +import com.intellij.execution.target.TargetEnvironmentConfiguration import com.intellij.icons.AllIcons import com.intellij.notification.NotificationAction import com.intellij.notification.NotificationGroupManager @@ -17,12 +18,9 @@ import com.intellij.ui.dsl.builder.Panel import com.jetbrains.python.PyBundle.message import com.jetbrains.python.icons.PythonIcons import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo -import com.jetbrains.python.sdk.LOGGER -import com.jetbrains.python.sdk.PyDetectedSdk -import com.jetbrains.python.sdk.installSdkIfNeeded +import com.jetbrains.python.sdk.* import com.jetbrains.python.sdk.pipenv.PIPENV_ICON import com.jetbrains.python.sdk.poetry.POETRY_ICON -import com.jetbrains.python.sdk.setup import com.jetbrains.python.statistics.InterpreterTarget import kotlinx.coroutines.CoroutineScope import javax.swing.Icon @@ -38,12 +36,13 @@ interface PythonTargetEnvironmentInterpreterCreator { fun createStatisticsInfo(): InterpreterStatisticsInfo = throw NotImplementedError() } -abstract class PythonAddEnvironment(val presenter: PythonAddInterpreterPresenter) { - val state: PythonAddInterpreterState - get() = presenter.state +abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) { + + val state: AddInterpreterState + get() = model.state internal val propertyGraph - get() = state.propertyGraph + get() = model.propertyGraph abstract fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) open fun onShown() {} @@ -51,6 +50,13 @@ abstract class PythonAddEnvironment(val presenter: PythonAddInterpreterPresenter abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo } +abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTargetAddInterpreterModel) : PythonAddEnvironment(model) +abstract class PythonExistingEnvironmentConfigurator(model: PythonAddInterpreterModel) : PythonAddEnvironment(model) + + + + + enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Icon) { VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv), CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda), @@ -111,4 +117,20 @@ internal fun setupSdkIfDetected(sdk: Sdk, existingSdks: List): Sdk = when ( newSdk } else -> sdk +} + + + +internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? { + if (interpreter is ExistingSelectableInterpreter) return interpreter.sdk + + val homeDir = interpreter.homePath.virtualFileOnTarget(targetConfig) ?: return null // todo handle + val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), + homeDir, + PythonSdkType.getInstance(), + false, + null, // todo create additional data for target + null) ?: return null + SdkConfigurationUtil.addSdk(newSdk) + return newSdk } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/condaUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/condaUtils.kt index ff98cfffdb7a..daa5213ed98e 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/condaUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/condaUtils.kt @@ -25,23 +25,23 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity import com.jetbrains.python.sdk.flavors.conda.PyCondaFlavorData import kotlinx.coroutines.Dispatchers -internal val PythonAddInterpreterPresenter.condaExecutableOnTarget: String - @RequiresEdt get() = state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration) +@RequiresEdt +internal fun PythonAddInterpreterModel.createCondaCommand(): PyCondaCommand = + PyCondaCommand(state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration), + targetConfig = targetEnvironmentConfiguration) @RequiresEdt -internal fun PythonAddInterpreterPresenter.createCondaCommand(): PyCondaCommand = - PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration) - -@RequiresEdt -internal fun PythonAddInterpreterPresenter.createCondaEnvironment(request: NewCondaEnvRequest): Sdk? { +internal fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Sdk? { + val project = ProjectManager.getInstance().defaultProject + val existingSdks = this@createCondaEnvironment.existingSdks val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(), PyBundle.message("sdk.create.custom.conda.create.progress"), TaskCancellation.nonCancellable()) { createCondaCommand() .createCondaSdkAlongWithNewEnv(request, Dispatchers.EDT, - state.allExistingSdks.get(), - ProjectManager.getInstance().defaultProject).getOrNull() + existingSdks, + project).getOrNull() } ?: return null (sdk.sdkType as PythonSdkType).setupSdkPaths(sdk) @@ -49,25 +49,25 @@ internal fun PythonAddInterpreterPresenter.createCondaEnvironment(request: NewCo return sdk } -@RequiresEdt -internal fun PythonAddInterpreterPresenter.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk { - val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName) - if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk +//@RequiresEdt +//internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk { +// val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName) +// if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk +// +// val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(), +// PyBundle.message("sdk.create.custom.conda.create.progress"), +// TaskCancellation.nonCancellable()) { +// createCondaCommand().createCondaSdkFromExistingEnv(identity, +// state.allExistingSdks.get(), +// ProjectManager.getInstance().defaultProject) +// } +// +// (sdk.sdkType as PythonSdkType).setupSdkPaths(sdk) +// SdkConfigurationUtil.addSdk(sdk) +// return sdk +//} - val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(), - PyBundle.message("sdk.create.custom.conda.create.progress"), - TaskCancellation.nonCancellable()) { - createCondaCommand().createCondaSdkFromExistingEnv(identity, - state.allExistingSdks.get(), - ProjectManager.getInstance().defaultProject) - } - - (sdk.sdkType as PythonSdkType).setupSdkPaths(sdk) - SdkConfigurationUtil.addSdk(sdk) - return sdk -} - -private fun isCondaSdk(sdk: Sdk): Boolean = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data is PyCondaFlavorData +internal fun isCondaSdk(sdk: Sdk): Boolean = (sdk.sdkAdditionalData as? PythonSdkAdditionalData)?.flavorAndData?.data is PyCondaFlavorData @RequiresEdt internal fun PythonAddInterpreterPresenter.createExecutor(): TargetCommandExecutor = targetEnvironmentConfiguration.toExecutor() diff --git a/python/src/com/jetbrains/python/sdk/add/v2/dialogs.kt b/python/src/com/jetbrains/python/sdk/add/v2/dialogs.kt new file mode 100644 index 000000000000..29fd77ec3bb2 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/dialogs.kt @@ -0,0 +1,78 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.components.service +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED +import com.intellij.ui.AncestorListenerAdapter +import com.intellij.ui.dsl.builder.panel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.awt.Dimension +import javax.swing.BoxLayout +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.event.AncestorEvent + + +class PythonAddLocalInterpreterDialog(project: Project) : DialogWrapper(project) { + val outerPanel = JPanel().apply { + layout = BoxLayout(this, BoxLayout.X_AXIS) + preferredSize = Dimension(500, 250) // todo scale dimensions + } + + private lateinit var model: PythonLocalAddInterpreterModel + private lateinit var mainPanel: PythonAddCustomInterpreter + + init { + title = "Add Python Interpreter" + isResizable = true + init() + + outerPanel.addAncestorListener(object : AncestorListenerAdapter() { + override fun ancestorAdded(event: AncestorEvent?) { + val basePath = project.basePath!! + model = PythonLocalAddInterpreterModel(service().coroutineScope, + Dispatchers.EDT + ModalityState.current().asContextElement(), AtomicProperty(basePath)) + model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM) + mainPanel = PythonAddCustomInterpreter(model) + + val dialogPanel = panel { + mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath))) + } + + dialogPanel.registerValidators(myDisposable) { validations -> + val anyErrors = validations.entries.any { (key, value) -> key.isVisible && !value.okEnabled } + isOKActionEnabled = !anyErrors + } + + outerPanel.add(dialogPanel) + + model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) { + model.initialize() + mainPanel.onShown() + } + } + }) + + } + + + override fun doOKAction() { + super.doOKAction() + val sdk = mainPanel.getSdk() + if (sdk != null) { + val existing = ProjectJdkTable.getInstance().findJdk(sdk.name) + SdkConfigurationUtil.addSdk(sdk) + } + } + + override fun createCenterPanel(): JComponent = outerPanel +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/models.kt b/python/src/com/jetbrains/python/sdk/add/v2/models.kt new file mode 100644 index 000000000000..300777791461 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/models.kt @@ -0,0 +1,312 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.ide.util.PropertiesComponent +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.diagnostic.getOrLogException +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.io.FileUtil +import com.jetbrains.python.configuration.PyConfigurableInterpreterList +import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep +import com.jetbrains.python.psi.LanguageLevel +import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.add.ProjectLocationContexts +import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath +import com.jetbrains.python.sdk.add.v2.PythonAddInterpreterPresenter.ProjectPathWithContext +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity +import com.jetbrains.python.sdk.interpreters.detectSystemInterpreters +import com.jetbrains.python.sdk.pipenv.pipEnvPath +import com.jetbrains.python.sdk.poetry.poetryPath +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.nio.file.Path +import kotlin.coroutines.CoroutineContext + + +@OptIn(ExperimentalCoroutinesApi::class) +abstract class PythonAddInterpreterModel(val scope: CoroutineScope, val uiContext: CoroutineContext, projectPathProperty: ObservableProperty? = null) { + + val propertyGraph = PropertyGraph() + val navigator = PythonNewEnvironmentDialogNavigator() + open val state = AddInterpreterState(propertyGraph) + open val targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null + + val projectPath = projectPathProperty ?: propertyGraph.property("") // todo how to populate? + + internal val knownInterpreters: MutableStateFlow> = MutableStateFlow(emptyList()) + internal val detectedInterpreters: MutableStateFlow> = MutableStateFlow(emptyList()) + val manuallyAddedInterpreters: MutableStateFlow> = MutableStateFlow(emptyList()) + private var installable: List = emptyList() + val condaEnvironments: MutableStateFlow> = MutableStateFlow(emptyList()) + + var allInterpreters: StateFlow> = combine(knownInterpreters, + detectedInterpreters, + manuallyAddedInterpreters) { known, detected, added -> + added + known + detected + } + .stateIn(scope + uiContext, started = SharingStarted.Eagerly, initialValue = emptyList()) + + + val baseInterpreters: StateFlow> = allInterpreters + .mapLatest { + it.filter { it !is ExistingSelectableInterpreter || it.isSystemWide } + installable + } + .stateIn(scope + uiContext, started = SharingStarted.Eagerly, initialValue = emptyList()) + + + val interpreterLoading = MutableStateFlow(false) + val condaEnvironmentsLoading = MutableStateFlow(false) + + open fun createBrowseAction(): () -> String? = TODO() + + open suspend fun initialize() { + interpreterLoading.value = true + initInterpreterList() + detectCondaExecutable() + condaEnvironmentsLoading.value = true + detectCondaEnvironments() + condaEnvironmentsLoading.value = false + interpreterLoading.value = false + } + + + suspend fun detectCondaExecutable() { + withContext(Dispatchers.IO) { + val executor = targetEnvironmentConfiguration.toExecutor() + val suggestedCondaPath = runCatching { + suggestCondaPath(targetCommandExecutor = executor) + }.getOrLogException(PythonAddInterpreterPresenter.LOG) + val suggestedCondaLocalPath = suggestedCondaPath?.toLocalPathOn(targetEnvironmentConfiguration) + withContext(uiContext) { + state.condaExecutable.set(suggestedCondaLocalPath?.toString().orEmpty()) + } + + //val environments = suggestedCondaPath?.let { PyCondaEnv.getEnvs(executor, suggestedCondaPath).getOrLogException( + // PythonAddInterpreterPresenter.LOG) } + //baseConda = environments?.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } } + + } + } + suspend fun detectCondaEnvironments() { + withContext(Dispatchers.IO) { + val commandExecutor = targetEnvironmentConfiguration.toExecutor() + val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get()).getOrLogException(LOG) ?: emptyList() + val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } } + + withContext(uiContext) { + condaEnvironments.value = environments + state.baseCondaEnv.set(baseConda) + } + } + } + + + + suspend fun initInterpreterList() { + withContext(Dispatchers.IO) { + val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList() + val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks) + .map { ExistingSelectableInterpreter(it, PySdkUtil.getLanguageLevelForSdk(it), it.isSystemWide) } + + val languageLevels = allValidSdks.mapTo(HashSet()) { it.languageLevel } // todo add detected here + val filteredInstallable = getSdksToInstall() + .map { LanguageLevel.fromPythonVersion(it.versionString) to it } + .filter { it.first !in languageLevels } + .sortedByDescending { it.first } + .map { InstallableSelectableInterpreter(it.second) } + + val detected = runCatching { + detectSystemInterpreters(projectDir = null, null, targetEnvironmentConfiguration, existingSdks) + // todo check fix about appending recently used + //return@runCatching appendMostRecentlyUsedBaseSdk(detected, context.targetEnvironmentConfiguration) + //detected.filter { it.homePath !in savedPaths } + }.getOrLogException(PythonAddInterpreterPresenter.LOG) ?: emptyList() + + withContext(uiContext) { + installable = filteredInstallable + knownInterpreters.value = allValidSdks // todo check target? + detectedInterpreters.value = detected + } + + } + } + + open fun addInterpreter(path: String): PythonSelectableInterpreter = TODO() + + open fun suggestVenvPath(): String? = "" +} + + +abstract class PythonMutableTargetAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext, projectPathProperty: ObservableProperty? = null) + : PythonAddInterpreterModel(scope, uiContext, projectPathProperty) { + override val state: MutableTargetState = MutableTargetState(propertyGraph) + + override suspend fun initialize() { + super.initialize() + detectPoetryExecutable() + detectPipEnvExecutable() + } + + suspend fun detectPoetryExecutable() { + // todo this is local case, fix for targets + val savedPath = PropertiesComponent.getInstance().poetryPath + if (savedPath != null) { + state.poetryExecutable.set(savedPath) + } + else { + val modalityState = ModalityState.current().asContextElement() + scope.launch(Dispatchers.IO) { + val poetryExecutable = com.jetbrains.python.sdk.poetry.detectPoetryExecutable() + withContext(Dispatchers.EDT + modalityState) { + poetryExecutable?.let { state.poetryExecutable.set(it.path) } + } + } + } + } + + suspend fun detectPipEnvExecutable() { + // todo this is local case, fix for targets + val savedPath = PropertiesComponent.getInstance().pipEnvPath + if (savedPath != null) { + state.pipenvExecutable.set(savedPath) + } + else { + val modalityState = ModalityState.current().asContextElement() + scope.launch(Dispatchers.IO) { + val detectedExecutable = com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable() + withContext(Dispatchers.EDT + modalityState) { + detectedExecutable?.let { state.pipenvExecutable.set(it.path) } + } + } + } + } +} + + +open class PythonLocalAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext, projectPathProperty: ObservableProperty? = null) + : PythonMutableTargetAddInterpreterModel(scope, uiContext, projectPathProperty) { + + override suspend fun initialize() { + super.initialize() + + val mostRecentlyUsedBasePath = PySdkSettings.instance.preferredVirtualEnvBaseSdk + val interpreterToSelect = detectedInterpreters.value.find { it.homePath == mostRecentlyUsedBasePath } + ?: baseInterpreters.value + .filterIsInstance() + .maxByOrNull { it.languageLevel } + + if (interpreterToSelect != null) { + state.baseInterpreter.set(interpreterToSelect) + } + } + + override fun createBrowseAction(): () -> String? { + return { + var path: Path? = null + FileChooser.chooseFile(PythonSdkType.getInstance().homeChooserDescriptor, null, null) { file -> + path = file?.toNioPath() + } + path?.toString() + } + } + + override fun addInterpreter(path: String): PythonSelectableInterpreter { + val interpreter = ManuallyAddedSelectableInterpreter(path) + manuallyAddedInterpreters.value += interpreter + return interpreter + } + + override fun suggestVenvPath(): String? { + // todo should this be a coroutine? + return FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.get())) + } +} + +class PythonWslCapableLocalAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonMutableTargetAddInterpreterModel(scope, uiContext) { + + private val projectLocationContexts = ProjectLocationContexts() + private val _projectWithContextFlow: MutableStateFlow = + MutableStateFlow(projectPath.get().associateWithContext()) + + init { + projectPath.afterChange { projectPath -> + val context = projectLocationContexts.getProjectLocationContextFor(projectPath) + _projectWithContextFlow.value = ProjectPathWithContext(projectPath, context) + } + } + + private fun String.associateWithContext(): ProjectPathWithContext = + ProjectPathWithContext(projectPath = this, projectLocationContexts.getProjectLocationContextFor(projectPath = this)) +} + +class PythonSshAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonMutableTargetAddInterpreterModel(scope, uiContext) + +class PythonDockerAddInterpreterModel(scope: CoroutineScope, uiContext: CoroutineContext) : PythonAddInterpreterModel(scope, uiContext) + + + +// todo does it need target configuration +abstract class PythonSelectableInterpreter { + abstract val homePath: String +} + +class ExistingSelectableInterpreter(val sdk: Sdk, val languageLevel: LanguageLevel, val isSystemWide: Boolean) : PythonSelectableInterpreter() { + override val homePath = sdk.homePath!! // todo is it safe +} + + +class DetectedSelectableInterpreter(override val homePath: String, targetConfiguration: TargetEnvironmentConfiguration? = null) : PythonSelectableInterpreter() + +class ManuallyAddedSelectableInterpreter(override val homePath: String) : PythonSelectableInterpreter() +class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectableInterpreter() { + override val homePath: String = "" +} + + + + +class InterpreterSeparator(val text: String) : PythonSelectableInterpreter() { + override val homePath: String = "" +} + + + + + + +open class AddInterpreterState(propertyGraph: PropertyGraph) { + val selectedInterpreter: ObservableMutableProperty = propertyGraph.property(null) + val condaExecutable: ObservableMutableProperty = propertyGraph.property("") + val selectedCondaEnv: ObservableMutableProperty = propertyGraph.property(null) + val baseCondaEnv: ObservableMutableProperty = propertyGraph.property(null) +} + +class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) { + val baseInterpreter: ObservableMutableProperty = propertyGraph.property(null) + val newCondaEnvName: ObservableMutableProperty = propertyGraph.property("") + val poetryExecutable: ObservableMutableProperty = propertyGraph.property("") + val pipenvExecutable: ObservableMutableProperty = propertyGraph.property("") + val venvPath: ObservableMutableProperty = propertyGraph.property("") + val inheritSitePackages = propertyGraph.property(false) + val makeAvailable = propertyGraph.property(false) +} + + + +val PythonAddInterpreterModel.existingSdks + get() = allInterpreters.value.filterIsInstance().map { it.sdk } + +val PythonAddInterpreterModel.baseSdks + get() = baseInterpreters.value.filterIsInstance().map { it.sdk } + +fun PythonAddInterpreterModel.findInterpreter(path: String): PythonSelectableInterpreter? { + return allInterpreters.value.asSequence().find { it.homePath == path} +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/presentersExt.kt b/python/src/com/jetbrains/python/sdk/add/v2/presentersExt.kt new file mode 100644 index 000000000000..a621d2066abf --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/presentersExt.kt @@ -0,0 +1,107 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.execution.ExecutionException +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.projectRoots.ProjectJdkTable +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.jetbrains.python.PyBundle +import com.jetbrains.python.sdk.* +import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv +import com.jetbrains.python.sdk.configuration.createVirtualEnvSynchronously +import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity +import com.jetbrains.python.sdk.suggestAssociatedSdkName +import java.nio.file.Path + + +// todo should it be overriden for targets? +internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: String, baseSdk: PythonSelectableInterpreter): Sdk? { + + val venvPathOnTarget = venvPath.convertToPathOnTarget(targetEnvironmentConfiguration) + + val baseSdkPath = when (baseSdk) { + is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.homePath // todo handle errors + is ExistingSelectableInterpreter -> baseSdk.sdk.homePath + is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath + else -> error("Unknown interpreter") + } + + + createVirtualenv(baseSdkPath!!, // todo handle null + venvPathOnTarget, + projectPath, + targetEnvironmentConfiguration, + this.existingSdks, + null, + null, + inheritSitePackages = state.inheritSitePackages.get(), + makeShared = state.makeAvailable.get()) + + if (targetEnvironmentConfiguration == null) { + val venvPython = PythonSdkUtil.getPythonExecutable(venvPathOnTarget) + + val homeFile = try { + StandardFileSystems.local().refreshAndFindFileByPath(venvPython!!)!! + } + catch (e: ExecutionException) { + showSdkExecutionException(null, e, PyBundle.message("python.sdk.failed.to.create.interpreter.title")) + return null + } + + val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath) + val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile, + PythonSdkType.getInstance(), + false, null, suggestedName) + + SdkConfigurationUtil.addSdk(newSdk!!) + + // todo check exclude + ProjectManager.getInstance().openProjects + .firstNotNullOfOrNull { ModuleUtil.findModuleForFile(homeFile, it) } + ?.excludeInnerVirtualEnv(newSdk) + + + return newSdk + + } + // todo find venv path on target + + return null +} + + + +// todo rewrite this +internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk { + val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName) + if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk + + val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(), + PyBundle.message("sdk.create.custom.conda.create.progress"), + TaskCancellation.nonCancellable()) { + //PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration) + PyCondaCommand(state.condaExecutable.get(), targetConfig = targetEnvironmentConfiguration).createCondaSdkFromExistingEnv(identity, + this@selectCondaEnvironment.existingSdks, + ProjectManager.getInstance().defaultProject) + } + + (sdk.sdkType as PythonSdkType).setupSdkPaths(sdk) + SdkConfigurationUtil.addSdk(sdk) + return sdk +} + + +internal fun PythonAddInterpreterModel.installPythonIfNeeded(interpreter: PythonSelectableInterpreter): String? { + // todo use target config + return if (interpreter is InstallableSelectableInterpreter) { + installBaseSdk(interpreter.sdk, existingSdks)?.homePath ?: return null + } else interpreter.homePath +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/targetUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/targetUtils.kt index 72876628d4a7..d42f84379078 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/targetUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/targetUtils.kt @@ -3,6 +3,8 @@ package com.jetbrains.python.sdk.add.v2 import com.intellij.execution.target.FullPathOnTarget import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VirtualFile import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory import java.nio.file.Path @@ -16,4 +18,11 @@ internal fun Path.convertToPathOnTarget(target: TargetEnvironmentConfiguration?) internal fun FullPathOnTarget.toLocalPathOn(target: TargetEnvironmentConfiguration?): Path { val mapper = target?.let { PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(it) } return mapper?.getLocalPath(this) ?: Path.of(this) +} + +internal fun String.virtualFileOnTarget(target: TargetEnvironmentConfiguration? = null): VirtualFile? { + if (target == null) return StandardFileSystems.local().findFileByPath(this) + val path = Path.of(this) + val mapper = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(target) ?: return null + return mapper.getVfsFromTargetPath(mapper.getTargetPath(path)!!) } \ No newline at end of file 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 defcad93c289..fb712215ed35 100644 --- a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -21,14 +21,12 @@ import com.intellij.openapi.ui.validation.and import com.intellij.openapi.util.IconLoader import com.intellij.openapi.util.Key import com.intellij.openapi.util.NlsSafe -import com.intellij.ui.AnimatedIcon -import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.SimpleColoredComponent -import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.* import com.intellij.ui.components.ActionLink import com.intellij.ui.components.fields.ExtendableTextComponent import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.dsl.builder.* +import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.components.ValidationType import com.intellij.ui.dsl.builder.components.validationTooltip import com.intellij.ui.util.preferredHeight @@ -53,6 +51,7 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.annotations.Nls +import java.awt.Component import java.nio.file.Paths import javax.swing.JList import javax.swing.JPanel @@ -67,7 +66,7 @@ internal fun PropertyGraph.booleanProperty(dependency: ObservableProperty lazyProperty { dependency.get() == value }.apply { dependsOn(dependency) { dependency.get() == value } } class PythonNewEnvironmentDialogNavigator { - lateinit var selectionMode: ObservableMutableProperty + var selectionMode: ObservableMutableProperty? = null lateinit var selectionMethod: ObservableMutableProperty lateinit var newEnvManager: ObservableMutableProperty lateinit var existingEnvManager: ObservableMutableProperty @@ -75,7 +74,7 @@ class PythonNewEnvironmentDialogNavigator { fun navigateTo(newMode: PythonInterpreterSelectionMode? = null, newMethod: PythonInterpreterSelectionMethod? = null, newManager: PythonSupportedEnvironmentManagers? = null) { - newMode?.let { selectionMode.set(it) } + newMode?.let { selectionMode?.set(it) } newMethod?.let { method -> selectionMethod.set(method) } @@ -88,11 +87,16 @@ class PythonNewEnvironmentDialogNavigator { } } + // todo think about whether i need to save state in regular dialog fun saveLastState() { val properties = PropertiesComponent.getInstance() - val mode = selectionMode.get() - properties.setValue(FAV_MODE, mode.toString()) + val mode = selectionMode?.let { + val mode = selectionMode!!.get() + properties.setValue(FAV_MODE, it.get().toString()) + mode + } ?: VIRTUALENV + if (mode == CUSTOM) { val method = selectionMethod.get() val manager = if (method == CREATE_NEW) newEnvManager.get() else existingEnvManager.get() @@ -116,7 +120,7 @@ class PythonNewEnvironmentDialogNavigator { val modeString = properties.getValue(FAV_MODE) ?: return val mode = PythonInterpreterSelectionMode.valueOf(modeString) if (mode !in onlyAllowedSelectionModes) return - selectionMode.set(mode) + selectionMode?.set(mode) if (mode == CUSTOM) { val method = PythonInterpreterSelectionMethod.valueOf(properties.getValue(FAV_METHOD) ?: return) @@ -134,31 +138,39 @@ class PythonNewEnvironmentDialogNavigator { } } -internal fun SimpleColoredComponent.customizeForPythonSdk(sdk: Sdk) { - when (sdk) { - is PyDetectedSdk -> { + +internal fun SimpleColoredComponent.customizeForPythonInterpreter(interpreter: PythonSelectableInterpreter) { + when (interpreter) { + is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> { icon = IconLoader.getTransparentIcon(PythonPsiApiIcons.Python) - append(sdk.homePath!!) + append(interpreter.homePath) append(" " + message("sdk.rendering.detected.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) } - is PySdkToInstall -> { + is InstallableSelectableInterpreter -> { icon = AllIcons.Actions.Download - append(sdk.name) + append(interpreter.sdk.name) append(" " + message("sdk.rendering.installable.grey.text"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) } - else -> { + is ExistingSelectableInterpreter -> { icon = PythonPsiApiIcons.Python - append(sdk.versionString!!) - append(" " + sdk.homePath!!, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) + append(interpreter.sdk.versionString!!) + append(" " + interpreter.homePath, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) } + is InterpreterSeparator -> return + else -> error("Unknown PythonSelectableInterpreter type") } } class PythonSdkComboBoxListCellRenderer : ColoredListCellRenderer() { + + override fun getListCellRendererComponent(list: JList?, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean): Component { + if (value is InterpreterSeparator) return TitledSeparator(value.text).apply { setLabelFocusable(false) } + return super.getListCellRendererComponent(list, value, index, selected, hasFocus) + } + override fun customizeCellRenderer(list: JList, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean) { - if (value !is Sdk) error("Not an Sdk") - customizeForPythonSdk(value) + if (value is PythonSelectableInterpreter) customizeForPythonInterpreter(value) } } @@ -190,30 +202,97 @@ class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer() { } } -internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty, - presenter: PythonAddInterpreterPresenter, - sdksFlow: StateFlow>, - onPathSelected: (String) -> Unit): Cell> = - comboBox(emptyList(), PythonSdkComboBoxListCellRenderer()) +internal fun Row.pythonInterpreterComboBox(selectedSdkProperty: ObservableMutableProperty, // todo not sdk + model: PythonAddInterpreterModel, + onPathSelected: (String) -> Unit, busyState: StateFlow? = null): Cell { + + val comboBox = PythonInterpreterComboBox(selectedSdkProperty, model, onPathSelected) + val cell = cell(comboBox) .bindItem(selectedSdkProperty) .applyToComponent { preferredHeight = 30 isEditable = true - editor = PythonSdkComboBoxWithBrowseButtonEditor(this, presenter, onPathSelected) + } - presenter.scope.launch(start = CoroutineStart.UNDISPATCHED) { - sdksFlow.collectLatest { sdks -> - withContext(presenter.uiContext) { - removeAllItems() - sdks.forEach(this@applyToComponent::addItem) - - val pathToSelect = tryGetAndRemovePathToSelectAfterModelUpdate() as? String - val newValue = if (pathToSelect != null) sdks.find { it.homePath == pathToSelect } else findPrioritySdk(sdks) - selectedSdkProperty.set(newValue) - } + model.scope.launch(model.uiContext, start = CoroutineStart.UNDISPATCHED) { + busyState?.collectLatest { currentValue -> + withContext(model.uiContext) { + comboBox.setBusy(currentValue) + if (currentValue) { + // todo disable cell } } } + } + return cell + + +} + +class PythonInterpreterComboBox(val backingProperty: ObservableMutableProperty, + val controller: PythonAddInterpreterModel, + val onPathSelected: (String) -> Unit) : ComboBox() { + + private lateinit var itemsFlow: StateFlow> + val items: List + get() = itemsFlow.value + + private val interpreterToSelect = controller.propertyGraph.property(null) + + init { + renderer = PythonSdkComboBoxListCellRenderer() + val newOnPathSelected: (String) -> Unit = { + interpreterToSelect.set(it) + onPathSelected(it) + } + editor = PythonSdkComboBoxWithBrowseButtonEditor(this, controller, newOnPathSelected) + + } + + fun setItems(flow: StateFlow>) { + itemsFlow = flow + controller.scope.launch(start = CoroutineStart.UNDISPATCHED) { + flow.collectLatest { interpreters -> + withContext(controller.uiContext) { + with(this@PythonInterpreterComboBox) { + val currentlySelected = selectedItem as PythonSelectableInterpreter? + removeAllItems() + interpreters.forEach(this::addItem) + + val newPath = interpreterToSelect.get() + val newValue = if (newPath != null) { + val newItem = interpreters.find { it.homePath == newPath } + if (newItem == null) error("path but no item") + interpreterToSelect.set(null) + newItem + } + else if (currentlySelected == null || currentlySelected !in interpreters) { + interpreters.firstOrNull() // todo is there better fallback value? + } + else { + currentlySelected + } + + + //val newValue = if (newPath != null) { + // val newItem = interpreters.find { it.homePath == newPath } + // newPath = null + // newItem ?: currentlySelected + //} else currentlySelected + + + backingProperty.set(newValue) // todo do I even need to set it? + } + + } + } + } + } + + fun setBusy(busy: Boolean) { + (editor as PythonSdkComboBoxWithBrowseButtonEditor).setBusy(busy) + } +} private fun findPrioritySdk(sdkList: List): Sdk? { val preferredSdkPath = PySdkSettings.instance.preferredVirtualEnvBaseSdk @@ -374,12 +453,15 @@ fun Panel.executableSelector(executable: ObservableMutableProperty, return textFieldCell!! } -internal fun createInstallCondaFix(presenter: PythonAddInterpreterPresenter): ActionLink { +internal fun createInstallCondaFix(model: PythonAddInterpreterModel): ActionLink { return ActionLink(message("sdk.create.conda.install.fix")) { PythonSdkFlavor.clearExecutablesCache() CondaInstallManager.installLatest(null) - presenter.scope.launch(presenter.uiContext) { - presenter.reloadConda(presenter.projectLocationContext) + model.scope.launch(model.uiContext) { + model.condaEnvironmentsLoading.value = true + model.detectCondaExecutable() + model.detectCondaEnvironments() + model.condaEnvironmentsLoading.value = false } } } diff --git a/python/src/com/jetbrains/python/sdk/add/v2/virtualenv.kt b/python/src/com/jetbrains/python/sdk/add/v2/virtualenv.kt new file mode 100644 index 000000000000..132e60cb8e1a --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/virtualenv.kt @@ -0,0 +1,92 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.ColoredProcessHandler +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.execution.target.TargetProgressIndicator +import com.intellij.execution.target.TargetedCommandLineBuilder +import com.intellij.execution.target.value.TargetValue +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.blockingContext +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.runWithModalProgressBlocking +import com.jetbrains.python.PythonHelper +import com.jetbrains.python.run.* +import com.jetbrains.python.run.target.HelpersAwareLocalTargetEnvironmentRequest +import com.jetbrains.python.sdk.PySdkSettings +import com.jetbrains.python.sdk.add.target.TargetPanelExtension +import com.jetbrains.python.sdk.configureBuilderToRunPythonOnTarget + + +fun createVirtualenv(baseInterpreterPath: String, + venvRoot: String, + projectBasePath: String?, + targetConfiguration: TargetEnvironmentConfiguration?, + existingSdks: List, + project: Project?, + module: Module?, + inheritSitePackages: Boolean = false, + makeShared: Boolean = false, + targetPanelExtension: TargetPanelExtension? = null) { + + // todo find request for targets (need sdk, maybe can work around it ) + //PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter() + val request = HelpersAwareLocalTargetEnvironmentRequest() + //val targetRequest = request.targetEnvironmentRequest + + val execution = prepareHelperScriptExecution(PythonHelper.VIRTUALENV_ZIPAPP, request) // todo what about legacy pythons? + if (inheritSitePackages) { + execution.addParameter("--system-site-packages") + } + + execution.addParameter(venvRoot) + request.preparePyCharmHelpers() + + val targetEnvironment = request.targetEnvironmentRequest.prepareEnvironment(TargetProgressIndicator.EMPTY) + + //val targetedCommandLine = execution.buildTargetedCommandLine(targetEnvironment, sdk = null, emptyList()) + + + val commandLineBuilder = TargetedCommandLineBuilder(targetEnvironment.request) + commandLineBuilder.setWorkingDirectory(projectBasePath!!) + + commandLineBuilder.setExePath(baseInterpreterPath) + + execution.pythonScriptPath?.let { commandLineBuilder.addParameter(it.apply(targetEnvironment)) } + ?: throw IllegalArgumentException("Python script path must be set") + + execution.parameters.forEach { parameter -> + val resolvedParameter = parameter.apply(targetEnvironment) + if (resolvedParameter != PythonExecution.SKIP_ARGUMENT) { + commandLineBuilder.addParameter(resolvedParameter) + } + } + + + for ((name, value) in execution.envs) { + commandLineBuilder.addEnvironmentVariable(name, value.apply(targetEnvironment)) + } + + val targetedCommandLine = commandLineBuilder.build() + + + // todo poerty/pipenv + //val targetedCommandLineBuilder = TargetedCommandLineBuilder(request.targetEnvironmentRequest) + //targetedCommandLineBuilder.exePath = TargetValue.fixed("") + //val targetedCommandLine = targetedCommandLineBuilder.build() + + val process = targetEnvironment.createProcess(targetedCommandLine) + + val handler = CapturingProcessHandler(process, targetedCommandLine.charset, targetedCommandLine.getCommandPresentation(targetEnvironment)) + + val output = runWithModalProgressBlocking(ModalTaskOwner.guess(), "creating venv") { + handler.runProcess(60 * 1000) + } + + PySdkSettings.instance.preferredVirtualEnvBaseSdk = baseInterpreterPath +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/interpreters/detect.kt b/python/src/com/jetbrains/python/sdk/interpreters/detect.kt new file mode 100644 index 000000000000..05431b5c6b06 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/interpreters/detect.kt @@ -0,0 +1,78 @@ +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.interpreters + +import com.intellij.execution.target.TargetConfigurationWithLocalFsAccess +import com.intellij.execution.target.TargetEnvironmentConfiguration +import com.intellij.openapi.module.Module +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.UserDataHolderBase +import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory +import com.jetbrains.python.sdk.TargetAndPath +import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter +import com.jetbrains.python.sdk.detectSdkPaths +import com.jetbrains.python.sdk.flavors.PythonSdkFlavor +import com.jetbrains.python.sdk.targetEnvConfiguration +import com.jetbrains.python.sdk.tryFindPythonBinaries +import kotlin.io.path.pathString + + +fun detectSystemInterpreters(projectDir: String?, module: Module?, targetConfiguration: TargetEnvironmentConfiguration?, existingSdks: List): List { + return if (targetConfiguration == null) detectLocalSystemInterpreters(module, existingSdks) + else detectSystemInterpretersOnTarget(targetConfiguration) +} + +fun detectSystemInterpretersOnTarget(targetConfiguration: TargetEnvironmentConfiguration): List { + val targetWithMappedLocalVfs = PythonInterpreterTargetEnvironmentFactory.getTargetWithMappedLocalVfs(targetConfiguration) + if (targetWithMappedLocalVfs != null) { + val searchRoots = listOf("/usr/bin/", "/usr/local/bin/") + return searchRoots.flatMap { searchRoot -> + targetWithMappedLocalVfs.getLocalPath(searchRoot)?.tryFindPythonBinaries()?.mapNotNull { + val pythonBinaryPath = targetWithMappedLocalVfs.getTargetPath(it) ?: return@mapNotNull null + DetectedSelectableInterpreter(pythonBinaryPath, targetConfiguration) + } ?: emptyList() + } + } + else { + // TODO Try to execute `which python` or introspect the target + //val request = targetEnvironmentConfiguration.createEnvironmentRequest(project = null) + //request.prepareEnvironment(TargetProgressIndicator.EMPTY).createProcess() + return emptyList() + } +} + + + +// todo +// remove context -- it's only platform independent flavours +// +fun detectLocalSystemInterpreters(module: Module?, existingSdks: List): List { + + if (module != null && module.isDisposed) return emptyList() + val existingPaths = existingSdks.mapTo(HashSet()) { it.homePath } + + return PythonSdkFlavor.getApplicableFlavors(false) + .asSequence() + .flatMap { it.suggestLocalHomePaths(module, null) } + .map { it.pathString } + .filter { it !in existingPaths } + .map { DetectedSelectableInterpreter(it) } + .toList() + + // todo sort + + //return PythonSdkFlavor.getApplicableFlavors(false) + // + // .flatMap { flavor -> flavor.detectInterpreters(module, context, targetModuleSitsOn, existingPaths) } // sorting + //.sortedWith(compareBy({ it.guessedLanguageLevel }, + // { it.homePath }).reversed()) +} + + +private fun PythonSdkFlavor<*>.detectInterpreters(module: Module?, + context: UserDataHolder, + targetModuleSitsOn: TargetConfigurationWithLocalFsAccess?, + existingPaths: HashSet): List { + return detectSdkPaths(module, context, targetModuleSitsOn, existingPaths) + .map { DetectedSelectableInterpreter(it, targetModuleSitsOn?.asTargetConfig) } +} \ No newline at end of file