Add qmlImportPaths property to .pyproject file

Users should be able to add custom QML import paths for
Python/PySide2/PyQt5 projects in Qt Creator in order to
get syntax highlighting and code completion for custom
QML modules.

Fixes: QTCREATORBUG-23679
Change-Id: Iec7c691c4b8709c48a790cd27ac7c6e755967796
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Alexander Mishin
2020-09-21 19:39:24 +03:00
parent 31ec38dba5
commit 229dfaab95
9 changed files with 234 additions and 35 deletions

View File

@@ -1,4 +1,5 @@
add_qtc_plugin(Python add_qtc_plugin(Python
DEPENDS QmlJS
PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor PLUGIN_DEPENDS Core LanguageClient ProjectExplorer TextEditor
SOURCES SOURCES
python.qrc python.qrc

View File

@@ -4,6 +4,8 @@ QtcPlugin {
name: "Python" name: "Python"
Depends { name: "Qt.widgets" } Depends { name: "Qt.widgets" }
Depends { name: "QmlJS" }
Depends { name: "Utils" } Depends { name: "Utils" }
Depends { name: "Core" } Depends { name: "Core" }

View File

@@ -1,6 +1,7 @@
QTC_PLUGIN_NAME = Python QTC_PLUGIN_NAME = Python
QTC_LIB_DEPENDS += \ QTC_LIB_DEPENDS += \
extensionsystem \ extensionsystem \
qmljs \
utils utils
QTC_PLUGIN_DEPENDS += \ QTC_PLUGIN_DEPENDS += \
coreplugin \ coreplugin \

View File

@@ -46,6 +46,8 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h> #include <coreplugin/messagemanager.h>
#include <qmljs/qmljsmodelmanagerinterface.h>
#include <utils/fileutils.h> #include <utils/fileutils.h>
using namespace Core; using namespace Core;
@@ -80,10 +82,12 @@ public:
private: private:
QStringList m_rawFileList; QStringList m_rawFileList;
QStringList m_files; QStringList m_files;
QStringList m_rawQmlImportPathList;
QStringList m_qmlImportPaths;
QHash<QString, QString> m_rawListEntries; QHash<QString, QString> m_rawListEntries;
QHash<QString, QString> m_rawQmlImportPathEntries;
}; };
/** /**
* @brief Provides displayName relative to project node * @brief Provides displayName relative to project node
*/ */
@@ -101,6 +105,38 @@ private:
QString m_displayName; QString m_displayName;
}; };
static QJsonObject readObjJson(const FilePath &projectFile, QString *errorMessage)
{
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 QJsonObject();
}
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 QJsonObject();
}
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 QJsonObject();
}
return doc.object();
}
static QStringList readLines(const FilePath &projectFile) static QStringList readLines(const FilePath &projectFile)
{ {
const QString projectFileName = projectFile.fileName(); const QString projectFileName = projectFile.fileName();
@@ -127,37 +163,9 @@ static QStringList readLines(const FilePath &projectFile)
static QStringList readLinesJson(const FilePath &projectFile, QString *errorMessage) static QStringList readLinesJson(const FilePath &projectFile, QString *errorMessage)
{ {
const QString projectFileName = projectFile.fileName(); QStringList lines = { projectFile.fileName() };
QStringList lines = { projectFileName };
QFile file(projectFile.toString()); const QJsonObject obj = readObjJson(projectFile, errorMessage);
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")) { if (obj.contains("files")) {
const QJsonValue files = obj.value("files"); const QJsonValue files = obj.value("files");
const QJsonArray files_array = files.toArray(); const QJsonArray files_array = files.toArray();
@@ -171,6 +179,26 @@ static QStringList readLinesJson(const FilePath &projectFile, QString *errorMess
return lines; return lines;
} }
static QStringList readImportPathsJson(const FilePath &projectFile, QString *errorMessage)
{
QStringList importPaths;
const QJsonObject obj = readObjJson(projectFile, errorMessage);
if (obj.contains("qmlImportPaths")) {
const QJsonValue dirs = obj.value("qmlImportPaths");
const QJsonArray dirs_array = dirs.toArray();
QSet<QString> visited;
for (const auto &dir : dirs_array)
visited.insert(dir.toString());
importPaths.append(Utils::toList(visited));
}
return importPaths;
}
class PythonProjectNode : public ProjectNode class PythonProjectNode : public ProjectNode
{ {
public: public:
@@ -211,6 +239,7 @@ static FileType getFileType(const FilePath &f)
void PythonBuildSystem::triggerParsing() void PythonBuildSystem::triggerParsing()
{ {
ParseGuard guard = guardParsingRun(); ParseGuard guard = guardParsingRun();
parse(); parse();
const QDir baseDir(projectDirectory().toString()); const QDir baseDir(projectDirectory().toString());
@@ -235,6 +264,18 @@ void PythonBuildSystem::triggerParsing()
setApplicationTargets(appTargets); setApplicationTargets(appTargets);
auto modelManager = QmlJS::ModelManagerInterface::instance();
if (modelManager) {
auto projectInfo = modelManager->defaultProjectInfoForProject(project());
for (const QString &importPath : m_qmlImportPaths) {
const Utils::FilePath filePath = Utils::FilePath::fromString(importPath);
projectInfo.importPaths.maybeInsert(filePath, QmlJS::Dialect::Qml);
}
modelManager->updateProjectInfo(projectInfo, project());
}
guard.markAsSuccess(); guard.markAsSuccess();
emitBuildSystemUpdated(); emitBuildSystemUpdated();
@@ -355,6 +396,8 @@ bool PythonBuildSystem::renameFile(Node *, const QString &filePath, const QStrin
void PythonBuildSystem::parse() void PythonBuildSystem::parse()
{ {
m_rawListEntries.clear(); m_rawListEntries.clear();
m_rawQmlImportPathEntries.clear();
const FilePath filePath = projectFilePath(); const FilePath filePath = projectFilePath();
// The PySide project file is JSON based // The PySide project file is JSON based
if (filePath.endsWith(".pyproject")) { if (filePath.endsWith(".pyproject")) {
@@ -362,13 +405,19 @@ void PythonBuildSystem::parse()
m_rawFileList = readLinesJson(filePath, &errorMessage); m_rawFileList = readLinesJson(filePath, &errorMessage);
if (!errorMessage.isEmpty()) if (!errorMessage.isEmpty())
MessageManager::write(errorMessage); MessageManager::write(errorMessage);
}
// To keep compatibility with PyQt we keep the compatibility with plain errorMessage.clear();
// text files as project files. m_rawQmlImportPathList = readImportPathsJson(filePath, &errorMessage);
else if (filePath.endsWith(".pyqtc")) if (!errorMessage.isEmpty())
MessageManager::write(errorMessage);
} else if (filePath.endsWith(".pyqtc")) {
// To keep compatibility with PyQt we keep the compatibility with plain
// text files as project files.
m_rawFileList = readLines(filePath); m_rawFileList = readLines(filePath);
}
m_files = processEntries(m_rawFileList, &m_rawListEntries); m_files = processEntries(m_rawFileList, &m_rawListEntries);
m_qmlImportPaths = processEntries(m_rawQmlImportPathList, &m_rawQmlImportPathEntries);
} }
/** /**

View File

@@ -0,0 +1,36 @@
/****************************************************************************
**
** Copyright (C) 2020 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.
**
****************************************************************************/
import QtQuick 2.12
Rectangle {
width: 48
height: 48
border {
width: 1
color: "black"
}
color: "lightgrey"
}

View File

@@ -0,0 +1,2 @@
module Charts
ChartBackground 1.0 ./chartbackground.qml

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#############################################################################
##
## Copyright (C) 2020 The Qt Company Ltd.
## Contact: https://www.qt.io/licensing/
##
## This file is part of Qt for Python.
##
## $QT_BEGIN_LICENSE:LGPL$
## 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 Lesser General Public License Usage
## Alternatively, this file may be used under the terms of the GNU Lesser
## General Public License version 3 as published by the Free Software
## Foundation and appearing in the file LICENSE.LGPL3 included in the
## packaging of this file. Please review the following information to
## ensure the GNU Lesser General Public License version 3 requirements
## will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
##
## GNU General Public License Usage
## Alternatively, this file may be used under the terms of the GNU
## General Public License version 2.0 or (at your option) the GNU General
## Public license version 3 or any later version approved by the KDE Free
## Qt Foundation. The licenses are as published by the Free Software
## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
## 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-2.0.html and
## https://www.gnu.org/licenses/gpl-3.0.html.
##
## $QT_END_LICENSE$
##
#############################################################################
import os
import sys
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine
if __name__ == "__main__":
app = QGuiApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load(os.path.join(os.path.dirname(__file__), "main.qml"))
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec_())

View File

@@ -0,0 +1,41 @@
/****************************************************************************
**
** Copyright (C) 2020 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.
**
****************************************************************************/
import QtQuick 2.12
import QtQuick.Window 2.12
import Charts 1.0 as Charts // Qt Creator displays "QML module not found (Charts)."
// if qmlImportPaths value is missing from pyproject.pyproject file.
Window {
width: 640
height: 480
visible: true
title: qsTr("pyproject")
Charts.ChartBackground { // Syntax highlight and code completion doesn't work
// if qmlImportPaths value is missing from pyproject.pyproject file.
anchors.centerIn: parent
}
}

View File

@@ -0,0 +1,9 @@
{
"files": [
"main.py",
"main.qml"
],
"qmlImportPaths": [
"./imports"
]
}