PY-75910: Update project name automatically in NPW even if field is hidden.

`getComponent` accepts `projectPathField`.
We use `ProjectPathProvider` to update project name from this field.

V2 panels are also affected: they now share logic (`ProjectPathFlows`) with project name component.

See `com.jetbrains.python.newProjectWizard.projectPath`

GitOrigin-RevId: f7b306fbbd9777925274513effd56009a0614f9a
This commit is contained in:
Ilya.Kazakevich
2024-09-27 04:08:41 +02:00
committed by intellij-monorepo-bot
parent 54c75955e7
commit 75801d4c3e
20 changed files with 283 additions and 143 deletions

View File

@@ -8,8 +8,8 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.wm.impl.welcomeScreen.collapsedActionGroup.CollapsedActionGroup
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle
import com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.emptyProject.PyV3EmptyProjectGenerator
import com.intellij.pycharm.community.ide.impl.newProject.steps.PythonGenerateProjectCallback
import com.intellij.pycharm.community.ide.impl.newProjectWizard.impl.emptyProject.PyV3EmptyProjectGenerator
import com.jetbrains.python.newProject.PyNewProjectSettings
import com.jetbrains.python.newProject.PythonProjectGenerator
import com.jetbrains.python.newProject.steps.PythonProjectSpecificSettingsStep

View File

