forked from qt-creator/qt-creator
QmlDesigner: Improve code model update at bundle component import
When the bundle folder is completely missing, we can use importScan to update code model, but if the folder already exists, then full code model reset is required. Since there is no proper notification for reset finishing, we listen to library info updates and after those no longer come for a brief period, we consider reset done. Task-number: QDS-11496 Change-Id: I472f6fdb47ccb7b8131783f86025f3d3538a946d Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io> Reviewed-by: Ali Kianian <ali.kianian@qt.io>
This commit is contained in:
@@ -11,7 +11,9 @@
|
|||||||
#include <qmldesignerplugin.h>
|
#include <qmldesignerplugin.h>
|
||||||
#include <rewritingexception.h>
|
#include <rewritingexception.h>
|
||||||
|
|
||||||
|
#include <model/modelutils.h>
|
||||||
#include <qmljs/qmljsmodelmanagerinterface.h>
|
#include <qmljs/qmljsmodelmanagerinterface.h>
|
||||||
|
#include <utils/async.h>
|
||||||
|
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
@@ -21,10 +23,11 @@ using namespace Utils;
|
|||||||
|
|
||||||
namespace QmlDesigner {
|
namespace QmlDesigner {
|
||||||
|
|
||||||
|
static constexpr int normalImportDelay = 200;
|
||||||
|
|
||||||
ContentLibraryBundleImporter::ContentLibraryBundleImporter(QObject *parent)
|
ContentLibraryBundleImporter::ContentLibraryBundleImporter(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
m_importTimer.setInterval(200);
|
|
||||||
connect(&m_importTimer, &QTimer::timeout, this, &ContentLibraryBundleImporter::handleImportTimer);
|
connect(&m_importTimer, &QTimer::timeout, this, &ContentLibraryBundleImporter::handleImportTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,19 +52,21 @@ QString ContentLibraryBundleImporter::importComponent(const QString &bundleDir,
|
|||||||
if (!bundleImportPath.exists() && !bundleImportPath.createDir())
|
if (!bundleImportPath.exists() && !bundleImportPath.createDir())
|
||||||
return QStringLiteral("Failed to create bundle import folder: '%1'").arg(bundleImportPath.toString());
|
return QStringLiteral("Failed to create bundle import folder: '%1'").arg(bundleImportPath.toString());
|
||||||
|
|
||||||
|
bool doScan = false;
|
||||||
|
bool doReset = false;
|
||||||
FilePath qmldirPath = bundleImportPath.pathAppended("qmldir");
|
FilePath qmldirPath = bundleImportPath.pathAppended("qmldir");
|
||||||
QString qmldirContent = QString::fromUtf8(qmldirPath.fileContents().value_or(QByteArray()));
|
QString qmldirContent = QString::fromUtf8(qmldirPath.fileContents().value_or(QByteArray()));
|
||||||
if (qmldirContent.isEmpty()) {
|
if (qmldirContent.isEmpty()) {
|
||||||
qmldirContent.append("module ");
|
qmldirContent.append("module ");
|
||||||
qmldirContent.append(module);
|
qmldirContent.append(module);
|
||||||
qmldirContent.append('\n');
|
qmldirContent.append('\n');
|
||||||
|
doScan = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
FilePath qmlSourceFile = bundleImportPath.pathAppended(qmlFile);
|
FilePath qmlSourceFile = bundleImportPath.pathAppended(qmlFile);
|
||||||
const bool qmlFileExists = qmlSourceFile.exists();
|
|
||||||
const QString qmlType = qmlSourceFile.baseName();
|
const QString qmlType = qmlSourceFile.baseName();
|
||||||
|
|
||||||
if (m_pendingTypes.contains(type) && !m_pendingTypes.value(type))
|
if (m_pendingImports.contains(type) && !m_pendingImports[type].isImport)
|
||||||
return QStringLiteral("Unable to import while unimporting the same type: '%1'").arg(QLatin1String(type));
|
return QStringLiteral("Unable to import while unimporting the same type: '%1'").arg(QLatin1String(type));
|
||||||
|
|
||||||
if (!qmldirContent.contains(qmlFile)) {
|
if (!qmldirContent.contains(qmlFile)) {
|
||||||
@@ -70,6 +75,7 @@ QString ContentLibraryBundleImporter::importComponent(const QString &bundleDir,
|
|||||||
qmldirContent.append(qmlFile);
|
qmldirContent.append(qmlFile);
|
||||||
qmldirContent.append('\n');
|
qmldirContent.append('\n');
|
||||||
qmldirPath.writeFileContents(qmldirContent.toUtf8());
|
qmldirPath.writeFileContents(qmldirContent.toUtf8());
|
||||||
|
doReset = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList allFiles;
|
QStringList allFiles;
|
||||||
@@ -104,16 +110,22 @@ QString ContentLibraryBundleImporter::importComponent(const QString &bundleDir,
|
|||||||
if (writeAssetRefs)
|
if (writeAssetRefs)
|
||||||
writeAssetRefMap(bundleImportPath, assetRefMap);
|
writeAssetRefMap(bundleImportPath, assetRefMap);
|
||||||
|
|
||||||
m_fullReset = !qmlFileExists;
|
|
||||||
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
||||||
Model *model = doc ? doc->currentModel() : nullptr;
|
Model *model = doc ? doc->currentModel() : nullptr;
|
||||||
if (!model)
|
if (!model)
|
||||||
return "Model not available, cannot add import statement or update code model";
|
return "Model not available, cannot add import statement or update code model";
|
||||||
|
|
||||||
|
ImportData data;
|
||||||
|
data.isImport = true;
|
||||||
|
data.type = type;
|
||||||
|
if (doScan)
|
||||||
|
data.pathToScan = bundleImportPath;
|
||||||
|
else
|
||||||
|
data.fullReset = doReset;
|
||||||
|
|
||||||
Import import = Import::createLibraryImport(module, "1.0");
|
Import import = Import::createLibraryImport(module, "1.0");
|
||||||
if (!model->hasImport(import)) {
|
if (!model->hasImport(import)) {
|
||||||
if (model->possibleImports().contains(import)) {
|
if (model->possibleImports().contains(import)) {
|
||||||
m_pendingImport.clear();
|
|
||||||
try {
|
try {
|
||||||
model->changeImports({import}, {});
|
model->changeImports({import}, {});
|
||||||
} catch (const RewritingException &) {
|
} catch (const RewritingException &) {
|
||||||
@@ -123,12 +135,12 @@ QString ContentLibraryBundleImporter::importComponent(const QString &bundleDir,
|
|||||||
} else {
|
} else {
|
||||||
// If import is not yet possible, import statement needs to be added asynchronously to
|
// If import is not yet possible, import statement needs to be added asynchronously to
|
||||||
// avoid errors, as code model update takes a while.
|
// avoid errors, as code model update takes a while.
|
||||||
m_pendingImport = module;
|
data.importToAdd = module;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m_pendingTypes.insert(type, true);
|
m_pendingImports.insert(type, data);
|
||||||
m_importTimerCount = 0;
|
m_importTimerCount = 0;
|
||||||
m_importTimer.start();
|
m_importTimer.start(normalImportDelay);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -137,21 +149,20 @@ void ContentLibraryBundleImporter::handleImportTimer()
|
|||||||
{
|
{
|
||||||
auto handleFailure = [this] {
|
auto handleFailure = [this] {
|
||||||
m_importTimer.stop();
|
m_importTimer.stop();
|
||||||
m_fullReset = false;
|
|
||||||
m_pendingImport.clear();
|
|
||||||
m_importTimerCount = 0;
|
m_importTimerCount = 0;
|
||||||
|
|
||||||
|
disconnect(m_libInfoConnection);
|
||||||
|
|
||||||
// Emit dummy finished signals for all pending types
|
// Emit dummy finished signals for all pending types
|
||||||
const QList<TypeName> pendingTypes = m_pendingTypes.keys();
|
const QList<TypeName> pendingTypes = m_pendingImports.keys();
|
||||||
for (const TypeName &pendingType : pendingTypes) {
|
for (const TypeName &pendingType : pendingTypes) {
|
||||||
m_pendingTypes.remove(pendingType);
|
ImportData data = m_pendingImports.take(pendingType);
|
||||||
if (m_pendingTypes.value(pendingType))
|
if (data.isImport)
|
||||||
emit importFinished({}, m_bundleId);
|
emit importFinished({}, m_bundleId);
|
||||||
else
|
else
|
||||||
emit unimportFinished({}, m_bundleId);
|
emit unimportFinished({}, m_bundleId);
|
||||||
|
|
||||||
m_bundleId.clear();
|
|
||||||
}
|
}
|
||||||
|
m_bundleId.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
|
||||||
@@ -161,55 +172,125 @@ void ContentLibraryBundleImporter::handleImportTimer()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_fullReset) {
|
auto modelManager = QmlJS::ModelManagerInterface::instance();
|
||||||
// Force code model reset to notice changes to existing module
|
if (modelManager) {
|
||||||
auto modelManager = QmlJS::ModelManagerInterface::instance();
|
const QList<TypeName> keys = m_pendingImports.keys();
|
||||||
if (modelManager)
|
int scanDone = 0;
|
||||||
modelManager->resetCodeModel();
|
bool refreshImports = false;
|
||||||
m_fullReset = false;
|
for (const TypeName &type : keys) {
|
||||||
return;
|
ImportData &data = m_pendingImports[type];
|
||||||
}
|
if (data.state == ImportData::Starting) {
|
||||||
|
if (data.pathToScan.isEmpty()) {
|
||||||
QmlDesignerPlugin::instance()->documentManager().resetPossibleImports();
|
data.state = ImportData::FullReset;
|
||||||
|
} else {
|
||||||
if (!m_pendingImport.isEmpty()) {
|
// A new bundle module was added, so we can scan it without doing full code
|
||||||
try {
|
// model reset, which is faster
|
||||||
Import import = Import::createLibraryImport(m_pendingImport, "1.0");
|
QmlJS::PathsAndLanguages pathToScan;
|
||||||
if (model->possibleImports().contains(import)) {
|
pathToScan.maybeInsert(data.pathToScan);
|
||||||
model->changeImports({import}, {});
|
data.future = Utils::asyncRun(&QmlJS::ModelManagerInterface::importScan,
|
||||||
m_pendingImport.clear();
|
modelManager->workingCopy(),
|
||||||
|
pathToScan, modelManager,
|
||||||
|
true, true, true);
|
||||||
|
data.state = ImportData::WaitingForImportScan;
|
||||||
|
}
|
||||||
|
} else if (data.state == ImportData::WaitingForImportScan) {
|
||||||
|
// Import scan is asynchronous, so we need to wait for it to be finished
|
||||||
|
if ((data.future.isCanceled() || data.future.isFinished()))
|
||||||
|
data.state = ImportData::RefreshImports;
|
||||||
|
} else if (data.state >= ImportData::RefreshImports) {
|
||||||
|
// Do not mode to next stage until all pending scans are done
|
||||||
|
++scanDone;
|
||||||
|
if (data.state == ImportData::RefreshImports)
|
||||||
|
refreshImports = true;
|
||||||
}
|
}
|
||||||
} catch (const RewritingException &) {
|
|
||||||
// Import adding is unlikely to succeed later, either, so just bail out
|
|
||||||
handleFailure();
|
|
||||||
}
|
}
|
||||||
return;
|
if (scanDone != m_pendingImports.size()) {
|
||||||
}
|
return;
|
||||||
|
} else {
|
||||||
|
if (refreshImports) {
|
||||||
|
// The new import is now available in code model, so reset the possible imports
|
||||||
|
QmlDesignerPlugin::instance()->documentManager().resetPossibleImports();
|
||||||
|
model->rewriterView()->forceAmend();
|
||||||
|
|
||||||
// Detect when the code model has the new material(s) fully available
|
for (const TypeName &type : keys) {
|
||||||
const QList<TypeName> pendingTypes = m_pendingTypes.keys();
|
ImportData &data = m_pendingImports[type];
|
||||||
for (const TypeName &pendingType : pendingTypes) {
|
if (data.state == ImportData::RefreshImports) {
|
||||||
NodeMetaInfo metaInfo = model->metaInfo(pendingType);
|
if (!data.importToAdd.isEmpty()) {
|
||||||
const bool isImport = m_pendingTypes.value(pendingType);
|
try {
|
||||||
const bool typeComplete = metaInfo.isValid() && !metaInfo.prototypes().empty();
|
RewriterTransaction transaction = model->rewriterView()->beginRewriterTransaction(
|
||||||
if (isImport == typeComplete) {
|
QByteArrayLiteral(__FUNCTION__));
|
||||||
m_pendingTypes.remove(pendingType);
|
bool success = ModelUtils::addImportWithCheck(data.importToAdd, model);
|
||||||
if (isImport)
|
if (!success)
|
||||||
|
handleFailure();
|
||||||
|
transaction.commit();
|
||||||
|
} catch (const RewritingException &) {
|
||||||
|
handleFailure();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.state = ImportData::FullReset;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fullReset = false;
|
||||||
|
for (const TypeName &type : keys) {
|
||||||
|
ImportData &data = m_pendingImports[type];
|
||||||
|
if (data.state == ImportData::FullReset) {
|
||||||
|
if (data.fullReset)
|
||||||
|
fullReset = true;
|
||||||
|
data.state = ImportData::Finalize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fullReset) {
|
||||||
|
// Force code model reset to notice changes to existing module
|
||||||
|
auto modelManager = QmlJS::ModelManagerInterface::instance();
|
||||||
|
if (modelManager) {
|
||||||
|
modelManager->resetCodeModel();
|
||||||
|
disconnect(m_libInfoConnection);
|
||||||
|
m_libInfoConnection = connect(modelManager, &QmlJS::ModelManagerInterface::libraryInfoUpdated,
|
||||||
|
this, [this]() {
|
||||||
|
// This signal comes for each library in code model, so we need to compress
|
||||||
|
// it until no more notifications come
|
||||||
|
m_importTimer.start(1000);
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
// Stop the import timer for a bit to allow full reset to complete
|
||||||
|
m_importTimer.stop();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const TypeName &type : keys) {
|
||||||
|
ImportData &data = m_pendingImports[type];
|
||||||
|
if (data.state == ImportData::Finalize) {
|
||||||
|
// Reset delay just in case full reset was done earlier
|
||||||
|
m_importTimer.start(normalImportDelay);
|
||||||
|
disconnect(m_libInfoConnection);
|
||||||
|
model->rewriterView()->forceAmend();
|
||||||
|
// Verify that code model has the new material fully available (or removed for unimport)
|
||||||
|
NodeMetaInfo metaInfo = model->metaInfo(type);
|
||||||
|
const bool typeComplete = metaInfo.isValid() && !metaInfo.prototypes().empty();
|
||||||
|
if (data.isImport == typeComplete) {
|
||||||
|
m_pendingImports.remove(type);
|
||||||
|
if (data.isImport)
|
||||||
#ifdef QDS_USE_PROJECTSTORAGE
|
#ifdef QDS_USE_PROJECTSTORAGE
|
||||||
emit importFinished(pendingType, m_bundleId);
|
emit importFinished(type, m_bundleId);
|
||||||
#else
|
#else
|
||||||
emit importFinished(metaInfo, m_bundleId);
|
emit importFinished(metaInfo, m_bundleId);
|
||||||
#endif
|
#endif
|
||||||
else
|
else
|
||||||
emit unimportFinished(metaInfo, m_bundleId);
|
emit unimportFinished(metaInfo, m_bundleId);
|
||||||
|
}
|
||||||
m_bundleId.clear();
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_pendingTypes.isEmpty()) {
|
if (m_pendingImports.isEmpty()) {
|
||||||
|
m_bundleId.clear();
|
||||||
m_importTimer.stop();
|
m_importTimer.stop();
|
||||||
m_importTimerCount = 0;
|
m_importTimerCount = 0;
|
||||||
|
disconnect(m_libInfoConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +348,7 @@ QString ContentLibraryBundleImporter::unimportComponent(const TypeName &type, co
|
|||||||
QByteArray newContent;
|
QByteArray newContent;
|
||||||
|
|
||||||
QString qmlType = qmlFilePath.baseName();
|
QString qmlType = qmlFilePath.baseName();
|
||||||
if (m_pendingTypes.contains(type) && m_pendingTypes.value(type)) {
|
if (m_pendingImports.contains(type) && m_pendingImports[type].isImport) {
|
||||||
return QStringLiteral("Unable to unimport while importing the same type: '%1'")
|
return QStringLiteral("Unable to unimport while importing the same type: '%1'")
|
||||||
.arg(QString::fromLatin1(type));
|
.arg(QString::fromLatin1(type));
|
||||||
}
|
}
|
||||||
@@ -286,8 +367,6 @@ QString ContentLibraryBundleImporter::unimportComponent(const TypeName &type, co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_pendingTypes.insert(type, false);
|
|
||||||
|
|
||||||
QVariantHash assetRefMap = loadAssetRefMap(bundleImportPath);
|
QVariantHash assetRefMap = loadAssetRefMap(bundleImportPath);
|
||||||
bool writeAssetRefs = false;
|
bool writeAssetRefs = false;
|
||||||
const auto keys = assetRefMap.keys();
|
const auto keys = assetRefMap.keys();
|
||||||
@@ -326,9 +405,14 @@ QString ContentLibraryBundleImporter::unimportComponent(const TypeName &type, co
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m_fullReset = true;
|
ImportData data;
|
||||||
|
data.isImport = false;
|
||||||
|
data.type = type;
|
||||||
|
data.fullReset = true;
|
||||||
|
m_pendingImports.insert(type, data);
|
||||||
|
|
||||||
m_importTimerCount = 0;
|
m_importTimerCount = 0;
|
||||||
m_importTimer.start();
|
m_importTimer.start(normalImportDelay);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <utils/filepath.h>
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
#include <QFuture>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QVariantHash>
|
#include <QVariantHash>
|
||||||
|
|
||||||
@@ -46,10 +47,27 @@ private:
|
|||||||
|
|
||||||
QTimer m_importTimer;
|
QTimer m_importTimer;
|
||||||
int m_importTimerCount = 0;
|
int m_importTimerCount = 0;
|
||||||
QString m_pendingImport;
|
|
||||||
QString m_bundleId;
|
QString m_bundleId;
|
||||||
bool m_fullReset = false;
|
struct ImportData
|
||||||
QHash<TypeName, bool> m_pendingTypes; // <type, isImport>
|
{
|
||||||
|
enum State {
|
||||||
|
Starting,
|
||||||
|
WaitingForImportScan,
|
||||||
|
RefreshImports,
|
||||||
|
FullReset,
|
||||||
|
Finalize
|
||||||
|
};
|
||||||
|
bool isImport = true; // false = unimport
|
||||||
|
TypeName type;
|
||||||
|
Utils::FilePath pathToScan; // If set, do importScan
|
||||||
|
QFuture<void> future;
|
||||||
|
QString importToAdd; // If set, add import to model
|
||||||
|
bool fullReset = false; // If true, reset the entire code model.
|
||||||
|
State state = Starting;
|
||||||
|
};
|
||||||
|
|
||||||
|
QHash<TypeName, ImportData> m_pendingImports;
|
||||||
|
QMetaObject::Connection m_libInfoConnection;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QmlDesigner
|
} // namespace QmlDesigner
|
||||||
|
Reference in New Issue
Block a user