Python: extract PythonRunConfiguration and PythonProject

Change-Id: I4ff0f43fdb8beb9a7f2f7816197de0c796da8d89
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2019-07-22 14:39:01 +02:00
parent 358574f44d
commit a90c9c6409
8 changed files with 958 additions and 787 deletions

View File

@@ -8,6 +8,8 @@ add_qtc_plugin(Python
pythonformattoken.h pythonformattoken.h
pythonhighlighter.cpp pythonhighlighter.h pythonhighlighter.cpp pythonhighlighter.h
pythonindenter.cpp pythonindenter.h pythonindenter.cpp pythonindenter.h
pythonproject.cpp pythonproject.h
pythonrunconfiguration.cpp pythonrunconfiguration.h
pythonsettings.cpp pythonsettings.h pythonsettings.cpp pythonsettings.h
pythonscanner.cpp pythonscanner.h pythonscanner.cpp pythonscanner.h
) )

View File

@@ -10,6 +10,8 @@ HEADERS += \
pythonhighlighter.h \ pythonhighlighter.h \
pythonindenter.h \ pythonindenter.h \
pythonformattoken.h \ pythonformattoken.h \
pythonproject.h \
pythonrunconfiguration.h \
pythonscanner.h \ pythonscanner.h \
pythonsettings.h pythonsettings.h
@@ -18,6 +20,8 @@ SOURCES += \
pythoneditor.cpp \ pythoneditor.cpp \
pythonhighlighter.cpp \ pythonhighlighter.cpp \
pythonindenter.cpp \ pythonindenter.cpp \
pythonproject.cpp \
pythonrunconfiguration.cpp \
pythonscanner.cpp \ pythonscanner.cpp \
pythonsettings.cpp pythonsettings.cpp

View File

@@ -14,14 +14,24 @@ QtcPlugin {
name: "General" name: "General"
files: [ files: [
"python.qrc", "python.qrc",
"pythoneditor.cpp", "pythoneditor.h", "pythoneditor.cpp",
"pythoneditor.h",
"pythonconstants.h", "pythonconstants.h",
"pythonplugin.cpp", "pythonplugin.h", "pythonplugin.cpp",
"pythonhighlighter.h", "pythonhighlighter.cpp", "pythonplugin.h",
"pythonindenter.cpp", "pythonindenter.h", "pythonhighlighter.h",
"pythonhighlighter.cpp",
"pythonindenter.cpp",
"pythonindenter.h",
"pythonformattoken.h", "pythonformattoken.h",
"pythonscanner.h", "pythonscanner.cpp", "pythonproject.cpp",
"pythonsettings.cpp", "pythonsettings.h", "pythonproject.h",
"pythonrunconfiguration.cpp",
"pythonrunconfiguration.h",
"pythonscanner.h",
"pythonscanner.cpp",
"pythonsettings.cpp",
"pythonsettings.h",
] ]
} }
} }

View File

