diff --git a/src/plugins/python/pythoneditor.cpp b/src/plugins/python/pythoneditor.cpp index f758ae3d9b6..42771fc91a2 100644 --- a/src/plugins/python/pythoneditor.cpp +++ b/src/plugins/python/pythoneditor.cpp @@ -163,6 +163,7 @@ void PythonEditorWidget::setUserDefinedPython(const Interpreter &interpreter) } } definePythonForDocument(textDocument()->filePath(), interpreter.command); + updateInterpretersSelector(); pythonDocument->checkForPyls(); } @@ -212,33 +213,51 @@ void PythonEditorWidget::updateInterpretersSelector() m_interpreters->setText(text); }; - const FilePath currentInterpreter = detectPython(textDocument()->filePath()); + const FilePath currentInterpreterPath = detectPython(textDocument()->filePath()); const QList configuredInterpreters = PythonSettings::interpreters(); - bool foundCurrentInterpreter = false; auto interpretersGroup = new QActionGroup(menu); interpretersGroup->setExclusive(true); + std::optional currentInterpreter; for (const Interpreter &interpreter : configuredInterpreters) { QAction *action = interpretersGroup->addAction(interpreter.name); connect(action, &QAction::triggered, this, [this, interpreter]() { setUserDefinedPython(interpreter); }); action->setCheckable(true); - if (!foundCurrentInterpreter && interpreter.command == currentInterpreter) { - foundCurrentInterpreter = true; + if (!currentInterpreter && interpreter.command == currentInterpreterPath) { + currentInterpreter = interpreter; action->setChecked(true); setButtonText(interpreter.name); m_interpreters->setToolTip(interpreter.command.toUserOutput()); } } menu->addActions(interpretersGroup->actions()); - if (!foundCurrentInterpreter) { - if (currentInterpreter.exists()) - setButtonText(currentInterpreter.toUserOutput()); + if (!currentInterpreter) { + if (currentInterpreterPath.exists()) + setButtonText(currentInterpreterPath.toUserOutput()); else setButtonText(Tr::tr("No Python Selected")); } - if (!interpretersGroup->actions().isEmpty()) - menu->addSeparator(); + if (!interpretersGroup->actions().isEmpty()) { + menu->addSeparator(); + auto venvAction = menu->addAction(Tr::tr("Create Virtual Environment")); + connect(venvAction, + &QAction::triggered, + this, + [self = QPointer(this), currentInterpreter]() { + if (!currentInterpreter) + return; + auto callback = [self](const std::optional &venvInterpreter) { + if (self && venvInterpreter) + self->setUserDefinedPython(*venvInterpreter); + }; + PythonSettings::createVirtualEnvironment(self->textDocument() + ->filePath() + .parentDir(), + *currentInterpreter, + callback); + }); + } auto settingsAction = menu->addAction(Tr::tr("Manage Python Interpreters")); connect(settingsAction, &QAction::triggered, this, []() { Core::ICore::showOptionsDialog(Constants::C_PYTHONOPTIONS_PAGE_ID); diff --git a/src/plugins/python/pythonsettings.cpp b/src/plugins/python/pythonsettings.cpp index 9306fe3b6f4..f22f0c3c8ce 100644 --- a/src/plugins/python/pythonsettings.cpp +++ b/src/plugins/python/pythonsettings.cpp @@ -6,6 +6,7 @@ #include "pythonconstants.h" #include "pythonplugin.h" #include "pythontr.h" +#include "pythonutils.h" #include #include @@ -30,19 +31,22 @@ #include #include +#include +#include +#include #include +#include +#include +#include +#include #include -#include #include +#include #include #include #include -#include #include -#include -#include -#include -#include +#include using namespace ProjectExplorer; using namespace Utils; @@ -69,7 +73,7 @@ static Interpreter createInterpreter(const FilePath &python, result.name = defaultName; QDir pythonDir(python.parentDir().toString()); if (pythonDir.exists() && pythonDir.exists("activate") && pythonDir.cdUp()) - result.name += QString(" (%1 Virtual Environment)").arg(pythonDir.dirName()); + result.name += QString(" (%1)").arg(pythonDir.dirName()); if (!suffix.isEmpty()) result.name += ' ' + suffix; @@ -769,12 +773,75 @@ void PythonSettings::addInterpreter(const Interpreter &interpreter, bool isDefau saveSettings(); } +Interpreter PythonSettings::addInterpreter(const FilePath &interpreterPath, bool isDefault) +{ + const Interpreter interpreter = createInterpreter(interpreterPath, {}); + addInterpreter(interpreter, isDefault); + return interpreter; +} + PythonSettings *PythonSettings::instance() { QTC_CHECK(settingsInstance); return settingsInstance; } +void PythonSettings::createVirtualEnvironment( + const FilePath &startDirectory, + const Interpreter &defaultInterpreter, + const std::function)> &callback) +{ + QDialog dialog; + dialog.setModal(true); + auto layout = new QFormLayout(&dialog); + auto interpreters = new QComboBox; + const QString preselectedId = defaultInterpreter.id.isEmpty() + ? PythonSettings::defaultInterpreter().id + : defaultInterpreter.id; + for (const Interpreter &interpreter : PythonSettings::interpreters()) { + interpreters->addItem(interpreter.name, interpreter.id); + if (!preselectedId.isEmpty() && interpreter.id == preselectedId) + interpreters->setCurrentIndex(interpreters->count() - 1); + } + layout->addRow(Tr::tr("Python Interpreter"), interpreters); + auto pathChooser = new PathChooser(); + pathChooser->setInitialBrowsePathBackup(startDirectory); + pathChooser->setExpectedKind(PathChooser::Directory); + pathChooser->setPromptDialogTitle(Tr::tr("New Python Virtual Environment Directory")); + layout->addRow(Tr::tr("Virtual Environment Directory"), pathChooser); + auto buttons = new QDialogButtonBox(QDialogButtonBox::Cancel); + auto createButton = buttons->addButton(Tr::tr("Create"), QDialogButtonBox::AcceptRole); + createButton->setEnabled(false); + connect(pathChooser, + &PathChooser::validChanged, + createButton, + [createButton](bool valid) { createButton->setEnabled(valid); }); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addRow(buttons); + dialog.setLayout(layout); + if (dialog.exec() == QDialog::Rejected) { + callback({}); + return; + } + + const Interpreter interpreter = PythonSettings::interpreter( + interpreters->currentData().toString()); + + auto venvDir = pathChooser->filePath(); + createVenv(interpreter.command, venvDir, [venvDir, callback](bool success){ + std::optional result; + if (success) { + FilePath venvPython = venvDir.osType() == Utils::OsTypeWindows ? venvDir / "Scripts" + : venvDir / "bin"; + venvPython = venvPython.pathAppended("python").withExecutableSuffix(); + if (venvPython.exists()) + result = PythonSettings::addInterpreter(venvPython); + } + callback(result); + }); +} + QList PythonSettings::detectPythonVenvs(const FilePath &path) { QList result; diff --git a/src/plugins/python/pythonsettings.h b/src/plugins/python/pythonsettings.h index 693c7322085..2e27e266162 100644 --- a/src/plugins/python/pythonsettings.h +++ b/src/plugins/python/pythonsettings.h @@ -24,12 +24,14 @@ public: static Interpreter interpreter(const QString &interpreterId); static void setInterpreter(const QList &interpreters, const QString &defaultId); static void addInterpreter(const Interpreter &interpreter, bool isDefault = false); + static Interpreter addInterpreter(const Utils::FilePath &interpreterPath, + bool isDefault = false); static void setPyLSConfiguration(const QString &configuration); static bool pylsEnabled(); static void setPylsEnabled(const bool &enabled); static QString pylsConfiguration(); static PythonSettings *instance(); - + static void createVirtualEnvironment(const Utils::FilePath &startDirectory, const Interpreter &defaultInterpreter, const std::function)> &callback); static QList detectPythonVenvs(const Utils::FilePath &path); signals: diff --git a/src/plugins/python/pythonutils.cpp b/src/plugins/python/pythonutils.cpp index 86356050928..1f89eef5b5f 100644 --- a/src/plugins/python/pythonutils.cpp +++ b/src/plugins/python/pythonutils.cpp @@ -8,6 +8,7 @@ #include "pythontr.h" #include +#include #include #include @@ -164,4 +165,24 @@ PythonProject *pythonProjectForFile(const FilePath &pythonFile) return nullptr; } +void createVenv(const Utils::FilePath &python, + const Utils::FilePath &venvPath, + const std::function &callback) +{ + QTC_ASSERT(python.isExecutableFile(), callback(false); return); + QTC_ASSERT(!venvPath.exists() || venvPath.isDir(), callback(false); return); + + const CommandLine command(python, QStringList{"-m", "venv", venvPath.toUserOutput()}); + + auto process = new QtcProcess; + auto progress = new Core::ProcessProgress(process); + progress->setDisplayName(Tr::tr("Create Python venv")); + QObject::connect(process, &QtcProcess::done, [process, callback](){ + callback(process->result() == ProcessResult::FinishedWithSuccess); + process->deleteLater(); + }); + process->setCommand(command); + process->start(); +} + } // Python::Internal diff --git a/src/plugins/python/pythonutils.h b/src/plugins/python/pythonutils.h index 8d5b06974ef..f3e685b4ae0 100644 --- a/src/plugins/python/pythonutils.h +++ b/src/plugins/python/pythonutils.h @@ -16,4 +16,8 @@ QString pythonName(const Utils::FilePath &pythonPath); class PythonProject; PythonProject *pythonProjectForFile(const Utils::FilePath &pythonFile); +void createVenv(const Utils::FilePath &python, + const Utils::FilePath &venvPath, + const std::function &callback); + } // Python::Internal