mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
Python NPW: Heavy refactoring:
1. lots of funs now `suspend` 2. errors reported to `ErrorSink` 3. validation improved (yet, still not perfect) 4. tests added GitOrigin-RevId: 5cbf674a70ad0e0b40180dd358398bf498d6f76d
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7ca537b422
commit
8457d2ae09
@@ -318,6 +318,8 @@ python.sdk.switch.to=Switch to {0}
|
||||
python.sdk.installing=Installing {0}
|
||||
python.sdk.downloading.package.progress.title=Downloading package from {0}
|
||||
python.sdk.select.conda.path.title=Select Path to Conda Executable
|
||||
python.sdk.conda.no.env.selected.error=No Conda enviroment selected
|
||||
python.sdk.conda.no.base.env.error=No base Conda envrionment available
|
||||
python.sdk.conda.problem.running=Problem running conda
|
||||
python.sdk.conda.problem.env.empty.invalid=Environment name is empty or invalid
|
||||
python.sdk.conda.problem.env.name.used=This name is used already. Please, choose another one.
|
||||
@@ -570,6 +572,7 @@ remote.interpreter.feature.is.not.available=Remote interpreter feature is not av
|
||||
|
||||
# What to display of user entered junk
|
||||
commandLine.commandNotFound={0}: command not found
|
||||
commandLine.directoryCantBeAccessed={0}: bad directory
|
||||
|
||||
# Window with actions
|
||||
# "X" button title
|
||||
@@ -1541,4 +1544,5 @@ progress.text.installing=Installing\u2026
|
||||
package.install.with.options.dialog.message=Options:
|
||||
package.install.with.options.dialog.title=Package Install With Options
|
||||
python.toolwindow.packages.collapse.all.action=Collapse All
|
||||
django.template.language=Template Language
|
||||
django.template.language=Template Language
|
||||
python.error=Error
|
||||
@@ -38,6 +38,7 @@ import com.jetbrains.python.newProject.promotion.PromoProjectGenerator
|
||||
import com.jetbrains.python.psi.PyUtil
|
||||
import com.jetbrains.python.sdk.PyLazySdk
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.io.File
|
||||
import java.nio.file.InvalidPathException
|
||||
@@ -121,7 +122,7 @@ class PythonProjectSpecificSettingsStep<T : PyNewProjectSettings>(
|
||||
|
||||
// Instead of setting this type as default, we limit types to it
|
||||
val onlyAllowedInterpreterTypes = projectGenerator.preferredEnvironmentType?.let { setOf(it) }
|
||||
val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocationFlow, onlyAllowedInterpreterTypes).also { interpreterPanel = it }
|
||||
val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocationFlow, onlyAllowedInterpreterTypes, errorSink = ShowingMessageErrorSync).also { interpreterPanel = it }
|
||||
|
||||
mainPanel = panel {
|
||||
row(message("new.project.name")) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector.logPythonNewProjectGenerated
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.PySdkCreator
|
||||
@@ -22,7 +23,7 @@ import kotlinx.coroutines.*
|
||||
class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
|
||||
lateinit var sdkCreator: PySdkCreator
|
||||
|
||||
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Sdk = coroutineScope {
|
||||
suspend fun generateAndGetSdk(module: Module, baseDir: VirtualFile): Result<Sdk> = coroutineScope {
|
||||
val project = module.project
|
||||
if (createGitRepository) {
|
||||
launch(CoroutineName("Generating git") + Dispatchers.IO) {
|
||||
@@ -31,9 +32,7 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
val (sdk, statistics) = withContext(Dispatchers.EDT) {
|
||||
Pair(sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module)), sdkCreator.createStatisticsInfo())
|
||||
}
|
||||
val (sdk: Sdk, statistics: InterpreterStatisticsInfo?) = getSdkAndInterpreter(module).getOrElse { return@coroutineScope Result.failure(it) }
|
||||
sdk.setAssociationToModule(module)
|
||||
module.pythonSdk = sdk
|
||||
if (statistics != null) {
|
||||
@@ -42,7 +41,12 @@ class PyV3BaseProjectSettings(var createGitRepository: Boolean = false) {
|
||||
this::class.java,
|
||||
emptyList())
|
||||
}
|
||||
return@coroutineScope sdk
|
||||
return@coroutineScope Result.success(sdk)
|
||||
}
|
||||
|
||||
private suspend fun getSdkAndInterpreter(module: Module): Result<Pair<Sdk, InterpreterStatisticsInfo?>> = withContext(Dispatchers.EDT) {
|
||||
val sdk: Sdk = sdkCreator.getSdk(ModuleOrProject.ModuleAndProject(module)).getOrElse { return@withContext Result.failure(it) }
|
||||
return@withContext Result.success(Pair<Sdk, InterpreterStatisticsInfo?>(sdk, sdkCreator.createStatisticsInfo()))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package com.jetbrains.python.newProjectWizard
|
||||
|
||||
import com.intellij.facet.ui.ValidationResult
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.module.Module
|
||||
@@ -13,9 +14,13 @@ import com.intellij.util.SystemProperties
|
||||
import com.jetbrains.python.Result
|
||||
import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
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
|
||||
import org.jetbrains.annotations.Nls
|
||||
import java.nio.file.Path
|
||||
|
||||
@@ -29,6 +34,7 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
private val typeSpecificSettings: TYPE_SPECIFIC_SETTINGS,
|
||||
private val typeSpecificUI: PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>?,
|
||||
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
|
||||
private val errorSink: ErrorSink = ShowingMessageErrorSync,
|
||||
) : DirectoryProjectGenerator<PyV3BaseProjectSettings> {
|
||||
private val baseSettings = PyV3BaseProjectSettings()
|
||||
private val projectPathFlow = MutableStateFlow(Path.of(SystemProperties.getUserHome()))
|
||||
@@ -36,7 +42,13 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
|
||||
override fun generateProject(project: Project, baseDir: VirtualFile, settings: PyV3BaseProjectSettings, module: Module) {
|
||||
val coroutineScope = project.service<MyService>().coroutineScope
|
||||
coroutineScope.launch {
|
||||
typeSpecificSettings.generateProject(module, baseDir, settings.generateAndGetSdk(module, baseDir))
|
||||
val sdk = settings.generateAndGetSdk(module, baseDir).getOrElse {
|
||||
withContext(Dispatchers.EDT) {
|
||||
errorSink.emit(it.localizedMessage)
|
||||
}
|
||||
throw it
|
||||
}
|
||||
typeSpecificSettings.generateProject(module, baseDir, sdk)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
|
||||
import com.jetbrains.python.sdk.add.PySdkCreator
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.nio.file.Path
|
||||
@@ -24,7 +25,7 @@ internal class Py3VUI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>
|
||||
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
|
||||
allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
|
||||
) {
|
||||
private val sdkPanel = PythonAddNewEnvironmentPanel(projectPath, allowedInterpreterTypes)
|
||||
private val sdkPanel = PythonAddNewEnvironmentPanel(projectPath, allowedInterpreterTypes, ShowingMessageErrorSync)
|
||||
|
||||
private val _mainPanel = panel {
|
||||
val checkBoxRow = row {
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.jetbrains.python.sdk.add.target.PyAddTargetBasedSdkDialog
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterDialog
|
||||
import com.jetbrains.python.sdk.add.v2.PythonAddLocalInterpreterPresenter
|
||||
import com.jetbrains.python.target.PythonLanguageRuntimeType
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import java.util.function.Consumer
|
||||
|
||||
fun collectAddInterpreterActions(moduleOrProject: ModuleOrProject, onSdkCreated: Consumer<Sdk>): List<AnAction> {
|
||||
@@ -63,7 +64,7 @@ private class AddLocalInterpreterAction(
|
||||
) : AnAction(PyBundle.messagePointer("python.sdk.action.add.local.interpreter.text"), AllIcons.Nodes.HomeFolder), DumbAware {
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
if (Registry.`is`("python.unified.interpreter.configuration")) {
|
||||
val dialogPresenter = PythonAddLocalInterpreterPresenter(moduleOrProject).apply {
|
||||
val dialogPresenter = PythonAddLocalInterpreterPresenter(moduleOrProject, errorSink = ShowingMessageErrorSync).apply {
|
||||
// Model provides flow, but we need to call Consumer
|
||||
sdkCreatedFlow.oneShotConsumer(onSdkCreated)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
|
||||
interface PySdkCreator {
|
||||
@RequiresEdt
|
||||
fun getSdk(moduleOrProject: ModuleOrProject): Sdk
|
||||
/**
|
||||
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
|
||||
*/
|
||||
suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk>
|
||||
|
||||
@RequiresEdt
|
||||
fun createStatisticsInfo(): InterpreterStatisticsInfo? = null
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.platform.util.progress.RawProgressReporter
|
||||
import com.jetbrains.extensions.failure
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.flavors.PyFlavorAndData
|
||||
@@ -112,7 +113,7 @@ suspend fun PyCondaCommand.createCondaSdkAlongWithNewEnv(newCondaEnvInfo: NewCon
|
||||
val process = PyCondaEnv.createEnv(this, newCondaEnvInfo).getOrElse { return Result.failure(it) }
|
||||
val error = ProcessHandlerReader(process).runProcessAndGetError(uiContext, reporter)
|
||||
|
||||
return error?.let { Result.failure(Exception(it)) }
|
||||
return error?.let { failure(it) }
|
||||
?: Result.success(
|
||||
createCondaSdkFromExistingEnv(newCondaEnvInfo.toIdentity(), existingSdks, project)).apply {
|
||||
onSuccess {
|
||||
|
||||
@@ -7,7 +7,10 @@ import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.openapi.ui.validation.and
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.bindItem
|
||||
import com.intellij.ui.layout.predicate
|
||||
@@ -18,11 +21,13 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import kotlinx.coroutines.*
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : PythonExistingEnvironmentConfigurator(model) {
|
||||
class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel, private val errorSink: ErrorSink) : PythonExistingEnvironmentConfigurator(model) {
|
||||
private lateinit var envComboBox: ComboBox<PyCondaEnv?>
|
||||
|
||||
override fun buildOptions(panel: Panel, validationRequestor: DialogValidationRequestor) {
|
||||
@@ -31,7 +36,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
|
||||
validationRequestor,
|
||||
message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
createInstallCondaFix(model))
|
||||
createInstallCondaFix(model, errorSink))
|
||||
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
|
||||
|
||||
row(message("sdk.create.custom.env.creation.type")) {
|
||||
@@ -41,6 +46,11 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
|
||||
.bindItem(state.selectedCondaEnv)
|
||||
.displayLoaderWhen(model.condaEnvironmentsLoading, makeTemporaryEditable = true,
|
||||
scope = model.scope, uiContext = model.uiContext)
|
||||
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(state.condaExecutable))
|
||||
.validationOnInput {
|
||||
return@validationOnInput if (it.isVisible && it.selectedItem == null) ValidationInfo(message("python.sdk.conda.no.env.selected.error")) else null
|
||||
}
|
||||
.component
|
||||
|
||||
link(message("sdk.create.custom.conda.refresh.envs"), action = { onReloadCondaEnvironments() })
|
||||
@@ -52,7 +62,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
|
||||
private fun onReloadCondaEnvironments() {
|
||||
model.scope.launch(Dispatchers.EDT + ModalityState.current().asContextElement()) {
|
||||
model.condaEnvironmentsLoading.value = true
|
||||
model.detectCondaEnvironments()
|
||||
model.detectCondaEnvironmentsOrError(errorSink)
|
||||
model.condaEnvironmentsLoading.value = false
|
||||
}
|
||||
}
|
||||
@@ -95,9 +105,8 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
|
||||
//}
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
return model.selectCondaEnvironment(state.selectedCondaEnv.get()!!.envIdentity)
|
||||
}
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> =
|
||||
model.selectCondaEnvironment(base = false)
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
|
||||
@@ -109,7 +118,7 @@ class CondaExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pytho
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
//presenter.projectLocationContext is WslContext,
|
||||
//presenter.projectLocationContext is WslContext,
|
||||
false,
|
||||
InterpreterCreationMode.CUSTOM)
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.ui.flow.bindText
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.name
|
||||
|
||||
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val projectPath: StateFlow<Path>?) : PythonNewEnvironmentCreator(model) {
|
||||
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val projectPath: StateFlow<Path>?, private val errorSink:ErrorSink) : PythonNewEnvironmentCreator(model) {
|
||||
|
||||
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
|
||||
private lateinit var versionComboBox: ComboBox<LanguageLevel>
|
||||
@@ -48,7 +49,7 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
|
||||
validationRequestor,
|
||||
message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
createInstallCondaFix(model))
|
||||
createInstallCondaFix(model, errorSink))
|
||||
.displayLoaderWhen(model.condaEnvironmentsLoading, scope = model.scope, uiContext = model.uiContext)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +58,7 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
|
||||
model.state.newCondaEnvName.set(model.projectPath.value.name)
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
|
||||
return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.intellij.openapi.module.Module
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
@@ -49,7 +48,7 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth
|
||||
basePythonComboBox.setItems(model.baseInterpreters)
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
|
||||
savePathToExecutableToProperties()
|
||||
|
||||
// todo think about better error handling
|
||||
@@ -66,8 +65,8 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth
|
||||
model.projectPath.value.toString(),
|
||||
homePath,
|
||||
false)!!
|
||||
SdkConfigurationUtil.addSdk(newSdk)
|
||||
return newSdk
|
||||
addSdk(newSdk)
|
||||
return Result.success(newSdk)
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo =
|
||||
|
||||
@@ -13,10 +13,11 @@ import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.*
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import java.nio.file.Path
|
||||
|
||||
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, private val projectPath: StateFlow<Path>? = null) {
|
||||
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, projectPath: StateFlow<Path>? = null, errorSink: ErrorSink) {
|
||||
|
||||
private val propertyGraph = model.propertyGraph
|
||||
private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
|
||||
@@ -27,14 +28,14 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod
|
||||
|
||||
private val newInterpreterCreators = mapOf(
|
||||
VIRTUALENV to PythonNewVirtualenvCreator(model),
|
||||
CONDA to CondaNewEnvironmentCreator(model, projectPath),
|
||||
CONDA to CondaNewEnvironmentCreator(model, projectPath, errorSink),
|
||||
PIPENV to PipEnvNewEnvironmentCreator(model),
|
||||
POETRY to PoetryNewEnvironmentCreator(model, moduleOrProject),
|
||||
)
|
||||
|
||||
private val existingInterpreterSelectors = mapOf(
|
||||
PYTHON to PythonExistingEnvironmentSelector(model),
|
||||
CONDA to CondaExistingEnvironmentSelector(model),
|
||||
CONDA to CondaExistingEnvironmentSelector(model, errorSink),
|
||||
)
|
||||
|
||||
val currentSdkManager: PythonAddEnvironment
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -44,19 +45,22 @@ class PythonAddLocalInterpreterDialog(private val dialogPresenter: PythonAddLoca
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCenterPanel(): JComponent = panel {
|
||||
model = PythonLocalAddInterpreterModel(PyInterpreterModelParams(service<PythonAddSdkService>().coroutineScope,
|
||||
// At this moment dialog is not displayed, so there is no modality state
|
||||
// The whole idea of context passing is doubtful
|
||||
Dispatchers.EDT + ModalityState.any().asContextElement(), MutableStateFlow(basePath)))
|
||||
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
|
||||
mainPanel = PythonAddCustomInterpreter(model)
|
||||
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val errorSink = ShowingMessageErrorSync
|
||||
return panel {
|
||||
model = PythonLocalAddInterpreterModel(PyInterpreterModelParams(service<PythonAddSdkService>().coroutineScope,
|
||||
// At this moment dialog is not displayed, so there is no modality state
|
||||
// The whole idea of context passing is doubtful
|
||||
Dispatchers.EDT + ModalityState.any().asContextElement(), MutableStateFlow(basePath)))
|
||||
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
|
||||
mainPanel = PythonAddCustomInterpreter(model, errorSink = errorSink)
|
||||
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))
|
||||
|
||||
}.apply {
|
||||
model.scope.launch(model.uiContext) {
|
||||
model.initialize()
|
||||
mainPanel.onShown()
|
||||
}.apply {
|
||||
model.scope.launch(model.uiContext) {
|
||||
model.initialize()
|
||||
mainPanel.onShown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.writeIntentReadAction
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.VirtualEnvReader
|
||||
import com.jetbrains.python.sdk.rootManager
|
||||
import com.jetbrains.python.sdk.service.PySdkService.Companion.pySdkService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
@@ -22,7 +19,7 @@ import java.nio.file.Path
|
||||
*
|
||||
* @see PythonAddLocalInterpreterDialog
|
||||
*/
|
||||
class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, val envReader: VirtualEnvReader = VirtualEnvReader.Instance) {
|
||||
class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, val envReader: VirtualEnvReader = VirtualEnvReader.Instance, val errorSink: ErrorSink) {
|
||||
|
||||
/**
|
||||
* Default path to create virtualenv it
|
||||
@@ -37,7 +34,10 @@ class PythonAddLocalInterpreterPresenter(val moduleOrProject: ModuleOrProject, v
|
||||
val sdkCreatedFlow: Flow<Sdk> = _sdkShared.asSharedFlow()
|
||||
|
||||
suspend fun okClicked(addEnvironment: PythonAddEnvironment) {
|
||||
val sdk = withContext(Dispatchers.EDT) { writeIntentReadAction { addEnvironment.getOrCreateSdk(moduleOrProject) } }
|
||||
val sdk = addEnvironment.getOrCreateSdk(moduleOrProject).getOrElse {
|
||||
errorSink.emit(it.localizedMessage)
|
||||
return
|
||||
}
|
||||
moduleOrProject.project.pySdkService.persistSdk(sdk)
|
||||
_sdkShared.emit(sdk)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
@@ -9,9 +10,12 @@ import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.observable.util.and
|
||||
import com.intellij.openapi.observable.util.notEqualsTo
|
||||
import com.intellij.openapi.observable.util.or
|
||||
import com.intellij.openapi.progress.runBlockingCancellable
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.ui.dsl.builder.TopGap
|
||||
@@ -25,6 +29,8 @@ import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
|
||||
import com.jetbrains.python.statistics.InterpreterCreationMode
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
import com.jetbrains.python.statistics.InterpreterType
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import com.jetbrains.python.util.ShowingMessageErrorSync
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -36,7 +42,7 @@ import java.nio.file.Path
|
||||
/**
|
||||
* If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise
|
||||
*/
|
||||
class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null) : PySdkCreator {
|
||||
class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null, private val errorSink: ErrorSink) : PySdkCreator {
|
||||
|
||||
companion object {
|
||||
private const val VENV_DIR = ".venv"
|
||||
@@ -76,7 +82,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
|
||||
model.navigator.selectionMode = selectedMode
|
||||
//presenter.controller = model
|
||||
|
||||
custom = PythonAddCustomInterpreter(model, projectPath = projectPath)
|
||||
custom = PythonAddCustomInterpreter(model, projectPath = projectPath, errorSink = ShowingMessageErrorSync)
|
||||
|
||||
val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode)
|
||||
|
||||
@@ -104,7 +110,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
|
||||
validationRequestor,
|
||||
message("sdk.create.custom.venv.executable.path", "conda"),
|
||||
message("sdk.create.custom.venv.missing.text", "conda"),
|
||||
createInstallCondaFix(model))
|
||||
createInstallCondaFix(model, errorSink))
|
||||
//.displayLoaderWhen(presenter.detectingCondaExecutable, scope = presenter.scope, uiContext = presenter.uiContext)
|
||||
}.visibleIf(_baseConda)
|
||||
|
||||
@@ -143,19 +149,29 @@ class PythonAddNewEnvironmentPanel(val projectPath: StateFlow<Path>, onlyAllowed
|
||||
}
|
||||
|
||||
@Deprecated("Use one with module or project")
|
||||
fun getSdk(): Sdk = getSdk(ModuleOrProject.ProjectOnly(ProjectManager.getInstance().defaultProject))
|
||||
fun getSdk(): Sdk {
|
||||
val moduleOrProject = ModuleOrProject.ProjectOnly(ProjectManager.getInstance().defaultProject)
|
||||
return if (ApplicationManager.getApplication().isDispatchThread) {
|
||||
runWithModalProgressBlocking(ModalTaskOwner.guess(), "...") {
|
||||
getSdk(moduleOrProject)
|
||||
}
|
||||
}
|
||||
else {
|
||||
runBlockingCancellable { getSdk(moduleOrProject) }
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
override fun getSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
override suspend fun getSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
|
||||
model.navigator.saveLastState()
|
||||
return when (selectedMode.get()) {
|
||||
PROJECT_VENV -> {
|
||||
val projectPath = projectPath.value
|
||||
model.setupVirtualenv(projectPath.resolve(VENV_DIR), // todo just keep venv path, all the rest is in the model
|
||||
projectPath,
|
||||
projectPath
|
||||
//pythonBaseVersion.get()!!)
|
||||
model.state.baseInterpreter.get()!!).getOrThrow()
|
||||
)
|
||||
}
|
||||
BASE_CONDA -> model.selectCondaEnvironment(model.state.baseCondaEnv.get()!!.envIdentity)
|
||||
BASE_CONDA -> model.selectCondaEnvironment(base = true)
|
||||
CUSTOM -> custom.currentSdkManager.getOrCreateSdk(moduleOrProject)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ class PythonExistingEnvironmentSelector(model: PythonAddInterpreterModel) : Pyth
|
||||
comboBox.setItems(model.allInterpreters)
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
|
||||
// todo error handling, nullability issues
|
||||
return setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!
|
||||
return Result.success(setupSdkIfDetected(model.state.selectedInterpreter.get()!!, model.existingSdks)!!)
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.wsl.WslPath.Companion.parseWindowsUncPath
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.ui.TextFieldWithBrowseButton
|
||||
import com.intellij.openapi.ui.ValidationInfo
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
|
||||
import com.intellij.openapi.ui.validation.and
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.OSAgnosticPathUtil
|
||||
import com.intellij.ui.components.ActionLink
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.dsl.builder.components.validationTooltip
|
||||
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
|
||||
import com.intellij.util.ui.showingScope
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.newProject.collector.PythonNewProjectWizardCollector
|
||||
import com.jetbrains.python.newProjectWizard.validateProjectPathAndGetPath
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
|
||||
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
|
||||
@@ -32,6 +30,7 @@ import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel) : PythonNewEnvironmentCreator(model) {
|
||||
private lateinit var versionComboBox: PythonInterpreterComboBox
|
||||
@@ -77,16 +76,22 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
|
||||
.component
|
||||
}
|
||||
row(message("sdk.create.custom.location")) {
|
||||
// TODO" Extract this logic to the presenter or view model, do not touch nio from EDT, cover with test
|
||||
textFieldWithBrowseButton(FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(message("sdk.create.custom.venv.location.browse.title")))
|
||||
.bindText(model.state.venvPath)
|
||||
.whenTextChangedFromUi { locationModified = true }
|
||||
.validationRequestor(validationRequestor and WHEN_PROPERTY_CHANGED(model.state.venvPath))
|
||||
.cellValidation { textField ->
|
||||
addInputRule("") {
|
||||
val pathExists = textField.isVisible && textField.doesPathExist()
|
||||
addInputRule {
|
||||
if (!textField.isVisible) return@addInputRule null // We are hidden, hence valid
|
||||
locationValidationFailed.set(false)
|
||||
val locationPath = when (val path = validateProjectPathAndGetPath(textField.text)) {
|
||||
is com.jetbrains.python.Result.Failure -> return@addInputRule ValidationInfo(path.error) // Path is invalid
|
||||
is com.jetbrains.python.Result.Success -> path.result
|
||||
}
|
||||
val pathExists = locationPath.exists()
|
||||
locationValidationFailed.set(pathExists)
|
||||
if (pathExists) {
|
||||
val locationPath = Paths.get(textField.text)
|
||||
if (locationPath.resolve(pythonInVenvPath).exists()) {
|
||||
val typedName = locationPath.last().toString()
|
||||
suggestedVenvName = suggestVenvName(typedName)
|
||||
@@ -99,12 +104,14 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
|
||||
locationValidationMessage.set(message("sdk.create.custom.venv.folder.not.empty"))
|
||||
suggestedVenvName = ".venv"
|
||||
suggestedLocation = locationPath
|
||||
val suggestedPath = (if (locationPath.isDirectory()) locationPath else locationPath.parent).resolve(suggestedVenvName)
|
||||
firstFixLink.text = message("sdk.create.custom.venv.use.different.venv.link",
|
||||
Paths.get("..", locationPath.last().toString(), suggestedVenvName))
|
||||
suggestedPath)
|
||||
secondFixLink.isVisible = false
|
||||
}
|
||||
}
|
||||
pathExists
|
||||
// Path exists means error
|
||||
if (pathExists) ValidationInfo(locationValidationMessage.get()) else null
|
||||
}
|
||||
}
|
||||
.align(Align.FILL)
|
||||
@@ -184,49 +191,15 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
|
||||
return currentName.removeSuffix(digitSuffix) + newSuffix
|
||||
}
|
||||
|
||||
override fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk {
|
||||
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> =
|
||||
// todo remove project path, or move to controller
|
||||
return model.setupVirtualenv((Path.of(model.state.venvPath.get())), model.projectPath.value, model.state.baseInterpreter.get()!!).getOrThrow()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
/**
|
||||
* Checks if [this] field's text points to an existing file or directory. Calls [Path.exists] from EDT with care to avoid freezing the
|
||||
* UI.
|
||||
*
|
||||
* In case of Windows machine, this method skips testing UNC paths on existence (except WSL paths) to prevent [Path.exists] checking the
|
||||
* network resources. Such checks might freeze the UI for several seconds when performed from EDT. The freezes reveal themselves when
|
||||
* a user starts typing a WSL path with `\\w` and continues symbol by symbol (`\\ws`, `\\wsl`, etc.) all the way to the root WSL path.
|
||||
*
|
||||
* @see [Path.exists]
|
||||
*/
|
||||
@RequiresBackgroundThread(generateAssertion = false)
|
||||
private fun TextFieldWithBrowseButton.doesPathExist(): Boolean =
|
||||
text.let { probablyIncompletePath ->
|
||||
if (SystemInfo.isWindows && OSAgnosticPathUtil.isUncPath(probablyIncompletePath)) {
|
||||
val parseWindowsUncPath = parseWindowsUncPath(probablyIncompletePath)
|
||||
if (parseWindowsUncPath?.linuxPath?.isNotBlank() == true) {
|
||||
probablyIncompletePath.safeCheckCorrespondingPathExist()
|
||||
}
|
||||
else {
|
||||
false
|
||||
}
|
||||
}
|
||||
else {
|
||||
probablyIncompletePath.safeCheckCorrespondingPathExist()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.safeCheckCorrespondingPathExist() =
|
||||
try {
|
||||
Paths.get(this).exists()
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
try {
|
||||
val venvPath = Path.of(model.state.venvPath.get())
|
||||
model.setupVirtualenv(venvPath, model.projectPath.value)
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
Result.failure(e)
|
||||
}
|
||||
|
||||
override fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo {
|
||||
//val statisticsTarget = if (presenter.projectLocationContext is WslContext) InterpreterTarget.TARGET_WSL else target.toStatisticsField()
|
||||
|
||||
@@ -15,11 +15,13 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.ui.validation.DialogValidationRequestor
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.icons.PythonIcons
|
||||
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
|
||||
import com.jetbrains.python.sdk.*
|
||||
import com.jetbrains.python.sdk.LOGGER
|
||||
import com.jetbrains.python.sdk.ModuleOrProject
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.installSdkIfNeeded
|
||||
import com.jetbrains.python.sdk.pipenv.PIPENV_ICON
|
||||
import com.jetbrains.python.sdk.poetry.POETRY_ICON
|
||||
import com.jetbrains.python.statistics.InterpreterTarget
|
||||
@@ -43,9 +45,10 @@ abstract class PythonAddEnvironment(open val model: PythonAddInterpreterModel) {
|
||||
|
||||
/**
|
||||
* Returns created SDK ready to use
|
||||
*
|
||||
* Error is shown to user. Do not catch all exceptions, only return exceptions valuable to user
|
||||
*/
|
||||
@RequiresEdt
|
||||
abstract fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Sdk
|
||||
abstract suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk>
|
||||
abstract fun createStatisticsInfo(target: PythonInterpreterCreationTargets): InterpreterStatisticsInfo
|
||||
}
|
||||
|
||||
@@ -53,9 +56,6 @@ abstract class PythonNewEnvironmentCreator(override val model: PythonMutableTarg
|
||||
abstract class PythonExistingEnvironmentConfigurator(model: PythonAddInterpreterModel) : PythonAddEnvironment(model)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
enum class PythonSupportedEnvironmentManagers(val nameKey: String, val icon: Icon) {
|
||||
VIRTUALENV("sdk.create.custom.virtualenv", PythonIcons.Python.Virtualenv),
|
||||
CONDA("sdk.create.custom.conda", PythonIcons.Python.Anaconda),
|
||||
@@ -109,7 +109,7 @@ internal fun installBaseSdk(sdk: Sdk, existingSdks: List<Sdk>): Sdk? {
|
||||
}
|
||||
|
||||
|
||||
internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
|
||||
internal suspend fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existingSdks: List<Sdk>, targetConfig: TargetEnvironmentConfiguration? = null): Sdk? {
|
||||
if (interpreter is ExistingSelectableInterpreter) return interpreter.sdk
|
||||
|
||||
val homeDir = interpreter.homePath.virtualFileOnTarget(targetConfig) ?: return null // todo handle
|
||||
@@ -119,6 +119,6 @@ internal fun setupSdkIfDetected(interpreter: PythonSelectableInterpreter, existi
|
||||
false,
|
||||
null, // todo create additional data for target
|
||||
null) ?: return null
|
||||
SdkConfigurationUtil.addSdk(newSdk)
|
||||
addSdk(newSdk)
|
||||
return newSdk
|
||||
}
|
||||
@@ -6,10 +6,9 @@ import com.intellij.execution.target.local.LocalTargetEnvironmentRequest
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.sdk.PythonSdkAdditionalData
|
||||
@@ -27,23 +26,22 @@ internal fun PythonAddInterpreterModel.createCondaCommand(): PyCondaCommand =
|
||||
PyCondaCommand(state.condaExecutable.get().convertToPathOnTarget(targetEnvironmentConfiguration),
|
||||
targetConfig = targetEnvironmentConfiguration)
|
||||
|
||||
@RequiresEdt
|
||||
internal fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Sdk {
|
||||
suspend fun PythonAddInterpreterModel.createCondaEnvironment(request: NewCondaEnvRequest): Result<Sdk> {
|
||||
val project = ProjectManager.getInstance().defaultProject
|
||||
val existingSdks = this@createCondaEnvironment.existingSdks
|
||||
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
|
||||
PyBundle.message("sdk.create.custom.conda.create.progress"),
|
||||
TaskCancellation.nonCancellable()) {
|
||||
|
||||
val sdk = withModalProgress(ModalTaskOwner.guess(),
|
||||
PyBundle.message("sdk.create.custom.conda.create.progress"),
|
||||
TaskCancellation.nonCancellable()) {
|
||||
createCondaCommand()
|
||||
.createCondaSdkAlongWithNewEnv(request,
|
||||
Dispatchers.EDT,
|
||||
existingSdks,
|
||||
project).getOrThrow()
|
||||
}
|
||||
project)
|
||||
}.getOrElse { return Result.failure(it) }
|
||||
|
||||
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
|
||||
SdkConfigurationUtil.addSdk(sdk)
|
||||
return sdk
|
||||
addSdk(sdk)
|
||||
return Result.success(sdk)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,12 @@ import com.intellij.openapi.fileChooser.FileChooser
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.observable.properties.PropertyGraph
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.util.SystemProperties
|
||||
import com.jetbrains.extensions.failure
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.configuration.PyConfigurableInterpreterList
|
||||
import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
@@ -23,6 +27,7 @@ import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.pipenv.pipEnvPath
|
||||
import com.jetbrains.python.sdk.poetry.poetryPath
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.nio.file.Path
|
||||
@@ -97,21 +102,26 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun detectCondaEnvironments() {
|
||||
/**
|
||||
* Returns error or `null` if no error
|
||||
*/
|
||||
suspend fun detectCondaEnvironments(): @NlsSafe String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val commandExecutor = targetEnvironmentConfiguration.toExecutor()
|
||||
val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get()).getOrLogException(LOG) ?: emptyList()
|
||||
val fullCondaPathOnTarget = state.condaExecutable.get()
|
||||
if (fullCondaPathOnTarget.isBlank()) return@withContext message("python.sdk.conda.no.exec")
|
||||
val environments = PyCondaEnv.getEnvs(commandExecutor, fullCondaPathOnTarget).getOrElse { return@withContext it.localizedMessage }
|
||||
val baseConda = environments.find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } }
|
||||
|
||||
withContext(uiContext) {
|
||||
condaEnvironments.value = environments
|
||||
state.baseCondaEnv.set(baseConda)
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
suspend fun initInterpreterList() {
|
||||
private suspend fun initInterpreterList() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList()
|
||||
val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks)
|
||||
@@ -182,10 +192,9 @@ abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModel
|
||||
state.pipenvExecutable.set(savedPath)
|
||||
}
|
||||
else {
|
||||
val modalityState = ModalityState.current().asContextElement()
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val detectedExecutable = com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable()
|
||||
withContext(Dispatchers.EDT + modalityState) {
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
detectedExecutable?.let { state.pipenvExecutable.set(it.path) }
|
||||
}
|
||||
}
|
||||
@@ -259,8 +268,18 @@ class InstallableSelectableInterpreter(val sdk: PySdkToInstall) : PythonSelectab
|
||||
open class AddInterpreterState(propertyGraph: PropertyGraph) {
|
||||
val selectedInterpreter: ObservableMutableProperty<PythonSelectableInterpreter?> = propertyGraph.property(null)
|
||||
val condaExecutable: ObservableMutableProperty<String> = propertyGraph.property("")
|
||||
|
||||
/**
|
||||
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
|
||||
*/
|
||||
val selectedCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
|
||||
/**
|
||||
* Use [PythonAddInterpreterModel.getBaseCondaOrError]
|
||||
*/
|
||||
val baseCondaEnv: ObservableMutableProperty<PyCondaEnv?> = propertyGraph.property(null)
|
||||
|
||||
|
||||
}
|
||||
|
||||
class MutableTargetState(propertyGraph: PropertyGraph) : AddInterpreterState(propertyGraph) {
|
||||
@@ -282,4 +301,18 @@ val PythonAddInterpreterModel.baseSdks
|
||||
|
||||
fun PythonAddInterpreterModel.findInterpreter(path: String): PythonSelectableInterpreter? {
|
||||
return allInterpreters.value.asSequence().find { it.homePath == path }
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.detectCondaEnvironmentsOrError(errorSink: ErrorSink) {
|
||||
detectCondaEnvironments()?.let {
|
||||
errorSink.emit(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun PythonAddInterpreterModel.getBaseCondaOrError(): Result<PyCondaEnv> {
|
||||
var baseConda = state.baseCondaEnv.get()
|
||||
if (baseConda != null) return Result.success(baseConda)
|
||||
detectCondaEnvironments()?.let { return failure(it) }
|
||||
baseConda = state.baseCondaEnv.get()
|
||||
return if (baseConda != null) Result.success(baseConda) else failure(PyBundle.message("python.sdk.conda.no.base.env.error"))
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.module.ModuleUtil
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
@@ -10,21 +11,24 @@ import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
|
||||
import com.intellij.openapi.vfs.StandardFileSystems
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.jetbrains.python.PyBundle
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.jetbrains.extensions.failure
|
||||
import com.jetbrains.python.PyBundle.message
|
||||
import com.jetbrains.python.sdk.PythonSdkType
|
||||
import com.jetbrains.python.sdk.PythonSdkUtil
|
||||
import com.jetbrains.python.sdk.VirtualEnvReader
|
||||
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
|
||||
import com.jetbrains.python.sdk.excludeInnerVirtualEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.sdk.suggestAssociatedSdkName
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.nio.file.InvalidPathException
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
// todo should it be overriden for targets?
|
||||
internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path, baseSdk: PythonSelectableInterpreter): Result<Sdk> {
|
||||
suspend fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Path, projectPath: Path): Result<Sdk> {
|
||||
val baseSdk = state.baseInterpreter.get()!!
|
||||
|
||||
val venvPathOnTarget = venvPath.convertToPathOnTarget(targetEnvironmentConfiguration)
|
||||
|
||||
@@ -32,35 +36,52 @@ internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pa
|
||||
is InstallableSelectableInterpreter -> installBaseSdk(baseSdk.sdk, this.existingSdks)?.homePath // todo handle errors
|
||||
is ExistingSelectableInterpreter -> baseSdk.sdk.homePath
|
||||
is DetectedSelectableInterpreter, is ManuallyAddedSelectableInterpreter -> baseSdk.homePath
|
||||
else -> error("Unknown interpreter")
|
||||
}
|
||||
|
||||
|
||||
createVirtualenv(baseSdkPath!!,
|
||||
venvPathOnTarget,
|
||||
projectPath,
|
||||
inheritSitePackages = state.inheritSitePackages.get())
|
||||
withContext(Dispatchers.EDT) {
|
||||
createVirtualenv(baseSdkPath!!,
|
||||
venvPathOnTarget,
|
||||
projectPath,
|
||||
inheritSitePackages = state.inheritSitePackages.get())
|
||||
}
|
||||
|
||||
if (targetEnvironmentConfiguration != null) error("Remote targets aren't supported")
|
||||
val venvPython = VirtualEnvReader.Instance.findPythonInPythonRoot(Path.of(venvPathOnTarget))?.toString()
|
||||
val dir = try {
|
||||
Path.of(venvPathOnTarget)
|
||||
}
|
||||
catch (e: InvalidPathException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
val venvPython = VirtualEnvReader.Instance.findPythonInPythonRoot(dir)?.toString()
|
||||
if (venvPython == null) {
|
||||
return failure(message("commandLine.directoryCantBeAccessed", venvPathOnTarget))
|
||||
}
|
||||
|
||||
val homeFile = try {
|
||||
StandardFileSystems.local().refreshAndFindFileByPath(venvPython!!)!!
|
||||
StandardFileSystems.local().refreshAndFindFileByPath(venvPython)
|
||||
}
|
||||
catch (e: ExecutionException) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
if (homeFile == null) {
|
||||
return failure(message("commandLine.directoryCantBeAccessed", venvPathOnTarget))
|
||||
}
|
||||
|
||||
val suggestedName = /*suggestedSdkName ?:*/ suggestAssociatedSdkName(homeFile.path, projectPath.toString())
|
||||
val newSdk = SdkConfigurationUtil.setupSdk(existingSdks.toTypedArray(), homeFile,
|
||||
PythonSdkType.getInstance(),
|
||||
false, null, suggestedName)
|
||||
false, null, suggestedName)!!
|
||||
|
||||
SdkConfigurationUtil.addSdk(newSdk!!)
|
||||
addSdk(newSdk)
|
||||
|
||||
// todo check exclude
|
||||
ProjectManager.getInstance().openProjects
|
||||
.firstNotNullOfOrNull { ModuleUtil.findModuleForFile(homeFile, it) }
|
||||
.firstNotNullOfOrNull {
|
||||
withContext(Dispatchers.IO) {
|
||||
ModuleUtil.findModuleForFile(homeFile, it)
|
||||
}
|
||||
}
|
||||
?.excludeInnerVirtualEnv(newSdk)
|
||||
return Result.success(newSdk)
|
||||
|
||||
@@ -68,13 +89,24 @@ internal fun PythonMutableTargetAddInterpreterModel.setupVirtualenv(venvPath: Pa
|
||||
|
||||
|
||||
// todo rewrite this
|
||||
internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaEnvIdentity): Sdk {
|
||||
/**
|
||||
* [base] or selected
|
||||
*/
|
||||
suspend fun PythonAddInterpreterModel.selectCondaEnvironment(base: Boolean): Result<Sdk> {
|
||||
val identity = if (base) {
|
||||
getBaseCondaOrError()
|
||||
}
|
||||
else {
|
||||
state.selectedCondaEnv.get()?.let { Result.success(it) } ?: failure(message("python.sdk.conda.no.env.selected.error"))
|
||||
}
|
||||
.getOrElse { return Result.failure(it) }
|
||||
.envIdentity
|
||||
val existingSdk = ProjectJdkTable.getInstance().findJdk(identity.userReadableName)
|
||||
if (existingSdk != null && isCondaSdk(existingSdk)) return existingSdk
|
||||
if (existingSdk != null && isCondaSdk(existingSdk)) return Result.success(existingSdk)
|
||||
|
||||
val sdk = runWithModalProgressBlocking(ModalTaskOwner.guess(),
|
||||
PyBundle.message("sdk.create.custom.conda.create.progress"),
|
||||
TaskCancellation.nonCancellable()) {
|
||||
val sdk = withModalProgress(ModalTaskOwner.guess(),
|
||||
message("sdk.create.custom.conda.create.progress"),
|
||||
TaskCancellation.nonCancellable()) {
|
||||
//PyCondaCommand(condaExecutableOnTarget, targetConfig = targetEnvironmentConfiguration)
|
||||
PyCondaCommand(state.condaExecutable.get(), targetConfig = targetEnvironmentConfiguration).createCondaSdkFromExistingEnv(identity,
|
||||
this@selectCondaEnvironment.existingSdks,
|
||||
@@ -82,8 +114,8 @@ internal fun PythonAddInterpreterModel.selectCondaEnvironment(identity: PyCondaE
|
||||
}
|
||||
|
||||
(sdk.sdkType as PythonSdkType).setupSdkPaths(sdk)
|
||||
SdkConfigurationUtil.addSdk(sdk)
|
||||
return sdk
|
||||
addSdk(sdk)
|
||||
return Result.success(sdk)
|
||||
}
|
||||
|
||||
|
||||
|
||||
12
python/src/com/jetbrains/python/sdk/add/v2/sdk.kt
Normal file
12
python/src/com/jetbrains/python/sdk/add/v2/sdk.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.sdk.add.v2
|
||||
|
||||
import com.intellij.openapi.application.writeAction
|
||||
import com.intellij.openapi.projectRoots.ProjectJdkTable
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
|
||||
internal suspend fun addSdk(sdk: Sdk) {
|
||||
writeAction {
|
||||
ProjectJdkTable.getInstance().addJdk(sdk)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import com.jetbrains.python.sdk.conda.CondaInstallManager
|
||||
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv
|
||||
import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity
|
||||
import com.jetbrains.python.util.ErrorSink
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@@ -447,14 +448,14 @@ fun Panel.executableSelector(
|
||||
return textFieldCell!!
|
||||
}
|
||||
|
||||
internal fun createInstallCondaFix(model: PythonAddInterpreterModel): ActionLink {
|
||||
internal fun createInstallCondaFix(model: PythonAddInterpreterModel, errorSink: ErrorSink): ActionLink {
|
||||
return ActionLink(message("sdk.create.custom.venv.install.fix.title", "Miniconda", "")) {
|
||||
PythonSdkFlavor.clearExecutablesCache()
|
||||
CondaInstallManager.installLatest(null)
|
||||
model.scope.launch(model.uiContext) {
|
||||
model.condaEnvironmentsLoading.value = true
|
||||
model.detectCondaExecutable()
|
||||
model.detectCondaEnvironments()
|
||||
model.detectCondaEnvironmentsOrError(errorSink)
|
||||
model.condaEnvironmentsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.google.gson.Gson
|
||||
import com.intellij.execution.target.*
|
||||
import com.intellij.openapi.progress.*
|
||||
import com.intellij.openapi.projectRoots.Sdk
|
||||
import com.jetbrains.extensions.failure
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.sdk.add.target.conda.TargetCommandExecutor
|
||||
import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv
|
||||
@@ -38,7 +39,8 @@ data class PyCondaEnv(
|
||||
* @return unparsed output of conda info --envs --json
|
||||
*/
|
||||
private suspend fun getEnvsInfo(command: TargetCommandExecutor, fullCondaPathOnTarget: FullPathOnTarget): Result<String> {
|
||||
return runCatching { command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).thenApply { it.stdout }.await() }
|
||||
val output = command.execute(listOf(fullCondaPathOnTarget, "info", "--envs", "--json")).await()
|
||||
return if (output.exitCode == 0) Result.success(output.stdout) else failure(output.stderr)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
62
python/src/com/jetbrains/python/util/ErrorSink.kt
Normal file
62
python/src/com/jetbrains/python/util/ErrorSink.kt
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.util
|
||||
|
||||
import com.intellij.openapi.application.EDT
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.diagnostic.thisLogger
|
||||
import com.intellij.openapi.ui.Messages
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.jetbrains.python.PyBundle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* [FlowCollector.emit] user-readable errors here.
|
||||
*
|
||||
* This class should be used by the topmost classes, tightly coupled to the UI.
|
||||
* For the most business-logic and backend functions please return [Result] or error.
|
||||
*
|
||||
* Please do not report *all* exceptions here: This is *not* the class for NPEs and AOOBs:
|
||||
* do not pass exceptions caught by `catch(e: Exception)` or `runCatching`: only report exceptions user interested in.
|
||||
* `IOException` or `ExecutionException` are generally ok.
|
||||
*
|
||||
* There will be unified sink soon to show and log errors.
|
||||
* Currently, only [ShowingMessageErrorSync] is a well-known implementation
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* suspend fun someLogic(): Result<@NlsSafe String> = withContext(Dispatchers.IO) {
|
||||
* try {
|
||||
* Result.success(Path.of("1.txt").readText())
|
||||
* }
|
||||
* catch (e: IOException) {
|
||||
* Result.failure(e)
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* suspend fun ui(errorSink: ErrorSink) {
|
||||
* someLogic()
|
||||
* .onSuccess {
|
||||
* Messages.showInfoMessage("..", it)
|
||||
* }
|
||||
* .onFailure {
|
||||
* errorSink.emit(it.localizedMessage)
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
typealias ErrorSink = FlowCollector<@NlsSafe String>
|
||||
|
||||
/**
|
||||
* Displays error with a message box and writes it to a log.
|
||||
*/
|
||||
object ShowingMessageErrorSync : ErrorSink {
|
||||
override suspend fun emit(value: @NlsSafe String) {
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
thisLogger().warn(value)
|
||||
Messages.showErrorDialog(value, PyBundle.message("python.error"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user