@@ -24,796 +24,27 @@
****************************************************************************/ ****************************************************************************/
#include "pythonplugin.h" #include "pythonplugin.h"
#include "pythoneditor.h"
#include "pythonconstants.h"
#include "pythonhighlighter.h"
#include "pythonsettings.h"
#include <coreplugin/icore.h> #include "pythoneditor.h"
#include <coreplugin/coreconstants.h> #include "pythonproject.h"
#include <coreplugin/documentmanager.h> #include "pythonsettings.h"
#include "pythonrunconfiguration.h"
#include <coreplugin/fileiconprovider.h> #include <coreplugin/fileiconprovider.h>
#include <coreplugin/id.h>
#include <coreplugin/messagemanager.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/buildtargetinfo.h> #include <projectexplorer/buildtargetinfo.h>
#include <projectexplorer/localenvironmentaspect.h> #include <projectexplorer/localenvironmentaspect.h>
#include <projectexplorer/runcontrol.h>
#include <projectexplorer/runconfiguration.h>
#include <projectexplorer/runconfigurationaspects.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <projectexplorer/projectnodes.h> #include <projectexplorer/runcontrol.h>
#include <projectexplorer/target.h>
#include <projectexplorer/task.h>
#include <projectexplorer/taskhub.h> #include <projectexplorer/taskhub.h>
#include <texteditor/texteditorconstants.h> #include <utils/theme/theme.h>
#include <utils/algorithm.h>
#include <utils/outputformatter.h>
#include <utils/qtcprocess.h>
#include <utils/utilsicons.h>
#include <QComboBox>
#include <QDir>
#include <QFormLayout>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
#include <QJsonValue>
#include <QPushButton>
#include <QRegExp>
#include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QTextCursor>
using namespace Core;
using namespace ProjectExplorer; using namespace ProjectExplorer;
using namespace Python::Constants;
using namespace Utils;
namespace Python { namespace Python {
namespace Internal { 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<QString, QString> *map = nullptr) const;
QStringList m_rawFileList;
QStringList m_files;
QHash<QString, QString> 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<Task> 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<Interpreter> &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<Interpreter> m_interpreters;
QPointer<QComboBox> 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<Interpreter> &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<MainScriptAspect>()->value(); }
QString arguments() const { return aspect<ArgumentsAspect>()->arguments(macroExpander()); }
QString interpreter() const { return aspect<InterpreterAspect>()->currentInterpreter().command.toString(); }
void updateTargetInformation();
};
PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id)
: RunConfiguration(target, id)
{
auto interpreterAspect = addAspect<InterpreterAspect>();
interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter");
connect(PythonSettings::instance(), &PythonSettings::interpretersChanged,
interpreterAspect, &InterpreterAspect::updateInterpreters);
interpreterAspect->updateInterpreters(PythonSettings::interpreters());
interpreterAspect->setDefaultInterpreter(PythonSettings::defaultInterpreter());
auto scriptAspect = addAspect<MainScriptAspect>();
scriptAspect->setSettingsKey("PythonEditor.RunConfiguation.Script");
scriptAspect->setLabelText(tr("Script:"));
scriptAspect->setDisplayStyle(BaseStringAspect::LabelDisplay);
addAspect<LocalEnvironmentAspect>(target);
auto argumentsAspect = addAspect<ArgumentsAspect>();
addAspect<TerminalAspect>();
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<MainScriptAspect>()->setValue(script);
}
class PythonRunConfigurationFactory : public RunConfigurationFactory
{
public:
PythonRunConfigurationFactory()
{
registerRunConfiguration<PythonRunConfiguration>("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<QString> 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<QString> 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<QString, QString>::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<QString, QString>::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<BuildTargetInfo> appTargets;
auto newRoot = std::make_unique<PythonProjectNode>(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<PythonFileNode>(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<QString, QString> *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<QString, QStringList> sortFilesIntoPaths(const QString &base, const QSet<QString> &files)
{
QHash<QString, QStringList> 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 // PythonPlugin
@@ -856,14 +87,12 @@ bool PythonPlugin::initialize(const QStringList &arguments, QString *errorMessag
void PythonPlugin::extensionsInitialized() void PythonPlugin::extensionsInitialized()
{ {
// Add MIME overlay icons (these icons displayed at Project dock panel) // Add MIME overlay icons (these icons displayed at Project dock panel)
QString imageFile = creatorTheme()->imageFile(Theme::IconOverlayPro, QString imageFile = Utils::creatorTheme()->imageFile(Utils::Theme::IconOverlayPro,
ProjectExplorer::Constants::FILEOVERLAY_PY); Constants::FILEOVERLAY_PY);
FileIconProvider::registerIconOverlayForSuffix(imageFile, "py"); Core::FileIconProvider::registerIconOverlayForSuffix(imageFile, "py");
TaskHub::addCategory(PythonErrorTaskCategory, "Python", true); TaskHub::addCategory(PythonErrorTaskCategory, "Python", true);
} }
} // namespace Internal } // namespace Internal
} // namespace Python } // namespace Python
#include "pythonplugin.moc"

View File

@@ -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 <projectexplorer/buildtargetinfo.h>
#include <projectexplorer/kitmanager.h>
#include <projectexplorer/projectexplorerconstants.h>
#include <projectexplorer/projectnodes.h>
#include <projectexplorer/target.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcessEnvironment>
#include <QRegularExpression>
#include <coreplugin/documentmanager.h>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <utils/fileutils.h>
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<QString> 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<QString> 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<BuildTargetInfo> appTargets;
auto newRoot = std::make_unique<PythonProjectNode>(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<PythonFileNode>(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<QString, QString>::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<QString, QString>::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<QString, QString> *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

View File

@@ -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 <projectexplorer/project.h>
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<QString, QString> *map = nullptr) const;
QStringList m_rawFileList;
QStringList m_files;
QHash<QString, QString> m_rawListEntries;
};
} // namespace Internal
} // namespace Python

View File

@@ -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 <coreplugin/icore.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/localenvironmentaspect.h>
#include <projectexplorer/projectconfigurationaspects.h>
#include <projectexplorer/runconfigurationaspects.h>
#include <projectexplorer/target.h>
#include <projectexplorer/taskhub.h>
#include <utils/fileutils.h>
#include <utils/outputformatter.h>
#include <utils/theme/theme.h>
#include <QBoxLayout>
#include <QComboBox>
#include <QFormLayout>
#include <QPlainTextEdit>
#include <QPushButton>
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<Task> 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<Interpreter> &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<Interpreter> m_interpreters;
QPointer<QComboBox> 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<Interpreter> &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>();
interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter");
connect(PythonSettings::instance(), &PythonSettings::interpretersChanged,
interpreterAspect, &InterpreterAspect::updateInterpreters);
interpreterAspect->updateInterpreters(PythonSettings::interpreters());
interpreterAspect->setDefaultInterpreter(PythonSettings::defaultInterpreter());
auto scriptAspect = addAspect<MainScriptAspect>();
scriptAspect->setSettingsKey("PythonEditor.RunConfiguation.Script");
scriptAspect->setLabelText(tr("Script:"));
scriptAspect->setDisplayStyle(BaseStringAspect::LabelDisplay);
addAspect<LocalEnvironmentAspect>(target);
auto argumentsAspect = addAspect<ArgumentsAspect>();
addAspect<TerminalAspect>();
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<MainScriptAspect>()->value();
}
QString PythonRunConfiguration::arguments() const
{
return aspect<ArgumentsAspect>()->arguments(macroExpander());
}
QString PythonRunConfiguration::interpreter() const
{
return aspect<InterpreterAspect>()->currentInterpreter().command.toString();
}
void PythonRunConfiguration::updateTargetInformation()
{
const BuildTargetInfo bti = buildTargetInfo();
const QString script = bti.targetFilePath.toString();
setDefaultDisplayName(tr("Run %1").arg(script));
aspect<MainScriptAspect>()->setValue(script);
}
PythonRunConfigurationFactory::PythonRunConfigurationFactory()
{
registerRunConfiguration<PythonRunConfiguration>("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"

View File

@@ -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 <projectexplorer/runconfiguration.h>
#include <projectexplorer/runcontrol.h>
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