From a90c9c64099a0ccbd2b896ce5c272f83c92e6ed1 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Mon, 22 Jul 2019 14:39:01 +0200 Subject: [PATCH] Python: extract PythonRunConfiguration and PythonProject Change-Id: I4ff0f43fdb8beb9a7f2f7816197de0c796da8d89 Reviewed-by: Christian Stenger --- src/plugins/python/CMakeLists.txt | 2 + src/plugins/python/python.pro | 4 + src/plugins/python/python.qbs | 22 +- src/plugins/python/pythonplugin.cpp | 791 +----------------- src/plugins/python/pythonproject.cpp | 454 ++++++++++ src/plugins/python/pythonproject.h | 70 ++ src/plugins/python/pythonrunconfiguration.cpp | 332 ++++++++ src/plugins/python/pythonrunconfiguration.h | 70 ++ 8 files changed, 958 insertions(+), 787 deletions(-) create mode 100644 src/plugins/python/pythonproject.cpp create mode 100644 src/plugins/python/pythonproject.h create mode 100644 src/plugins/python/pythonrunconfiguration.cpp create mode 100644 src/plugins/python/pythonrunconfiguration.h diff --git a/src/plugins/python/CMakeLists.txt b/src/plugins/python/CMakeLists.txt index 196188f01e0..7ecc160f3a9 100644 --- a/src/plugins/python/CMakeLists.txt +++ b/src/plugins/python/CMakeLists.txt @@ -8,6 +8,8 @@ add_qtc_plugin(Python pythonformattoken.h pythonhighlighter.cpp pythonhighlighter.h pythonindenter.cpp pythonindenter.h + pythonproject.cpp pythonproject.h + pythonrunconfiguration.cpp pythonrunconfiguration.h pythonsettings.cpp pythonsettings.h pythonscanner.cpp pythonscanner.h ) diff --git a/src/plugins/python/python.pro b/src/plugins/python/python.pro index 788d4b142b8..ec4ea74c06e 100644 --- a/src/plugins/python/python.pro +++ b/src/plugins/python/python.pro @@ -10,6 +10,8 @@ HEADERS += \ pythonhighlighter.h \ pythonindenter.h \ pythonformattoken.h \ + pythonproject.h \ + pythonrunconfiguration.h \ pythonscanner.h \ pythonsettings.h @@ -18,6 +20,8 @@ SOURCES += \ pythoneditor.cpp \ pythonhighlighter.cpp \ pythonindenter.cpp \ + pythonproject.cpp \ + pythonrunconfiguration.cpp \ pythonscanner.cpp \ pythonsettings.cpp diff --git a/src/plugins/python/python.qbs b/src/plugins/python/python.qbs index 56a5820528a..c90f76d5d1d 100644 --- a/src/plugins/python/python.qbs +++ b/src/plugins/python/python.qbs @@ -14,14 +14,24 @@ QtcPlugin { name: "General" files: [ "python.qrc", - "pythoneditor.cpp", "pythoneditor.h", + "pythoneditor.cpp", + "pythoneditor.h", "pythonconstants.h", - "pythonplugin.cpp", "pythonplugin.h", - "pythonhighlighter.h", "pythonhighlighter.cpp", - "pythonindenter.cpp", "pythonindenter.h", + "pythonplugin.cpp", + "pythonplugin.h", + "pythonhighlighter.h", + "pythonhighlighter.cpp", + "pythonindenter.cpp", + "pythonindenter.h", "pythonformattoken.h", - "pythonscanner.h", "pythonscanner.cpp", - "pythonsettings.cpp", "pythonsettings.h", + "pythonproject.cpp", + "pythonproject.h", + "pythonrunconfiguration.cpp", + "pythonrunconfiguration.h", + "pythonscanner.h", + "pythonscanner.cpp", + "pythonsettings.cpp", + "pythonsettings.h", ] } } diff --git a/src/plugins/python/pythonplugin.cpp b/src/plugins/python/pythonplugin.cpp index 51ede9b83dd..bdb0d6a32e6 100644 --- a/src/plugins/python/pythonplugin.cpp +++ b/src/plugins/python/pythonplugin.cpp @@ -24,796 +24,27 @@ ****************************************************************************/ #include "pythonplugin.h" -#include "pythoneditor.h" -#include "pythonconstants.h" -#include "pythonhighlighter.h" -#include "pythonsettings.h" -#include -#include -#include +#include "pythoneditor.h" +#include "pythonproject.h" +#include "pythonsettings.h" +#include "pythonrunconfiguration.h" + #include -#include -#include -#include #include #include -#include -#include -#include -#include #include -#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 Core; using namespace ProjectExplorer; -using namespace Python::Constants; -using namespace Utils; namespace Python { namespace Internal { -const char PythonMimeType[] = "text/x-python-project"; // ### FIXME -const char PythonProjectId[] = "PythonProject"; -const char PythonErrorTaskCategory[] = "Task.Category.Python"; - -class PythonProject : public Project -{ - Q_OBJECT -public: - explicit PythonProject(const Utils::FilePath &filename); - - bool addFiles(const QStringList &filePaths); - bool removeFiles(const QStringList &filePaths); - bool setFiles(const QStringList &filePaths); - bool renameFile(const QString &filePath, const QString &newFilePath); - void refresh(Target *target = nullptr); - - bool needsConfiguration() const final { return false; } - - bool writePyProjectFile(const QString &fileName, QString &content, - const QStringList &rawList, QString *errorMessage); - -private: - RestoreResult fromMap(const QVariantMap &map, QString *errorMessage) override; - bool setupTarget(Target *t) override - { - refresh(t); - return Project::setupTarget(t); - } - - bool saveRawFileList(const QStringList &rawFileList); - bool saveRawList(const QStringList &rawList, const QString &fileName); - void parseProject(); - QStringList processEntries(const QStringList &paths, - QHash *map = nullptr) const; - - QStringList m_rawFileList; - QStringList m_files; - QHash m_rawListEntries; -}; - -class PythonProjectNode : public ProjectNode -{ -public: - PythonProjectNode(PythonProject *project); - - bool supportsAction(ProjectAction action, const Node *node) const override; - bool addFiles(const QStringList &filePaths, QStringList *) override; - ProjectExplorer::RemovedFilesFromProject removeFiles(const QStringList &filePaths, - QStringList *) override; - bool deleteFiles(const QStringList &) override; - bool renameFile(const QString &filePath, const QString &newFilePath) override; - -private: - PythonProject *m_project; -}; - -static QTextCharFormat linkFormat(const QTextCharFormat &inputFormat, const QString &href) -{ - QTextCharFormat result = inputFormat; - result.setForeground(creatorTheme()->color(Theme::TextColorLink)); - result.setUnderlineStyle(QTextCharFormat::SingleUnderline); - result.setAnchor(true); - result.setAnchorHref(href); - return result; -} - -class PythonOutputFormatter : public OutputFormatter -{ -public: - PythonOutputFormatter() - // Note that moc dislikes raw string literals. - : filePattern("^(\\s*)(File \"([^\"]+)\", line (\\d+), .*$)") - { - TaskHub::clearTasks(PythonErrorTaskCategory); - } - -private: - void appendMessage(const QString &text, OutputFormat format) final - { - const bool isTrace = (format == StdErrFormat - || format == StdErrFormatSameLine) - && (text.startsWith("Traceback (most recent call last):") - || text.startsWith("\nTraceback (most recent call last):")); - - if (!isTrace) { - OutputFormatter::appendMessage(text, format); - return; - } - - const QTextCharFormat frm = charFormat(format); - const Core::Id id(PythonErrorTaskCategory); - QVector tasks; - const QStringList lines = text.split('\n'); - unsigned taskId = unsigned(lines.size()); - - for (const QString &line : lines) { - const QRegularExpressionMatch match = filePattern.match(line); - if (match.hasMatch()) { - QTextCursor tc = plainTextEdit()->textCursor(); - tc.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); - tc.insertText('\n' + match.captured(1)); - tc.insertText(match.captured(2), linkFormat(frm, match.captured(2))); - - const auto fileName = FilePath::fromString(match.captured(3)); - const int lineNumber = match.capturedRef(4).toInt(); - Task task(Task::Warning, - QString(), fileName, lineNumber, id); - task.taskId = --taskId; - tasks.append(task); - } else { - if (!tasks.isEmpty()) { - Task &task = tasks.back(); - if (!task.description.isEmpty()) - task.description += ' '; - task.description += line.trimmed(); - } - OutputFormatter::appendMessage('\n' + line, format); - } - } - if (!tasks.isEmpty()) { - tasks.back().type = Task::Error; - for (auto rit = tasks.crbegin(), rend = tasks.crend(); rit != rend; ++rit) - TaskHub::addTask(*rit); - } - } - - void handleLink(const QString &href) final - { - const QRegularExpressionMatch match = filePattern.match(href); - if (!match.hasMatch()) - return; - const QString fileName = match.captured(3); - const int lineNumber = match.capturedRef(4).toInt(); - Core::EditorManager::openEditorAt(fileName, lineNumber); - } - - const QRegularExpression filePattern; -}; - -class PythonOutputFormatterFactory : public OutputFormatterFactory -{ -public: - PythonOutputFormatterFactory() - { - setFormatterCreator([](Target *t) -> OutputFormatter * { - if (t->project()->mimeType() == Constants::C_PY_MIMETYPE) - return new PythonOutputFormatter; - return nullptr; - }); - } -}; - -//////////////////////////////////////////////////////////////// - -class InterpreterAspect : public ProjectConfigurationAspect -{ - Q_OBJECT - -public: - InterpreterAspect() = default; - - Interpreter currentInterpreter() const; - void updateInterpreters(const QList &interpreters); - void setDefaultInterpreter(const Interpreter &interpreter) { m_defaultId = interpreter.id; } - - void fromMap(const QVariantMap &) override; - void toMap(QVariantMap &) const override; - void addToConfigurationLayout(QFormLayout *layout) override; - -private: - void updateCurrentInterpreter(); - void updateComboBox(); - QList m_interpreters; - QPointer m_comboBox; - QString m_defaultId; - QString m_currentId; -}; - -Interpreter InterpreterAspect::currentInterpreter() const -{ - return m_comboBox ? m_interpreters.value(m_comboBox->currentIndex()) : Interpreter(); -} - -void InterpreterAspect::updateInterpreters(const QList &interpreters) -{ - m_interpreters = interpreters; - if (m_comboBox) - updateComboBox(); -} - -void InterpreterAspect::fromMap(const QVariantMap &map) -{ - m_currentId = map.value(settingsKey(), m_defaultId).toString(); -} - -void InterpreterAspect::toMap(QVariantMap &map) const -{ - map.insert(settingsKey(), m_currentId); -} - -void InterpreterAspect::addToConfigurationLayout(QFormLayout *layout) -{ - if (QTC_GUARD(m_comboBox.isNull())) - m_comboBox = new QComboBox; - - updateComboBox(); - connect(m_comboBox, - &QComboBox::currentTextChanged, - this, - &InterpreterAspect::updateCurrentInterpreter); - - auto manageButton = new QPushButton(tr("Manage...")); - connect(manageButton, &QPushButton::clicked, []() { - Core::ICore::showOptionsDialog(Constants::C_PYTHONOPTIONS_PAGE_ID); - }); - - auto rowLayout = new QHBoxLayout; - rowLayout->addWidget(m_comboBox); - rowLayout->addWidget(manageButton); - layout->addRow(tr("Interpreter"), rowLayout); -} - -void InterpreterAspect::updateCurrentInterpreter() -{ - m_currentId = currentInterpreter().id; - m_comboBox->setToolTip(currentInterpreter().command.toUserOutput()); - emit changed(); -} - -void InterpreterAspect::updateComboBox() -{ - int currentIndex = -1; - int defaultIndex = -1; - const QString currentId = m_currentId; - m_comboBox->clear(); - for (const Interpreter &interpreter : m_interpreters) { - int index = m_comboBox->count(); - m_comboBox->addItem(interpreter.name); - m_comboBox->setItemData(index, interpreter.command.toUserOutput(), Qt::ToolTipRole); - if (interpreter.id == currentId) - currentIndex = index; - if (interpreter.id == m_defaultId) - defaultIndex = index; - } - if (currentIndex >= 0) - m_comboBox->setCurrentIndex(currentIndex); - else if (defaultIndex >= 0) - m_comboBox->setCurrentIndex(defaultIndex); - updateCurrentInterpreter(); -} - -class MainScriptAspect : public BaseStringAspect -{ - Q_OBJECT - -public: - MainScriptAspect() = default; -}; - -class PythonRunConfiguration : public RunConfiguration -{ - Q_OBJECT - - Q_PROPERTY(bool supportsDebugger READ supportsDebugger) - Q_PROPERTY(QString interpreter READ interpreter) - Q_PROPERTY(QString mainScript READ mainScript) - Q_PROPERTY(QString arguments READ arguments) - -public: - PythonRunConfiguration(Target *target, Core::Id id); - -private: - void doAdditionalSetup(const RunConfigurationCreationInfo &) final { updateTargetInformation(); } - - bool supportsDebugger() const { return true; } - QString mainScript() const { return aspect()->value(); } - QString arguments() const { return aspect()->arguments(macroExpander()); } - QString interpreter() const { return aspect()->currentInterpreter().command.toString(); } - - void updateTargetInformation(); -}; - -PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id) - : RunConfiguration(target, id) -{ - auto interpreterAspect = addAspect(); - interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); - - connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, - interpreterAspect, &InterpreterAspect::updateInterpreters); - - interpreterAspect->updateInterpreters(PythonSettings::interpreters()); - interpreterAspect->setDefaultInterpreter(PythonSettings::defaultInterpreter()); - - auto scriptAspect = addAspect(); - scriptAspect->setSettingsKey("PythonEditor.RunConfiguation.Script"); - scriptAspect->setLabelText(tr("Script:")); - scriptAspect->setDisplayStyle(BaseStringAspect::LabelDisplay); - - addAspect(target); - - auto argumentsAspect = addAspect(); - - addAspect(); - - setCommandLineGetter([this, interpreterAspect, argumentsAspect] { - CommandLine cmd{interpreterAspect->currentInterpreter().command, {mainScript()}}; - cmd.addArgs(argumentsAspect->arguments(macroExpander()), CommandLine::Raw); - return cmd; - }); - - connect(target, &Target::applicationTargetsChanged, - this, &PythonRunConfiguration::updateTargetInformation); - connect(target->project(), &Project::parsingFinished, - this, &PythonRunConfiguration::updateTargetInformation); -} - -void PythonRunConfiguration::updateTargetInformation() -{ - const BuildTargetInfo bti = buildTargetInfo(); - const QString script = bti.targetFilePath.toString(); - setDefaultDisplayName(tr("Run %1").arg(script)); - aspect()->setValue(script); -} - -class PythonRunConfigurationFactory : public RunConfigurationFactory -{ -public: - PythonRunConfigurationFactory() - { - registerRunConfiguration("PythonEditor.RunConfiguration."); - addSupportedProjectType(PythonProjectId); - } -}; - -PythonProject::PythonProject(const FilePath &fileName) - : Project(Constants::C_PY_MIMETYPE, fileName) -{ - setId(PythonProjectId); - setProjectLanguages(Context(ProjectExplorer::Constants::CXX_LANGUAGE_ID)); - setDisplayName(fileName.toFileInfo().completeBaseName()); - - setNeedsBuildConfigurations(false); - - connect(this, &PythonProject::projectFileIsDirty, this, [this]() { refresh(); }); -} - -static QStringList readLines(const Utils::FilePath &projectFile) -{ - const QString projectFileName = projectFile.fileName(); - QSet visited = { projectFileName }; - QStringList lines = { projectFileName }; - - QFile file(projectFile.toString()); - if (file.open(QFile::ReadOnly)) { - QTextStream stream(&file); - - while (true) { - const QString line = stream.readLine(); - if (line.isNull()) - break; - if (visited.contains(line)) - continue; - lines.append(line); - visited.insert(line); - } - } - - return lines; -} - -static QStringList readLinesJson(const Utils::FilePath &projectFile, - QString *errorMessage) -{ - const QString projectFileName = projectFile.fileName(); - QStringList lines = { projectFileName }; - - QFile file(projectFile.toString()); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - *errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2") - .arg(projectFile.toUserOutput(), file.errorString()); - return lines; - } - - const QByteArray content = file.readAll(); - - // This assumes te project file is formed with only one field called - // 'files' that has a list associated of the files to include in the project. - if (content.isEmpty()) { - *errorMessage = PythonProject::tr("Unable to read \"%1\": The file is empty.") - .arg(projectFile.toUserOutput()); - return lines; - } - - QJsonParseError error; - const QJsonDocument doc = QJsonDocument::fromJson(content, &error); - if (doc.isNull()) { - const int line = content.left(error.offset).count('\n') + 1; - *errorMessage = PythonProject::tr("Unable to parse \"%1\":%2: %3") - .arg(projectFile.toUserOutput()).arg(line) - .arg(error.errorString()); - return lines; - } - - const QJsonObject obj = doc.object(); - if (obj.contains("files")) { - const QJsonValue files = obj.value("files"); - const QJsonArray files_array = files.toArray(); - QSet visited; - for (const auto &file : files_array) - visited.insert(file.toString()); - - lines.append(Utils::toList(visited)); - } - - return lines; -} - -bool PythonProject::saveRawFileList(const QStringList &rawFileList) -{ - const bool result = saveRawList(rawFileList, projectFilePath().toString()); -// refresh(PythonProject::Files); - return result; -} - -bool PythonProject::saveRawList(const QStringList &rawList, const QString &fileName) -{ - FileChangeBlocker changeGuarg(fileName); - bool result = false; - - // New project file - if (fileName.endsWith(".pyproject")) { - FileSaver saver(fileName, QIODevice::ReadOnly | QIODevice::Text); - if (!saver.hasError()) { - QString content = QTextStream(saver.file()).readAll(); - if (saver.finalize(ICore::mainWindow())) { - QString errorMessage; - result = writePyProjectFile(fileName, content, rawList, &errorMessage); - if (!errorMessage.isEmpty()) - Core::MessageManager::write(errorMessage); - } - } - } else { // Old project file - FileSaver saver(fileName, QIODevice::WriteOnly | QIODevice::Text); - if (!saver.hasError()) { - QTextStream stream(saver.file()); - for (const QString &filePath : rawList) - stream << filePath << '\n'; - saver.setResult(&stream); - result = saver.finalize(ICore::mainWindow()); - } - } - - return result; -} - -bool PythonProject::writePyProjectFile(const QString &fileName, QString &content, - const QStringList &rawList, QString *errorMessage) -{ - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { - *errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2") - .arg(fileName, file.errorString()); - return false; - } - - // Build list of files with the current rawList for the JSON file - QString files("["); - for (const QString &f : rawList) - if (!f.endsWith(".pyproject")) - files += QString("\"%1\",").arg(f); - files = files.left(files.lastIndexOf(',')); // Removing leading comma - files += ']'; - - // Removing everything inside square parenthesis - // to replace it with the new list of files for the JSON file. - QRegularExpression pattern(R"(\[.*\])"); - content.replace(pattern, files); - file.write(content.toUtf8()); - - return true; -} - -bool PythonProject::addFiles(const QStringList &filePaths) -{ - QStringList newList = m_rawFileList; - - const QDir baseDir(projectDirectory().toString()); - for (const QString &filePath : filePaths) - newList.append(baseDir.relativeFilePath(filePath)); - - return saveRawFileList(newList); -} - -bool PythonProject::removeFiles(const QStringList &filePaths) -{ - QStringList newList = m_rawFileList; - - for (const QString &filePath : filePaths) { - const QHash::iterator i = m_rawListEntries.find(filePath); - if (i != m_rawListEntries.end()) - newList.removeOne(i.value()); - } - - return saveRawFileList(newList); -} - -bool PythonProject::setFiles(const QStringList &filePaths) -{ - QStringList newList; - const QDir baseDir(projectDirectory().toString()); - for (const QString &filePath : filePaths) - newList.append(baseDir.relativeFilePath(filePath)); - - return saveRawFileList(newList); -} - -bool PythonProject::renameFile(const QString &filePath, const QString &newFilePath) -{ - QStringList newList = m_rawFileList; - - const QHash::iterator i = m_rawListEntries.find(filePath); - if (i != m_rawListEntries.end()) { - const int index = newList.indexOf(i.value()); - if (index != -1) { - const QDir baseDir(projectDirectory().toString()); - newList.replace(index, baseDir.relativeFilePath(newFilePath)); - } - } - - return saveRawFileList(newList); -} - -void PythonProject::parseProject() -{ - m_rawListEntries.clear(); - const Utils::FilePath filePath = projectFilePath(); - // The PySide project file is JSON based - if (filePath.endsWith(".pyproject")) { - QString errorMessage; - m_rawFileList = readLinesJson(filePath, &errorMessage); - if (!errorMessage.isEmpty()) - Core::MessageManager::write(errorMessage); - } - // To keep compatibility with PyQt we keep the compatibility with plain - // text files as project files. - else if (filePath.endsWith(".pyqtc")) - m_rawFileList = readLines(filePath); - - m_files = processEntries(m_rawFileList, &m_rawListEntries); -} - -/** - * @brief Provides displayName relative to project node - */ -class PythonFileNode : public FileNode -{ -public: - PythonFileNode(const Utils::FilePath &filePath, const QString &nodeDisplayName, - FileType fileType = FileType::Source) - : FileNode(filePath, fileType) - , m_displayName(nodeDisplayName) - {} - - QString displayName() const override { return m_displayName; } -private: - QString m_displayName; -}; - -void PythonProject::refresh(Target *target) -{ - ParseGuard guard = guardParsingRun(); - parseProject(); - - const QDir baseDir(projectDirectory().toString()); - QList appTargets; - auto newRoot = std::make_unique(this); - for (const QString &f : qAsConst(m_files)) { - const QString displayName = baseDir.relativeFilePath(f); - const FileType fileType = f.endsWith(".pyproject") || f.endsWith(".pyqtc") ? FileType::Project - : FileType::Source; - newRoot->addNestedNode(std::make_unique(FilePath::fromString(f), - displayName, fileType)); - if (fileType == FileType::Source) { - BuildTargetInfo bti; - bti.buildKey = f; - bti.targetFilePath = FilePath::fromString(f); - bti.projectFilePath = projectFilePath(); - appTargets.append(bti); - } - } - setRootProjectNode(std::move(newRoot)); - - if (!target) - target = activeTarget(); - if (target) - target->setApplicationTargets(appTargets); - - guard.markAsSuccess(); -} - -/** - * Expands environment variables in the given \a string when they are written - * like $$(VARIABLE). - */ -static void expandEnvironmentVariables(const QProcessEnvironment &env, QString &string) -{ - static QRegExp candidate(QLatin1String("\\$\\$\\((.+)\\)")); - - int index = candidate.indexIn(string); - while (index != -1) { - const QString value = env.value(candidate.cap(1)); - - string.replace(index, candidate.matchedLength(), value); - index += value.length(); - - index = candidate.indexIn(string, index); - } -} - -/** - * Expands environment variables and converts the path from relative to the - * project to an absolute path. - * - * The \a map variable is an optional argument that will map the returned - * absolute paths back to their original \a entries. - */ -QStringList PythonProject::processEntries(const QStringList &paths, - QHash *map) const -{ - const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); - const QDir projectDir(projectDirectory().toString()); - - QFileInfo fileInfo; - QStringList absolutePaths; - for (const QString &path : paths) { - QString trimmedPath = path.trimmed(); - if (trimmedPath.isEmpty()) - continue; - - expandEnvironmentVariables(env, trimmedPath); - - trimmedPath = FilePath::fromUserInput(trimmedPath).toString(); - - fileInfo.setFile(projectDir, trimmedPath); - if (fileInfo.exists()) { - const QString absPath = fileInfo.absoluteFilePath(); - absolutePaths.append(absPath); - if (map) - map->insert(absPath, trimmedPath); - } - } - absolutePaths.removeDuplicates(); - return absolutePaths; -} - -Project::RestoreResult PythonProject::fromMap(const QVariantMap &map, QString *errorMessage) -{ - Project::RestoreResult res = Project::fromMap(map, errorMessage); - if (res == RestoreResult::Ok) { - refresh(); - - if (!activeTarget()) - addTargetForDefaultKit(); - } - - return res; -} - -PythonProjectNode::PythonProjectNode(PythonProject *project) - : ProjectNode(project->projectDirectory()) - , m_project(project) -{ - setDisplayName(project->projectFilePath().toFileInfo().completeBaseName()); - setAddFileFilter("*.py"); -} - -QHash sortFilesIntoPaths(const QString &base, const QSet &files) -{ - QHash filesInPath; - const QDir baseDir(base); - - for (const QString &absoluteFileName : files) { - const QFileInfo fileInfo(absoluteFileName); - const FilePath absoluteFilePath = FilePath::fromString(fileInfo.path()); - QString relativeFilePath; - - if (absoluteFilePath.isChildOf(baseDir)) { - relativeFilePath = absoluteFilePath.relativeChildPath(FilePath::fromString(base)).toString(); - } else { - // 'file' is not part of the project. - relativeFilePath = baseDir.relativeFilePath(absoluteFilePath.toString()); - if (relativeFilePath.endsWith('/')) - relativeFilePath.chop(1); - } - - filesInPath[relativeFilePath].append(absoluteFileName); - } - return filesInPath; -} - -bool PythonProjectNode::supportsAction(ProjectAction action, const Node *node) const -{ - if (node->asFileNode()) { - return action == ProjectAction::Rename - || action == ProjectAction::RemoveFile; - } - if (node->isFolderNodeType() || node->isProjectNodeType()) { - return action == ProjectAction::AddNewFile - || action == ProjectAction::RemoveFile - || action == ProjectAction::AddExistingFile; - } - return ProjectNode::supportsAction(action, node); -} - -bool PythonProjectNode::addFiles(const QStringList &filePaths, QStringList *) -{ - return m_project->addFiles(filePaths); -} - -RemovedFilesFromProject PythonProjectNode::removeFiles(const QStringList &filePaths, QStringList *) -{ - return m_project->removeFiles(filePaths) ? RemovedFilesFromProject::Ok - : RemovedFilesFromProject::Error; -} - -bool PythonProjectNode::deleteFiles(const QStringList &) -{ - return true; -} - -bool PythonProjectNode::renameFile(const QString &filePath, const QString &newFilePath) -{ - return m_project->renameFile(filePath, newFilePath); -} - //////////////////////////////////////////////////////////////////////////////////// // // PythonPlugin @@ -856,14 +87,12 @@ bool PythonPlugin::initialize(const QStringList &arguments, QString *errorMessag void PythonPlugin::extensionsInitialized() { // Add MIME overlay icons (these icons displayed at Project dock panel) - QString imageFile = creatorTheme()->imageFile(Theme::IconOverlayPro, - ProjectExplorer::Constants::FILEOVERLAY_PY); - FileIconProvider::registerIconOverlayForSuffix(imageFile, "py"); + QString imageFile = Utils::creatorTheme()->imageFile(Utils::Theme::IconOverlayPro, + Constants::FILEOVERLAY_PY); + Core::FileIconProvider::registerIconOverlayForSuffix(imageFile, "py"); TaskHub::addCategory(PythonErrorTaskCategory, "Python", true); } } // namespace Internal } // namespace Python - -#include "pythonplugin.moc" diff --git a/src/plugins/python/pythonproject.cpp b/src/plugins/python/pythonproject.cpp new file mode 100644 index 00000000000..136ada7bb8f --- /dev/null +++ b/src/plugins/python/pythonproject.cpp @@ -0,0 +1,454 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "pythonproject.h" + +#include "pythonconstants.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +using namespace Core; +using namespace ProjectExplorer; +using namespace Utils; + +namespace Python { +namespace Internal { + +class PythonProjectNode : public ProjectNode +{ +public: + PythonProjectNode(PythonProject *project); + + bool supportsAction(ProjectAction action, const Node *node) const override; + bool addFiles(const QStringList &filePaths, QStringList *) override; + RemovedFilesFromProject removeFiles(const QStringList &filePaths, QStringList *) override; + bool deleteFiles(const QStringList &) override; + bool renameFile(const QString &filePath, const QString &newFilePath) override; + +private: + PythonProject *m_project; +}; + + +/** + * @brief Provides displayName relative to project node + */ +class PythonFileNode : public FileNode +{ +public: + PythonFileNode(const FilePath &filePath, const QString &nodeDisplayName, + FileType fileType = FileType::Source) + : FileNode(filePath, fileType) + , m_displayName(nodeDisplayName) + {} + + QString displayName() const override { return m_displayName; } +private: + QString m_displayName; +}; + +static QStringList readLines(const FilePath &projectFile) +{ + const QString projectFileName = projectFile.fileName(); + QSet visited = { projectFileName }; + QStringList lines = { projectFileName }; + + QFile file(projectFile.toString()); + if (file.open(QFile::ReadOnly)) { + QTextStream stream(&file); + + while (true) { + const QString line = stream.readLine(); + if (line.isNull()) + break; + if (visited.contains(line)) + continue; + lines.append(line); + visited.insert(line); + } + } + + return lines; +} + +static QStringList readLinesJson(const FilePath &projectFile, QString *errorMessage) +{ + const QString projectFileName = projectFile.fileName(); + QStringList lines = { projectFileName }; + + QFile file(projectFile.toString()); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + *errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2") + .arg(projectFile.toUserOutput(), file.errorString()); + return lines; + } + + const QByteArray content = file.readAll(); + + // This assumes the project file is formed with only one field called + // 'files' that has a list associated of the files to include in the project. + if (content.isEmpty()) { + *errorMessage = PythonProject::tr("Unable to read \"%1\": The file is empty.") + .arg(projectFile.toUserOutput()); + return lines; + } + + QJsonParseError error; + const QJsonDocument doc = QJsonDocument::fromJson(content, &error); + if (doc.isNull()) { + const int line = content.left(error.offset).count('\n') + 1; + *errorMessage = PythonProject::tr("Unable to parse \"%1\":%2: %3") + .arg(projectFile.toUserOutput()).arg(line) + .arg(error.errorString()); + return lines; + } + + const QJsonObject obj = doc.object(); + if (obj.contains("files")) { + const QJsonValue files = obj.value("files"); + const QJsonArray files_array = files.toArray(); + QSet visited; + for (const auto &file : files_array) + visited.insert(file.toString()); + + lines.append(Utils::toList(visited)); + } + + return lines; +} + +PythonProject::PythonProject(const FilePath &fileName) + : Project(Constants::C_PY_MIMETYPE, fileName) +{ + setId(PythonProjectId); + setProjectLanguages(Context(ProjectExplorer::Constants::CXX_LANGUAGE_ID)); + setDisplayName(fileName.toFileInfo().completeBaseName()); + + setNeedsBuildConfigurations(false); + + connect(this, &PythonProject::projectFileIsDirty, this, [this]() { refresh(); }); +} + +void PythonProject::refresh(Target *target) +{ + ParseGuard guard = guardParsingRun(); + parseProject(); + + const QDir baseDir(projectDirectory().toString()); + QList appTargets; + auto newRoot = std::make_unique(this); + for (const QString &f : qAsConst(m_files)) { + const QString displayName = baseDir.relativeFilePath(f); + const FileType fileType = f.endsWith(".pyproject") || f.endsWith(".pyqtc") ? FileType::Project + : FileType::Source; + newRoot->addNestedNode(std::make_unique(FilePath::fromString(f), + displayName, fileType)); + if (fileType == FileType::Source) { + BuildTargetInfo bti; + bti.buildKey = f; + bti.targetFilePath = FilePath::fromString(f); + bti.projectFilePath = projectFilePath(); + appTargets.append(bti); + } + } + setRootProjectNode(std::move(newRoot)); + + if (!target) + target = activeTarget(); + if (target) + target->setApplicationTargets(appTargets); + + guard.markAsSuccess(); +} + +bool PythonProject::saveRawFileList(const QStringList &rawFileList) +{ + const bool result = saveRawList(rawFileList, projectFilePath().toString()); +// refresh(PythonProject::Files); + return result; +} + +bool PythonProject::saveRawList(const QStringList &rawList, const QString &fileName) +{ + FileChangeBlocker changeGuarg(fileName); + bool result = false; + + // New project file + if (fileName.endsWith(".pyproject")) { + FileSaver saver(fileName, QIODevice::ReadOnly | QIODevice::Text); + if (!saver.hasError()) { + QString content = QTextStream(saver.file()).readAll(); + if (saver.finalize(ICore::mainWindow())) { + QString errorMessage; + result = writePyProjectFile(fileName, content, rawList, &errorMessage); + if (!errorMessage.isEmpty()) + MessageManager::write(errorMessage); + } + } + } else { // Old project file + FileSaver saver(fileName, QIODevice::WriteOnly | QIODevice::Text); + if (!saver.hasError()) { + QTextStream stream(saver.file()); + for (const QString &filePath : rawList) + stream << filePath << '\n'; + saver.setResult(&stream); + result = saver.finalize(ICore::mainWindow()); + } + } + + return result; +} + +bool PythonProject::writePyProjectFile(const QString &fileName, QString &content, + const QStringList &rawList, QString *errorMessage) +{ + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + *errorMessage = PythonProject::tr("Unable to open \"%1\" for reading: %2") + .arg(fileName, file.errorString()); + return false; + } + + // Build list of files with the current rawList for the JSON file + QString files("["); + for (const QString &f : rawList) + if (!f.endsWith(".pyproject")) + files += QString("\"%1\",").arg(f); + files = files.left(files.lastIndexOf(',')); // Removing leading comma + files += ']'; + + // Removing everything inside square parenthesis + // to replace it with the new list of files for the JSON file. + QRegularExpression pattern(R"(\[.*\])"); + content.replace(pattern, files); + file.write(content.toUtf8()); + + return true; +} + +bool PythonProject::addFiles(const QStringList &filePaths) +{ + QStringList newList = m_rawFileList; + + const QDir baseDir(projectDirectory().toString()); + for (const QString &filePath : filePaths) + newList.append(baseDir.relativeFilePath(filePath)); + + return saveRawFileList(newList); +} + +bool PythonProject::removeFiles(const QStringList &filePaths) +{ + QStringList newList = m_rawFileList; + + for (const QString &filePath : filePaths) { + const QHash::iterator i = m_rawListEntries.find(filePath); + if (i != m_rawListEntries.end()) + newList.removeOne(i.value()); + } + + return saveRawFileList(newList); +} + +bool PythonProject::setFiles(const QStringList &filePaths) +{ + QStringList newList; + const QDir baseDir(projectDirectory().toString()); + for (const QString &filePath : filePaths) + newList.append(baseDir.relativeFilePath(filePath)); + + return saveRawFileList(newList); +} + +bool PythonProject::renameFile(const QString &filePath, const QString &newFilePath) +{ + QStringList newList = m_rawFileList; + + const QHash::iterator i = m_rawListEntries.find(filePath); + if (i != m_rawListEntries.end()) { + const int index = newList.indexOf(i.value()); + if (index != -1) { + const QDir baseDir(projectDirectory().toString()); + newList.replace(index, baseDir.relativeFilePath(newFilePath)); + } + } + + return saveRawFileList(newList); +} + +void PythonProject::parseProject() +{ + m_rawListEntries.clear(); + const FilePath filePath = projectFilePath(); + // The PySide project file is JSON based + if (filePath.endsWith(".pyproject")) { + QString errorMessage; + m_rawFileList = readLinesJson(filePath, &errorMessage); + if (!errorMessage.isEmpty()) + MessageManager::write(errorMessage); + } + // To keep compatibility with PyQt we keep the compatibility with plain + // text files as project files. + else if (filePath.endsWith(".pyqtc")) + m_rawFileList = readLines(filePath); + + m_files = processEntries(m_rawFileList, &m_rawListEntries); +} +/** + * Expands environment variables in the given \a string when they are written + * like $$(VARIABLE). + */ +static void expandEnvironmentVariables(const QProcessEnvironment &env, QString &string) +{ + static QRegExp candidate(QLatin1String("\\$\\$\\((.+)\\)")); + + int index = candidate.indexIn(string); + while (index != -1) { + const QString value = env.value(candidate.cap(1)); + + string.replace(index, candidate.matchedLength(), value); + index += value.length(); + + index = candidate.indexIn(string, index); + } +} + +/** + * Expands environment variables and converts the path from relative to the + * project to an absolute path. + * + * The \a map variable is an optional argument that will map the returned + * absolute paths back to their original \a paths. + */ +QStringList PythonProject::processEntries(const QStringList &paths, + QHash *map) const +{ + const QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + const QDir projectDir(projectDirectory().toString()); + + QFileInfo fileInfo; + QStringList absolutePaths; + for (const QString &path : paths) { + QString trimmedPath = path.trimmed(); + if (trimmedPath.isEmpty()) + continue; + + expandEnvironmentVariables(env, trimmedPath); + + trimmedPath = FilePath::fromUserInput(trimmedPath).toString(); + + fileInfo.setFile(projectDir, trimmedPath); + if (fileInfo.exists()) { + const QString absPath = fileInfo.absoluteFilePath(); + absolutePaths.append(absPath); + if (map) + map->insert(absPath, trimmedPath); + } + } + absolutePaths.removeDuplicates(); + return absolutePaths; +} + +Project::RestoreResult PythonProject::fromMap(const QVariantMap &map, QString *errorMessage) +{ + Project::RestoreResult res = Project::fromMap(map, errorMessage); + if (res == RestoreResult::Ok) { + refresh(); + + if (!activeTarget()) + addTargetForDefaultKit(); + } + + return res; +} + +bool PythonProject::setupTarget(Target *t) +{ + refresh(t); + return Project::setupTarget(t); +} + +PythonProjectNode::PythonProjectNode(PythonProject *project) + : ProjectNode(project->projectDirectory()) + , m_project(project) +{ + setDisplayName(project->projectFilePath().toFileInfo().completeBaseName()); + setAddFileFilter("*.py"); +} + +bool PythonProjectNode::supportsAction(ProjectAction action, const Node *node) const +{ + if (node->asFileNode()) { + return action == ProjectAction::Rename + || action == ProjectAction::RemoveFile; + } + if (node->isFolderNodeType() || node->isProjectNodeType()) { + return action == ProjectAction::AddNewFile + || action == ProjectAction::RemoveFile + || action == ProjectAction::AddExistingFile; + } + return ProjectNode::supportsAction(action, node); +} + +bool PythonProjectNode::addFiles(const QStringList &filePaths, QStringList *) +{ + return m_project->addFiles(filePaths); +} + +RemovedFilesFromProject PythonProjectNode::removeFiles(const QStringList &filePaths, QStringList *) +{ + return m_project->removeFiles(filePaths) ? RemovedFilesFromProject::Ok + : RemovedFilesFromProject::Error; +} + +bool PythonProjectNode::deleteFiles(const QStringList &) +{ + return true; +} + +bool PythonProjectNode::renameFile(const QString &filePath, const QString &newFilePath) +{ + return m_project->renameFile(filePath, newFilePath); +} + +} // namespace Internal +} // namespace Python diff --git a/src/plugins/python/pythonproject.h b/src/plugins/python/pythonproject.h new file mode 100644 index 00000000000..9f53cb6a7f7 --- /dev/null +++ b/src/plugins/python/pythonproject.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include + +namespace Python { +namespace Internal { + +const char PythonMimeType[] = "text/x-python-project"; // ### FIXME +const char PythonProjectId[] = "PythonProject"; +const char PythonErrorTaskCategory[] = "Task.Category.Python"; + +class PythonProject : public ProjectExplorer::Project +{ + Q_OBJECT +public: + explicit PythonProject(const Utils::FilePath &filename); + + bool addFiles(const QStringList &filePaths); + bool removeFiles(const QStringList &filePaths); + bool setFiles(const QStringList &filePaths); + bool renameFile(const QString &filePath, const QString &newFilePath); + void refresh(ProjectExplorer::Target *target = nullptr); + + bool needsConfiguration() const final { return false; } + + bool writePyProjectFile(const QString &fileName, QString &content, + const QStringList &rawList, QString *errorMessage); + +private: + RestoreResult fromMap(const QVariantMap &map, QString *errorMessage) override; + bool setupTarget(ProjectExplorer::Target *t) override; + + bool saveRawFileList(const QStringList &rawFileList); + bool saveRawList(const QStringList &rawList, const QString &fileName); + void parseProject(); + QStringList processEntries(const QStringList &paths, + QHash *map = nullptr) const; + + QStringList m_rawFileList; + QStringList m_files; + QHash m_rawListEntries; +}; + +} // namespace Internal +} // namespace Python diff --git a/src/plugins/python/pythonrunconfiguration.cpp b/src/plugins/python/pythonrunconfiguration.cpp new file mode 100644 index 00000000000..bb61f275ae7 --- /dev/null +++ b/src/plugins/python/pythonrunconfiguration.cpp @@ -0,0 +1,332 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "pythonconstants.h" +#include "pythonproject.h" +#include "pythonrunconfiguration.h" +#include "pythonsettings.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace ProjectExplorer; +using namespace Utils; + +namespace Python { +namespace Internal { + +static QTextCharFormat linkFormat(const QTextCharFormat &inputFormat, const QString &href) +{ + QTextCharFormat result = inputFormat; + result.setForeground(creatorTheme()->color(Theme::TextColorLink)); + result.setUnderlineStyle(QTextCharFormat::SingleUnderline); + result.setAnchor(true); + result.setAnchorHref(href); + return result; +} + +class PythonOutputFormatter : public OutputFormatter +{ +public: + PythonOutputFormatter() + // Note that moc dislikes raw string literals. + : filePattern("^(\\s*)(File \"([^\"]+)\", line (\\d+), .*$)") + { + TaskHub::clearTasks(PythonErrorTaskCategory); + } + +private: + void appendMessage(const QString &text, OutputFormat format) final + { + const bool isTrace = (format == StdErrFormat + || format == StdErrFormatSameLine) + && (text.startsWith("Traceback (most recent call last):") + || text.startsWith("\nTraceback (most recent call last):")); + + if (!isTrace) { + OutputFormatter::appendMessage(text, format); + return; + } + + const QTextCharFormat frm = charFormat(format); + const Core::Id id(PythonErrorTaskCategory); + QVector tasks; + const QStringList lines = text.split('\n'); + unsigned taskId = unsigned(lines.size()); + + for (const QString &line : lines) { + const QRegularExpressionMatch match = filePattern.match(line); + if (match.hasMatch()) { + QTextCursor tc = plainTextEdit()->textCursor(); + tc.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + tc.insertText('\n' + match.captured(1)); + tc.insertText(match.captured(2), linkFormat(frm, match.captured(2))); + + const auto fileName = FilePath::fromString(match.captured(3)); + const int lineNumber = match.capturedRef(4).toInt(); + Task task(Task::Warning, + QString(), fileName, lineNumber, id); + task.taskId = --taskId; + tasks.append(task); + } else { + if (!tasks.isEmpty()) { + Task &task = tasks.back(); + if (!task.description.isEmpty()) + task.description += ' '; + task.description += line.trimmed(); + } + OutputFormatter::appendMessage('\n' + line, format); + } + } + if (!tasks.isEmpty()) { + tasks.back().type = Task::Error; + for (auto rit = tasks.crbegin(), rend = tasks.crend(); rit != rend; ++rit) + TaskHub::addTask(*rit); + } + } + + void handleLink(const QString &href) final + { + const QRegularExpressionMatch match = filePattern.match(href); + if (!match.hasMatch()) + return; + const QString fileName = match.captured(3); + const int lineNumber = match.capturedRef(4).toInt(); + Core::EditorManager::openEditorAt(fileName, lineNumber); + } + + const QRegularExpression filePattern; +}; + +//////////////////////////////////////////////////////////////// + +class InterpreterAspect : public ProjectConfigurationAspect +{ + Q_OBJECT + +public: + InterpreterAspect() = default; + + Interpreter currentInterpreter() const; + void updateInterpreters(const QList &interpreters); + void setDefaultInterpreter(const Interpreter &interpreter) { m_defaultId = interpreter.id; } + + void fromMap(const QVariantMap &) override; + void toMap(QVariantMap &) const override; + void addToConfigurationLayout(QFormLayout *layout) override; + +private: + void updateCurrentInterpreter(); + void updateComboBox(); + QList m_interpreters; + QPointer m_comboBox; + QString m_defaultId; + QString m_currentId; +}; + +Interpreter InterpreterAspect::currentInterpreter() const +{ + return m_comboBox ? m_interpreters.value(m_comboBox->currentIndex()) : Interpreter(); +} + +void InterpreterAspect::updateInterpreters(const QList &interpreters) +{ + m_interpreters = interpreters; + if (m_comboBox) + updateComboBox(); +} + +void InterpreterAspect::fromMap(const QVariantMap &map) +{ + m_currentId = map.value(settingsKey(), m_defaultId).toString(); +} + +void InterpreterAspect::toMap(QVariantMap &map) const +{ + map.insert(settingsKey(), m_currentId); +} + +void InterpreterAspect::addToConfigurationLayout(QFormLayout *layout) +{ + if (QTC_GUARD(m_comboBox.isNull())) + m_comboBox = new QComboBox; + + updateComboBox(); + connect(m_comboBox, + &QComboBox::currentTextChanged, + this, + &InterpreterAspect::updateCurrentInterpreter); + + auto manageButton = new QPushButton(tr("Manage...")); + connect(manageButton, &QPushButton::clicked, []() { + Core::ICore::showOptionsDialog(Constants::C_PYTHONOPTIONS_PAGE_ID); + }); + + auto rowLayout = new QHBoxLayout; + rowLayout->addWidget(m_comboBox); + rowLayout->addWidget(manageButton); + layout->addRow(tr("Interpreter"), rowLayout); +} + +void InterpreterAspect::updateCurrentInterpreter() +{ + m_currentId = currentInterpreter().id; + m_comboBox->setToolTip(currentInterpreter().command.toUserOutput()); + emit changed(); +} + +void InterpreterAspect::updateComboBox() +{ + int currentIndex = -1; + int defaultIndex = -1; + const QString currentId = m_currentId; + m_comboBox->clear(); + for (const Interpreter &interpreter : m_interpreters) { + int index = m_comboBox->count(); + m_comboBox->addItem(interpreter.name); + m_comboBox->setItemData(index, interpreter.command.toUserOutput(), Qt::ToolTipRole); + if (interpreter.id == currentId) + currentIndex = index; + if (interpreter.id == m_defaultId) + defaultIndex = index; + } + if (currentIndex >= 0) + m_comboBox->setCurrentIndex(currentIndex); + else if (defaultIndex >= 0) + m_comboBox->setCurrentIndex(defaultIndex); + updateCurrentInterpreter(); +} + +class MainScriptAspect : public BaseStringAspect +{ + Q_OBJECT + +public: + MainScriptAspect() = default; +}; + +PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id) + : RunConfiguration(target, id) +{ + auto interpreterAspect = addAspect(); + interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); + + connect(PythonSettings::instance(), &PythonSettings::interpretersChanged, + interpreterAspect, &InterpreterAspect::updateInterpreters); + + interpreterAspect->updateInterpreters(PythonSettings::interpreters()); + interpreterAspect->setDefaultInterpreter(PythonSettings::defaultInterpreter()); + + auto scriptAspect = addAspect(); + scriptAspect->setSettingsKey("PythonEditor.RunConfiguation.Script"); + scriptAspect->setLabelText(tr("Script:")); + scriptAspect->setDisplayStyle(BaseStringAspect::LabelDisplay); + + addAspect(target); + + auto argumentsAspect = addAspect(); + + addAspect(); + + setCommandLineGetter([this, interpreterAspect, argumentsAspect] { + CommandLine cmd{interpreterAspect->currentInterpreter().command, {mainScript()}}; + cmd.addArgs(argumentsAspect->arguments(macroExpander()), CommandLine::Raw); + return cmd; + }); + + connect(target, &Target::applicationTargetsChanged, + this, &PythonRunConfiguration::updateTargetInformation); + connect(target->project(), &Project::parsingFinished, + this, &PythonRunConfiguration::updateTargetInformation); +} + +void PythonRunConfiguration::doAdditionalSetup(const RunConfigurationCreationInfo &) +{ + updateTargetInformation(); +} + +bool PythonRunConfiguration::supportsDebugger() const +{ + return true; +} + +QString PythonRunConfiguration::mainScript() const +{ + return aspect()->value(); +} + +QString PythonRunConfiguration::arguments() const +{ + return aspect()->arguments(macroExpander()); +} + +QString PythonRunConfiguration::interpreter() const +{ + return aspect()->currentInterpreter().command.toString(); +} + +void PythonRunConfiguration::updateTargetInformation() +{ + const BuildTargetInfo bti = buildTargetInfo(); + const QString script = bti.targetFilePath.toString(); + setDefaultDisplayName(tr("Run %1").arg(script)); + aspect()->setValue(script); +} + +PythonRunConfigurationFactory::PythonRunConfigurationFactory() +{ + registerRunConfiguration("PythonEditor.RunConfiguration."); + addSupportedProjectType(PythonProjectId); +} + +PythonOutputFormatterFactory::PythonOutputFormatterFactory() +{ + setFormatterCreator([](Target *t) -> OutputFormatter * { + if (t->project()->mimeType() == Constants::C_PY_MIMETYPE) + return new PythonOutputFormatter; + return nullptr; + }); +} + +} // namespace Internal +} // namespace Python + +#include "pythonrunconfiguration.moc" diff --git a/src/plugins/python/pythonrunconfiguration.h b/src/plugins/python/pythonrunconfiguration.h new file mode 100644 index 00000000000..753be4c2968 --- /dev/null +++ b/src/plugins/python/pythonrunconfiguration.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include +#include + +namespace Python { +namespace Internal { + +class PythonRunConfiguration : public ProjectExplorer::RunConfiguration +{ + Q_OBJECT + + Q_PROPERTY(bool supportsDebugger READ supportsDebugger) + Q_PROPERTY(QString interpreter READ interpreter) + Q_PROPERTY(QString mainScript READ mainScript) + Q_PROPERTY(QString arguments READ arguments) + +public: + PythonRunConfiguration(ProjectExplorer::Target *target, Core::Id id); + +private: + void doAdditionalSetup(const ProjectExplorer::RunConfigurationCreationInfo &) final; + + bool supportsDebugger() const; + QString mainScript() const; + QString arguments() const; + QString interpreter() const; + + void updateTargetInformation(); +}; + +class PythonRunConfigurationFactory : public ProjectExplorer::RunConfigurationFactory +{ +public: + PythonRunConfigurationFactory(); +}; + +class PythonOutputFormatterFactory : public ProjectExplorer::OutputFormatterFactory +{ +public: + PythonOutputFormatterFactory(); +}; + +} // namespace Internal +} // namespace Python