mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python] (PY-84885) sdk v2 model refactoring
* move tool specific properties to tool sections (cherry picked from commit 543c1f6ba44c564e31a956309bfaee44c6492b67) GitOrigin-RevId: 84b1f2af49dffe7a5e1928b4362917c2a9194fb0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7e64eff197
commit
5bd261f8ea
@@ -1204,6 +1204,7 @@ python.add.sdk.panel.name.virtualenv.environment=Virtualenv environment
|
||||
python.add.sdk.panel.name.poetry.environment=Poetry Environment
|
||||
python.add.sdk.wait.for.validation=Wait for the executable validation
|
||||
python.add.sdk.version=Version: {0}
|
||||
python.add.sdk.already.contains.python.with.version=Already contains Python with version {0}
|
||||
|
||||
python.add.sdk.conda.executable.path.is.empty=Conda executable path is empty
|
||||
python.add.sdk.conda.executable.path.is.not.found=Conda executable is not found
|
||||
@@ -1714,6 +1715,8 @@ tracecontext.detecting.pip.executable=Detecting Pip Executable
|
||||
tracecontext.detecting.uv.executable=Detecting uv Executable
|
||||
tracecontext.detecting.hatch.executable=Detecting Hatch Executable
|
||||
tracecontext.detecting.hatch.environments=Detecting Hatch Environments
|
||||
tracecontext.detecting.executable=Detecting {0} Executable
|
||||
tracecontext.detecting.venv=Detecting venv folder
|
||||
tracecontext.generating.git=Generating git
|
||||
tracecontext.packaging.tool.window=Packaging tool window
|
||||
tracecontext.packages.sdk.controller=Packages SDK Controller
|
||||
|
||||
@@ -4,7 +4,7 @@ package com.jetbrains.python.sdk.add.v2
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
@@ -23,7 +23,7 @@ import java.util.*
|
||||
|
||||
|
||||
@Internal
|
||||
internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
|
||||
internal abstract class CustomExistingEnvironmentSelector<P : PathHolder>(
|
||||
private val name: String,
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
private val module: Module?,
|
||||
@@ -35,21 +35,14 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
|
||||
private val existingEnvironments: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
protected val selectedEnv: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
|
||||
open suspend fun onBinarySelection(pathOnFileSystem: P): ValidatedPath.Executable<P> {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(pathOnFileSystem)
|
||||
return ValidatedPath.Executable(pathOnFileSystem, binaryToExec.getToolVersion(name))
|
||||
}
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
executablePath = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
pathValidator = toolState,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", name),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
|
||||
selectedPathValidator = ::onBinarySelection
|
||||
)
|
||||
|
||||
val nameTitle = name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
||||
@@ -60,7 +53,7 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
|
||||
validationRequestor = validationRequestor,
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
) {
|
||||
visibleIf(executable.isNotNull())
|
||||
visibleIf(toolState.backProperty.transform { it?.validationResult?.successOrNull != null })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +97,7 @@ internal abstract class CustomExistingEnvironmentSelector<P: PathHolder>(
|
||||
// return interpreter
|
||||
//}
|
||||
|
||||
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
|
||||
internal abstract val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>>
|
||||
internal abstract val interpreterType: InterpreterType
|
||||
internal abstract suspend fun detectEnvironments(modulePath: Path): List<DetectedSelectableInterpreter<P>>
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
@@ -48,15 +47,13 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
executablePath = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
pathValidator = toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", name),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", name),
|
||||
installAction = createInstallFix(errorSink),
|
||||
selectedPathValidator = ::onBinarySelection
|
||||
)
|
||||
|
||||
row("") {
|
||||
@@ -96,7 +93,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
|
||||
newSdk.persist()
|
||||
if (module != null) {
|
||||
if (!model.state.makeAvailableForAllProjects.get()) {
|
||||
if (!model.venvState.makeAvailableForAllProjects.get()) {
|
||||
newSdk.setAssociationToModule(module)
|
||||
}
|
||||
module.baseDir?.refresh(true, false)
|
||||
@@ -112,7 +109,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
type = interpreterType,
|
||||
target = target.toStatisticsField(),
|
||||
globalSitePackage = false,
|
||||
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
|
||||
makeAvailableToAllProjects = model.venvState.makeAvailableForAllProjects.get(),
|
||||
previouslyConfigured = false,
|
||||
isWSLContext = false, // todo fix for wsl
|
||||
creationMode = InterpreterCreationMode.CUSTOM
|
||||
@@ -135,7 +132,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
PythonSdkFlavor.clearExecutablesCache()
|
||||
installExecutable(errorSink)
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
|
||||
detectExecutable()
|
||||
toolValidator.autodetectExecutable()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,7 +181,7 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
|
||||
internal abstract val interpreterType: InterpreterType
|
||||
|
||||
internal abstract val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?>
|
||||
internal abstract val toolValidator: ToolValidator<P>
|
||||
|
||||
internal open val installationVersion: String? = null
|
||||
|
||||
@@ -198,8 +195,6 @@ internal abstract class CustomNewEnvironmentCreator<P: PathHolder>(
|
||||
|
||||
protected abstract suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk>
|
||||
|
||||
internal abstract suspend fun detectExecutable()
|
||||
|
||||
internal open fun onVenvSelectExisting() {}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,57 @@ package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.platform.eel.LocalEelApi
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
|
||||
import com.jetbrains.python.sdk.pipenv.pipEnvPath
|
||||
import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkWithProgressReport
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class EnvironmentCreatorPip<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
|
||||
class PipenvState<P : PathHolder>(
|
||||
fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
) : ToolState {
|
||||
val pipenvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
toolVersionPrefix = "pipenv",
|
||||
backProperty = pipenvExecutable,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
when (fileSystem) {
|
||||
is FileSystem.Eel -> {
|
||||
if (fileSystem.eelApi == localEel) getPipEnvExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
else null // getPipEnvExecutable() works only with localEel currently
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
toolValidator.initialize(scope)
|
||||
}
|
||||
}
|
||||
|
||||
internal class EnvironmentCreatorPip<P : PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, errorSink: ErrorSink) : CustomNewEnvironmentCreator<P>("pipenv", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.PIPENV
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.pipenvExecutable
|
||||
override val toolValidator: ToolValidator<P> = model.pipenvState.toolValidator
|
||||
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
savingPath?.let {
|
||||
PropertiesComponent.getInstance().pipEnvPath = it.toString()
|
||||
}
|
||||
@@ -33,8 +65,4 @@ internal class EnvironmentCreatorPip<P: PathHolder>(model: PythonMutableTargetAd
|
||||
else -> PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
model.detectPipEnvExecutable()
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ import com.intellij.python.community.services.internal.impl.VanillaPythonWithLan
|
||||
import com.intellij.python.community.services.shared.VanillaPythonWithLanguageLevel
|
||||
import com.intellij.python.community.services.systemPython.SystemPython
|
||||
import com.intellij.python.community.services.systemPython.SystemPythonService
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.MessageError
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.pathValidation.PlatformAndRoot.Companion.getPlatformAndRoot
|
||||
@@ -35,6 +37,7 @@ import kotlinx.coroutines.withContext
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.Path
|
||||
import kotlin.io.path.exists
|
||||
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
@@ -42,6 +45,10 @@ private val LOG: Logger = fileLogger()
|
||||
|
||||
data class SdkWrapper<P>(val sdk: Sdk, val homePath: P)
|
||||
|
||||
internal class VenvAlreadyExistsError<P : PathHolder>(
|
||||
val detectedSelectableInterpreter: DetectedSelectableInterpreter<P>,
|
||||
) : MessageError(message("python.add.sdk.already.contains.python.with.version", detectedSelectableInterpreter.languageLevel))
|
||||
|
||||
sealed interface FileSystem<P : PathHolder> {
|
||||
val isReadOnly: Boolean
|
||||
|
||||
@@ -49,8 +56,8 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
|
||||
suspend fun getSystemPythonFromSelection(pathToPython: P): PyResult<DetectedSelectableInterpreter<P>>
|
||||
|
||||
suspend fun validateVenv(homePath: P): ValidatedPath.Folder<P>
|
||||
suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<P>
|
||||
suspend fun validateVenv(homePath: P): PyResult<Unit>
|
||||
suspend fun suggestVenv(projectPath: Path): PyResult<P>
|
||||
fun wrapSdk(sdk: Sdk): SdkWrapper<P>
|
||||
suspend fun detectSelectableVenv(): List<DetectedSelectableInterpreter<P>>
|
||||
fun preferredInterpreterBasePath(): P? = null
|
||||
@@ -75,27 +82,29 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
PyResult.localizedError(e.localizedMessage)
|
||||
}
|
||||
|
||||
override suspend fun validateVenv(homePath: PathHolder.Eel): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
|
||||
val pythonBinaryPath = homePath.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
|
||||
val existingPython = pythonBinaryPath?.let { getSystemPythonFromSelection(it) }?.successOrNull
|
||||
|
||||
val validationResult = if (existingPython == null) {
|
||||
PyResult.success(Unit)
|
||||
}
|
||||
else {
|
||||
PyResult.failure(VenvAlreadyExistsError(existingPython))
|
||||
override suspend fun validateVenv(homePath: PathHolder.Eel): PyResult<Unit> = withContext(Dispatchers.IO) {
|
||||
val validationResult = when {
|
||||
!homePath.path.isAbsolute -> PyResult.localizedError(message("python.sdk.new.error.no.absolute"))
|
||||
homePath.path.exists() -> {
|
||||
val pythonBinaryPath = homePath.path.resolvePythonBinary()?.let { PathHolder.Eel(it) }
|
||||
val existingPython = pythonBinaryPath?.let { getSystemPythonFromSelection(it) }?.successOrNull
|
||||
if (existingPython == null) {
|
||||
PyResult.localizedError(message("sdk.create.custom.venv.folder.not.empty"))
|
||||
}
|
||||
else {
|
||||
PyResult.failure(VenvAlreadyExistsError(existingPython))
|
||||
}
|
||||
}
|
||||
else -> PyResult.success(Unit)
|
||||
}
|
||||
|
||||
ValidatedPath.Folder(homePath, validationResult)
|
||||
validationResult
|
||||
}
|
||||
|
||||
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Eel> = withContext(Dispatchers.IO) {
|
||||
override suspend fun suggestVenv(projectPath: Path): PyResult<PathHolder.Eel> = withContext(Dispatchers.IO) {
|
||||
val preferedFilePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.toString())
|
||||
val suggestedVirtualEnvPath = FileUtil.toSystemDependentName(preferedFilePath)
|
||||
val path = parsePath(suggestedVirtualEnvPath).getOr {
|
||||
return@withContext ValidatedPath.Folder(null, it)
|
||||
}
|
||||
validateVenv(path)
|
||||
parsePath(suggestedVirtualEnvPath)
|
||||
}
|
||||
|
||||
override suspend fun getSystemPythonFromSelection(pathToPython: PathHolder.Eel): PyResult<DetectedSelectableInterpreter<PathHolder.Eel>> {
|
||||
@@ -174,7 +183,7 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
return PyResult.success(PathHolder.Target(raw))
|
||||
}
|
||||
|
||||
override suspend fun validateVenv(homePath: PathHolder.Target): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
|
||||
override suspend fun validateVenv(homePath: PathHolder.Target): PyResult<Unit> = withContext(Dispatchers.IO) {
|
||||
val pythonBinaryPath = resolvePythonBinary(homePath)
|
||||
|
||||
val existingPython = getSystemPythonFromSelection(pythonBinaryPath).successOrNull
|
||||
@@ -199,18 +208,17 @@ sealed interface FileSystem<P : PathHolder> {
|
||||
PyResult.failure(VenvAlreadyExistsError(existingPython))
|
||||
}
|
||||
|
||||
ValidatedPath.Folder(homePath, validationResult)
|
||||
validationResult
|
||||
}
|
||||
|
||||
override suspend fun suggestVenv(projectPath: Path): ValidatedPath.Folder<PathHolder.Target> = withContext(Dispatchers.IO) {
|
||||
override suspend fun suggestVenv(projectPath: Path): PyResult<PathHolder.Target> = withContext(Dispatchers.IO) {
|
||||
val homePathString = when {
|
||||
projectPath.toString().isEmpty() -> pythonLanguageRuntimeConfiguration.userHome
|
||||
else -> joinTargetPaths(pythonLanguageRuntimeConfiguration.userHome, VirtualEnvReader.DEFAULT_VIRTUALENVS_DIR,
|
||||
projectPath.fileName.toString(), fileSeparator = '/')
|
||||
}
|
||||
|
||||
val homePath = PathHolder.Target(homePathString)
|
||||
validateVenv(homePath)
|
||||
PyResult.success(PathHolder.Target(homePathString))
|
||||
}
|
||||
|
||||
private suspend fun registerSystemPython(pathToPython: PathHolder.Target): PyResult<DetectedSelectableInterpreter<PathHolder.Target>> {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.UI
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
|
||||
import com.intellij.openapi.ui.validation.CHECK_NO_RESERVED_WORDS
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.Result.Failure
|
||||
import com.jetbrains.python.Result.Success
|
||||
import com.jetbrains.python.TraceContext
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class FolderValidator<P : PathHolder>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
override val backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?>,
|
||||
propertyGraph: PropertyGraph,
|
||||
val defaultPathSupplier: suspend () -> PyResult<P>,
|
||||
val pathValidator: suspend (P) -> PyResult<Unit>,
|
||||
) : PathValidator<Unit, P, ValidatedPath.Folder<P>> {
|
||||
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(true)
|
||||
override val isValidationInProgress: Boolean
|
||||
get() = validationJob.isActive
|
||||
|
||||
lateinit var scope: CoroutineScope
|
||||
private lateinit var validationJob: Deferred<Unit>
|
||||
|
||||
fun initialize(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
this.validationJob = autodetectFolderPath()
|
||||
}
|
||||
|
||||
suspend fun autodetectFolder() {
|
||||
validationJob.cancelAndJoin()
|
||||
validationJob = autodetectFolderPath()
|
||||
}
|
||||
|
||||
|
||||
private fun autodetectFolderPath(): Deferred<Unit> {
|
||||
return scope.async(TraceContext(PyBundle.message("python.sdk.validating.environment"), scope)) {
|
||||
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
|
||||
val pathResult = defaultPathSupplier.invoke()
|
||||
val path = runValidation(pathResult)
|
||||
withContext(Dispatchers.UI) { backProperty.set(path) }
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
isDirtyValue.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun runValidation(pathResult: PyResult<P>): ValidatedPath.Folder<P> {
|
||||
return when (pathResult) {
|
||||
is Failure -> ValidatedPath.Folder(null, pathResult)
|
||||
is Success -> {
|
||||
val path = pathResult.result
|
||||
ValidatedPath.Folder(path, pathValidator(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun validate(input: String) {
|
||||
scope.launch {
|
||||
if (input.isEmpty()) {
|
||||
autodetectFolderPath()
|
||||
return@launch
|
||||
}
|
||||
|
||||
validationJob.cancelAndJoin()
|
||||
validationJob = scope.async {
|
||||
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
|
||||
|
||||
val validatedFolderPath = withContext(Dispatchers.IO) {
|
||||
for (validator in arrayOf(CHECK_NON_EMPTY, CHECK_NO_RESERVED_WORDS)) {
|
||||
validator.curry { input }.validate()?.let {
|
||||
return@withContext ValidatedPath.Folder<P>(null, PyResult.localizedError(it.message))
|
||||
}
|
||||
}
|
||||
|
||||
val path = fileSystem.parsePath(input).getOr { error ->
|
||||
return@withContext ValidatedPath.Folder<P>(null, error)
|
||||
}
|
||||
|
||||
ValidatedPath.Folder(path, pathValidator.invoke(path))
|
||||
}
|
||||
withContext(Dispatchers.UI) { backProperty.set(validatedFolderPath) }
|
||||
}
|
||||
validationJob.invokeOnCompletion {
|
||||
isDirtyValue.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,9 @@ import com.jetbrains.python.sdk.add.v2.venv.PythonExistingEnvironmentSelector
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.jetbrains.annotations.ApiStatus.Internal
|
||||
|
||||
class VenvAlreadyExistsError<P: PathHolder>(
|
||||
val detectedSelectableInterpreter: DetectedSelectableInterpreter<P>,
|
||||
) : MessageError("Already contains python installation with version ${detectedSelectableInterpreter.languageLevel}")
|
||||
|
||||
class ValidationInfoError(val validationInfo: ValidationInfo) : MessageError(validationInfo.message)
|
||||
|
||||
class PythonAddCustomInterpreter<P : PathHolder>(
|
||||
internal class PythonAddCustomInterpreter<P : PathHolder>(
|
||||
val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
val module: Module?,
|
||||
private val errorSink: ErrorSink,
|
||||
|
||||
@@ -127,18 +127,13 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
}
|
||||
|
||||
rowsRange {
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
executablePath = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.condaExecutable,
|
||||
pathValidator = model.condaState.toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink),
|
||||
selectedPathValidator = {
|
||||
val binToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binToExec.getToolVersion("conda"))
|
||||
}
|
||||
)
|
||||
}.visibleIf(_baseConda)
|
||||
|
||||
@@ -146,7 +141,7 @@ internal class PythonSdkPanelBuilderAndSdkCreator(
|
||||
|
||||
row("") {
|
||||
comment("").bindText(venvHint)
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.state.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom)
|
||||
}.visibleIf(_projectVenv or (_baseConda and model.condaState.condaExecutable.isNotNull()) or uvSection.hintVisiblePredicate() or _custom)
|
||||
|
||||
rowsRange {
|
||||
custom.setupUI(this, validationRequestor)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.UI
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.TraceContext
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
|
||||
class ToolValidator<P : PathHolder>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
val toolVersionPrefix: String,
|
||||
override val backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
propertyGraph: PropertyGraph,
|
||||
val defaultPathSupplier: suspend () -> P?,
|
||||
val pathValidator: suspend (P) -> PyResult<Version> = { fileSystem.getBinaryToExec(it).getToolVersion(toolVersionPrefix) },
|
||||
) : PathValidator<Version, P, ValidatedPath.Executable<P>> {
|
||||
override val isDirtyValue: ObservableMutableProperty<Boolean> = propertyGraph.property(true)
|
||||
override val isValidationInProgress: Boolean
|
||||
get() = validationJob.isActive
|
||||
|
||||
lateinit var scope: CoroutineScope
|
||||
private lateinit var validationJob: Deferred<Unit>
|
||||
|
||||
fun initialize(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
this.validationJob = autodetectExecutableJob()
|
||||
}
|
||||
|
||||
override fun validate(input: String) {
|
||||
scope.launch {
|
||||
if (input.isEmpty()) {
|
||||
autodetectExecutable()
|
||||
return@launch
|
||||
}
|
||||
validationJob.cancelAndJoin()
|
||||
validationJob = scope.async {
|
||||
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
|
||||
|
||||
val exec = withContext(Dispatchers.IO) {
|
||||
val path = fileSystem.parsePath(input).getOr { error ->
|
||||
return@withContext ValidatedPath.Executable<P>(null, error)
|
||||
}
|
||||
val validationResult = pathValidator(path)
|
||||
ValidatedPath.Executable(path, validationResult)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.UI) { backProperty.set(exec) }
|
||||
}
|
||||
validationJob.invokeOnCompletion {
|
||||
isDirtyValue.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun autodetectExecutable() {
|
||||
validationJob.cancelAndJoin()
|
||||
validationJob = autodetectExecutableJob()
|
||||
}
|
||||
|
||||
private fun autodetectExecutableJob(): Deferred<Unit> {
|
||||
return scope.async(TraceContext(PyBundle.message("tracecontext.detecting.executable", toolVersionPrefix), scope)) {
|
||||
withContext(Dispatchers.UI) { isDirtyValue.set(true) }
|
||||
val validatedPath = fileSystem.autodetectWithVersionProbe(toolVersionPrefix, defaultPathSupplier)
|
||||
withContext(Dispatchers.UI) { backProperty.set(validatedPath) }
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
isDirtyValue.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
suspend fun <P : PathHolder> FileSystem<P>.autodetectWithVersionProbe(
|
||||
toolVersionPrefix: String,
|
||||
toolPathSupplier: suspend () -> P?,
|
||||
): ValidatedPath.Executable<P> = withContext(Dispatchers.IO) {
|
||||
val path = toolPathSupplier.invoke()
|
||||
val validatedPath = path?.validateToolExecutableByVersionProbe(this@autodetectWithVersionProbe, toolVersionPrefix)
|
||||
?: ValidatedPath.Executable(
|
||||
pathHolder = path,
|
||||
validationResult = PyResult.localizedError(PyBundle.message("python.sdk.executable.is.not.detected"))
|
||||
)
|
||||
validatedPath
|
||||
}
|
||||
|
||||
private suspend fun <P : PathHolder> P.validateToolExecutableByVersionProbe(fileSystem: FileSystem<P>, toolVersionPrefix: String): ValidatedPath.Executable<P> {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(this)
|
||||
val validationResult = binaryToExec.getToolVersion(toolVersionPrefix)
|
||||
return ValidatedPath.Executable(
|
||||
pathHolder = this,
|
||||
validationResult = validationResult
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,14 +7,12 @@ import com.intellij.execution.target.getTargetType
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.CustomShortcutSet
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptor
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.isNull
|
||||
import com.intellij.openapi.observable.util.not
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.ui.TextComponentAccessor
|
||||
@@ -40,11 +38,13 @@ import com.intellij.util.asDisposable
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.onSuccess
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.awt.AlphaComposite
|
||||
import java.awt.Component
|
||||
@@ -60,7 +60,17 @@ import javax.swing.event.DocumentEvent
|
||||
import kotlin.concurrent.atomics.AtomicBoolean
|
||||
import kotlin.concurrent.atomics.ExperimentalAtomicApi
|
||||
import kotlin.math.cos
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
interface PathValidator<T, P : PathHolder, VP : ValidatedPath<T, P>> {
|
||||
val backProperty: ObservableMutableProperty<VP?>
|
||||
val isDirtyValue: ObservableMutableProperty<Boolean>
|
||||
val isValidationInProgress: Boolean
|
||||
fun validate(input: String)
|
||||
fun markDirty() {
|
||||
isDirtyValue.set(true)
|
||||
backProperty.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
private class ValidationSuccessExtension<T>(val validationInfo: T) : ExtendableTextComponent.Extension {
|
||||
override fun getIcon(hovered: Boolean): Icon = AllIcons.General.GreenCheckmark
|
||||
@@ -136,17 +146,14 @@ private class AnimatedFadingIcon(private val icon: DebounceCounterIcon) : Animat
|
||||
private const val VALIDATION_DELAY = 2000
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalAtomicApi::class)
|
||||
class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
internal class ValidatedPathField<T, P : PathHolder, VP : ValidatedPath<T, P>>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
val backProperty: ObservableMutableProperty<V?>,
|
||||
val pathValidator: PathValidator<T, P, VP>,
|
||||
browseFolderDialogTitle: @Nls String,
|
||||
isFileSelectionMode: Boolean,
|
||||
val isValidationActiveProperty: ObservableMutableProperty<Boolean>,
|
||||
val pathValidator: suspend (P) -> V,
|
||||
) : TextFieldWithBrowseButton() {
|
||||
private lateinit var scope: CoroutineScope
|
||||
private val textInputFlow: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
private var validationJob: Deferred<Unit>? = null
|
||||
|
||||
/**
|
||||
* Single Back Property is shared across multiple forms,
|
||||
@@ -155,49 +162,11 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
*/
|
||||
private val editorMode = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* There are several UIs for the same back property,
|
||||
* this method emulates the validation job for the current field when the user switches to another form.
|
||||
*/
|
||||
private fun runJobFor3rdPartyValidation() {
|
||||
scope.launch {
|
||||
resetValidationJob {
|
||||
if (backProperty.get() == null) delay(1.minutes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resetValidationJob(block: suspend CoroutineScope.() -> Unit) {
|
||||
validationJob?.cancelAndJoin()
|
||||
|
||||
validationJob = scope.async(Dispatchers.EDT) {
|
||||
isEnabled = false
|
||||
isValidationActiveProperty.set(true)
|
||||
block()
|
||||
}.apply {
|
||||
invokeOnCompletion {
|
||||
isEnabled = true
|
||||
isValidationActiveProperty.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val validationAction = object : DumbAwareAction(AllIcons.Gutter.SuggestedRefactoringBulb) {
|
||||
fun doValidate() {
|
||||
if (!editorMode.load()) return
|
||||
|
||||
scope.launch {
|
||||
resetValidationJob {
|
||||
val exec = withContext(Dispatchers.IO) {
|
||||
val path = fileSystem.parsePath(text).getOr { error ->
|
||||
//TODO HANDLE PATH ERRORS
|
||||
return@withContext null
|
||||
}
|
||||
pathValidator.invoke(path)
|
||||
}
|
||||
backProperty.set(exec)
|
||||
}
|
||||
}
|
||||
pathValidator.validate(text.trim())
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
@@ -243,25 +212,26 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
}
|
||||
|
||||
private fun registerPropertyCallbacks() {
|
||||
backProperty.afterChange(scope.asDisposable()) { validatedPath ->
|
||||
pathValidator.backProperty.afterChange(scope.asDisposable()) { validatedPath ->
|
||||
if (validatedPath == null) {
|
||||
isValidationActiveProperty.set(true)
|
||||
if (!editorMode.load()) runJobFor3rdPartyValidation()
|
||||
return@afterChange
|
||||
}
|
||||
|
||||
if (validatedPath.pathHolder != null) {
|
||||
text = validatedPath.pathHolder.toString()
|
||||
}
|
||||
validationJob?.cancel()
|
||||
else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
|
||||
isValidationActiveProperty.afterChange(scope.asDisposable()) { isValidationActive ->
|
||||
pathValidator.isDirtyValue.afterChange(scope.asDisposable()) { isDirtyValue ->
|
||||
with(textField as ExtendableTextComponent) {
|
||||
extensions.forEach { removeExtension(it) }
|
||||
|
||||
if (isValidationActive) {
|
||||
if (validationJob?.isActive == true) {
|
||||
if (isDirtyValue) {
|
||||
if (pathValidator.isValidationInProgress) {
|
||||
isEnabled = false
|
||||
addExtension(ValidationInProgressExtension)
|
||||
}
|
||||
else {
|
||||
@@ -270,10 +240,10 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
}
|
||||
else {
|
||||
editorMode.store(false)
|
||||
if (browseFolderActionLister != null) {
|
||||
setButtonVisible(true)
|
||||
}
|
||||
backProperty.get()?.validationResult?.let { validationResult ->
|
||||
browseFolderActionLister?.let { setButtonVisible(true) }
|
||||
isEnabled = true
|
||||
|
||||
pathValidator.backProperty.get()?.validationResult?.let { validationResult ->
|
||||
validationResult
|
||||
.onFailure {
|
||||
addExtension(ValidationErrorExtension)
|
||||
@@ -290,7 +260,6 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
fun initialize(scope: CoroutineScope) {
|
||||
this.scope = scope
|
||||
registerPropertyCallbacks()
|
||||
runJobFor3rdPartyValidation() // initial validation is processed by the common v2 model
|
||||
|
||||
scope.launch {
|
||||
textInputFlow
|
||||
@@ -298,12 +267,14 @@ class ValidatedPathField<T, P : PathHolder, V : ValidatedPath<T, P>>(
|
||||
.map {
|
||||
if (it == null) return@map null
|
||||
|
||||
if (!editorMode.load() && backProperty.get()?.pathHolder?.toString() != it) {
|
||||
if (editorMode.load()) {
|
||||
validationWaitIcon.reset()
|
||||
}
|
||||
else if ((pathValidator.backProperty.get()?.pathHolder?.toString() ?: "") != it) {
|
||||
editorMode.store(true)
|
||||
backProperty.set(null)
|
||||
pathValidator.markDirty()
|
||||
}
|
||||
|
||||
validationWaitIcon.reset()
|
||||
it
|
||||
}
|
||||
.debounce(VALIDATION_DELAY.toLong())
|
||||
@@ -391,72 +362,21 @@ private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.installToolRow(
|
||||
}
|
||||
}
|
||||
|
||||
fun <P : PathHolder> Panel.validatableVenvField(
|
||||
propertyGraph: PropertyGraph,
|
||||
internal fun <T, P : PathHolder, VP : ValidatedPath<T, P>> Panel.validatablePathField(
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?>,
|
||||
pathValidator: PathValidator<T, P, VP>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
selectedPathValidator: suspend (P) -> ValidatedPath.Folder<P>,
|
||||
): ValidatedPathField<Unit, P, ValidatedPath.Folder<P>> {
|
||||
return validatablePathField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = labelText,
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
isFileSelectionMode = false,
|
||||
selectedPathValidator = selectedPathValidator,
|
||||
)
|
||||
}
|
||||
|
||||
fun <P : PathHolder> Panel.validatableExecutableField(
|
||||
propertyGraph: PropertyGraph,
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
selectedPathValidator: suspend (P) -> ValidatedPath.Executable<P>,
|
||||
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
|
||||
return validatablePathField(
|
||||
propertyGraph = propertyGraph,
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = labelText,
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
isFileSelectionMode = true,
|
||||
selectedPathValidator = selectedPathValidator,
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.validatablePathField(
|
||||
propertyGraph: PropertyGraph,
|
||||
fileSystem: FileSystem<P>,
|
||||
backProperty: ObservableMutableProperty<V?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
labelText: @Nls String,
|
||||
missingExecutableText: @Nls String?,
|
||||
installAction: ActionLink? = null,
|
||||
isFileSelectionMode: Boolean,
|
||||
selectedPathValidator: suspend (P) -> V,
|
||||
): ValidatedPathField<T, P, V> {
|
||||
val isValidationActiveProperty = propertyGraph.property(false)
|
||||
isFileSelectionMode: Boolean = true,
|
||||
): ValidatedPathField<T, P, VP> {
|
||||
|
||||
val validatedPathField = ValidatedPathField(
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
pathValidator = pathValidator,
|
||||
browseFolderDialogTitle = labelText,
|
||||
isFileSelectionMode = isFileSelectionMode,
|
||||
isValidationActiveProperty = isValidationActiveProperty,
|
||||
pathValidator = selectedPathValidator,
|
||||
)
|
||||
|
||||
missingExecutableText?.let {
|
||||
@@ -465,26 +385,26 @@ private fun <T, P : PathHolder, V : ValidatedPath<T, P>> Panel.validatablePathFi
|
||||
missingExecutableText = missingExecutableText,
|
||||
installAction = installAction,
|
||||
validatedPathField = validatedPathField
|
||||
).visibleIf(backProperty.isNull().and(isValidationActiveProperty.not()))
|
||||
).visibleIf(pathValidator.backProperty.transform { it?.pathHolder == null }.and(pathValidator.isDirtyValue.not()))
|
||||
}
|
||||
|
||||
row(labelText) {
|
||||
cell(validatedPathField)
|
||||
.align(AlignX.FILL)
|
||||
.validationRequestor(validationRequestor
|
||||
and WHEN_PROPERTY_CHANGED(isValidationActiveProperty)
|
||||
and WHEN_PROPERTY_CHANGED(backProperty)
|
||||
and WHEN_PROPERTY_CHANGED(pathValidator.isDirtyValue)
|
||||
and WHEN_PROPERTY_CHANGED(pathValidator.backProperty)
|
||||
)
|
||||
.validationOnInput { component ->
|
||||
if (!component.isVisible) return@validationOnInput null
|
||||
|
||||
val pyErrorMessage = backProperty.get()?.validationResult?.errorOrNull?.message
|
||||
val pyErrorMessage = pathValidator.backProperty.get()?.validationResult?.errorOrNull?.message
|
||||
|
||||
when {
|
||||
pyErrorMessage != null -> {
|
||||
ValidationInfo(pyErrorMessage)
|
||||
}
|
||||
isValidationActiveProperty.get() -> {
|
||||
pathValidator.isDirtyValue.get() -> {
|
||||
ValidationInfo(message("python.add.sdk.wait.for.validation"))
|
||||
}
|
||||
else -> null
|
||||
|
||||
@@ -196,7 +196,7 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
|
||||
}
|
||||
|
||||
|
||||
suspend fun <P: PathHolder> setupSdk(
|
||||
internal suspend fun <P: PathHolder> setupSdk(
|
||||
project: Project?,
|
||||
allSdks: List<Sdk>,
|
||||
fileSystem: FileSystem<P>,
|
||||
|
||||
+28
-29
@@ -17,6 +17,7 @@ import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
@@ -46,18 +47,14 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
with(panel) {
|
||||
condaExecutable = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
condaExecutable = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = state.condaExecutable,
|
||||
pathValidator = model.condaState.toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink)
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
|
||||
}
|
||||
)
|
||||
|
||||
rowsRange {
|
||||
row(message("sdk.create.custom.env.creation.type")) {
|
||||
@@ -65,18 +62,28 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
|
||||
items = emptyList(),
|
||||
renderer = CondaEnvComboBoxListCellRenderer()
|
||||
).withExtendableTextFieldEditor()
|
||||
.bindItem(state.selectedCondaEnv)
|
||||
.bindItem(model.condaState.selectedCondaEnv)
|
||||
.validationRequestor(
|
||||
validationRequestor
|
||||
and WHEN_PROPERTY_CHANGED(state.selectedCondaEnv)
|
||||
and WHEN_PROPERTY_CHANGED(state.condaExecutable)
|
||||
and WHEN_PROPERTY_CHANGED(model.modificationCounter)
|
||||
and WHEN_PROPERTY_CHANGED(model.condaState.selectedCondaEnv)
|
||||
and WHEN_PROPERTY_CHANGED(model.condaState.condaExecutable)
|
||||
and WHEN_PROPERTY_CHANGED(isReloadLinkVisible)
|
||||
)
|
||||
.validationOnInput {
|
||||
if (!it.isVisible) return@validationOnInput null
|
||||
|
||||
val environmentsResult = model.condaState.condaEnvironmentsResult.value
|
||||
when {
|
||||
!it.isVisible -> null
|
||||
!isReloadLinkVisible.get() -> ValidationInfo(message("python.add.sdk.panel.wait")).asWarning()
|
||||
it.selectedItem == null -> ValidationInfo(message("python.sdk.conda.no.env.selected.error"))
|
||||
environmentsResult == null || !isReloadLinkVisible.get() -> {
|
||||
ValidationInfo(message("python.add.sdk.panel.wait")).asWarning()
|
||||
}
|
||||
environmentsResult is Result.Failure -> {
|
||||
ValidationInfo(environmentsResult.error.message)
|
||||
}
|
||||
it.selectedItem == null -> {
|
||||
ValidationInfo(message("python.sdk.conda.no.env.selected.error"))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -95,44 +102,36 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
|
||||
.align(AlignX.RIGHT)
|
||||
.visibleIf(isReloadLinkVisible).component
|
||||
}
|
||||
}.visibleIf(state.condaExecutable.transform { it?.validationResult?.successOrNull != null })
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReloadCondaEnvironments(scope: CoroutineScope) {
|
||||
scope.launch(Dispatchers.EDT) {
|
||||
model.condaEnvironmentsLoading.value = true
|
||||
model.detectCondaEnvironmentsOrError(errorSink)
|
||||
model.condaEnvironmentsLoading.value = false
|
||||
}.visibleIf(model.condaState.condaExecutable.transform { it?.validationResult?.successOrNull != null })
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
scope.launch(Dispatchers.EDT) {
|
||||
model.condaEnvironments.collectLatest { environments ->
|
||||
model.condaState.condaEnvironmentsResult.collectLatest { environmentsResult ->
|
||||
envComboBox.removeAllItems()
|
||||
environments.forEach(envComboBox::addItem)
|
||||
environmentsResult?.successOrNull?.forEach(envComboBox::addItem)
|
||||
}
|
||||
}
|
||||
|
||||
reloadLink.action = object : AbstractAction(message("sdk.create.custom.conda.refresh.envs")) {
|
||||
override fun actionPerformed(e: ActionEvent?) {
|
||||
onReloadCondaEnvironments(scope)
|
||||
model.condaState.detectCondaEnvironments()
|
||||
}
|
||||
}
|
||||
|
||||
model.condaEnvironmentsLoading.onEach { isLoading ->
|
||||
model.condaState.condaEnvironmentsLoading.onEach { isLoading ->
|
||||
isReloadLinkVisible.set(!isLoading)
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
|
||||
envComboBox.displayLoaderWhen(
|
||||
loading = model.condaEnvironmentsLoading,
|
||||
loading = model.condaState.condaEnvironmentsLoading,
|
||||
makeTemporaryEditable = true,
|
||||
scope = scope,
|
||||
)
|
||||
condaExecutable.initialize(scope)
|
||||
condaExecutable.displayLoaderWhen(
|
||||
loading = model.condaEnvironmentsLoading,
|
||||
loading = model.condaState.condaEnvironmentsLoading,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
@@ -142,7 +141,7 @@ internal class CondaExistingEnvironmentSelector<P : PathHolder>(model: PythonAdd
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
val identity = model.state.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
|
||||
val identity = model.condaState.selectedCondaEnv.get()?.envIdentity as? PyCondaEnvIdentity.UnnamedEnv
|
||||
val selectedConda = if (identity?.isBase == true) InterpreterType.BASE_CONDA else InterpreterType.CONDAVENV
|
||||
return InterpreterStatisticsInfo(
|
||||
type = selectedConda,
|
||||
|
||||
@@ -39,35 +39,31 @@ internal class CondaNewEnvironmentCreator<P: PathHolder>(model: PythonMutableTar
|
||||
}
|
||||
row(message("sdk.create.custom.conda.env.name")) {
|
||||
textField()
|
||||
.bindText(model.state.newCondaEnvName) // property setter for getOrCreateSdk
|
||||
.bindText(model.condaState.newCondaEnvName) // property setter for getOrCreateSdk
|
||||
.bindText(model.projectPathFlows.projectName) // default value getter
|
||||
}
|
||||
|
||||
condaExecutable = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
condaExecutable = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.condaExecutable,
|
||||
pathValidator = model.condaState.toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
installAction = createInstallCondaFix(model, errorSink)
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binaryToExec.getToolVersion("conda"))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
condaExecutable.initialize(scope)
|
||||
condaExecutable.displayLoaderWhen(
|
||||
loading = model.condaEnvironmentsLoading,
|
||||
loading = model.condaState.condaEnvironmentsLoading,
|
||||
scope = scope,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
return model.createCondaEnvironment(moduleOrProject, NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))
|
||||
return model.createCondaEnvironment(moduleOrProject, NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.condaState.newCondaEnvName.get()))
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.conda
|
||||
|
||||
import com.intellij.openapi.application.UI
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.onFailure
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.conda.suggestCondaPath
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
|
||||
class CondaState<P : PathHolder>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
) : ToolState {
|
||||
val condaExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val condaEnvironmentsResult: MutableStateFlow<PyResult<List<PyCondaEnv>>?> = MutableStateFlow(null)
|
||||
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
lateinit var scope: CoroutineScope
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
toolVersionPrefix = "conda",
|
||||
backProperty = condaExecutable,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
val executor = targetEnvironmentConfiguration.toExecutor()
|
||||
val suggestedCondaPath = runCatching {
|
||||
suggestCondaPath(targetCommandExecutor = executor)
|
||||
}.getOrLogException(LOG)
|
||||
|
||||
val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) }
|
||||
condaPathOnFS
|
||||
}
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
toolValidator.initialize(scope)
|
||||
this.scope = scope
|
||||
|
||||
condaExecutable.afterChange { condaExecutable ->
|
||||
condaEnvironmentsResult.value = null
|
||||
selectedCondaEnv.set(null)
|
||||
baseCondaEnv.set(null)
|
||||
|
||||
if (condaExecutable?.validationResult?.successOrNull != null) {
|
||||
detectCondaEnvironments()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun detectCondaEnvironments() {
|
||||
condaEnvironmentsLoading.value = true
|
||||
scope.launch(Dispatchers.UI) {
|
||||
condaEnvironmentsResult.value = updateCondaEnvironments()
|
||||
}.invokeOnCompletion {
|
||||
condaEnvironmentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error or `null` if no error
|
||||
*/
|
||||
private suspend fun updateCondaEnvironments(): PyResult<List<PyCondaEnv>> = withContext(Dispatchers.IO) {
|
||||
val executable = condaExecutable.get()
|
||||
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
|
||||
executable.validationResult.getOr { return@withContext it }
|
||||
|
||||
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
|
||||
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(Dispatchers.UI) {
|
||||
baseCondaEnv.set(baseConda)
|
||||
selectedCondaEnv.set(environments.firstOrNull())
|
||||
}
|
||||
return@withContext PyResult.success(environments)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,6 @@ package com.jetbrains.python.sdk.add.v2.conda
|
||||
import com.intellij.execution.target.TargetEnvironmentConfiguration
|
||||
import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.fileLogger
|
||||
import com.intellij.openapi.diagnostic.getOrLogException
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
@@ -14,7 +11,6 @@ import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrLogException
|
||||
import com.jetbrains.python.isCondaVirtualEnv
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
@@ -24,18 +20,15 @@ import com.jetbrains.python.sdk.conda.*
|
||||
import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.persist
|
||||
import com.jetbrains.python.sdk.setAssociationToModule
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private val LOG: Logger = fileLogger()
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
|
||||
@RequiresEdt
|
||||
internal fun PythonAddInterpreterModel<*>.createCondaCommand(): PyResult<PyCondaCommand> {
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
val executable = condaState.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
return PyCondaCommand(
|
||||
fullCondaPathOnTarget = executable.pathHolder.toString().convertToPathOnTarget(targetEnvironmentConfiguration),
|
||||
targetConfig = targetEnvironmentConfiguration
|
||||
@@ -67,20 +60,28 @@ internal fun TargetEnvironmentConfiguration?.toExecutor(): TargetCommandExecutor
|
||||
return TargetEnvironmentRequestCommandExecutor(this?.createEnvironmentRequest(project = null) ?: LocalTargetEnvironmentRequest())
|
||||
}
|
||||
|
||||
|
||||
internal fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult<PyCondaEnv> {
|
||||
val baseConda = condaState.baseCondaEnv.get()
|
||||
return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error"))
|
||||
}
|
||||
|
||||
/**
|
||||
* [base] or selected
|
||||
*/
|
||||
suspend fun PythonAddInterpreterModel<*>.selectCondaEnvironment(base: Boolean): PyResult<Sdk> {
|
||||
condaState.condaEnvironmentsLoading.takeWhile { it }.collect { }
|
||||
val pyCondaEnv = if (base) {
|
||||
getBaseCondaOrError()
|
||||
}
|
||||
else {
|
||||
state.selectedCondaEnv.get()?.let { PyResult.success(it) } ?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
|
||||
condaState.selectedCondaEnv.get()?.let { PyResult.success(it) }
|
||||
?: PyResult.localizedError(message("python.sdk.conda.no.env.selected.error"))
|
||||
}
|
||||
.getOr { return it }
|
||||
val existingSdk = ProjectJdkTable.getInstance().findJdk(pyCondaEnv.envIdentity.userReadableName)
|
||||
if (existingSdk != null && existingSdk.isCondaVirtualEnv) return PyResult.success(existingSdk)
|
||||
val executable = state.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
val executable = condaState.condaExecutable.get() ?: return PyResult.localizedError(message("python.sdk.select.conda.path.title"))
|
||||
executable.validationResult.getOr { return it }
|
||||
|
||||
val sdk = PyCondaCommand(
|
||||
@@ -105,45 +106,4 @@ suspend fun BinaryToExec.getCondaVersion(): PyResult<Version> {
|
||||
catch (ex: VersionFormatException) {
|
||||
PyResult.localizedError(ex.localizedMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns error or `null` if no error
|
||||
*/
|
||||
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaEnvironments(): PyResult<Unit> = withContext(Dispatchers.IO) {
|
||||
val executable = state.condaExecutable.get()
|
||||
if (executable == null) return@withContext PyResult.localizedError(message("python.sdk.conda.no.exec"))
|
||||
executable.validationResult.getOr { return@withContext it }
|
||||
|
||||
val binaryToExec = executable.pathHolder?.let { fileSystem.getBinaryToExec(it) }!!
|
||||
val environments = PyCondaEnv.getEnvs(binaryToExec).getOr { return@withContext it }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
condaEnvironments.value = environments
|
||||
state.baseCondaEnv.set(baseConda)
|
||||
}
|
||||
return@withContext PyResult.success(Unit)
|
||||
}
|
||||
|
||||
internal suspend fun <P: PathHolder> PythonAddInterpreterModel<P>.detectCondaExecutable(): Unit = withContext(Dispatchers.IO) {
|
||||
val targetEnvironmentConfiguration = (fileSystem as? FileSystem.Target)?.targetEnvironmentConfiguration
|
||||
val executor = targetEnvironmentConfiguration.toExecutor()
|
||||
val suggestedCondaPath = runCatching {
|
||||
suggestCondaPath(targetCommandExecutor = executor)
|
||||
}.getOrLogException(LOG)
|
||||
val condaPathOnFS = suggestedCondaPath?.let { fileSystem.parsePath(suggestedCondaPath).getOrLogException(LOG) }
|
||||
|
||||
val executable = if (condaPathOnFS != null) {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(condaPathOnFS)
|
||||
val versionResult = binaryToExec.getCondaVersion()
|
||||
ValidatedPath.Executable(condaPathOnFS, versionResult)
|
||||
}
|
||||
else {
|
||||
ValidatedPath.Executable<P>(null, PyResult.localizedError(message("python.add.sdk.conda.executable.path.is.not.found")))
|
||||
}
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
state.condaExecutable.set(executable)
|
||||
}
|
||||
}
|
||||
+4
-24
@@ -1,10 +1,8 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.hatch
|
||||
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.PythonVirtualEnvironment
|
||||
import com.intellij.python.hatch.resolveHatchWorkingDirectory
|
||||
@@ -24,47 +22,29 @@ import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class HatchExistingEnvironmentSelector<P: PathHolder>(
|
||||
override val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
) : PythonExistingEnvironmentConfigurator<P>(model) {
|
||||
val interpreterType: InterpreterType = InterpreterType.HATCH
|
||||
val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.hatchExecutable
|
||||
|
||||
private lateinit var hatchFormFields: HatchFormFields<P>
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
hatchFormFields = panel.buildHatchFormFields(
|
||||
model = model,
|
||||
hatchEnvironmentProperty = state.selectedHatchEnv,
|
||||
hatchExecutableProperty = executable,
|
||||
validationRequestor = validationRequestor,
|
||||
isGenerateNewMode = false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = true)
|
||||
executable.afterChange { hatchExecutable ->
|
||||
if (hatchExecutable?.validationResult?.successOrNull == null) {
|
||||
model.hatchEnvironmentsResult.value = null
|
||||
return@afterChange
|
||||
}
|
||||
|
||||
val binaryToExec = hatchExecutable.pathHolder?.let { model.fileSystem.getBinaryToExec(it) }
|
||||
?: return@afterChange
|
||||
scope.launch(Dispatchers.IO) {
|
||||
model.detectHatchEnvironments(binaryToExec).also {
|
||||
model.hatchEnvironmentsResult.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
hatchFormFields.onShown(scope, model, isFilterOnlyExisting = true)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
val environment = state.selectedHatchEnv.get()
|
||||
val environment = model.hatchState.selectedHatchEnv.get()
|
||||
val existingHatchVenv = environment?.pythonVirtualEnvironment as? PythonVirtualEnvironment.Existing
|
||||
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
|
||||
|
||||
@@ -83,8 +63,8 @@ internal class HatchExistingEnvironmentSelector<P: PathHolder>(
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
when (val binaryToExec = executable.get()?.pathHolder) {
|
||||
is BinOnEel -> HatchConfiguration.persistPathForTarget(hatchExecutablePath = binaryToExec.path)
|
||||
when (val pathHolder = model.hatchState.hatchExecutable.get()?.pathHolder) {
|
||||
is PathHolder.Eel -> HatchConfiguration.persistPathForTarget(hatchExecutablePath = pathHolder.path)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.roots.ModuleRootModificationUtil
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.vfs.VfsUtilCore
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.hatch.HatchConfiguration
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
@@ -18,28 +17,23 @@ import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.hatch.sdk.createSdk
|
||||
import com.jetbrains.python.onSuccess
|
||||
import com.jetbrains.python.sdk.add.v2.CustomNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class HatchNewEnvironmentCreator<P: PathHolder>(
|
||||
internal class HatchNewEnvironmentCreator<P : PathHolder>(
|
||||
override val model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("hatch", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.HATCH
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.hatchExecutable
|
||||
override val toolValidator: ToolValidator<P> = model.hatchState.toolValidator
|
||||
private val hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
|
||||
private lateinit var hatchFormFields: HatchFormFields<P>
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
hatchFormFields = panel.buildHatchFormFields(
|
||||
model = model,
|
||||
hatchExecutableProperty = executable,
|
||||
hatchEnvironmentProperty = hatchEnvironmentProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
isGenerateNewMode = true,
|
||||
installHatchActionLink = createInstallFix(errorSink)
|
||||
@@ -50,17 +44,17 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
|
||||
|
||||
override fun onShown(scope: CoroutineScope) {
|
||||
super.onShown(scope)
|
||||
hatchFormFields.onShown(scope, model, state, isFilterOnlyExisting = false)
|
||||
hatchFormFields.onShown(scope, model, isFilterOnlyExisting = false)
|
||||
}
|
||||
|
||||
override suspend fun savePathToExecutableToProperties(pathHolder: PathHolder?) {
|
||||
val savingPath = pathHolder ?: executable.get()?.pathHolder ?: return
|
||||
val savingPath = pathHolder ?: toolValidator.backProperty.get()?.pathHolder ?: return
|
||||
val eelPath = (savingPath as? PathHolder.Eel)?.path ?: return
|
||||
HatchConfiguration.persistPathForTarget(hatchExecutablePath = eelPath)
|
||||
}
|
||||
|
||||
override suspend fun createPythonModuleStructure(module: Module): PyResult<Unit> {
|
||||
val hatchExecutablePath = (executable.get()?.pathHolder as? BinOnEel)?.path
|
||||
val hatchExecutablePath = (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
?: return Result.failure(HatchUIError.HatchExecutablePathIsNotValid(null))
|
||||
|
||||
val hatchService = module.getHatchService(hatchExecutablePath).getOr { return it }
|
||||
@@ -82,15 +76,15 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
|
||||
override suspend fun setupEnvSdk(moduleBasePath: Path, baseSdks: List<Sdk>, basePythonBinaryPath: P?, installPackages: Boolean): PyResult<Sdk> {
|
||||
val hatchEnv = hatchEnvironmentProperty.get()?.hatchEnvironment
|
||||
?: return Result.failure(HatchUIError.HatchEnvironmentIsNotSelected())
|
||||
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
|
||||
val basePythonBinaryEelPath = when (basePythonBinaryPath) {
|
||||
is PathHolder.Eel -> basePythonBinaryPath.path
|
||||
else -> return PyResult.localizedError(PyBundle.message("target.is.not.supported", basePythonBinaryPath))
|
||||
}
|
||||
val hatchExecutablePath = when (val hatchBinary = executable.get()?.pathHolder) {
|
||||
val hatchExecutablePath = when (val hatchBinary = toolValidator.backProperty.get()?.pathHolder) {
|
||||
is PathHolder.Eel -> hatchBinary.path
|
||||
else -> null
|
||||
}
|
||||
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
|
||||
val hatchService = moduleBasePath.getHatchService(hatchExecutablePath = hatchExecutablePath).getOr { return it }
|
||||
|
||||
val virtualEnvironment = hatchService.createVirtualEnvironment(
|
||||
basePythonBinaryPath = basePythonBinaryEelPath,
|
||||
@@ -103,8 +97,4 @@ internal class HatchNewEnvironmentCreator<P: PathHolder>(
|
||||
}
|
||||
return createdSdk
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
model.detectHatchExecutable()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.hatch
|
||||
|
||||
import com.intellij.openapi.application.UI
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.hatch.HatchConfiguration.getOrDetectHatchExecutablePath
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.Result.Companion.success
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
class HatchState<P : PathHolder>(
|
||||
val fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
val projectPathFlows: ProjectPathFlows,
|
||||
) : ToolState {
|
||||
val selectedHatchEnv: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
|
||||
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(null)
|
||||
val hatchExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
toolVersionPrefix = "hatch",
|
||||
backProperty = hatchExecutable,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
when (fileSystem) {
|
||||
is FileSystem.Eel -> getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
toolValidator.initialize(scope)
|
||||
|
||||
hatchExecutable.afterChange { hatchExecutable ->
|
||||
if (hatchExecutable?.validationResult?.successOrNull == null) {
|
||||
hatchEnvironmentsResult.value = null
|
||||
return@afterChange
|
||||
}
|
||||
|
||||
val binaryToExec = hatchExecutable.pathHolder?.let { fileSystem.getBinaryToExec(it) }
|
||||
?: return@afterChange
|
||||
scope.launch(Dispatchers.UI) {
|
||||
hatchEnvironmentsResult.value = detectHatchEnvironments(binaryToExec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun detectHatchEnvironments(hatchExecutable: BinaryToExec): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
|
||||
val projectPath = projectPathFlows.projectPathWithDefault.first()
|
||||
val hatchExecutablePath = (hatchExecutable as? BinOnEel)?.path
|
||||
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutable.toString()))
|
||||
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
|
||||
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
|
||||
|
||||
val hatchEnvironments = hatchService.findVirtualEnvironments().getOr { return@withContext it }
|
||||
val availableEnvironments = when {
|
||||
hatchWorkingDirectory == projectPath -> hatchEnvironments
|
||||
else -> HatchVirtualEnvironment.AVAILABLE_ENVIRONMENTS_FOR_NEW_PROJECT
|
||||
}
|
||||
success(availableEnvironments)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ package com.jetbrains.python.sdk.add.v2.hatch
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.observable.properties.AtomicBooleanProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
@@ -101,7 +100,6 @@ private class HatchEnvComboBoxListCellRenderer(val contentFlow: StateFlow<PyResu
|
||||
|
||||
private fun <P : PathHolder> Panel.addEnvironmentComboBox(
|
||||
model: PythonAddInterpreterModel<P>,
|
||||
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
isValidateOnlyNotExisting: Boolean,
|
||||
): ComboBox<HatchVirtualEnvironment> {
|
||||
@@ -110,9 +108,9 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
|
||||
lateinit var environmentComboBox: ComboBox<HatchVirtualEnvironment>
|
||||
|
||||
row(message("sdk.create.custom.hatch.environment")) {
|
||||
environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer(model.hatchEnvironmentsResult))
|
||||
.bindItem(hatchEnvironmentProperty)
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(hatchEnvironmentProperty))
|
||||
environmentComboBox = comboBox(emptyList(), HatchEnvComboBoxListCellRenderer(model.hatchState.hatchEnvironmentsResult))
|
||||
.bindItem(model.hatchState.selectedHatchEnv)
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.hatchState.selectedHatchEnv))
|
||||
.validationInfo { component ->
|
||||
environmentAlreadyExists.set(false)
|
||||
when {
|
||||
@@ -139,7 +137,7 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
|
||||
message = message("sdk.create.custom.hatch.environment.exists"),
|
||||
firstActionLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
|
||||
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
|
||||
model.state.selectedHatchEnv.set(environmentComboBox.item)
|
||||
model.hatchState.selectedHatchEnv.set(environmentComboBox.item)
|
||||
model.navigator.navigateTo(newMethod = SELECT_EXISTING, newManager = PythonSupportedEnvironmentManagers.HATCH)
|
||||
},
|
||||
validationType = ValidationType.ERROR
|
||||
@@ -150,22 +148,18 @@ private fun <P : PathHolder> Panel.addEnvironmentComboBox(
|
||||
|
||||
private fun <P : PathHolder> Panel.addExecutableSelector(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
installHatchActionLink: ActionLink? = null,
|
||||
binaryValidator: suspend (P) -> ValidatedPath.Executable<P>,
|
||||
): ValidatedPathField<Version, P, ValidatedPath.Executable<P>> {
|
||||
|
||||
|
||||
val executablePath = validatableExecutableField(
|
||||
propertyGraph = model.propertyGraph,
|
||||
val executablePath = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = hatchExecutableProperty,
|
||||
pathValidator = model.hatchState.toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "hatch"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "hatch"),
|
||||
installAction = installHatchActionLink,
|
||||
selectedPathValidator = binaryValidator
|
||||
)
|
||||
|
||||
return executablePath
|
||||
@@ -176,23 +170,23 @@ internal data class HatchFormFields<P : PathHolder>(
|
||||
val basePythonComboBox: PythonInterpreterComboBox<P>?,
|
||||
val validatedPathField: ValidatedPathField<Version, P, ValidatedPath.Executable<P>>,
|
||||
) {
|
||||
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel<P>, state: AddInterpreterState<P>, isFilterOnlyExisting: Boolean) {
|
||||
model.hatchEnvironmentsResult.onEach { environmentsResult ->
|
||||
fun onShown(scope: CoroutineScope, model: PythonMutableTargetAddInterpreterModel<P>, isFilterOnlyExisting: Boolean) {
|
||||
model.hatchState.hatchEnvironmentsResult.onEach { environmentsResult ->
|
||||
when (environmentsResult) {
|
||||
null -> environmentComboBox.isEnabled = false
|
||||
else -> {
|
||||
environmentComboBox.isEnabled = true
|
||||
environmentComboBox.syncWithEnvs(environmentsResult, isFilterOnlyExisting = isFilterOnlyExisting)
|
||||
if (environmentsResult.isFailure) state.selectedHatchEnv.set(null)
|
||||
if (environmentsResult.isFailure) model.hatchState.selectedHatchEnv.set(null)
|
||||
}
|
||||
}
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
|
||||
with(validatedPathField) {
|
||||
initialize(scope)
|
||||
backProperty.afterChange { executable ->
|
||||
if (executable != model.state.hatchExecutable.get()) {
|
||||
model.state.hatchExecutable.set(executable)
|
||||
pathValidator.backProperty.afterChange { executable ->
|
||||
if (executable != model.hatchState.hatchExecutable.get()) {
|
||||
model.hatchState.hatchExecutable.set(executable)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,8 +195,6 @@ internal data class HatchFormFields<P : PathHolder>(
|
||||
|
||||
internal fun <P : PathHolder> Panel.buildHatchFormFields(
|
||||
model: PythonMutableTargetAddInterpreterModel<P>,
|
||||
hatchEnvironmentProperty: ObservableMutableProperty<HatchVirtualEnvironment?>,
|
||||
hatchExecutableProperty: ObservableMutableProperty<ValidatedPath.Executable<P>?>,
|
||||
validationRequestor: DialogValidationRequestor,
|
||||
isGenerateNewMode: Boolean = false,
|
||||
installHatchActionLink: ActionLink? = null,
|
||||
@@ -210,20 +202,15 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
|
||||
|
||||
val executablePath = addExecutableSelector(
|
||||
model,
|
||||
hatchExecutableProperty,
|
||||
validationRequestor,
|
||||
installHatchActionLink
|
||||
) {
|
||||
val binaryToExec = model.fileSystem.getBinaryToExec(it)
|
||||
ValidatedPath.Executable(it, binaryToExec.getToolVersion("hatch"))
|
||||
}
|
||||
)
|
||||
var environmentComboBox: ComboBox<HatchVirtualEnvironment> by lateinitVal()
|
||||
var basePythonComboBox: PythonInterpreterComboBox<P>? = null
|
||||
|
||||
rowsRange {
|
||||
environmentComboBox = addEnvironmentComboBox(
|
||||
model = model,
|
||||
hatchEnvironmentProperty = hatchEnvironmentProperty,
|
||||
validationRequestor = validationRequestor,
|
||||
isValidateOnlyNotExisting = isGenerateNewMode
|
||||
)
|
||||
@@ -237,7 +224,7 @@ internal fun <P : PathHolder> Panel.buildHatchFormFields(
|
||||
onPathSelected = model::addManuallyAddedInterpreter,
|
||||
)
|
||||
}
|
||||
}.visibleIf(hatchExecutableProperty.transform { it?.validationResult?.successOrNull != null })
|
||||
}.visibleIf(model.hatchState.hatchExecutable.transform { it?.validationResult?.successOrNull != null })
|
||||
|
||||
|
||||
return HatchFormFields(environmentComboBox, basePythonComboBox, executablePath)
|
||||
|
||||
@@ -2,21 +2,16 @@
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.target.FullPathOnTarget
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.UI
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.AtomicProperty
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.intellij.python.community.execService.BinOnEel
|
||||
import com.intellij.python.community.execService.BinaryToExec
|
||||
import com.intellij.python.community.services.shared.*
|
||||
import com.intellij.python.hatch.HatchConfiguration.getOrDetectHatchExecutablePath
|
||||
import com.intellij.python.hatch.HatchVirtualEnvironment
|
||||
import com.intellij.python.hatch.getHatchService
|
||||
import com.intellij.python.community.services.shared.LanguageLevelHolder
|
||||
import com.intellij.python.community.services.shared.LanguageLevelWithUiComparator
|
||||
import com.intellij.python.community.services.shared.UiHolder
|
||||
import com.intellij.python.pyproject.PyProjectToml
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.*
|
||||
@@ -29,23 +24,27 @@ import com.jetbrains.python.errorProcessing.ErrorSink
|
||||
import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaEnvironments
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
|
||||
import com.jetbrains.python.sdk.add.v2.hatch.HatchUIError
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.pipenv.getPipEnvExecutable
|
||||
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
|
||||
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
|
||||
import com.jetbrains.python.sdk.PySdkToInstall
|
||||
import com.jetbrains.python.sdk.PySdkUtil
|
||||
import com.jetbrains.python.sdk.add.v2.conda.CondaState
|
||||
import com.jetbrains.python.sdk.add.v2.hatch.HatchState
|
||||
import com.jetbrains.python.sdk.add.v2.poetry.PoetryState
|
||||
import com.jetbrains.python.sdk.add.v2.uv.UvState
|
||||
import com.jetbrains.python.sdk.add.v2.venv.VenvState
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.isSystemWide
|
||||
import com.jetbrains.python.target.ui.TargetPanelExtension
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
interface ToolState {
|
||||
fun initialize(scope: CoroutineScope)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
val projectPathFlows: ProjectPathFlows,
|
||||
@@ -57,18 +56,21 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
val navigator: PythonNewEnvironmentDialogNavigator = PythonNewEnvironmentDialogNavigator()
|
||||
open val state: AddInterpreterState<P> = AddInterpreterState(propertyGraph)
|
||||
|
||||
val condaState: CondaState<P> = CondaState(fileSystem, propertyGraph)
|
||||
val uvState: UvState<P> = UvState(fileSystem, propertyGraph)
|
||||
val pipenvState: PipenvState<P> = PipenvState(fileSystem, propertyGraph)
|
||||
val poetryState: PoetryState<P> = PoetryState(fileSystem, propertyGraph)
|
||||
val hatchState: HatchState<P> = HatchState(fileSystem, propertyGraph, projectPathFlows)
|
||||
val venvState: VenvState<P> = VenvState(fileSystem, propertyGraph, projectPathFlows)
|
||||
|
||||
internal val knownInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
private val _detectedInterpreters: MutableStateFlow<List<DetectedSelectableInterpreter<P>>?> = MutableStateFlow(null)
|
||||
val detectedInterpreters: StateFlow<List<DetectedSelectableInterpreter<P>>?> = _detectedInterpreters
|
||||
val manuallyAddedInterpreters: MutableStateFlow<List<PythonSelectableInterpreter<P>>> = MutableStateFlow(emptyList())
|
||||
private var installable: List<InstallableSelectableInterpreter<P>> = emptyList()
|
||||
val condaEnvironments: MutableStateFlow<List<PyCondaEnv>> = MutableStateFlow(emptyList())
|
||||
val hatchEnvironmentsResult: MutableStateFlow<PyResult<List<HatchVirtualEnvironment>>?> = MutableStateFlow(null)
|
||||
|
||||
lateinit var allInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
|
||||
lateinit var baseInterpreters: StateFlow<List<PythonSelectableInterpreter<P>>?>
|
||||
|
||||
val condaEnvironmentsLoading: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
|
||||
@TestOnly
|
||||
@ApiStatus.Internal
|
||||
@@ -80,18 +82,20 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
|
||||
// If the project is provided, sdks associated with it will be kept in the list of interpreters. If not, then they will be filtered out.
|
||||
open fun initialize(scope: CoroutineScope) {
|
||||
listOf(condaState, uvState, pipenvState, poetryState, hatchState, venvState).forEach { it.initialize(scope) }
|
||||
|
||||
merge(
|
||||
projectPathFlows.projectPathWithDefault,
|
||||
knownInterpreters,
|
||||
detectedInterpreters,
|
||||
manuallyAddedInterpreters,
|
||||
condaEnvironments,
|
||||
hatchEnvironmentsResult,
|
||||
condaState.condaEnvironmentsResult,
|
||||
hatchState.hatchEnvironmentsResult,
|
||||
).map {
|
||||
modificationCounter.updateAndGet { it + 1 }
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
}.launchIn(scope + Dispatchers.UI)
|
||||
|
||||
scope.launch(TraceContext(message("tracecontext.loading.interpreter.list"), scope) + Dispatchers.EDT) {
|
||||
scope.launch(TraceContext(message("tracecontext.loading.interpreter.list"), scope) + Dispatchers.UI) {
|
||||
installable = fileSystem.getInstallableInterpreters()
|
||||
val projectPathPrefix = projectPathFlows.projectPathWithDefault.first()
|
||||
val existingSelectableInterpreters = fileSystem.getExistingSelectableInterpreters(projectPathPrefix)
|
||||
@@ -119,29 +123,6 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
val nonExistingInstallable = installable.filter { it.languageLevel !in existingLanguageLevels }
|
||||
manual + base.sorted() + nonExistingInstallable
|
||||
}.stateIn(scope, started = SharingStarted.Eagerly, initialValue = null)
|
||||
|
||||
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.conda.executable.and.environments"), scope) + Dispatchers.IO) {
|
||||
detectCondaExecutable()
|
||||
detectCondaEnvironments()
|
||||
}.invokeOnCompletion {
|
||||
this.condaEnvironmentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectHatchEnvironments(hatchExecutable: BinaryToExec): PyResult<List<HatchVirtualEnvironment>> = withContext(Dispatchers.IO) {
|
||||
val projectPath = projectPathFlows.projectPathWithDefault.first()
|
||||
val hatchExecutablePath = (hatchExecutable as? BinOnEel)?.path
|
||||
?: return@withContext Result.failure(HatchUIError.HatchExecutablePathIsNotValid(hatchExecutable.toString()))
|
||||
val hatchWorkingDirectory = if (projectPath.isDirectory()) projectPath else projectPath.parent
|
||||
val hatchService = hatchWorkingDirectory.getHatchService(hatchExecutablePath).getOr { return@withContext it }
|
||||
|
||||
val hatchEnvironments = hatchService.findVirtualEnvironments().getOr { return@withContext it }
|
||||
val availableEnvironments = when {
|
||||
hatchWorkingDirectory == projectPath -> hatchEnvironments
|
||||
else -> HatchVirtualEnvironment.AVAILABLE_ENVIRONMENTS_FOR_NEW_PROJECT
|
||||
}
|
||||
success(availableEnvironments)
|
||||
}
|
||||
|
||||
|
||||
@@ -178,79 +159,6 @@ abstract class PythonAddInterpreterModel<P : PathHolder>(
|
||||
|
||||
abstract class PythonMutableTargetAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonAddInterpreterModel<P>(projectPathFlows, fileSystem) {
|
||||
override val state: MutableTargetState<P> = MutableTargetState(propertyGraph)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
super.initialize(scope)
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.poetry.executable"), scope)) {
|
||||
detectPoetryExecutable()
|
||||
}
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.pip.executable"), scope)) {
|
||||
detectPipEnvExecutable()
|
||||
}
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.uv.executable"), scope)) {
|
||||
detectUvExecutable()
|
||||
}
|
||||
scope.launch(TraceContext(message("tracecontext.detecting.hatch.executable"), scope)) {
|
||||
detectHatchExecutable()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectPoetryExecutable() {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPoetryExecutable() works only with localEel currently
|
||||
|
||||
state.poetryExecutable.autodetectWithVersionProbe("poetry") {
|
||||
getPoetryExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectPipEnvExecutable() {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getPipEnvExecutable() works only with localEel currently
|
||||
|
||||
state.pipenvExecutable.autodetectWithVersionProbe("pipenv") {
|
||||
getPipEnvExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectUvExecutable() {
|
||||
if ((fileSystem as? FileSystem.Eel)?.eelApi != localEel) return // getUvExecutable() works only with localEel currently
|
||||
|
||||
state.uvExecutable.autodetectWithVersionProbe("uv") {
|
||||
getUvExecutable()?.let { PathHolder.Eel(it) } as P?
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectHatchExecutable() {
|
||||
if (fileSystem !is FileSystem.Eel) return // getOrDetectHatchExecutablePath() works only with eel filesystem currently
|
||||
|
||||
state.hatchExecutable.autodetectWithVersionProbe("hatch") {
|
||||
getOrDetectHatchExecutablePath(fileSystem.eelApi).getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ObservableMutableProperty<ValidatedPath.Executable<P>?>.autodetectWithVersionProbe(
|
||||
toolVersionPrefix: String,
|
||||
toolPathSupplier: suspend () -> P?,
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val path = toolPathSupplier.invoke()
|
||||
val validatedPath = path?.validateToolExecutableByVersionProbe(toolVersionPrefix)
|
||||
?: ValidatedPath.Executable(
|
||||
pathHolder = path,
|
||||
validationResult = PyResult.localizedError(message("python.sdk.executable.is.not.detected"))
|
||||
)
|
||||
|
||||
withContext(Dispatchers.EDT) {
|
||||
set(validatedPath)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun P.validateToolExecutableByVersionProbe(toolVersionPrefix: String): ValidatedPath.Executable<P> {
|
||||
val binaryToExec = fileSystem.getBinaryToExec(this)
|
||||
val validationResult = binaryToExec.getToolVersion(toolVersionPrefix)
|
||||
return ValidatedPath.Executable(
|
||||
pathHolder = this,
|
||||
validationResult = validationResult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class PythonLocalAddInterpreterModel<P : PathHolder>(projectPathFlows: ProjectPathFlows, fileSystem: FileSystem<P>) : PythonMutableTargetAddInterpreterModel<P>(projectPathFlows, fileSystem) {
|
||||
@@ -357,58 +265,17 @@ sealed interface ValidatedPath<T, P : PathHolder> {
|
||||
|
||||
open class AddInterpreterState<P : PathHolder>(propertyGraph: PropertyGraph) {
|
||||
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
val condaExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
/**
|
||||
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
|
||||
*/
|
||||
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
|
||||
/**
|
||||
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
|
||||
*/
|
||||
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
|
||||
val selectedHatchEnv: ObservableMutableProperty<HatchVirtualEnvironment?> = propertyGraph.property(null)
|
||||
|
||||
val targetPanelExtension: ObservableMutableProperty<TargetPanelExtension?> = propertyGraph.property(null)
|
||||
}
|
||||
|
||||
class MutableTargetState<P : PathHolder>(propertyGraph: PropertyGraph) : AddInterpreterState<P>(propertyGraph) {
|
||||
val baseInterpreter: ObservableMutableProperty<PythonSelectableInterpreter<P>?> = propertyGraph.property(null)
|
||||
val newCondaEnvName: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
val poetryExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val hatchExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val pipenvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
val venvPath: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
|
||||
val inheritSitePackages: GraphProperty<Boolean> = propertyGraph.property(false)
|
||||
|
||||
/**
|
||||
* Associate SDK with particular module (if true)
|
||||
*/
|
||||
val makeAvailableForAllProjects: GraphProperty<Boolean> = propertyGraph.property(false)
|
||||
}
|
||||
|
||||
|
||||
internal val <P : PathHolder> PythonAddInterpreterModel<P>.existingSdks: List<Sdk>
|
||||
get() = allInterpreters.value?.filterIsInstance<ExistingSelectableInterpreter<P>>()?.map { it.sdkWrapper.sdk } ?: emptyList()
|
||||
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel<*>.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
|
||||
detectCondaEnvironments().onFailure {
|
||||
errorSink.emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel<*>.getBaseCondaOrError(): PyResult<PyCondaEnv> {
|
||||
var baseConda = state.baseCondaEnv.get()
|
||||
if (baseConda != null) return PyResult.success(baseConda)
|
||||
detectCondaEnvironments().getOr { return it }
|
||||
baseConda = state.baseCondaEnv.get()
|
||||
return if (baseConda != null) PyResult.success(baseConda) else PyResult.localizedError(message("python.sdk.conda.no.base.env.error"))
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel<*>.getBasePath(module: Module?): Path = withContext(Dispatchers.IO) {
|
||||
val pyProjectTomlBased = module?.let { PyProjectToml.findFile(it)?.toNioPathOrNull()?.parent }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.components.*
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.vfs.VirtualFileManager
|
||||
@@ -46,7 +45,7 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("poetry", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.POETRY
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
|
||||
override val toolValidator: ToolValidator<P> = model.poetryState.toolValidator
|
||||
override val installationVersion: String = "1.8.0"
|
||||
|
||||
private val isInProjectEnvFlow = MutableStateFlow(service<PoetryConfigService>().state.isInProjectEnv)
|
||||
@@ -104,7 +103,7 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
|
||||
savingPath?.let {
|
||||
PropertiesComponent.getInstance().poetryPath = it.toString()
|
||||
@@ -119,10 +118,6 @@ internal class EnvironmentCreatorPoetry<P: PathHolder>(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
model.detectPoetryExecutable()
|
||||
}
|
||||
|
||||
override fun onVenvSelectExisting() {
|
||||
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
|
||||
|
||||
|
||||
+3
-2
@@ -2,7 +2,6 @@
|
||||
package com.jetbrains.python.sdk.add.v2.poetry
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.PyBundle
|
||||
@@ -14,7 +13,9 @@ import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PathValidator
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.add.v2.Version
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.poetry.createPoetrySdk
|
||||
import com.jetbrains.python.sdk.poetry.detectPoetryEnvs
|
||||
@@ -25,7 +26,7 @@ import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
internal class PoetryExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?) : CustomExistingEnvironmentSelector<P>("poetry", model, module) {
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.poetryExecutable
|
||||
override val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>> = model.poetryState.toolValidator
|
||||
override val interpreterType: InterpreterType = InterpreterType.POETRY
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.poetry
|
||||
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.jetbrains.python.getOrNull
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.poetry.getPoetryExecutable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class PoetryState<P : PathHolder>(
|
||||
fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
) : ToolState {
|
||||
val poetryExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
toolVersionPrefix = "poetry",
|
||||
backProperty = poetryExecutable,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
when (fileSystem) {
|
||||
is FileSystem.Eel -> {
|
||||
if (fileSystem.eelApi == localEel) getPoetryExecutable().getOrNull()?.let { PathHolder.Eel(it) } as P?
|
||||
else null // getPoetryExecutable() works only with localEel currently
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
toolValidator.initialize(scope)
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.CREATE_N
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.CUSTOM
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.VIRTUALENV
|
||||
import com.jetbrains.python.sdk.add.v2.conda.detectCondaExecutable
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
@@ -425,10 +424,7 @@ internal fun <P : PathHolder> createInstallCondaFix(model: PythonAddInterpreterM
|
||||
PythonSdkFlavor.clearExecutablesCache()
|
||||
CondaInstallManager.installLatest(null)
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.venv.progress.title.detect.executable")) {
|
||||
model.condaEnvironmentsLoading.value = true
|
||||
model.detectCondaExecutable()
|
||||
model.detectCondaEnvironmentsOrError(errorSink)
|
||||
model.condaEnvironmentsLoading.value = false
|
||||
model.condaState.toolValidator.autodetectExecutable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +62,15 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
errorSink: ErrorSink,
|
||||
) : CustomNewEnvironmentCreator<P>("uv", model, errorSink) {
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
|
||||
private val executableFlow = MutableStateFlow(model.state.uvExecutable.get())
|
||||
override val toolValidator: ToolValidator<P> = model.uvState.toolValidator
|
||||
private val executableFlow = MutableStateFlow(model.uvState.uvExecutable.get())
|
||||
private val pythonVersion: ObservableMutableProperty<Version?> = propertyGraph.property(null)
|
||||
private lateinit var versionComboBox: ComboBox<Version?>
|
||||
|
||||
private val loading = AtomicBooleanProperty(false)
|
||||
|
||||
init {
|
||||
executable.afterChange {
|
||||
toolValidator.backProperty.afterChange {
|
||||
executableFlow.value = it
|
||||
}
|
||||
}
|
||||
@@ -91,18 +91,14 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
.visibleIf(loading)
|
||||
}
|
||||
|
||||
executablePath = validatableExecutableField(
|
||||
propertyGraph = propertyGraph,
|
||||
executablePath = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = executable,
|
||||
pathValidator = toolValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.venv.executable.path", "uv"),
|
||||
missingExecutableText = message("sdk.create.custom.venv.missing.text", "uv"),
|
||||
installAction = createInstallFix(errorSink)
|
||||
) {
|
||||
val result = model.fileSystem.getBinaryToExec(it).getToolVersion("uv")
|
||||
ValidatedPath.Executable(it, result)
|
||||
}
|
||||
)
|
||||
|
||||
row("") {
|
||||
venvExistenceValidationAlert(validationRequestor) {
|
||||
@@ -184,7 +180,7 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
if ((model.fileSystem as? FileSystem.Eel)?.eelApi !is LocalEelApi) return
|
||||
|
||||
val savingPath = (pathHolder as? PathHolder.Eel)?.path
|
||||
?: (executable.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
?: (toolValidator.backProperty.get()?.pathHolder as? PathHolder.Eel)?.path
|
||||
savingPath?.let {
|
||||
setUvExecutable(it)
|
||||
}
|
||||
@@ -198,9 +194,4 @@ internal class EnvironmentCreatorUv<P : PathHolder>(
|
||||
): PyResult<Sdk> {
|
||||
return setupNewUvSdkAndEnv(moduleBasePath, baseSdks, pythonVersion.get())
|
||||
}
|
||||
|
||||
override suspend fun detectExecutable() {
|
||||
model.detectUvExecutable()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
package com.jetbrains.python.sdk.add.v2.uv
|
||||
|
||||
import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.Result
|
||||
@@ -13,7 +12,9 @@ import com.jetbrains.python.sdk.add.v2.CustomExistingEnvironmentSelector
|
||||
import com.jetbrains.python.sdk.add.v2.DetectedSelectableInterpreter
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PathValidator
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.add.v2.Version
|
||||
import com.jetbrains.python.sdk.associatedModulePath
|
||||
import com.jetbrains.python.sdk.basePath
|
||||
import com.jetbrains.python.sdk.isAssociatedWithModule
|
||||
@@ -28,7 +29,7 @@ import kotlin.io.path.pathString
|
||||
|
||||
internal class UvExistingEnvironmentSelector<P: PathHolder>(model: PythonMutableTargetAddInterpreterModel<P>, module: Module?)
|
||||
: CustomExistingEnvironmentSelector<P>("uv", model, module) {
|
||||
override val executable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = model.state.uvExecutable
|
||||
override val toolState: PathValidator<Version, P, ValidatedPath.Executable<P>> = model.uvState.toolValidator
|
||||
override val interpreterType: InterpreterType = InterpreterType.UV
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
|
||||
@@ -38,10 +38,10 @@ internal class UvInterpreterSection(
|
||||
uvCreator.onShown(scope)
|
||||
}
|
||||
|
||||
fun hintVisiblePredicate() = _uv and model.state.uvExecutable.isNotNull()
|
||||
fun hintVisiblePredicate() = _uv and model.uvState.uvExecutable.isNotNull()
|
||||
|
||||
private fun selectUvIfExists() {
|
||||
if (model.state.uvExecutable.get() != null
|
||||
if (model.uvState.uvExecutable.get() != null
|
||||
&& selectedMode.get() != PythonInterpreterSelectionMode.PROJECT_UV) {
|
||||
selectedMode.set(PythonInterpreterSelectionMode.PROJECT_UV)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2.uv
|
||||
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.platform.eel.provider.localEel
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.sdk.uv.impl.getUvExecutable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class UvState<P : PathHolder>(
|
||||
fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
) : ToolState {
|
||||
val uvExecutable: ObservableMutableProperty<ValidatedPath.Executable<P>?> = propertyGraph.property(null)
|
||||
|
||||
val toolValidator: ToolValidator<P> = ToolValidator(
|
||||
fileSystem = fileSystem,
|
||||
toolVersionPrefix = "uv",
|
||||
backProperty = uvExecutable,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
when (fileSystem) {
|
||||
is FileSystem.Eel -> {
|
||||
if (fileSystem.eelApi == localEel) getUvExecutable()?.let { PathHolder.Eel(it) } as P?
|
||||
else null // getUvExecutable() works only with localEel currently
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
toolValidator.initialize(scope)
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
package com.jetbrains.python.sdk.add.v2.venv
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.util.isNotNull
|
||||
import com.intellij.openapi.observable.util.transform
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.ui.components.ActionLink
|
||||
@@ -14,19 +14,7 @@ import com.jetbrains.python.errorProcessing.PyResult
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.v2.PathHolder
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod
|
||||
import com.jetbrains.python.sdk.add.v2.PythonMutableTargetAddInterpreterModel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonNewEnvironmentCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPath
|
||||
import com.jetbrains.python.sdk.add.v2.ValidatedPathField
|
||||
import com.jetbrains.python.sdk.add.v2.VenvAlreadyExistsError
|
||||
import com.jetbrains.python.sdk.add.v2.pythonInterpreterComboBox
|
||||
import com.jetbrains.python.sdk.add.v2.toStatisticsField
|
||||
import com.jetbrains.python.sdk.add.v2.validatableVenvField
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -39,14 +27,25 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
|
||||
private lateinit var versionComboBox: PythonInterpreterComboBox<P>
|
||||
private lateinit var venvPathField: ValidatedPathField<Unit, P, ValidatedPath.Folder<P>>
|
||||
|
||||
private val locationValidationMessage = propertyGraph.property("Current location already exists")
|
||||
private val venvAlreadyExistsError = propertyGraph.property<VenvAlreadyExistsError<P>?>(null)
|
||||
private val venvAlreadyExistsErrorMessage: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
|
||||
private var locationModified = false
|
||||
|
||||
init {
|
||||
propertyGraph.dependsOn(venvAlreadyExistsError, model.venvState.backProperty, deleteWhenChildModified = false) {
|
||||
model.venvState.backProperty.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError<P>
|
||||
}
|
||||
propertyGraph.dependsOn(venvAlreadyExistsErrorMessage, venvAlreadyExistsError, deleteWhenChildModified = false) {
|
||||
venvAlreadyExistsError.get()?.message ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun setupUI(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
val secondFixLink = ActionLink(message("sdk.create.custom.venv.select.existing.link")) {
|
||||
PythonNewProjectWizardCollector.logExistingVenvFixUsed()
|
||||
val venvAlreadyExistsError = model.state.venvPath.get()?.validationResult?.errorOrNull as? VenvAlreadyExistsError<P>
|
||||
val venvAlreadyExistsError = venvAlreadyExistsError.get()
|
||||
|
||||
venvAlreadyExistsError?.let { error ->
|
||||
val interpreter = error.detectedSelectableInterpreter.also {
|
||||
@@ -71,29 +70,28 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
|
||||
)
|
||||
|
||||
|
||||
venvPathField = validatableVenvField(
|
||||
propertyGraph = propertyGraph,
|
||||
venvPathField = validatablePathField(
|
||||
fileSystem = model.fileSystem,
|
||||
backProperty = model.state.venvPath,
|
||||
pathValidator = model.venvState.venvValidator,
|
||||
validationRequestor = validationRequestor,
|
||||
labelText = message("sdk.create.custom.location"),
|
||||
missingExecutableText = null,
|
||||
selectedPathValidator = { model.fileSystem.validateVenv(it) }
|
||||
isFileSelectionMode = false,
|
||||
)
|
||||
|
||||
|
||||
row("") {
|
||||
validationTooltip(locationValidationMessage, secondFixLink)
|
||||
validationTooltip(venvAlreadyExistsErrorMessage, secondFixLink)
|
||||
.align(Align.FILL)
|
||||
}.visibleIf(venvPathField.backProperty.transform { it?.validationResult?.errorOrNull }.isNotNull())
|
||||
}.visibleIf(venvAlreadyExistsError.isNotNull())
|
||||
|
||||
row("") {
|
||||
checkBox(message("sdk.create.custom.inherit.packages"))
|
||||
.bindSelected(model.state.inheritSitePackages)
|
||||
.bindSelected(model.venvState.inheritSitePackages)
|
||||
}
|
||||
row("") {
|
||||
checkBox(message("available.to.all.projects"))
|
||||
.bindSelected(model.state.makeAvailableForAllProjects)
|
||||
.bindSelected(model.venvState.makeAvailableForAllProjects)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,13 +104,12 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
|
||||
model.projectPathFlows.projectPathWithDefault.onEach {
|
||||
if (locationModified) return@onEach
|
||||
|
||||
val suggestedVirtualEnv = model.fileSystem.suggestVenv(it)
|
||||
model.state.venvPath.set(suggestedVirtualEnv)
|
||||
model.venvState.venvValidator.autodetectFolder()
|
||||
}.launchIn(scope + Dispatchers.EDT)
|
||||
}
|
||||
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): PyResult<Sdk> {
|
||||
val venv = model.state.venvPath.get()?.pathHolder
|
||||
val venv = model.venvState.backProperty.get()?.pathHolder
|
||||
?: return PyResult.localizedError(message("no.venv.path.specified"))
|
||||
return model.setupVirtualenv(venv, moduleOrProject)
|
||||
}
|
||||
@@ -121,8 +118,8 @@ class EnvironmentCreatorVenv<P : PathHolder>(model: PythonMutableTargetAddInterp
|
||||
return InterpreterStatisticsInfo(
|
||||
type = InterpreterType.VIRTUALENV,
|
||||
target = target.toStatisticsField(),
|
||||
globalSitePackage = model.state.inheritSitePackages.get(),
|
||||
makeAvailableToAllProjects = model.state.makeAvailableForAllProjects.get(),
|
||||
globalSitePackage = model.venvState.inheritSitePackages.get(),
|
||||
makeAvailableToAllProjects = model.venvState.makeAvailableForAllProjects.get(),
|
||||
previouslyConfigured = false,
|
||||
isWSLContext = false, // todo fix for wsl
|
||||
creationMode = InterpreterCreationMode.CUSTOM
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.jetbrains.python.sdk.add.v2.venv
|
||||
|
||||
import com.intellij.openapi.observable.properties.GraphProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
|
||||
import com.jetbrains.python.sdk.add.v2.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class VenvState<P : PathHolder>(
|
||||
fileSystem: FileSystem<P>,
|
||||
propertyGraph: PropertyGraph,
|
||||
projectPathFlows: ProjectPathFlows,
|
||||
) : ToolState {
|
||||
val backProperty: ObservableMutableProperty<ValidatedPath.Folder<P>?> = propertyGraph.property(null)
|
||||
val inheritSitePackages: GraphProperty<Boolean> = propertyGraph.property(false)
|
||||
val makeAvailableForAllProjects: GraphProperty<Boolean> = propertyGraph.property(false)
|
||||
|
||||
val venvValidator: FolderValidator<P> = FolderValidator(
|
||||
fileSystem = fileSystem,
|
||||
backProperty = backProperty,
|
||||
propertyGraph = propertyGraph,
|
||||
defaultPathSupplier = {
|
||||
val projectPath = projectPathFlows.projectPathWithDefault.first()
|
||||
fileSystem.suggestVenv(projectPath)
|
||||
},
|
||||
pathValidator = fileSystem::validateVenv
|
||||
)
|
||||
|
||||
override fun initialize(scope: CoroutineScope) {
|
||||
venvValidator.initialize(scope)
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirt
|
||||
project = moduleOrProject?.project,
|
||||
pathToBasePython = baseSdkPath,
|
||||
pathToVenvHome = venvFolder,
|
||||
inheritSitePackages = state.inheritSitePackages.get(),
|
||||
inheritSitePackages = venvState.inheritSitePackages.get(),
|
||||
existingSdks = existingSdks
|
||||
).getOr { return it }
|
||||
|
||||
@@ -34,7 +34,7 @@ suspend fun <P : PathHolder> PythonMutableTargetAddInterpreterModel<P>.setupVirt
|
||||
|
||||
if (module != null) {
|
||||
module.excludeInnerVirtualEnv(newSdk.sdk)
|
||||
if (!this.state.makeAvailableForAllProjects.get()) {
|
||||
if (!this.venvState.makeAvailableForAllProjects.get()) {
|
||||
newSdk.sdk.setAssociationToModule(module)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user