@@ -24,12 +24,12 @@ import com.intellij.ui.dsl.builder.Align
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.PyNewProjectSettings
import com.jetbrains.python.newProject.PythonProjectGenerator
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.newProjectWizard.promotion.PromoProjectGenerator
import com.jetbrains.python.sdk.PyLazySdk
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
@@ -52,7 +52,7 @@ class PythonProjectSpecificSettingsStep<T : PyNewProjectSettings>(
private val propertyGraph = PropertyGraph()
private val projectName = propertyGraph.property("")
private val projectLocation = propertyGraph.property("")
private val projectLocationFlow = MutableStateFlow(Path.of(SystemProperties.getUserHome()))
private val projectLocationFlowStr = MutableStateFlow(projectLocation.get())
private val locationHint = propertyGraph.property("").apply {
dependsOn(projectName, ::updateHint)
dependsOn(projectLocation, ::updateHint)
@@ -64,7 +64,7 @@ class PythonProjectSpecificSettingsStep<T : PyNewProjectSettings>(
init {
projectLocation.afterChange {
projectLocationFlow.value = Path.of(projectLocation.get())
projectLocationFlowStr.value = projectLocation.get()
}
}
@@ -115,7 +115,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, errorSink = ShowingMessageErrorSync).also { interpreterPanel = it }
val interpreterPanel = PythonAddNewEnvironmentPanel(ProjectPathFlows.create(projectLocationFlowStr), onlyAllowedInterpreterTypes, errorSink = ShowingMessageErrorSync).also { interpreterPanel = it }
mainPanel = panel {
row(message("new.project.name")) {

View File

@@ -1,35 +0,0 @@
// 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.newProjectWizard
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
import com.intellij.openapi.ui.validation.CHECK_NO_RESERVED_WORDS
import com.intellij.openapi.ui.validation.CHECK_NO_WHITESPACES
import com.intellij.openapi.util.NlsSafe
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
/**
* Returns either error or [Path] for project path
*/
fun validateProjectPathAndGetPath(baseDirPath: String): Result<Path, @NlsSafe String> {
val path = try {
Paths.get(baseDirPath)
}
catch (e: InvalidPathException) {
return Result.Failure(e.reason)
}
if (!path.isAbsolute) {
return Result.Failure(PyBundle.message("python.sdk.new.error.no.absolute"))
}
for (validator in arrayOf(CHECK_NON_EMPTY, CHECK_NO_WHITESPACES, CHECK_NO_RESERVED_WORDS)) {
validator.curry { baseDirPath }.validate()?.let {
return Result.Failure(it.message)
}
}
return Result.Success(path)
}

View File

@@ -1,38 +0,0 @@
// 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.newProjectWizard
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
import com.intellij.openapi.ui.validation.CHECK_NO_WHITESPACES
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.trimmedTextValidation
import com.jetbrains.python.ui.flow.bindText
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import java.nio.file.Path
import javax.swing.JTextField
import kotlin.io.path.name
import kotlin.reflect.KMutableProperty0
/**
* "New project path" field value.
* This flow is used by [PyV3ProjectTypeSpecificUI.advancedSettings] to [bindProjectName]
*/
typealias ProjectPathFlow = StateFlow<Path>
/**
* Binds [PyV3ProjectTypeSpecificSettings] property to the certain cell and updates it each time project path is changes
*/
fun Cell<JTextField>.bindProjectName(projectPath: ProjectPathFlow, property: KMutableProperty0<@NlsSafe String>) = apply {
property.set(getProjectNameFromPath(projectPath.value)) // set the initial value before we get any new value
trimmedTextValidation(CHECK_NON_EMPTY, CHECK_NO_WHITESPACES)
bindText(property)
bindText(projectPath.map { getProjectNameFromPath(it) })
}
/**
* Projects like Django are named after the last part of the path
*/
private fun getProjectNameFromPath(path: Path): @NlsSafe String = path.name

View File

@@ -11,15 +11,14 @@ import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.DirectoryProjectGenerator
import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.util.SystemProperties
import com.jetbrains.python.Result
import com.jetbrains.python.newProjectWizard.impl.PyV3GeneratorPeer
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
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
@@ -40,7 +39,6 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
private val _newProjectName: @NlsSafe String? = null,
) : DirectoryProjectGenerator<PyV3BaseProjectSettings> {
private val baseSettings = PyV3BaseProjectSettings()
private val projectPathFlow = MutableStateFlow(Path.of(SystemProperties.getUserHome()))
val newProjectName: @NlsSafe String get() = _newProjectName ?: "${name.replace(" ", "")}Project"
@@ -58,12 +56,11 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
}
override fun createPeer(): ProjectGeneratorPeer<PyV3BaseProjectSettings> =
PyV3GeneratorPeer(baseSettings, projectPathFlow, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes)
PyV3GeneratorPeer(baseSettings, typeSpecificUI?.let { Pair(it, typeSpecificSettings) }, allowedInterpreterTypes)
override fun validate(baseDirPath: String): ValidationResult =
when (val pathOrError = validateProjectPathAndGetPath(baseDirPath)) {
when (val pathOrError = validatePath(baseDirPath)) {
is Result.Success<Path, *> -> {
projectPathFlow.value = pathOrError.result
ValidationResult.OK
}
is Result.Failure<*, @Nls String> -> ValidationResult(pathOrError.error)
@@ -72,4 +69,4 @@ abstract class PyV3ProjectBaseGenerator<TYPE_SPECIFIC_SETTINGS : PyV3ProjectType
@Service(Service.Level.PROJECT)
private class MyService(val coroutineScope: CoroutineScope)
}
}

View File

@@ -3,6 +3,7 @@ package com.jetbrains.python.newProjectWizard
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.Row
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider
/**
* Binds [PROJECT_SPECIFIC_SETTINGS] to Kotlin DSL UI.
@@ -18,8 +19,8 @@ interface PyV3ProjectTypeSpecificUI<PROJECT_SPECIFIC_SETTINGS : PyV3ProjectTypeS
/**
* If you need to show something in "advanced settings".
* You also have a flow with project path,
* you might bind it to the cell using [bindProjectName] if you need project name.
* You also have an api with project name.
* you might bind it to the cell using [com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider.Companion.bindProjectName] if you need project name.
*/
val advancedSettings: (Panel.(settings: PROJECT_SPECIFIC_SETTINGS, projectPath: ProjectPathFlow) -> Unit)? get() = null
val advancedSettings: (Panel.(PROJECT_SPECIFIC_SETTINGS, ProjectPathProvider) -> Unit)? get() = null
}

View File

@@ -5,24 +5,26 @@ import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.platform.ProjectGeneratorPeer
import com.jetbrains.python.newProjectWizard.ProjectPathFlow
import com.jetbrains.python.newProjectWizard.PyV3BaseProjectSettings
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
import com.jetbrains.python.newProjectWizard.impl.projectPath.ProjectPathImpl
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
import javax.swing.JComponent
internal class PyV3GeneratorPeer<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>(
baseSettings: PyV3BaseProjectSettings,
projectPath: ProjectPathFlow,
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
allowedInterpreterTypes:Set<PythonInterpreterSelectionMode>?
private val specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
private val allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>?,
) : ProjectGeneratorPeer<PyV3BaseProjectSettings> {
private val settings = baseSettings
private val panel: PyV3VUI<*> = PyV3VUI(settings, projectPath, specificUiAndSettings, allowedInterpreterTypes)
private lateinit var panel: PyV3VUI<*>
override fun getComponent(myLocationField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent = panel.mainPanel
override fun getComponent(projectPathField: TextFieldWithBrowseButton, checkValid: Runnable): JComponent {
panel = PyV3VUI(settings, ProjectPathImpl(projectPathField), specificUiAndSettings, allowedInterpreterTypes)
return panel.mainPanel
}
override fun buildUI(settingsStep: SettingsStep) = Unit

View File

@@ -5,10 +5,10 @@ import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.PyBundle
import com.jetbrains.python.newProjectWizard.ProjectPathFlow
import com.jetbrains.python.newProjectWizard.PyV3BaseProjectSettings
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings
import com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificUI
import com.jetbrains.python.newProjectWizard.impl.projectPath.ProjectPathImpl
import com.jetbrains.python.sdk.add.v2.PySdkCreator
import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode
@@ -18,11 +18,12 @@ import javax.swing.JComponent
internal class PyV3VUI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings> @RequiresEdt constructor(
baseSettings: PyV3BaseProjectSettings,
projectPath: ProjectPathFlow,
projectNameProvider: ProjectPathImpl,
specificUiAndSettings: Pair<PyV3ProjectTypeSpecificUI<TYPE_SPECIFIC_SETTINGS>, TYPE_SPECIFIC_SETTINGS>?,
allowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null,
) {
private val sdkPanel = PythonAddNewEnvironmentPanel(projectPath, allowedInterpreterTypes, ShowingMessageErrorSync)
private val sdkPanel = PythonAddNewEnvironmentPanel(projectNameProvider.projectPathFlows, allowedInterpreterTypes, ShowingMessageErrorSync)
private val _mainPanel = panel {
val checkBoxRow = row {
@@ -32,7 +33,7 @@ internal class PyV3VUI<TYPE_SPECIFIC_SETTINGS : PyV3ProjectTypeSpecificSettings>
sdkPanel.buildPanel(this)
specificUiAndSettings?.first?.advancedSettings?.let {
collapsibleGroup(PyBundle.message("black.advanced.settings.panel.title")) {
it(this, specificUiAndSettings.second, projectPath)
it(this, specificUiAndSettings.second, projectNameProvider)
}
}
}.apply {

View File

@@ -0,0 +1,35 @@
// 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.newProjectWizard.impl.projectPath
import com.intellij.ui.TextAccessor
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.debounce
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
/**
* Emits changes in [component] to the [flow] with [debounceDelay]
*/
internal class DocumentListenerToFlowAdapter(private val component: TextAccessor, debounceDelay: Duration = 10.milliseconds) : DocumentListener {
private val _flow = MutableStateFlow<String>(component.text)
@OptIn(FlowPreview::class)
val flow: Flow<String> = _flow.debounce(debounceDelay)
override fun insertUpdate(e: DocumentEvent?) {
update()
}
override fun removeUpdate(e: DocumentEvent?) {
update()
}
override fun changedUpdate(e: DocumentEvent) = Unit
private fun update() {
_flow.value = component.text
}
}

View File

@@ -0,0 +1,53 @@
// 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.newProjectWizard.impl.projectPath
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.showingScope
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider
import kotlinx.coroutines.CoroutineScope
/**
* Wraps [field] that represents project path, and emits [projectPathFlows] out of it
*
* [onProjectFileNameChanged] allows caller to receive every [ProjectPathFlows.projectName] event as long as [field] is visible
* [fieldShowingScope] shouldn't be changes (except for test purposes)
*/
class ProjectPathImpl(
private val field: TextFieldWithBrowseButton,
private val fieldShowingScope: FieldShowingScopeRunner = object : FieldShowingScopeRunner {
override fun onShowingScope(code: suspend CoroutineScope.() -> Unit) {
field.showingScope("On Project Changed") {
code()
}
}
},
) : ProjectPathProvider {
private val listener = DocumentListenerToFlowAdapter(field)
override val projectPathFlows: ProjectPathFlows = ProjectPathFlows.create(listener.flow)
init {
field.addDocumentListener(listener)
Disposer.register(field) {
field.textField.document.removeDocumentListener(listener)
}
}
@RequiresEdt
override fun onProjectFileNameChanged(code: suspend (projectPathName: @NlsSafe String) -> Unit) {
fieldShowingScope.onShowingScope {
projectPathFlows.projectName.collect(code)
}
}
companion object {
fun interface FieldShowingScopeRunner {
fun onShowingScope(code: (suspend CoroutineScope.() -> Unit))
}
}
}

View File

@@ -0,0 +1,82 @@
// 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.newProjectWizard.projectPath
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
import com.intellij.openapi.ui.validation.CHECK_NO_RESERVED_WORDS
import com.intellij.openapi.ui.validation.CHECK_NO_WHITESPACES
import com.intellij.openapi.util.NlsSafe
import com.intellij.util.SystemProperties
import com.jetbrains.python.PyBundle
import com.jetbrains.python.Result
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.create
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import java.nio.file.InvalidPathException
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.Path
import kotlin.io.path.name
/**
* A pack of flows that represents project path changes.
* Create with [create] that accepts input flow with project path text and constructs other flows out of it.
*/
class ProjectPathFlows private constructor(val projectPath: Flow<Path?>) {
/**
* Flow that always emits value. If value is wrong it emits default
*/
val projectPathWithDefault: Flow<Path> = projectPath.map { it ?: defaultPath }
/**
* Flow emits project file name only when project path is valid
*/
val projectName: Flow<@NlsSafe String> = projectPath.filterNotNull().map { it.name }
companion object {
private val defaultPath = Path(SystemProperties.getUserHome())
/**
* Use [fixedPath] as input
*/
fun create(fixedPath: Path): ProjectPathFlows = ProjectPathFlows(MutableStateFlow(fixedPath))
/**
* Use [projectPathString] as input i.e `c:\foo`
*/
fun create(projectPathString: Flow<String>): ProjectPathFlows = ProjectPathFlows(projectPathString.map {
withContext(Dispatchers.Default) {
validatePath(it).successOrNull
}
})
/**
* checks that [pathAsString] is a valid path and returns it or error
*/
fun validatePath(pathAsString: String): Result<Path, @NlsSafe String> {
val path = try {
Paths.get(pathAsString)
}
catch (e: InvalidPathException) {
return Result.Failure(e.reason)
}
if (!path.isAbsolute) {
return Result.Failure(PyBundle.message("python.sdk.new.error.no.absolute"))
}
for (validator in arrayOf(CHECK_NON_EMPTY, CHECK_NO_WHITESPACES, CHECK_NO_RESERVED_WORDS)) {
validator.curry { pathAsString }.validate()?.let {
return Result.Failure(it.message)
}
}
return Result.Success(path)
}
}
}

View File

@@ -0,0 +1,51 @@
// 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.newProjectWizard.projectPath
import com.intellij.openapi.application.EDT
import com.intellij.openapi.ui.validation.CHECK_NON_EMPTY
import com.intellij.openapi.ui.validation.CHECK_NO_WHITESPACES
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.text
import com.intellij.ui.dsl.builder.trimmedTextValidation
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathProvider.Companion.bindProjectName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.swing.JTextField
import kotlin.reflect.KMutableProperty0
/**
* API to access project path field.
* To be used with [bindProjectName]
*/
interface ProjectPathProvider {
/**
* Calls [code] as long as project path field is visible on each project path change.
* this code accepts project path name
*/
@RequiresEdt
fun onProjectFileNameChanged(code: suspend (projectFileName: @NlsSafe String) -> Unit)
/**
* [ProjectPathFlows] made out of project path
*/
val projectPathFlows: ProjectPathFlows
companion object {
/**
* Binds [com.jetbrains.python.newProjectWizard.PyV3ProjectTypeSpecificSettings] property to the certain cell and updates it each time project path is changes
*/
@RequiresEdt
fun Cell<JTextField>.bindProjectName(projectPathProvider: ProjectPathProvider, property: KMutableProperty0<@NlsSafe String>) = apply {
trimmedTextValidation(CHECK_NON_EMPTY, CHECK_NO_WHITESPACES)
bindText(property)
projectPathProvider.onProjectFileNameChanged {
withContext(Dispatchers.EDT) {
text(it)
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
// 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.EDT
import com.intellij.openapi.observable.properties.ObservableMutableProperty
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.ComboBox
@@ -20,15 +19,9 @@ 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.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.io.path.name
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val projectPath: Flow<Path>?, private val errorSink: ErrorSink) : PythonNewEnvironmentCreator(model) {
// TODO: DOC
class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel, private val errorSink: ErrorSink) : PythonNewEnvironmentCreator(model) {
private lateinit var pythonVersion: ObservableMutableProperty<LanguageLevel>
private lateinit var versionComboBox: ComboBox<LanguageLevel>
@@ -44,9 +37,8 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
row(message("sdk.create.custom.conda.env.name")) {
val envName = textField()
.bindText(model.state.newCondaEnvName)
if (projectPath != null) {
envName.bindText(projectPath.map { it.name })
}
// TODO: DOC
envName.bindText(model.myProjectPathFlows.projectName)
}
executableSelector(model.state.condaExecutable,
@@ -58,11 +50,7 @@ class CondaNewEnvironmentCreator(model: PythonMutableTargetAddInterpreterModel,
}
}
override fun onShown() {
model.scope.launch(Dispatchers.EDT) {
model.state.newCondaEnvName.set(model.projectPath.first().name)
}
}
override fun onShown() = Unit
override suspend fun getOrCreateSdk(moduleOrProject: ModuleOrProject): Result<Sdk> {
return model.createCondaEnvironment(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), model.state.newCondaEnvName.get()))

View File

@@ -64,7 +64,7 @@ abstract class CustomNewEnvironmentCreator(private val name: String, model: Pyth
val newSdk = setupEnvSdk(moduleOrProject.project,
module,
ProjectJdkTable.getInstance().allJdks.asList(),
model.projectPath.first().toString(),
model.myProjectPathFlows.projectPathWithDefault.first().toString(),
homePath,
false)!!
addSdk(newSdk)

View File

@@ -1,11 +1,9 @@
// 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.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import org.jetbrains.annotations.ApiStatus
import java.nio.file.Path
import kotlin.coroutines.CoroutineContext
/**
@@ -15,5 +13,5 @@ import kotlin.coroutines.CoroutineContext
data class PyInterpreterModelParams(
val scope: CoroutineScope,
val uiContext: CoroutineContext,
val projectPathProperty: Flow<Path>? = null,
val projectPathFlows: ProjectPathFlows,
)

View File

@@ -17,7 +17,7 @@ import com.jetbrains.python.util.ErrorSink
import kotlinx.coroutines.flow.Flow
import java.nio.file.Path
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, projectPath: Flow<Path>? = null, errorSink: ErrorSink) {
class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterModel, val moduleOrProject: ModuleOrProject? = null, projectPathFlow: Flow<Path>? = null, errorSink: ErrorSink) {
private val propertyGraph = model.propertyGraph
private val selectionMethod = propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW)
@@ -28,7 +28,7 @@ class PythonAddCustomInterpreter(val model: PythonMutableTargetAddInterpreterMod
private val newInterpreterCreators = mapOf(
VIRTUALENV to PythonNewVirtualenvCreator(model),
CONDA to CondaNewEnvironmentCreator(model, projectPath, errorSink),
CONDA to CondaNewEnvironmentCreator(model, errorSink),
PIPENV to PipEnvNewEnvironmentCreator(model),
POETRY to PoetryNewEnvironmentCreator(model, moduleOrProject),
)

View File

@@ -12,9 +12,9 @@ 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.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.util.ShowingMessageErrorSync
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.swing.JComponent
@@ -51,7 +51,7 @@ class PythonAddLocalInterpreterDialog(private val dialogPresenter: PythonAddLoca
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)))
Dispatchers.EDT + ModalityState.any().asContextElement(), ProjectPathFlows.create(basePath)))
model.navigator.selectionMode = AtomicProperty(PythonInterpreterSelectionMode.CUSTOM)
mainPanel = PythonAddCustomInterpreter(model, errorSink = errorSink)
mainPanel.buildPanel(this, WHEN_PROPERTY_CHANGED(AtomicProperty(basePath)))

View File

@@ -23,6 +23,7 @@ import com.intellij.ui.dsl.builder.bindText
import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.*
@@ -32,19 +33,17 @@ 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.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.nio.file.Path
/**
* If `onlyAllowedInterpreterTypes` then only these types are displayed. All types displayed otherwise
*/
class PythonAddNewEnvironmentPanel(val projectPath: Flow<Path>, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null, private val errorSink: ErrorSink) : PySdkCreator {
class PythonAddNewEnvironmentPanel(val projectPathFlows: ProjectPathFlows, onlyAllowedInterpreterTypes: Set<PythonInterpreterSelectionMode>? = null, private val errorSink: ErrorSink) : PySdkCreator {
private val propertyGraph = PropertyGraph()
private val allowedInterpreterTypes = (onlyAllowedInterpreterTypes ?: PythonInterpreterSelectionMode.entries).also {
assert(it.isNotEmpty()) {
@@ -65,7 +64,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: Flow<Path>, onlyAllowedInter
private suspend fun updateVenvLocationHint(): Unit = withContext(Dispatchers.EDT) {
val get = selectedMode.get()
if (get == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPath.first().resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME).toString()))
if (get == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPathFlows.projectPathWithDefault.first().resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME).toString()))
else if (get == BASE_CONDA && PROJECT_VENV in allowedInterpreterTypes) venvHint.set(message("sdk.create.simple.conda.hint"))
}
@@ -75,11 +74,11 @@ class PythonAddNewEnvironmentPanel(val projectPath: Flow<Path>, onlyAllowedInter
fun buildPanel(outerPanel: Panel) {
//presenter = PythonAddInterpreterPresenter(state, uiContext = Dispatchers.EDT + ModalityState.current().asContextElement())
model = PythonLocalAddInterpreterModel(PyInterpreterModelParams(service<PythonAddSdkService>().coroutineScope,
Dispatchers.EDT + ModalityState.current().asContextElement(), projectPath))
Dispatchers.EDT + ModalityState.current().asContextElement(), projectPathFlows))
model.navigator.selectionMode = selectedMode
//presenter.controller = model
custom = PythonAddCustomInterpreter(model, projectPath = projectPath, errorSink = ShowingMessageErrorSync)
custom = PythonAddCustomInterpreter(model, errorSink = ShowingMessageErrorSync)
val validationRequestor = WHEN_PROPERTY_CHANGED(selectedMode)
@@ -114,7 +113,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: Flow<Path>, onlyAllowedInter
row("") {
comment("").bindText(venvHint).apply {
component.showingScope("Update hint") {
projectPath.collect {
projectPathFlows.projectPathWithDefault.collect {
updateVenvLocationHint()
}
}
@@ -166,7 +165,7 @@ class PythonAddNewEnvironmentPanel(val projectPath: Flow<Path>, onlyAllowedInter
model.navigator.saveLastState()
return when (selectedMode.get()) {
PROJECT_VENV -> {
val projectPath = projectPath.first()
val projectPath = projectPathFlows.projectPathWithDefault.first()
// todo just keep venv path, all the rest is in the model
model.setupVirtualenv(projectPath.resolve(VirtualEnvReader.DEFAULT_VIRTUALENV_DIRNAME), projectPath)
}

View File

@@ -11,6 +11,7 @@ import com.intellij.openapi.ui.validation.DialogValidationRequestor
import com.intellij.openapi.ui.validation.WHEN_PROPERTY_CHANGED
import com.intellij.openapi.ui.validation.and
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.ui.components.ActionLink
import com.intellij.ui.dsl.builder.*
import com.intellij.ui.dsl.builder.components.validationTooltip
@@ -18,9 +19,10 @@ import com.intellij.util.ui.showingScope
import com.jetbrains.python.PyBundle.message
import com.jetbrains.python.newProject.collector.InterpreterStatisticsInfo
import com.jetbrains.python.newProjectWizard.collector.PythonNewProjectWizardCollector
import com.jetbrains.python.newProjectWizard.projectPath.ProjectPathFlows.Companion.validatePath
import com.jetbrains.python.sdk.VirtualEnvReader
import com.jetbrains.python.newProjectWizard.validateProjectPathAndGetPath
import com.jetbrains.python.sdk.ModuleOrProject
import com.jetbrains.python.sdk.PySdkSettings
import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMethod.SELECT_EXISTING
import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.PYTHON
import com.jetbrains.python.statistics.InterpreterCreationMode
@@ -87,7 +89,7 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
addInputRule {
if (!textField.isVisible) return@addInputRule null // We are hidden, hence valid
locationValidationFailed.set(false)
val locationPath = when (val path = validateProjectPathAndGetPath(textField.text)) {
val locationPath = when (val path = validatePath(textField.text)) {
is com.jetbrains.python.Result.Failure -> return@addInputRule ValidationInfo(path.error) // Path is invalid
is com.jetbrains.python.Result.Success -> path.result
}
@@ -135,9 +137,10 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
}
versionComboBox.showingScope("...") {
model.projectPath.collect {
model.myProjectPathFlows.projectPathWithDefault.collect {
if (!locationModified) {
val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue
val suggestedVirtualEnvPath = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(it.toString())) // todo nullability issue
model.state.venvPath.set(suggestedVirtualEnvPath)
}
}
@@ -160,9 +163,10 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
override fun onShown() {
val modalityState = ModalityState.current().asContextElement()
model.scope.launch(Dispatchers.EDT + modalityState) {
val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue
model.state.venvPath.set(suggestedVirtualEnvPath)
// TODO: Check venv set
//
//val suggestedVirtualEnvPath = model.suggestVenvPath()!! // todo nullability issue
//model.state.venvPath.set(suggestedVirtualEnvPath)
//val projectBasePath = state.projectPath.get()
@@ -197,7 +201,7 @@ class PythonNewVirtualenvCreator(model: PythonMutableTargetAddInterpreterModel)
// todo remove project path, or move to controller
try {
val venvPath = Path.of(model.state.venvPath.get())
model.setupVirtualenv(venvPath, model.projectPath.first())
model.setupVirtualenv(venvPath, model.myProjectPathFlows.projectPathWithDefault.first())
}
catch (e: InvalidPathException) {
Result.failure(e)

View File

@@ -13,12 +13,12 @@ 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.newProjectWizard.projectPath.ProjectPathFlows
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.conda.suggestCondaPath
@@ -42,7 +42,9 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
open val state = AddInterpreterState(propertyGraph)
open val targetEnvironmentConfiguration: TargetEnvironmentConfiguration? = null
val projectPath = params.projectPathProperty ?: MutableStateFlow(Path.of(SystemProperties.getUserHome())) // todo how to populate?
// TODO: DOC
val myProjectPathFlows: ProjectPathFlows = params.projectPathFlows
internal val scope = params.scope
internal val uiContext = params.uiContext
@@ -159,7 +161,7 @@ abstract class PythonAddInterpreterModel(params: PyInterpreterModelParams) {
manuallyAddedInterpreters.value += ExistingSelectableInterpreter(sdk, PySdkUtil.getLanguageLevelForSdk(sdk), sdk.isSystemWide)
}
suspend fun suggestVenvPath(): String? = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(projectPath.first().toString()))
suspend fun suggestVenvPath(): String? = FileUtil.toSystemDependentName(PySdkSettings.instance.getPreferredVirtualEnvBasePath(myProjectPathFlows.projectPathWithDefault.first().toString()))
}
abstract class PythonMutableTargetAddInterpreterModel(params: PyInterpreterModelParams)