diff --git a/python/ide/impl/src/com/jetbrains/python/newProject/steps/PyCharmNewProjectStep.java b/python/ide/impl/src/com/jetbrains/python/newProject/steps/PyCharmNewProjectStep.java index 8d80d1d02640..4299e6147a29 100644 --- a/python/ide/impl/src/com/jetbrains/python/newProject/steps/PyCharmNewProjectStep.java +++ b/python/ide/impl/src/com/jetbrains/python/newProject/steps/PyCharmNewProjectStep.java @@ -4,6 +4,7 @@ package com.jetbrains.python.newProject.steps; import com.intellij.ide.util.projectWizard.AbstractNewProjectStep; import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase; import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.util.registry.Registry; import com.intellij.platform.DirectoryProjectGenerator; import com.intellij.util.ObjectUtils; import com.jetbrains.python.newProject.*; @@ -41,6 +42,9 @@ public final class PyCharmNewProjectStep extends AbstractNewProjectStep(npwGenerator); } + else if (Registry.is("python.new.interpreter.creation.ui")) { + return new PythonProjectSpecificSettingsStep<>(projectGenerator, callback); + } else { return new ProjectSpecificSettingsStep<>(projectGenerator, callback); } diff --git a/python/ide/impl/src/com/jetbrains/python/newProject/steps/PythonGenerateProjectCallback.java b/python/ide/impl/src/com/jetbrains/python/newProject/steps/PythonGenerateProjectCallback.java index 478ebe38c6d9..775981882d56 100644 --- a/python/ide/impl/src/com/jetbrains/python/newProject/steps/PythonGenerateProjectCallback.java +++ b/python/ide/impl/src/com/jetbrains/python/newProject/steps/PythonGenerateProjectCallback.java @@ -15,12 +15,16 @@ */ package com.jetbrains.python.newProject.steps; +import com.intellij.ide.util.PropertiesComponent; import com.intellij.ide.util.projectWizard.AbstractNewProjectStep; import com.intellij.ide.util.projectWizard.ProjectSettingsStepBase; import com.intellij.ide.util.projectWizard.WebProjectTemplate; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil; +import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.io.FileUtil; import com.intellij.platform.DirectoryProjectGenerator; @@ -29,15 +33,26 @@ import com.intellij.util.BooleanFunction; import com.jetbrains.python.PyBundle; import com.jetbrains.python.newProject.PyNewProjectSettings; import com.jetbrains.python.newProject.PythonProjectGenerator; +import com.jetbrains.python.newProject.welcome.PyWelcomeSettings; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Optional; + + public class PythonGenerateProjectCallback extends AbstractNewProjectStep.AbstractCallback { @Override public void consume(@Nullable ProjectSettingsStepBase step, @NotNull ProjectGeneratorPeer projectGeneratorPeer) { if (!(step instanceof ProjectSpecificSettingsStep settingsStep)) return; + if (step instanceof PythonProjectSpecificSettingsStep) { + // has to be set before project generation + boolean welcomeScript = PropertiesComponent.getInstance().getBoolean("PyCharm.NewProject.Welcome", false); + PyWelcomeSettings.getInstance().setCreateWelcomeScriptForEmptyProject(welcomeScript); + } + final DirectoryProjectGenerator generator = settingsStep.getProjectGenerator(); Sdk sdk = settingsStep.getSdk(); @@ -68,6 +83,20 @@ public class PythonGenerateProjectCallback extends AbstractNewProjectStep.Abs SdkConfigurationUtil.setDirectoryProjectSdk(newProject, sdk); ((PythonProjectGenerator)generator).afterProjectGenerated(newProject); } + + if (step instanceof PythonProjectSpecificSettingsStep newStep) { + // init git repostory + if (PropertiesComponent.getInstance().getBoolean("PyCharm.NewProject.Git", false)) { + ModuleManager moduleManager = ModuleManager.getInstance(newProject); + Optional module = Arrays.stream(moduleManager.getModules()).findFirst(); + module.ifPresent((value -> { + ModuleRootManager rootManager = ModuleRootManager.getInstance(value); + Arrays.stream(rootManager.getContentRoots()).findFirst().ifPresent(root -> { + PythonProjectSpecificSettingsStep.initializeGit(newProject, root); + }); + })); + } + } } @Nullable diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties index 8186717e8870..59cc554bc65e 100644 --- a/python/pluginResources/messages/PyBundle.properties +++ b/python/pluginResources/messages/PyBundle.properties @@ -477,6 +477,51 @@ sdk.create.venv.conda.dialog.label.interpreter=Interpreter: sdk.create.venv.conda.dialog.label.location=Location: sdk.create.venv.conda.dialog.label.python.version=Python version: + +sdk.create.interpreter.type=Interpreter type: +sdk.create.type.project.venv=Project venv +sdk.create.type.base.conda=Base conda +sdk.create.type.custom=Custom environment +sdk.create.python.version=Python version: +sdk.create.conda.executable.path=Path to conda: +sdk.create.simple.venv.hint=Python virtual environment will be created in the project root: {0}.venv +sdk.create.simple.conda.hint=To create a new conda environment or choose an existing one, proceed with Custom environment +sdk.create.custom.develop.on=Develop on: +sdk.create.custom.env.creation.type=Environment: +sdk.create.custom.generate.new=Generate new +sdk.create.custom.select.existing=Select existing +sdk.create.custom.type=Type: +sdk.create.custom.base.python=Base python: +sdk.create.custom.location=Location: +sdk.create.custom.inherit.packages=Inherit packages from base interpreter +sdk.create.custom.make.available=Make available to all projects +sdk.create.custom.python.path=Python path: +sdk.create.custom.python.browse.tooltip="Select Python executable" +sdk.create.custom.venv.location.browse.title=Select virtualenv location +sdk.create.custom.conda.browse.title=Select Conda Executable +sdk.create.custom.conda.env.name=Name: +sdk.create.custom.conda.create.progress=Creating conda interpreter +sdk.create.custom.conda.select.progress=Selecting conda interpreter +sdk.create.custom.conda.refresh.envs=Reload environments +sdk.create.custom.pipenv.path=Path to pipenv: +sdk.create.custom.pipenv.missing.text=Pipenv executable non found. Select executable +sdk.create.custom.poetry.path=Path to poetry: +sdk.create.custom.poetry.missing.text=Poetry executable non found. Select executable + +sdk.create.targets.local=Local Machine +sdk.create.custom.virtualenv=Virtualenv +sdk.create.custom.conda=Conda +sdk.create.custom.pipenv=Pipenv +sdk.create.custom.poetry=Poetry +sdk.create.custom.python=Python + +new.project.name=Name: +new.project.location=Location: +new.project.location.hint=Project will be created in: {0} +new.project.git=Create Git repository +new.project.welcome=Create a main.py welcome script + + # remote interpreters remote.interpreter.configure.title=Configure Remote Python Interpreter remote.interpreter.configure.path.title=Select Python Interpreter diff --git a/python/src/META-INF/python-core-common.xml b/python/src/META-INF/python-core-common.xml index c778ab554e54..761fbc97a019 100644 --- a/python/src/META-INF/python-core-common.xml +++ b/python/src/META-INF/python-core-common.xml @@ -573,6 +573,9 @@ key="black.configurable.name"/> + + @@ -892,6 +895,8 @@ + diff --git a/python/src/com/jetbrains/python/newProject/steps/ProjectSpecificSettingsStep.java b/python/src/com/jetbrains/python/newProject/steps/ProjectSpecificSettingsStep.java index 8a69ba60fa8b..71e432a53933 100644 --- a/python/src/com/jetbrains/python/newProject/steps/ProjectSpecificSettingsStep.java +++ b/python/src/com/jetbrains/python/newProject/steps/ProjectSpecificSettingsStep.java @@ -144,7 +144,7 @@ public class ProjectSpecificSettingsStep extends ProjectSettingsStepBase i * @return path for project on remote side provided by user */ @Nullable - public final String getRemotePath() { + public String getRemotePath() { final PyAddSdkGroupPanel interpreterPanel = myInterpreterPanel; if (interpreterPanel == null) return null; final PyAddExistingSdkPanel panel = ObjectUtils.tryCast(interpreterPanel.getSelectedPanel(), PyAddExistingSdkPanel.class); diff --git a/python/src/com/jetbrains/python/newProject/steps/PythonProjectSpecificSettingsStep.kt b/python/src/com/jetbrains/python/newProject/steps/PythonProjectSpecificSettingsStep.kt new file mode 100644 index 000000000000..21ee8de59f16 --- /dev/null +++ b/python/src/com/jetbrains/python/newProject/steps/PythonProjectSpecificSettingsStep.kt @@ -0,0 +1,154 @@ +// 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.newProject.steps + +import com.intellij.ide.IdeBundle +import com.intellij.ide.impl.ProjectUtil +import com.intellij.ide.impl.ProjectUtil.getUserHomeProjectDir +import com.intellij.ide.util.projectWizard.AbstractNewProjectStep +import com.intellij.openapi.GitRepositoryInitializer +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.bindBooleanStorage +import com.intellij.openapi.observable.util.joinSystemDependentPath +import com.intellij.openapi.progress.runBackgroundableTask +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.DirectoryProjectGenerator +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.* +import com.intellij.util.PlatformUtils +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.newProject.PythonProjectGenerator +import com.jetbrains.python.psi.PyUtil +import com.jetbrains.python.sdk.PyLazySdk +import com.jetbrains.python.sdk.add.v2.PythonAddNewEnvironmentPanel +import java.io.File +import java.nio.file.Paths +import java.util.* +import javax.swing.JPanel + +class PythonProjectSpecificSettingsStep(projectGenerator: DirectoryProjectGenerator, + callback: AbstractNewProjectStep.AbstractCallback) + : ProjectSpecificSettingsStep(projectGenerator, callback), DumbAware { + + private val propertyGraph = PropertyGraph() + private val projectName = propertyGraph.property("") + private val projectLocation = propertyGraph.property("") + private val locationHint = propertyGraph.property("").apply { + dependsOn(projectName, ::updateHint) + dependsOn(projectLocation, ::updateHint) + } + private val createRepository = propertyGraph.property(false) + .bindBooleanStorage("PyCharm.NewProject.Git") + private val createScript = propertyGraph.property(false) + .bindBooleanStorage("PyCharm.NewProject.Welcome") + + private lateinit var projectNameFiled: JBTextField + lateinit var mainPanel: DialogPanel + override fun createAndFillContentPanel(): JPanel { + return createContentPanelWithAdvancedSettingsPanel() + } + + override fun getProjectLocation(): String { + return FileUtil.expandUserHome(projectLocation.joinSystemDependentPath(projectName).get()) + } + + override fun getRemotePath(): String? { + return null + } + + private val interpreterPanel = PythonAddNewEnvironmentPanel(projectLocation.joinSystemDependentPath(projectName)) + + override fun createBasePanel(): JPanel { + if (myProjectGenerator !is PythonProjectGenerator<*>) return super.createBasePanel() + + myLocationField = TextFieldWithBrowseButton() + + val nextProjectName = myProjectDirectory.get() + projectName.set(nextProjectName.nameWithoutExtension) + projectLocation.set(nextProjectName.parent) + + mainPanel = panel { + row(message("new.project.name")) { + projectNameFiled = textField() + .bindText(projectName) + .component + } + row(message("new.project.location")) { + textFieldWithBrowseButton() + .bindText(projectLocation) + .align(Align.FILL) + } + row("") { + comment("", maxLineLength = 60).bindText(locationHint) + } + row("") { + checkBox(message("new.project.git")).bindSelected(createRepository) + checkBox(message("new.project.welcome")).bindSelected(createScript) + } + + panel { + interpreterPanel.buildPanel(this) + } + } + + mainPanel.registerValidators(this) + myCreateButton.addActionListener { mainPanel.apply() } + return mainPanel + } + + + + + override fun findSequentNonExistingUntitled(): File { + return Optional + .ofNullable(PyUtil.`as`(myProjectGenerator, PythonProjectGenerator::class.java)) + .map { it.newProjectPrefix } + .map { FileUtil.findSequentNonexistentFile(getBaseDir(), it!!, "") } + .orElseGet { super.findSequentNonExistingUntitled() } + } + + private fun getBaseDir(): File { + if (PlatformUtils.isDataSpell() && FileUtil.isAncestor(PathManager.getConfigDir(), Paths.get(ProjectUtil.getBaseDir()), false)) { + return File(getUserHomeProjectDir()) + } + return File(ProjectUtil.getBaseDir()) + } + + private fun updateHint(): String { + return message("new.project.location.hint", Paths.get(projectLocation.get(), projectName.get())) + } + + override fun checkValid(): Boolean { + // todo add proper validation with custom component + return true + } + + override fun installFramework(): Boolean { + return true + } + + override fun onPanelSelected() { + interpreterPanel.onShown() + } + + override fun getSdk(): Sdk { + return PyLazySdk("Uninitialized environment") { interpreterPanel.getSdk() } + } + + + companion object { + @JvmStatic + fun initializeGit(project: Project, root: VirtualFile) { + runBackgroundableTask(IdeBundle.message("progress.title.creating.git.repository"), project) { + GitRepositoryInitializer.getInstance()?.initRepository(project, root, true) + } + } + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt new file mode 100644 index 000000000000..0cdc5ec89aaa --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/CondaExistingEnvironmentSelector.kt @@ -0,0 +1,119 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.execution.target.local.LocalTargetEnvironmentRequest +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.progress.ModalTaskOwner +import com.intellij.openapi.progress.TaskCancellation +import com.intellij.openapi.progress.runWithModalProgressBlocking +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.components.fields.ExtendableTextComponent +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.add.target.conda.TargetEnvironmentRequestCommandExecutor +import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv +import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath +import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CondaExistingEnvironmentSelector(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + + private lateinit var envComboBox: ComboBox + private lateinit var condaExecutableComboBox: TextFieldWithBrowseButton + private var selectedEnvironment = propertyGraph.property(null) + private var lastLoadedConda = propertyGraph.property("") + private var condaEnvironmentsLoaded = propertyGraph.property(true) + + + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.conda.executable.path")) { + condaExecutableComboBox = textFieldWithBrowseButton() + .bindText(state.condaExecutable) + .align(Align.FILL) + .component + } + + row(message("sdk.create.custom.env.creation.type")) { + envComboBox = comboBox(emptyList(), CondaEnvComboBoxListCellRenderer()) + .bindItem(selectedEnvironment) + .enabledIf(condaEnvironmentsLoaded) + .component + + link(message("sdk.create.custom.conda.refresh.envs")) { + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()) + val environments = PyCondaEnv.getEnvs(commandExecutor, state.condaExecutable.get()) + withContext(Dispatchers.Main + modalityState) { + envComboBox.removeAllItems() + val envs = environments.getOrThrow() // todo error handling + selectedEnvironment.set(envs.first()) + envs.forEach(envComboBox::addItem) + lastLoadedConda.set(state.condaExecutable.get()) + } + } + }.visibleIf(!condaEnvironmentsLoaded) + } + + } + } + + override fun onShown() { + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + + val pathOnTarget = suggestCondaPath()!! // todo flow for undiscovered conda + val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()) + val environments = PyCondaEnv.getEnvs(commandExecutor, pathOnTarget) + + withContext(Dispatchers.Main + modalityState) { + + lastLoadedConda.set(pathOnTarget) + envComboBox.removeAllItems() + val envs = environments.getOrThrow() + selectedEnvironment.set(envs.first()) + envs.forEach(envComboBox::addItem) + + + val refreshIcon = ExtendableTextComponent.Extension.create(AllIcons.Actions.Refresh, AllIcons.Actions.Refresh, "Refresh conda") { + // todo decide whether we need icon or just link + } + val textComponent = condaExecutableComboBox.childComponent as ExtendableTextComponent + + condaEnvironmentsLoaded.dependsOn(state.condaExecutable, ::condasMatch) + condaEnvironmentsLoaded.dependsOn(lastLoadedConda, ::condasMatch) + + condaEnvironmentsLoaded.afterChange { + if (it) textComponent.removeExtension(refreshIcon) + else textComponent.addExtension(refreshIcon) + } + + + } + } + } + + private fun condasMatch(): Boolean = state.condaExecutable.get() == lastLoadedConda.get() + + override fun getOrCreateSdk(): Sdk { + return runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.select.progress"), TaskCancellation.nonCancellable()) { + PyCondaCommand(state.condaExecutable.get(), null) + .createCondaSdkFromExistingEnv(selectedEnvironment.get()!!.envIdentity, state.basePythonSdks.get(), + ProjectManager.getInstance().defaultProject) + } + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt new file mode 100644 index 000000000000..f91a412d17ce --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/CondaNewEnvironmentCreator.kt @@ -0,0 +1,69 @@ +// 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.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.progress.ModalTaskOwner +import com.intellij.openapi.progress.TaskCancellation +import com.intellij.openapi.progress.runWithModalProgressBlocking +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bindItem +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.listCellRenderer.textListCellRenderer +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.psi.LanguageLevel +import com.jetbrains.python.sdk.add.target.conda.condaSupportedLanguages +import com.jetbrains.python.sdk.add.target.conda.createCondaSdkAlongWithNewEnv +import com.jetbrains.python.sdk.flavors.conda.NewCondaEnvRequest +import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand +import kotlinx.coroutines.Dispatchers +import java.io.File + +class CondaNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + + private val envName = propertyGraph.property("") + private lateinit var pythonVersion: ObservableMutableProperty + private lateinit var versionComboBox: ComboBox + + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.python.version")) { + pythonVersion = propertyGraph.property(condaSupportedLanguages.first()) + versionComboBox = comboBox(condaSupportedLanguages, textListCellRenderer { it!!.toPythonVersion() }) + .bindItem(pythonVersion) + .component + } + row(message("sdk.create.custom.conda.env.name")) { + textField() + .bindText(envName) + } + row(message("sdk.create.conda.executable.path")) { + textFieldWithBrowseButton(message("sdk.create.custom.conda.browse.title"), + null, + FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor()) + .align(Align.FILL) + .bindText(state.condaExecutable) + } + } + } + + override fun onShown() { + envName.set(state.projectPath.get().substringAfterLast(File.separator)) + } + + override fun getOrCreateSdk(): Sdk { + return runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.create.progress"), TaskCancellation.nonCancellable()) { + PyCondaCommand(state.condaExecutable.get(), null, null) + .createCondaSdkAlongWithNewEnv(NewCondaEnvRequest.EmptyNamedEnv(pythonVersion.get(), envName.get()), + Dispatchers.EDT, + state.basePythonSdks.get(), + ProjectManager.getInstance().defaultProject).getOrThrow() + } + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt new file mode 100644 index 000000000000..710b0224ed7e --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PipEnvNewEnvironmentCreator.kt @@ -0,0 +1,59 @@ +// 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.ide.util.PropertiesComponent +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.dsl.builder.Panel +import com.intellij.util.text.nullize +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.pipenv.detectPipEnvExecutable +import com.jetbrains.python.sdk.pipenv.pipEnvPath +import com.jetbrains.python.sdk.pipenv.setupPipEnvSdkUnderProgress +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PipEnvNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + private val executable = propertyGraph.property(UNKNOWN_EXECUTABLE) + private lateinit var pipEnvPathField: TextFieldWithBrowseButton + private lateinit var basePythonComboBox: ComboBox + + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.custom.base.python")) { + basePythonComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths, state.basePythonHomePath) + } + + pipEnvPathField = executableSelector(message("sdk.create.custom.pipenv.path"), executable, + message("sdk.create.custom.pipenv.missing.text")) + } + + } + + + override fun onShown() { + val savedPath = PropertiesComponent.getInstance().pipEnvPath + if (savedPath != null) { + executable.set(savedPath) + } + else { + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + val detectedExecutable = detectPipEnvExecutable() + withContext(Dispatchers.Main + modalityState) { + detectedExecutable?.let { executable.set(it.path) } + } + } + } + } + + override fun getOrCreateSdk(): Sdk { + PropertiesComponent.getInstance().pipEnvPath = pipEnvPathField.text.nullize() + return setupPipEnvSdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(), + state.basePythonVersion.get()!!.homePath, false)!! + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt new file mode 100644 index 000000000000..c14e2515d3d5 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PoetryNewEnvironmentCreator.kt @@ -0,0 +1,60 @@ +// 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.ide.util.PropertiesComponent +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.dsl.builder.Panel +import com.intellij.util.text.nullize +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.poetry.detectPoetryExecutable +import com.jetbrains.python.sdk.poetry.poetryPath +import com.jetbrains.python.sdk.poetry.setupPoetrySdkUnderProgress +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PoetryNewEnvironmentCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + + val executable = propertyGraph.property(UNKNOWN_EXECUTABLE) + private lateinit var poetryPathField: TextFieldWithBrowseButton + private lateinit var basePythonComboBox: ComboBox + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.custom.base.python")) { + basePythonComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths, state.basePythonHomePath) + } + + poetryPathField = executableSelector(message("sdk.create.custom.poetry.path"), executable, + message("sdk.create.custom.poetry.missing.text")) + } + } + + override fun onShown() { + val savedPath = PropertiesComponent.getInstance().poetryPath + if (savedPath != null) { + executable.set(savedPath) + } + else { + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + val poetryExecutable = detectPoetryExecutable() + withContext(Dispatchers.Main + modalityState) { + poetryExecutable?.let { executable.set(it.path) } + } + } + } + } + + + override fun getOrCreateSdk(): Sdk { + PropertiesComponent.getInstance().poetryPath = executable.get().nullize() + return setupPoetrySdkUnderProgress(null, null, state.basePythonSdks.get(), state.projectPath.get(), + state.basePythonVersion.get()!!.homePath, false)!! + } + + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PySetPropertyAction.kt b/python/src/com/jetbrains/python/sdk/add/v2/PySetPropertyAction.kt new file mode 100644 index 000000000000..e9a6f731106b --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PySetPropertyAction.kt @@ -0,0 +1,32 @@ +// 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.ide.util.PropertiesComponent +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.observable.properties.AtomicProperty +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.ui.components.dialog +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel + +class PySetPropertyAction : DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + val key = AtomicProperty("") + val value = AtomicProperty("") + val panel = panel { + row("Key:") { + textField().bindText(key).align(Align.FILL) + } + row("Value:") { + textField().bindText(value).align(Align.FILL) + } + } + + val result = dialog("Set value in PropertiesComponent", panel, false).showAndGet() + if (result) { + PropertiesComponent.getInstance().setValue(key.get(), value.get()) + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt new file mode 100644 index 000000000000..a02300b4c019 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddCustomInterpreter.kt @@ -0,0 +1,47 @@ +// 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.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.Panel +import com.jetbrains.python.sdk.add.v2.PythonInterpreterCreationTargets.LOCAL_MACHINE + +class PythonAddCustomInterpreter(private val settings: PythonAddInterpreterState) { + + private lateinit var targetSelector: ComboBox + + private val targets = mapOf( + LOCAL_MACHINE to PythonLocalEnvironmentCreator(settings), + ) + + fun buildPanel(outerPanel: Panel) { + with(outerPanel) { + + // todo uncomment for all available targets + //row(message("sdk.create.custom.develop.on")) { + // targetSelector = comboBox(targets.keys, PythonEnvironmentComboBoxRenderer()) + // .widthGroup("env_aligned") + // .component + //} + //targets.forEach { target -> + // rowsRange { + // target.value.buildPanel(this) + // }.visibleIf(targetSelector.selectedValueMatches { it == target.key }) + //} + + + rowsRange { + targets[LOCAL_MACHINE]!!.buildPanel(this) + } + } + } + + fun onShown() { + targets.values.forEach(PythonLocalEnvironmentCreator::onShown) + } + + fun getSdk(): Sdk { + return targets[targetSelector.selectedItem]!!.getSdk() + } + +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt new file mode 100644 index 000000000000..3d25aec0e596 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddInterpreterState.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.transform +import com.intellij.openapi.projectRoots.Sdk +import com.jetbrains.python.sdk.add.target.createDetectedSdk +import kotlinx.coroutines.CoroutineScope + +class PythonAddInterpreterState( + val propertyGraph: PropertyGraph, + val projectPath: ObservableProperty, + val scope: CoroutineScope, + val basePythonSdks: ObservableMutableProperty>, + val allExistingSdks: ObservableMutableProperty>, + val basePythonVersion: ObservableMutableProperty, + val condaExecutable: ObservableMutableProperty, +) { + val basePythonHomePath = basePythonVersion.transform({ sdk -> sdk?.homePath ?: "" }, + { path -> basePythonSdks.get().find { it.homePath == path }!! }) + + val basePythonHomePaths = transformSdksToHomePath(basePythonSdks) + val allValidSdkPaths = transformSdksToHomePath(allExistingSdks) + + + + + + private fun transformSdksToHomePath(sdkList: ObservableMutableProperty>): ObservableMutableProperty> { + return sdkList.transform({ sdks -> sdks.map { it.homePath!! } }, + { paths -> + val existing = sdkList.get() + paths.map { path -> + existing.find { it.homePath == path } ?: createDetectedSdk(path, isLocal = true) + } + }) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt new file mode 100644 index 000000000000..ca1d3bd28d86 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonAddNewEnvironmentPanel.kt @@ -0,0 +1,185 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.execution.target.local.LocalTargetEnvironmentRequest +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.components.service +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.or +import com.intellij.openapi.progress.ModalTaskOwner +import com.intellij.openapi.progress.TaskCancellation +import com.intellij.openapi.progress.runWithModalProgressBlocking +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.ui.dsl.builder.* +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.configuration.PyConfigurableInterpreterList +import com.jetbrains.python.newProject.steps.ProjectSpecificSettingsStep +import com.jetbrains.python.sdk.add.target.conda.TargetEnvironmentRequestCommandExecutor +import com.jetbrains.python.sdk.add.target.conda.createCondaSdkFromExistingEnv +import com.jetbrains.python.sdk.add.target.conda.suggestCondaPath +import com.jetbrains.python.sdk.add.v2.PythonInterpreterSelectionMode.* +import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration +import com.jetbrains.python.sdk.findBaseSdks +import com.jetbrains.python.sdk.flavors.conda.PyCondaCommand +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnv +import com.jetbrains.python.sdk.flavors.conda.PyCondaEnvIdentity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.nio.file.Paths +import kotlin.io.path.exists + +class PythonAddNewEnvironmentPanel(val projectPath: ObservableProperty) { + private val propertyGraph = PropertyGraph() + + private var selectedMode = propertyGraph.property(PROJECT_VENV) + private var _projectVenv = propertyGraph.booleanProperty(selectedMode, PROJECT_VENV) + private var _baseConda = propertyGraph.booleanProperty(selectedMode, BASE_CONDA) + private var _custom = propertyGraph.booleanProperty(selectedMode, CUSTOM) + + private val allExistingSdks = propertyGraph.property>(emptyList()) + private val basePythonSdks = propertyGraph.property>(emptyList()) + private val pythonBaseVersion = propertyGraph.property(null) + + private val condaExecutable = propertyGraph.property("") + private var venvHint = propertyGraph.property("") + + private lateinit var pythonBaseVersionComboBox: ComboBox + private lateinit var baseCondaEnv: PyCondaEnv + + private var initialized = false + + private fun updateVenvLocationHint() { + if (selectedMode.get() == PROJECT_VENV) venvHint.set(message("sdk.create.simple.venv.hint", projectPath.get() + File.separator)) + else if (selectedMode.get() == BASE_CONDA) venvHint.set(message("sdk.create.simple.conda.hint")) + } + + val state = PythonAddInterpreterState(propertyGraph, + projectPath, + service().coroutineScope, + basePythonSdks, + allExistingSdks, + pythonBaseVersion, + condaExecutable) + + private val custom = PythonAddCustomInterpreter(state) + + + fun buildPanel(outerPanel: Panel) { + with(outerPanel) { + row(message("sdk.create.interpreter.type")) { + segmentedButton(PythonInterpreterSelectionMode.entries) { text = message(it.nameKey) } + .bind(selectedMode) + }.topGap(TopGap.SMALL) + + + row(message("sdk.create.python.version")) { + pythonBaseVersionComboBox = comboBox(emptyList(), PythonSdkComboBoxListCellRenderer()) + .bindItem(pythonBaseVersion) + .align(AlignX.FILL) + .component + }.visibleIf(_projectVenv) + + row(message("sdk.create.conda.executable.path")) { + textFieldWithBrowseButton() + .bindText(condaExecutable) + .validationOnInput { + if (!Paths.get(it.text).exists()) error("Executable does not exist") else null + } + .align(AlignX.FILL) + }.visibleIf(_baseConda) + + row("") { + comment("").bindText(venvHint) + }.visibleIf(_projectVenv or _baseConda) + + rowsRange { + custom.buildPanel(this) + }.visibleIf(_custom) + } + + basePythonSdks.afterChange { + pythonBaseVersionComboBox.removeAllItems() + it.forEach { pythonBaseVersionComboBox.addItem(it) } + } + + projectPath.afterChange { updateVenvLocationHint() } + selectedMode.afterChange { updateVenvLocationHint() } + + // todo why doesn't work? + //venvHint.dependsOn(projectPath, ::updateVenvLocationHint) + //venvHint.dependsOn(selectedMode, ::updateVenvLocationHint) + } + + fun onShown() { + if (!initialized) { + initialized = true + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + val existingSdks = PyConfigurableInterpreterList.getInstance(null).getModel().sdks.toList() + val baseSdks = findBaseSdks(existingSdks, null, UserDataHolderBase()) + val allValidSdks = ProjectSpecificSettingsStep.getValidPythonSdks(existingSdks) + val validBaseSdks = ProjectSpecificSettingsStep.getValidPythonSdks(baseSdks) + + withContext(Dispatchers.Main + modalityState) { + pythonBaseVersionComboBox.removeAllItems() + validBaseSdks.forEach { pythonBaseVersionComboBox.addItem(it) } + basePythonSdks.set(validBaseSdks) + allExistingSdks.set(allValidSdks) + + updateVenvLocationHint() + } + } + + + // todo maybe do this when env requested + state.scope.launch(Dispatchers.Default + modalityState) { + val condaPath = suggestCondaPath() ?: return@launch // todo make conda a executableSelector component + val commandExecutor = TargetEnvironmentRequestCommandExecutor(LocalTargetEnvironmentRequest()) + val environments = PyCondaEnv.getEnvs(commandExecutor, condaPath) + val baseConda = environments.getOrThrow().find { env -> env.envIdentity.let { it is PyCondaEnvIdentity.UnnamedEnv && it.isBase } } + withContext(Dispatchers.Main + modalityState) { + condaExecutable.set(condaPath) + baseCondaEnv = baseConda!! + } + } + + custom.onShown() + } + } + + //fun validateCurrent(): Boolean { + // return when (selectedMode.get()) { + // PROJECT_VENV -> pythonBaseVersion.get().let { it != null && it.sdkSeemsValid } + // BASE_CONDA -> { + // val condaPath = condaExecutable.get() + // return condaPath != null + // } + // CUSTOM -> error("") + // } + //} + + fun getSdk(): Sdk? { + return when (selectedMode.get()) { + PROJECT_VENV -> { + val venvPath = projectPath.get() + File.separator + ".venv" + return PyProjectVirtualEnvConfiguration.createVirtualEnvSynchronously(pythonBaseVersion.get(), basePythonSdks.get(), venvPath, + projectPath.get(), + null, null) + } + BASE_CONDA -> { + runWithModalProgressBlocking(ModalTaskOwner.guess(), message("sdk.create.custom.conda.select.progress"), TaskCancellation.nonCancellable()) { + PyCondaCommand(condaExecutable.get(), null) + .createCondaSdkFromExistingEnv(baseCondaEnv.envIdentity, basePythonSdks.get(), ProjectManager.getInstance().defaultProject) + } + } + CUSTOM -> custom.getSdk() + } + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt new file mode 100644 index 000000000000..fd955a2d4f4f --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonExistingEnvironmentSelector.kt @@ -0,0 +1,27 @@ +// 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.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.Panel +import com.jetbrains.python.PyBundle.message + +class PythonExistingEnvironmentSelector(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + private lateinit var versionComboBox: ComboBox + private val selectedPath = propertyGraph.property("") + + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.custom.python.path")) { + versionComboBox = pythonBaseInterpreterComboBox(state.allValidSdkPaths, selectedPath) + } + } + selectedPath.dependsOn(state.basePythonHomePath) { + state.basePythonHomePath.get() + } + } + + override fun getOrCreateSdk(): Sdk { + return state.allExistingSdks.get().find { it.homePath == selectedPath.get() }!! + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt new file mode 100644 index 000000000000..1b8a6bf5b4a3 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonLocalEnvironmentCreator.kt @@ -0,0 +1,87 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.sdk.add.v2 + +import com.intellij.openapi.observable.util.and +import com.intellij.openapi.observable.util.equalsTo +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.bind +import com.intellij.ui.dsl.builder.bindItem +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.add.v2.PythonSupportedEnvironmentManagers.* + +class PythonLocalEnvironmentCreator(private val settings: PythonAddInterpreterState) : PythonTargetEnvironmentInterpreterCreator { + + private val selectionMethod = settings.propertyGraph.property(PythonInterpreterSelectionMethod.CREATE_NEW) + + private val _createNew = settings.propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.CREATE_NEW) + private val _selectExisting = settings.propertyGraph.booleanProperty(selectionMethod, PythonInterpreterSelectionMethod.SELECT_EXISTING) + + private val newInterpreterManager = settings.propertyGraph.property(VIRTUALENV) + private val existingInterpreterManager = settings.propertyGraph.property(PYTHON) + + private val newInterpreterCreators = mapOf( + VIRTUALENV to PythonNewVirtualenvCreator(settings), + CONDA to CondaNewEnvironmentCreator(settings), + PIPENV to PipEnvNewEnvironmentCreator(settings), + POETRY to PoetryNewEnvironmentCreator(settings), + ) + + private val existingInterpreterSelectors = mapOf( + PYTHON to PythonExistingEnvironmentSelector(settings), + CONDA to CondaExistingEnvironmentSelector(settings), + ) + + + override fun buildPanel(outerPanel: Panel) { + with(outerPanel) { + buttonsGroup { + row(message("sdk.create.custom.env.creation.type")) { + radioButton(message("sdk.create.custom.generate.new"), PythonInterpreterSelectionMethod.CREATE_NEW).onChanged { + selectionMethod.set(if (it.isSelected) PythonInterpreterSelectionMethod.CREATE_NEW else PythonInterpreterSelectionMethod.SELECT_EXISTING) + } + radioButton(message("sdk.create.custom.select.existing"), PythonInterpreterSelectionMethod.SELECT_EXISTING).onChanged { + selectionMethod.set(if (it.isSelected) PythonInterpreterSelectionMethod.SELECT_EXISTING else PythonInterpreterSelectionMethod.CREATE_NEW) + } + } + }.bind({ selectionMethod.get() }, { selectionMethod.set(it) } ) + + row(message("sdk.create.custom.type")) { + comboBox(newInterpreterCreators.keys, PythonEnvironmentComboBoxRenderer()) + .bindItem(newInterpreterManager) + .widthGroup("env_aligned") + .visibleIf(_createNew) + + comboBox(existingInterpreterSelectors.keys, PythonEnvironmentComboBoxRenderer()) + .bindItem(existingInterpreterManager) + .widthGroup("env_aligned") + .visibleIf(_selectExisting) + } + + newInterpreterCreators.forEach { (type, creator) -> + rowsRange { + creator.buildOptions(this) + }.visibleIf(_createNew and newInterpreterManager.equalsTo(type)) + } + + existingInterpreterSelectors.forEach { (type, selector) -> + rowsRange { + selector.buildOptions(this) + }.visibleIf(_selectExisting and existingInterpreterManager.equalsTo(type)) + } + } + } + + + override fun onShown() { + newInterpreterCreators.values.forEach(PythonAddEnvironment::onShown) + existingInterpreterSelectors.values.forEach(PythonAddEnvironment::onShown) + } + + override fun getSdk(): Sdk { + val sdkManager = + if (_selectExisting.get()) existingInterpreterSelectors[existingInterpreterManager.get()] + else newInterpreterCreators[newInterpreterManager.get()] + return sdkManager!!.getOrCreateSdk() + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt new file mode 100644 index 000000000000..dd699a577bf3 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/PythonNewVirtualenvCreator.kt @@ -0,0 +1,81 @@ +// 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.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.* +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.PySdkSettings +import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.nio.file.Paths +import kotlin.io.path.exists + +class PythonNewVirtualenvCreator(state: PythonAddInterpreterState) : PythonAddEnvironment(state) { + private val location = propertyGraph.property("") + private val inheritSitePackages = propertyGraph.property(false) + private val makeAvailable = propertyGraph.property(false) + private lateinit var versionComboBox: ComboBox + private var locationModified = false + + override fun buildOptions(panel: Panel) { + with(panel) { + row(message("sdk.create.custom.base.python")) { + versionComboBox = pythonBaseInterpreterComboBox(state.basePythonHomePaths, + state.basePythonHomePath) + + } + row(message("sdk.create.custom.location")) { + textFieldWithBrowseButton(message("sdk.create.custom.venv.location.browse.title"), fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()) + .bindText(location) + .whenTextChangedFromUi { locationModified = true } + .cellValidation { + addInputRule("This directory already exists") { + Paths.get(it.text).exists() // todo validation with custom component, once available + } + } + .align(Align.FILL) + } + + row("") { + checkBox(message("sdk.create.custom.inherit.packages")) + .bindSelected(inheritSitePackages) + } + row("") { + checkBox(message("sdk.create.custom.make.available")) + .bindSelected(makeAvailable) + } + } + + state.projectPath.afterChange { + if (!locationModified) { + location.set(PySdkSettings.instance.getPreferredVirtualEnvBasePath(it)) + } + } + } + + override fun onShown() { + val modalityState = ModalityState.current().asContextElement() + state.scope.launch(Dispatchers.Default + modalityState) { + val basePath = PySdkSettings.instance.getPreferredVirtualEnvBasePath(state.projectPath.get()) + withContext(Dispatchers.Main + modalityState) { + location.set(basePath) + } + } + } + + override fun getOrCreateSdk(): Sdk { + return PyProjectVirtualEnvConfiguration.createVirtualEnvSynchronously(state.basePythonVersion.get(), + state.basePythonSdks.get(), + location.get(), + state.projectPath.get(), + null, null, + inheritSitePackages = inheritSitePackages.get(), + makeShared = makeAvailable.get())!! + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/sdk/add/v2/common.kt b/python/src/com/jetbrains/python/sdk/add/v2/common.kt new file mode 100644 index 000000000000..2c695c50ef3b --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/common.kt @@ -0,0 +1,57 @@ +// 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.icons.AllIcons +import com.intellij.openapi.components.Service +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.ui.dsl.builder.Panel +import com.jetbrains.python.sdk.pipenv.PIPENV_ICON +import com.jetbrains.python.sdk.poetry.POETRY_ICON +import icons.PythonIcons +import icons.PythonSdkIcons +import kotlinx.coroutines.CoroutineScope +import javax.swing.Icon + + +@Service(Service.Level.APP) +class PythonAddSdkService(val coroutineScope: CoroutineScope) + +interface PythonTargetEnvironmentInterpreterCreator { + fun buildPanel(outerPanel: Panel) + fun onShown() {} + fun getSdk(): Sdk +} + +abstract class PythonAddEnvironment(val state: PythonAddInterpreterState) { + + internal val propertyGraph + get() = state.propertyGraph + + abstract fun buildOptions(panel: Panel) + open fun onShown() {} + abstract fun getOrCreateSdk(): Sdk +} + +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), + POETRY("sdk.create.custom.poetry", POETRY_ICON), + PIPENV("sdk.create.custom.pipenv", PIPENV_ICON), + PYTHON("sdk.create.custom.python", PythonSdkIcons.Python) +} + +enum class PythonInterpreterSelectionMode(val nameKey: String) { + PROJECT_VENV("sdk.create.type.project.venv"), + BASE_CONDA("sdk.create.type.base.conda"), + CUSTOM("sdk.create.type.custom") +} + +enum class PythonInterpreterCreationTargets(val nameKey: String, val icon: Icon) { + LOCAL_MACHINE("sdk.create.targets.local", AllIcons.Nodes.HomeFolder), + SSH("", AllIcons.Nodes.HomeFolder), + DOCKER("", AllIcons.Nodes.HomeFolder) +} + +enum class PythonInterpreterSelectionMethod { + CREATE_NEW, SELECT_EXISTING +} diff --git a/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt new file mode 100644 index 000000000000..59898231d839 --- /dev/null +++ b/python/src/com/jetbrains/python/sdk/add/v2/uiUtils.kt @@ -0,0 +1,137 @@ +// 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.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.* +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.components.fields.ExtendableTextComponent +import com.intellij.ui.components.fields.ExtendableTextField +import com.intellij.ui.dsl.builder.* +import com.intellij.util.ui.JBUI +import com.jetbrains.python.PyBundle.message +import com.jetbrains.python.sdk.PyDetectedSdk +import com.jetbrains.python.sdk.flavors.conda.* +import org.jetbrains.annotations.Nls +import javax.swing.JList +import javax.swing.JTextField +import javax.swing.plaf.basic.BasicComboBoxEditor + + +internal fun PropertyGraph.booleanProperty(dependency: ObservableProperty, value: T) = + lazyProperty { false }.apply { dependsOn(dependency) { dependency.get() == value } } + + +class PythonSdkComboBoxListCellRenderer : ColoredListCellRenderer() { + override fun customizeCellRenderer(list: JList, value: Any?, index: Int, selected: Boolean, hasFocus: Boolean) { + when (value) { + is PyDetectedSdk -> append(value.homePath!!) + is Sdk -> { + append(value.versionString!!) + append(" " + value.homePath!!, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES) + } + else -> append("") + } + } +} +class CondaEnvComboBoxListCellRenderer : ColoredListCellRenderer() { + @Suppress("HardCodedStringLiteral") + override fun customizeCellRenderer(list: JList, value: PyCondaEnv?, index: Int, selected: Boolean, hasFocus: Boolean) { + when (val identity = value?.envIdentity) { + is PyCondaEnvIdentity.NamedEnv -> append(identity.userReadableName) + is PyCondaEnvIdentity.UnnamedEnv -> append(identity.envPath) + else -> append("") + } + } +} + + +class PythonEnvironmentComboBoxRenderer : ColoredListCellRenderer() { + override fun customizeCellRenderer(list: JList, value: Any, index: Int, selected: Boolean, hasFocus: Boolean) { + when (value) { + is PythonSupportedEnvironmentManagers, -> { + icon = value.icon + append(message(value.nameKey)) + } + is PythonInterpreterCreationTargets -> { + icon = value.icon + append(message(value.nameKey)) + } + } + + } +} + +fun Row.pythonBaseInterpreterComboBox(sdkHomePaths: ObservableMutableProperty>, + selectedPath: ObservableMutableProperty): ComboBox { + val component = comboBox(emptyList()) + .bindItem(selectedPath) + .align(Align.FILL) + .component + + val browseExtension = ExtendableTextComponent.Extension.create(AllIcons.General.OpenDisk, + AllIcons.General.OpenDiskHover, + message("sdk.create.custom.python.browse.tooltip")) { + FileChooser.chooseFile(FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), null, null) { file -> + val path = file?.path ?: return@chooseFile + val homePaths = sdkHomePaths.get() + if (!homePaths.contains(path)) { + sdkHomePaths.set(listOf(path) + homePaths) + } + selectedPath.set(path) + } + } + + component.isEditable = true + component.editor = object : BasicComboBoxEditor(){ + override fun createEditorComponent(): JTextField { + val field = ExtendableTextField() + field.addExtension(browseExtension) + field.setBorder(null) + field.isEditable = false + field.background = JBUI.CurrentTheme.Arrow.backgroundColor(true, true) + return field + } + } + + sdkHomePaths.afterChange { + component.removeAllItems() + it.forEach(component::addItem) + } + + return component +} + + +const val UNKNOWN_EXECUTABLE = "" + +fun Panel.executableSelector(labelText: @Nls String, executable: ObservableMutableProperty, missingExecutableText: @Nls String): TextFieldWithBrowseButton { + var textFieldComponent: TextFieldWithBrowseButton? = null + row("") { + icon(AllIcons.General.Warning) + text(missingExecutableText) { + FileChooser.chooseFile(FileChooserDescriptorFactory.createSingleFileOrExecutableAppDescriptor(), null, null) { + executable.set(it.path) + } + } + }.visibleIf(executable.equalsTo(UNKNOWN_EXECUTABLE)) + + row(labelText) { + textFieldComponent = textFieldWithBrowseButton() + .bindText(executable) + .align(AlignX.FILL) + .component + }.visibleIf(executable.notEqualsTo(UNKNOWN_EXECUTABLE)) + + return textFieldComponent!! +} + +