diff options
| author | David Schulz <david.schulz@qt.io> | 2019-07-15 12:50:29 +0200 |
|---|---|---|
| committer | David Schulz <david.schulz@qt.io> | 2019-07-17 06:21:23 +0000 |
| commit | 3213e12ce7411efd05a544645aaa1ecd74e533a2 (patch) | |
| tree | ae7187904453f530d4660832ff834371905cefaf /src/plugins/python/pythonplugin.cpp | |
| parent | 2778a5adab13a16f4a6b3cda0980fa3b4f2b3a5e (diff) | |
| download | qt-creator-3213e12ce7411efd05a544645aaa1ecd74e533a2.tar.gz | |
rename PythonEditor plugin to Python
The plugin does not only contain a pure editor, but all kind of support
for a programming language like project and run support.
Change-Id: I1251367c8db2e7a54986415ffc5b860cb210de3c
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
Diffstat (limited to 'src/plugins/python/pythonplugin.cpp')
| -rw-r--r-- | src/plugins/python/pythonplugin.cpp | 756 |
1 files changed, 756 insertions, 0 deletions
diff --git a/src/plugins/python/pythonplugin.cpp b/src/plugins/python/pythonplugin.cpp new file mode 100644 index 0000000000..97b0a7442c --- /dev/null +++ b/src/plugins/python/pythonplugin.cpp @@ -0,0 +1,756 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "pythonplugin.h" +#include "pythoneditor.h" +#include "pythonconstants.h" +#include "pythonhighlighter.h" + +#include <coreplugin/icore.h> +#include <coreplugin/coreconstants.h> +#include <coreplugin/documentmanager.h> +#include <coreplugin/fileiconprovider.h> +#include <coreplugin/id.h> +#include <coreplugin/messagemanager.h> +#include <coreplugin/editormanager/editormanager.h> + +#include <projectexplorer/buildtargetinfo.h> +#include <projectexplorer/kitmanager.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/projectnodes.h> +#include <projectexplorer/target.h> +#include <projectexplorer/task.h> +#include <projectexplorer/taskhub.h> + +#include <texteditor/texteditorconstants.h> + +#include <utils/algorithm.h> +#include <utils/outputformatter.h> +#include <utils/qtcprocess.h> +#include <utils/utilsicons.h> + +#include <QDir> +#include <QRegExp> +#include <QRegularExpression> +#include <QRegularExpressionMatch> +#include <QTextCursor> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QJsonValue> +#include <QJsonArray> + +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 needsBuildConfigurations() 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(Project *) + // 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 BaseStringAspect +{ + Q_OBJECT + +public: + InterpreterAspect() = default; +}; + +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>()->value(); } + + void updateTargetInformation(); +}; + +PythonRunConfiguration::PythonRunConfiguration(Target *target, Core::Id id) + : RunConfiguration(target, id) +{ + const Environment sysEnv = Environment::systemEnvironment(); + const QString exec = sysEnv.searchInPath("python").toString(); + + auto interpreterAspect = addAspect<InterpreterAspect>(); + interpreterAspect->setSettingsKey("PythonEditor.RunConfiguation.Interpreter"); + interpreterAspect->setLabelText(tr("Interpreter:")); + interpreterAspect->setDisplayStyle(BaseStringAspect::PathChooserDisplay); + interpreterAspect->setHistoryCompleter("PythonEditor.Interpreter.History"); + interpreterAspect->setValue(exec.isEmpty() ? "python" : exec); + + 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>(); + + setOutputFormatter<PythonOutputFormatter>(); + setCommandLineGetter([this, interpreterAspect, argumentsAspect] { + CommandLine cmd{FilePath::fromString(interpreterAspect->value()), {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, [this]() { refresh(); }) +{ + setId(PythonProjectId); + setProjectLanguages(Context(ProjectExplorer::Constants::CXX_LANGUAGE_ID)); + setDisplayName(fileName.toFileInfo().completeBaseName()); +} + +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) +{ + emitParsingStarted(); + 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); + + emitParsingFinished(true); +} + +/** + * 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(); + + Kit *defaultKit = KitManager::defaultKit(); + if (!activeTarget() && defaultKit) + addTarget(createTarget(defaultKit)); + } + + 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 +// +//////////////////////////////////////////////////////////////////////////////////// + +class PythonPluginPrivate +{ +public: + PythonEditorFactory editorFactory; + PythonRunConfigurationFactory runConfigFactory; + SimpleRunWorkerFactory<SimpleTargetRunner, PythonRunConfiguration> runWorkerFactory; +}; + +PythonPlugin::~PythonPlugin() +{ + delete d; +} + +bool PythonPlugin::initialize(const QStringList &arguments, QString *errorMessage) +{ + Q_UNUSED(arguments) + Q_UNUSED(errorMessage) + + d = new PythonPluginPrivate; + + ProjectManager::registerProjectType<PythonProject>(PythonMimeType); + + return true; +} + +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"); + + TaskHub::addCategory(PythonErrorTaskCategory, "Python", true); +} + +} // namespace Internal +} // namespace Python + +#include "pythonplugin.moc" |
