aboutsummaryrefslogtreecommitdiffstats
path: root/src/qmlls/qqmlcodemodel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/qmlls/qqmlcodemodel.cpp')
-rw-r--r--src/qmlls/qqmlcodemodel.cpp927
1 files changed, 927 insertions, 0 deletions
diff --git a/src/qmlls/qqmlcodemodel.cpp b/src/qmlls/qqmlcodemodel.cpp
new file mode 100644
index 0000000000..cf58fba760
--- /dev/null
+++ b/src/qmlls/qqmlcodemodel.cpp
@@ -0,0 +1,927 @@
+// Copyright (C) 2021 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qqmlcodemodel_p.h"
+#include "qqmllsplugin_p.h"
+#include "qtextdocument_p.h"
+#include "qqmllsutils_p.h"
+
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qthreadpool.h>
+#include <QtCore/qlibraryinfo.h>
+#include <QtCore/qprocess.h>
+#include <QtCore/qdiriterator.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+
+#include <memory>
+#include <algorithm>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+Q_STATIC_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
+
+using namespace QQmlJS::Dom;
+using namespace Qt::StringLiterals;
+
+/*!
+\internal
+\class QQmlCodeModel
+
+The code model offers a view of the current state of the current files, and traks open files.
+All methods are threadsafe, and generally return immutable or threadsafe objects that can be
+worked on from any thread (unless otherwise noted).
+The idea is the let all other operations be as lock free as possible, concentrating all tricky
+synchronization here.
+
+\section2 Global views
+\list
+\li currentEnv() offers a view that contains the latest version of all the loaded files
+\li validEnv() is just like current env but stores only the valid (meaning correctly parsed,
+ not necessarily without errors) version of a file, it is normally a better choice to load the
+ dependencies/symbol information from
+\endlist
+
+\section2 OpenFiles
+\list
+\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the
+ document, its latest valid version, scope, all connected to a specific version of the document
+ and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for
+ every partial change: document change, validDocument change, scope change).
+\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These
+ contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the
+ current version of the document, and has line by line support.
+ Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex()
+ before *any* read or write/modification to it.
+ It has a version nuber which is supposed to always change and increase.
+ It is mainly used for highlighting/indenting, and is immediately updated when the user edits a
+ document. Its use should be avoided if possible, preferring the snapshots.
+\endlist
+
+\section2 Parallelism/Theading
+Most operations are not parallel and usually take place in the main thread (but are still thread
+safe).
+There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate.
+Indexing is meant to keep the global view up to date.
+OpenDocumentUpdate keeps the snapshots of the open documents up to date.
+
+There is always a tension between being responsive, using all threads available, and avoid to hog
+too many resources. One can choose different parallelization strategies, we went with a flexiable
+approach.
+We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These
+do all locking needed, get some work, do it without locks, and at the end update the state of the
+code model. If there is more work, then they return true. Thus while (xxxSome()); works until there
+is no work left.
+
+addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do.
+
+indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a
+worker thread (or more) that work on it exist.
+*/
+
+QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings)
+ : QObject { parent },
+ m_importPaths(QLibraryInfo::path(QLibraryInfo::QmlImportsPath)),
+ m_currentEnv(std::make_shared<DomEnvironment>(
+ m_importPaths, DomEnvironment::Option::SingleThreaded,
+ DomCreationOptions{} | DomCreationOption::WithRecovery
+ | DomCreationOption::WithScriptExpressions
+ | DomCreationOption::WithSemanticAnalysis)),
+ m_validEnv(std::make_shared<DomEnvironment>(
+ m_importPaths, DomEnvironment::Option::SingleThreaded,
+ DomCreationOptions{} | DomCreationOption::WithRecovery
+ | DomCreationOption::WithScriptExpressions
+ | DomCreationOption::WithSemanticAnalysis)),
+ m_settings(settings),
+ m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s)
+{
+}
+
+/*!
+\internal
+Disable the functionality that uses CMake, and remove the already watched paths if there are some.
+*/
+void QQmlCodeModel::disableCMakeCalls()
+{
+ m_cmakeStatus = DoesNotHaveCMake;
+ m_cppFileWatcher.removePaths(m_cppFileWatcher.files());
+ QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
+}
+
+QQmlCodeModel::~QQmlCodeModel()
+{
+ QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
+ while (true) {
+ bool shouldWait;
+ {
+ QMutexLocker l(&m_mutex);
+ m_state = State::Stopping;
+ m_openDocumentsToUpdate.clear();
+ shouldWait = m_nIndexInProgress != 0 || m_nUpdateInProgress != 0;
+ }
+ if (!shouldWait)
+ break;
+ QThread::yieldCurrentThread();
+ }
+}
+
+OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url)
+{
+ return openDocumentByUrl(url).snapshot;
+}
+
+int QQmlCodeModel::indexEvalProgress() const
+{
+ Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
+ const int dirCost = 10;
+ int costToDo = 1;
+ for (const ToIndex &el : std::as_const(m_toIndex))
+ costToDo += dirCost * el.leftDepth;
+ costToDo += m_indexInProgressCost;
+ return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost);
+}
+
+void QQmlCodeModel::indexStart()
+{
+ Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
+ qCDebug(codeModelLog) << "indexStart";
+}
+
+void QQmlCodeModel::indexEnd()
+{
+ Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
+ qCDebug(codeModelLog) << "indexEnd";
+ m_lastIndexProgress = 0;
+ m_nIndexInProgress = 0;
+ m_toIndex.clear();
+ m_indexInProgressCost = 0;
+ m_indexDoneCost = 0;
+}
+
+void QQmlCodeModel::indexSendProgress(int progress)
+{
+ if (progress <= m_lastIndexProgress)
+ return;
+ m_lastIndexProgress = progress;
+ // ### actually send progress
+}
+
+bool QQmlCodeModel::indexCancelled()
+{
+ QMutexLocker l(&m_mutex);
+ if (m_state == State::Stopping)
+ return true;
+ return false;
+}
+
+void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft)
+{
+ if (indexCancelled())
+ return;
+ QDir dir(path);
+ if (depthLeft > 1) {
+ const QStringList dirs =
+ dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
+ for (const QString &child : dirs)
+ addDirectory(dir.filePath(child), --depthLeft);
+ }
+ const QStringList qmljs =
+ dir.entryList(QStringList({ u"*.qml"_s, u"*.js"_s, u"*.mjs"_s }), QDir::Files);
+ int progress = 0;
+ {
+ QMutexLocker l(&m_mutex);
+ m_indexInProgressCost += qmljs.size();
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ if (qmljs.isEmpty())
+ return;
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ for (const QString &file : qmljs) {
+ if (indexCancelled())
+ return;
+ QString fPath = dir.filePath(file);
+ auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
+ FileToLoad fileToLoad = FileToLoad::fromFileSystem(newCurrentPtr, fPath);
+ if (!fileToLoad.canonicalPath().isEmpty()) {
+ newCurrentPtr->loadBuiltins();
+ newCurrentPtr->loadFile(fileToLoad, [](Path, const DomItem &, const DomItem &) {});
+ newCurrentPtr->loadPendingDependencies();
+ newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
+ }
+ {
+ QMutexLocker l(&m_mutex);
+ ++m_indexDoneCost;
+ --m_indexInProgressCost;
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ }
+}
+
+void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server)
+{
+ Q_UNUSED(server);
+ // ### create progress, &scan in a separate instance
+ const int maxDepth = 5;
+ for (const auto &path : paths)
+ addDirectory(path, maxDepth);
+ indexNeedsUpdate();
+}
+
+void QQmlCodeModel::addDirectory(const QString &path, int depthLeft)
+{
+ if (depthLeft < 1)
+ return;
+ {
+ QMutexLocker l(&m_mutex);
+ for (auto it = m_toIndex.begin(); it != m_toIndex.end();) {
+ if (it->path.startsWith(path)) {
+ if (it->path.size() == path.size())
+ return;
+ if (it->path.at(path.size()) == u'/') {
+ it = m_toIndex.erase(it);
+ continue;
+ }
+ } else if (path.startsWith(it->path) && path.at(it->path.size()) == u'/')
+ return;
+ ++it;
+ }
+ m_toIndex.append({ path, depthLeft });
+ }
+}
+
+void QQmlCodeModel::removeDirectory(const QString &path)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto toRemove = [path](const QString &p) {
+ return p.startsWith(path) && (p.size() == path.size() || p.at(path.size()) == u'/');
+ };
+ auto it = m_toIndex.begin();
+ auto end = m_toIndex.end();
+ while (it != end) {
+ if (toRemove(it->path))
+ it = m_toIndex.erase(it);
+ else
+ ++it;
+ }
+ }
+ if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
+ validEnvPtr->removePath(path);
+ if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
+ currentEnvPtr->removePath(path);
+}
+
+QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options)
+{
+ QString res;
+ {
+ QMutexLocker l(&m_mutex);
+ res = m_url2path.value(url);
+ }
+ if (!res.isEmpty() && options == UrlLookup::Caching)
+ return res;
+ QUrl qurl(QString::fromUtf8(url));
+ QFileInfo f(qurl.toLocalFile());
+ QString cPath = f.canonicalFilePath();
+ if (cPath.isEmpty())
+ cPath = f.filePath();
+ {
+ QMutexLocker l(&m_mutex);
+ if (!res.isEmpty() && res != cPath)
+ m_path2url.remove(res);
+ m_url2path.insert(url, cPath);
+ m_path2url.insert(cPath, url);
+ }
+ return cPath;
+}
+
+void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto &openDoc = m_openDocuments[url];
+ if (!openDoc.textDocument)
+ openDoc.textDocument = std::make_shared<Utils::TextDocument>();
+ QMutexLocker l2(openDoc.textDocument->mutex());
+ openDoc.textDocument->setVersion(version);
+ openDoc.textDocument->setPlainText(docText);
+ }
+ addOpenToUpdate(url);
+ openNeedUpdate();
+}
+
+OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url)
+{
+ QMutexLocker l(&m_mutex);
+ return m_openDocuments.value(url);
+}
+
+RegisteredSemanticTokens &QQmlCodeModel::registeredTokens()
+{
+ QMutexLocker l(&m_mutex);
+ return m_tokens;
+}
+
+const RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() const
+{
+ QMutexLocker l(&m_mutex);
+ return m_tokens;
+}
+
+void QQmlCodeModel::indexNeedsUpdate()
+{
+ const int maxIndexThreads = 1;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads)
+ return;
+ if (++m_nIndexInProgress == 1)
+ indexStart();
+ }
+ QThreadPool::globalInstance()->start([this]() {
+ while (indexSome()) { }
+ });
+}
+
+bool QQmlCodeModel::indexSome()
+{
+ qCDebug(codeModelLog) << "indexSome";
+ ToIndex toIndex;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty()) {
+ if (--m_nIndexInProgress == 0)
+ indexEnd();
+ return false;
+ }
+ toIndex = m_toIndex.last();
+ m_toIndex.removeLast();
+ }
+ bool hasMore = false;
+ {
+ auto guard = qScopeGuard([this, &hasMore]() {
+ QMutexLocker l(&m_mutex);
+ if (m_toIndex.isEmpty()) {
+ if (--m_nIndexInProgress == 0)
+ indexEnd();
+ hasMore = false;
+ } else {
+ hasMore = true;
+ }
+ });
+ indexDirectory(toIndex.path, toIndex.leftDepth);
+ }
+ return hasMore;
+}
+
+void QQmlCodeModel::openNeedUpdate()
+{
+ qCDebug(codeModelLog) << "openNeedUpdate";
+ const int maxIndexThreads = 1;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads)
+ return;
+ if (++m_nUpdateInProgress == 1)
+ openUpdateStart();
+ }
+ QThreadPool::globalInstance()->start([this]() {
+ while (openUpdateSome()) { }
+ });
+}
+
+bool QQmlCodeModel::openUpdateSome()
+{
+ qCDebug(codeModelLog) << "openUpdateSome start";
+ QByteArray toUpdate;
+ {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty()) {
+ if (--m_nUpdateInProgress == 0)
+ openUpdateEnd();
+ return false;
+ }
+ auto it = m_openDocumentsToUpdate.find(m_lastOpenDocumentUpdated);
+ auto end = m_openDocumentsToUpdate.end();
+ if (it == end)
+ it = m_openDocumentsToUpdate.begin();
+ else if (++it == end)
+ it = m_openDocumentsToUpdate.begin();
+ toUpdate = *it;
+ m_openDocumentsToUpdate.erase(it);
+ }
+ bool hasMore = false;
+ {
+ auto guard = qScopeGuard([this, &hasMore]() {
+ QMutexLocker l(&m_mutex);
+ if (m_openDocumentsToUpdate.isEmpty()) {
+ if (--m_nUpdateInProgress == 0)
+ openUpdateEnd();
+ hasMore = false;
+ } else {
+ hasMore = true;
+ }
+ });
+ openUpdate(toUpdate);
+ }
+ return hasMore;
+}
+
+void QQmlCodeModel::openUpdateStart()
+{
+ qCDebug(codeModelLog) << "openUpdateStart";
+}
+
+void QQmlCodeModel::openUpdateEnd()
+{
+ qCDebug(codeModelLog) << "openUpdateEnd";
+}
+
+/*!
+\internal
+Performs initialization for m_cmakeStatus, including testing for CMake on the current system.
+*/
+void QQmlCodeModel::initializeCMakeStatus(const QString &pathForSettings)
+{
+ if (m_settings) {
+ const QString cmakeCalls = u"no-cmake-calls"_s;
+ m_settings->search(pathForSettings);
+ if (m_settings->isSet(cmakeCalls) && m_settings->value(cmakeCalls).toBool()) {
+ qWarning() << "Disabling CMake calls via .qmlls.ini setting.";
+ m_cmakeStatus = DoesNotHaveCMake;
+ return;
+ }
+ }
+
+ QProcess process;
+ process.setProgram(u"cmake"_s);
+ process.setArguments({ u"--version"_s });
+ process.start();
+ process.waitForFinished();
+ m_cmakeStatus = process.exitCode() == 0 ? HasCMake : DoesNotHaveCMake;
+
+ if (m_cmakeStatus == DoesNotHaveCMake) {
+ qWarning() << "Disabling CMake calls because CMake was not found.";
+ return;
+ }
+
+ QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, this,
+ &QQmlCodeModel::onCppFileChanged);
+}
+
+/*!
+\internal
+For each build path that is a also a CMake build path, call CMake with \l cmakeBuildCommand to
+generate/update the .qmltypes, qmldir and .qrc files.
+It is assumed here that the number of build folders is usually no more than one, so execute the
+CMake builds one at a time.
+
+If CMake cannot be executed, false is returned. This may happen when CMake does not exist on the
+current system, when the target executed by CMake does not exist (for example when something else
+than qt_add_qml_module is used to setup the module in CMake), or the when the CMake build itself
+fails.
+*/
+bool QQmlCodeModel::callCMakeBuild(const QStringList &buildPaths)
+{
+ bool success = true;
+ for (const auto &path : buildPaths) {
+ if (!QFileInfo::exists(path + u"/.cmake"_s))
+ continue;
+
+ QProcess process;
+ const auto command = QQmlLSUtils::cmakeBuildCommand(path);
+ process.setProgram(command.first);
+ process.setArguments(command.second);
+ qCDebug(codeModelLog) << "Running" << process.program() << process.arguments();
+ process.start();
+
+ // TODO: run process concurrently instead of blocking qmlls
+ success &= process.waitForFinished();
+ success &= (process.exitCode() == 0);
+ qCDebug(codeModelLog) << process.program() << process.arguments() << "terminated with"
+ << process.exitCode();
+ }
+ return success;
+}
+
+/*!
+\internal
+Iterate the entire source directory to find all C++ files that have their names in fileNames, and
+return all the found file paths.
+
+This is an overapproximation and might find unrelated files with the same name.
+*/
+QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &fileNames) const
+{
+ QStringList result;
+ for (const auto &rootUrl : m_rootUrls) {
+ const QString rootDir = QUrl(QString::fromUtf8(rootUrl)).toLocalFile();
+
+ if (rootDir.isEmpty())
+ continue;
+
+ qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;
+ QDirIterator it(rootDir, fileNames, QDir::Files, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ QFileInfo info = it.nextFileInfo();
+ result << info.absoluteFilePath();
+ }
+ }
+ return result;
+}
+
+/*!
+\internal
+Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
+of this method) that this qmlFile relies on.
+*/
+QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
+{
+ const QmlFile *file = qmlFile.as<QmlFile>();
+ if (!file)
+ return {};
+
+ auto resolver = file->typeResolver();
+ if (!resolver)
+ return {};
+
+ auto types = resolver->importedTypes();
+
+ QStringList result;
+ for (const auto &type : types) {
+ if (!type.scope)
+ continue;
+ // note: the factory only loads composite types
+ const bool isComposite = type.scope.factory() || type.scope->isComposite();
+ if (isComposite)
+ continue;
+
+ const QString filePath = QFileInfo(type.scope->filePath()).fileName();
+ result << filePath;
+ }
+
+ return result;
+}
+
+/*!
+\internal
+Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
+are modified.
+*/
+void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
+{
+ const auto filesToWatch = fileNamesToWatch(qmlFile);
+ const QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch);
+ const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
+ if (!unwatchedPaths.isEmpty()) {
+ qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
+ << filepathsToWatch;
+ }
+}
+
+void QQmlCodeModel::onCppFileChanged(const QString &)
+{
+ m_rebuildRequired = true;
+}
+
+void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText)
+{
+ qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
+ << docText.size() << "chars)";
+
+ const QString fPath = url2Path(url, UrlLookup::ForceLookup);
+ if (m_cmakeStatus == RequiresInitialization)
+ initializeCMakeStatus(fPath);
+
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ QStringList loadPaths = buildPathsForFileUrl(url);
+
+ if (m_cmakeStatus == HasCMake && !loadPaths.isEmpty() && m_rebuildRequired) {
+ callCMakeBuild(loadPaths);
+ m_rebuildRequired = false;
+ }
+
+ loadPaths.append(m_importPaths);
+ if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) {
+ newCurrentPtr->setLoadPaths(loadPaths);
+ }
+
+ // if the documentation root path is not set through the commandline,
+ // try to set it from the settings file (.qmlls.ini file)
+ if (m_documentationRootPath.isEmpty()) {
+ QString path = url2Path(url);
+ if (m_settings && m_settings->search(path)) {
+ QString docDir = QStringLiteral(u"docDir");
+ if (m_settings->isSet(docDir))
+ setDocumentationRootPath(m_settings->value(docDir).toString());
+ }
+ }
+
+ Path p;
+ auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
+ newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
+ [&p, this](Path, const DomItem &, const DomItem &newValue) {
+ const DomItem file = newValue.fileObject();
+ p = file.canonicalPath();
+ if (m_cmakeStatus == HasCMake)
+ addFileWatches(file);
+ });
+ newCurrentPtr->loadPendingDependencies();
+ if (p) {
+ newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
+ DomItem item = m_currentEnv.path(p);
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[url];
+ if (!doc.textDocument) {
+ qCWarning(lspServerLog)
+ << "ignoring update to closed document" << QString::fromUtf8(url);
+ return;
+ } else {
+ QMutexLocker l(doc.textDocument->mutex());
+ if (doc.textDocument->version() && *doc.textDocument->version() > version) {
+ qCWarning(lspServerLog)
+ << "docUpdate: version" << version << "of document"
+ << QString::fromUtf8(url) << "is not the latest anymore";
+ return;
+ }
+ }
+ if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) {
+ doc.snapshot.docVersion = version;
+ doc.snapshot.doc = item;
+ } else {
+ qCWarning(lspServerLog) << "skipping update of current doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(url);
+ }
+ if (item.field(Fields::isValid).value().toBool(false)) {
+ if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) {
+ DomItem vDoc = m_validEnv.path(p);
+ doc.snapshot.validDocVersion = version;
+ doc.snapshot.validDoc = vDoc;
+ } else {
+ qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(url);
+ }
+ } else {
+ qCWarning(lspServerLog)
+ << "avoid update of validDoc to " << version << "of document"
+ << QString::fromUtf8(url) << "as it is invalid";
+ }
+ }
+ }
+ if (codeModelLog().isDebugEnabled()) {
+ qCDebug(codeModelLog) << "finished update doc of " << url << "to version" << version;
+ snapshotByUrl(url).dump(qDebug() << "postSnapshot",
+ OpenDocumentSnapshot::DumpOption::AllCode);
+ }
+ // we should update the scope in the future thus call addOpen(url)
+ emit updatedSnapshot(url);
+}
+
+void QQmlCodeModel::closeOpenFile(const QByteArray &url)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocuments.remove(url);
+}
+
+void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls)
+{
+ QMutexLocker l(&m_mutex);
+ m_rootUrls = urls;
+}
+
+void QQmlCodeModel::addRootUrls(const QList<QByteArray> &urls)
+{
+ QMutexLocker l(&m_mutex);
+ for (const QByteArray &url : urls) {
+ if (!m_rootUrls.contains(url))
+ m_rootUrls.append(url);
+ }
+}
+
+void QQmlCodeModel::removeRootUrls(const QList<QByteArray> &urls)
+{
+ QMutexLocker l(&m_mutex);
+ for (const QByteArray &url : urls)
+ m_rootUrls.removeOne(url);
+}
+
+QList<QByteArray> QQmlCodeModel::rootUrls() const
+{
+ QMutexLocker l(&m_mutex);
+ return m_rootUrls;
+}
+
+QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url)
+{
+ QMutexLocker l(&m_mutex);
+ return m_buildPathsForRootUrl.value(url);
+}
+
+static bool isNotSeparator(char c)
+{
+ return c != '/';
+}
+
+QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url)
+{
+ QList<QByteArray> roots;
+ {
+ QMutexLocker l(&m_mutex);
+ roots = m_buildPathsForRootUrl.keys();
+ }
+ // we want to longest match to be first, as it should override shorter matches
+ std::sort(roots.begin(), roots.end(), [](const QByteArray &el1, const QByteArray &el2) {
+ if (el1.size() > el2.size())
+ return true;
+ if (el1.size() < el2.size())
+ return false;
+ return el1 < el2;
+ });
+ QStringList buildPaths;
+ QStringList defaultValues;
+ if (!roots.isEmpty() && roots.last().isEmpty())
+ roots.removeLast();
+ QByteArray urlSlash(url);
+ if (!urlSlash.isEmpty() && isNotSeparator(urlSlash.at(urlSlash.size() - 1)))
+ urlSlash.append('/');
+ // look if the file has a know prefix path
+ for (const QByteArray &root : roots) {
+ if (urlSlash.startsWith(root)) {
+ buildPaths += buildPathsForRootUrl(root);
+ break;
+ }
+ }
+ QString path = url2Path(url);
+
+ // fallback to the empty root, if is has an entry.
+ // This is the buildPath that is passed to qmlls via --build-dir.
+ if (buildPaths.isEmpty()) {
+ buildPaths += buildPathsForRootUrl(QByteArray());
+ }
+
+ // look in the QMLLS_BUILD_DIRS environment variable
+ if (buildPaths.isEmpty()) {
+ QStringList envPaths = qEnvironmentVariable("QMLLS_BUILD_DIRS")
+ .split(QDir::listSeparator(), Qt::SkipEmptyParts);
+ buildPaths += envPaths;
+ }
+
+ // look in the settings.
+ // This is the one that is passed via the .qmlls.ini file.
+ if (buildPaths.isEmpty() && m_settings) {
+ m_settings->search(path);
+ QString buildDir = QStringLiteral(u"buildDir");
+ if (m_settings->isSet(buildDir))
+ buildPaths += m_settings->value(buildDir).toString().split(QDir::listSeparator(),
+ Qt::SkipEmptyParts);
+ }
+
+ // heuristic to find build directory
+ if (buildPaths.isEmpty()) {
+ QDir d(path);
+ d.setNameFilters(QStringList({ u"build*"_s }));
+ const int maxDirDepth = 8;
+ int iDir = maxDirDepth;
+ QString dirName = d.dirName();
+ QDateTime lastModified;
+ while (d.cdUp() && --iDir > 0) {
+ for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs)) {
+ if (fInfo.completeBaseName() == u"build"
+ || fInfo.completeBaseName().startsWith(u"build-%1"_s.arg(dirName))) {
+ if (iDir > 1)
+ iDir = 1;
+ if (!lastModified.isValid() || lastModified < fInfo.lastModified()) {
+ buildPaths.clear();
+ buildPaths.append(fInfo.absoluteFilePath());
+ }
+ }
+ }
+ }
+ }
+ // add dependent build directories
+ QStringList res;
+ std::reverse(buildPaths.begin(), buildPaths.end());
+ const int maxDeps = 4;
+ while (!buildPaths.isEmpty()) {
+ QString bPath = buildPaths.last();
+ buildPaths.removeLast();
+ res += bPath;
+ if (QFile::exists(bPath + u"/_deps") && bPath.split(u"/_deps/"_s).size() < maxDeps) {
+ QDir d(bPath + u"/_deps");
+ for (const QFileInfo &fInfo : d.entryInfoList(QDir::Dirs))
+ buildPaths.append(fInfo.absoluteFilePath());
+ }
+ }
+ return res;
+}
+
+void QQmlCodeModel::setDocumentationRootPath(const QString &path)
+{
+ QMutexLocker l(&m_mutex);
+ if (m_documentationRootPath != path) {
+ m_documentationRootPath = path;
+ emit documentationRootPathChanged(path);
+ }
+}
+
+void QQmlCodeModel::setBuildPathsForRootUrl(QByteArray url, const QStringList &paths)
+{
+ QMutexLocker l(&m_mutex);
+ if (!url.isEmpty() && isNotSeparator(url.at(url.size() - 1)))
+ url.append('/');
+ if (paths.isEmpty())
+ m_buildPathsForRootUrl.remove(url);
+ else
+ m_buildPathsForRootUrl.insert(url, paths);
+}
+
+void QQmlCodeModel::openUpdate(const QByteArray &url)
+{
+ bool updateDoc = false;
+ bool updateScope = false;
+ std::optional<int> rNow = 0;
+ QString docText;
+ DomItem validDoc;
+ std::shared_ptr<Utils::TextDocument> document;
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[url];
+ document = doc.textDocument;
+ if (!document)
+ return;
+ {
+ QMutexLocker l2(document->mutex());
+ rNow = document->version();
+ }
+ if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow))
+ updateDoc = true;
+ else if (doc.snapshot.validDocVersion
+ && (!doc.snapshot.scopeVersion
+ || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion))
+ updateScope = true;
+ else
+ return;
+ if (updateDoc) {
+ QMutexLocker l2(doc.textDocument->mutex());
+ rNow = doc.textDocument->version();
+ docText = doc.textDocument->toPlainText();
+ } else {
+ validDoc = doc.snapshot.validDoc;
+ rNow = doc.snapshot.validDocVersion;
+ }
+ }
+ if (updateDoc) {
+ newDocForOpenFile(url, *rNow, docText);
+ }
+ if (updateScope) {
+ // to do
+ }
+}
+
+void QQmlCodeModel::addOpenToUpdate(const QByteArray &url)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocumentsToUpdate.insert(url);
+}
+
+QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
+{
+ dbg.noquote().nospace() << "{";
+ dbg << " url:" << QString::fromUtf8(url) << "\n";
+ dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
+ if (options & DumpOption::LatestCode) {
+ dbg << " doc: ------------\n"
+ << doc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" doc:"
+ << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size())
+ : u"*none*"_s)
+ << "\n";
+ }
+ dbg << " validDocVersion:"
+ << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
+ if (options & DumpOption::ValidCode) {
+ dbg << " validDoc: ------------\n"
+ << validDoc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" validDoc:"
+ << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
+ : u"*none*"_s)
+ << "\n";
+ }
+ dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_s)
+ << "\n";
+ dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
+ dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
+ dbg << "}";
+ return dbg;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE