aboutsummaryrefslogtreecommitdiffstats
path: root/tools/qmlls/qqmlcodemodel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'tools/qmlls/qqmlcodemodel.cpp')
-rw-r--r--tools/qmlls/qqmlcodemodel.cpp610
1 files changed, 610 insertions, 0 deletions
diff --git a/tools/qmlls/qqmlcodemodel.cpp b/tools/qmlls/qqmlcodemodel.cpp
new file mode 100644
index 0000000000..77e0298180
--- /dev/null
+++ b/tools/qmlls/qqmlcodemodel.cpp
@@ -0,0 +1,610 @@
+/****************************************************************************
+**
+** Copyright (C) 2021 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the qmllanguageserver tool of the Qt Toolkit.
+**
+** $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$
+**
+****************************************************************************/
+#include "qqmllanguageserver.h"
+#include "qqmlcodemodel.h"
+#include <QtCore/qfileinfo.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qthreadpool.h>
+#include <QtQmlDom/private/qqmldomtop_p.h>
+#include "textdocument.h"
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+namespace QmlLsp {
+
+Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
+
+using namespace QQmlJS::Dom;
+
+/*!
+\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 snapshotByUri() returns an OpenDocumentSnapshot of an open document. Form it you can get the
+ document, its latest valid version, scope,... and 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 openDocumentByUri() 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 s 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 usially 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 bein 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.
+
+An initial implementation allowed one to register a callback to be called when a given open document
+had some chosen parts of the snapshot up to date. But I did not need anythign more that the
+updatedSnapshot() signal, so that has been removed, but something like that might become useful in
+the future.
+*/
+
+QQmlCodeModel::QQmlCodeModel(QObject *parent)
+ : QObject { parent },
+ m_currentEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded)),
+ m_validEnv(std::make_shared<DomEnvironment>(QStringList(),
+ DomEnvironment::Option::SingleThreaded))
+{
+}
+
+OpenDocumentSnapshot QQmlCodeModel::snapshotByUri(const QByteArray &uri)
+{
+ return openDocumentByUri(uri).snapshot;
+}
+
+int QQmlCodeModel::indexEvalProgress() const
+{
+ // should be called with acquired mutex
+ const int dirCost = 10;
+ int costToDo = 1;
+ for (const ToIndex &el : qAsConst(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_nUpdateInProgress = 0;
+ m_toIndex.clear();
+ m_indexInProgressCost = 0;
+ m_indexDoneCost = 0;
+}
+
+void QQmlCodeModel::indexSendProgress(int progress)
+{
+ if (progress <= m_lastIndexProgress)
+ return;
+ m_lastIndexProgress = progress;
+ // to do: send progress
+}
+
+bool QQmlCodeModel::indexCancelled()
+{
+ 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({ "*.qml", "*.js", "*.mjs" }), 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();
+ int iFile = 0;
+ for (const QString &file : qmljs) {
+ if (indexCancelled())
+ return;
+ QString fPath = dir.filePath(file);
+ QFileInfo fInfo(fPath);
+ QString cPath = fInfo.canonicalFilePath();
+ if (!cPath.isEmpty()) {
+ bool isNew = false;
+ newCurrent.loadFile(cPath, fPath,
+ [&isNew](Path, DomItem &oldValue, DomItem &newValue) {
+ if (oldValue != newValue)
+ isNew = true;
+ },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (isNew) {
+ newCurrent.commitToBase();
+ m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ }
+ }
+ ++iFile;
+ {
+ QMutexLocker l(&m_mutex);
+ ++m_indexDoneCost;
+ --m_indexInProgressCost;
+ progress = indexEvalProgress();
+ }
+ indexSendProgress(progress);
+ }
+}
+
+void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server)
+{
+ Q_UNUSED(server);
+ // to do 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);
+ auto it = m_toIndex.begin();
+ auto end = m_toIndex.end();
+ while (it != 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;
+ }
+ 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::uri2Path(const QByteArray &uri, UriLookup options)
+{
+ QString res;
+ {
+ QMutexLocker l(&m_mutex);
+ res = m_uri2path.value(uri);
+ }
+ if (!res.isEmpty() && options == UriLookup::Caching)
+ return res;
+ QUrl url(QString::fromUtf8(uri));
+ QFileInfo f(url.toLocalFile());
+ QString cPath = f.canonicalFilePath();
+ if (cPath.isEmpty())
+ cPath = f.filePath();
+ {
+ QMutexLocker l(&m_mutex);
+ if (!res.isEmpty() && res != cPath)
+ m_path2uri.remove(res);
+ m_uri2path.insert(uri, cPath);
+ m_path2uri.insert(cPath, uri);
+ }
+ return cPath;
+}
+
+void QQmlCodeModel::newOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ {
+ QMutexLocker l(&m_mutex);
+ auto &openDoc = m_openDocuments[uri];
+ if (!openDoc.textDocument)
+ openDoc.textDocument = std::make_shared<Utils::TextDocument>();
+ QMutexLocker l2(openDoc.textDocument->mutex());
+ openDoc.textDocument->setVersion(version);
+ openDoc.textDocument->setPlainText(docText);
+ }
+ addOpenToUpdate(uri);
+ openNeedUpdate();
+}
+
+OpenDocument QQmlCodeModel::openDocumentByUri(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ return m_openDocuments.value(uri);
+}
+
+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";
+}
+
+DomItem QQmlCodeModel::validDocForUpdate(DomItem &item)
+{
+ if (item.field(Fields::isValid).value().toBool(false)) {
+ if (auto envPtr = m_validEnv.ownerAs<DomEnvironment>()) {
+ switch (item.fileObject().internalKind()) {
+ case DomType::QmlFile:
+ envPtr->addQmlFile(item.fileObject().ownerAs<QmlFile>());
+ break;
+ case DomType::JsFile:
+ envPtr->addJsFile(item.fileObject().ownerAs<JsFile>());
+ break;
+ default:
+ qCWarning(lspServerLog)
+ << "Unexpected file type " << item.fileObject().internalKindStr();
+ return DomItem();
+ }
+ return m_validEnv.path(item.canonicalPath());
+ }
+ }
+ return DomItem();
+}
+
+void QQmlCodeModel::newDocForOpenFile(const QByteArray &uri, int version, const QString &docText)
+{
+ qCDebug(codeModelLog) << "updating doc" << uri << "to version" << version << "("
+ << docText.length() << "chars)";
+ DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
+ QString fPath = uri2Path(uri, UriLookup::ForceLookup);
+ Path p;
+ newCurrent.loadFile(
+ fPath, fPath, docText, QDateTime::currentDateTimeUtc(),
+ [&p](Path, DomItem &, DomItem &newValue) { p = newValue.fileObject().canonicalPath(); },
+ {});
+ newCurrent.loadPendingDependencies();
+ if (p) {
+ newCurrent.commitToBase();
+ DomItem item = m_currentEnv.path(p);
+ DomItem vDoc = validDocForUpdate(item);
+ {
+ QMutexLocker l(&m_mutex);
+ OpenDocument &doc = m_openDocuments[uri];
+ if (!doc.textDocument) {
+ qCWarning(lspServerLog)
+ << "ignoring update to closed document" << QString::fromUtf8(uri);
+ return;
+ } else {
+ QMutexLocker l(doc.textDocument->mutex());
+ if (doc.textDocument->version() && *doc.textDocument->version() > version) {
+ qCWarning(lspServerLog)
+ << "docUpdate: version" << version << "of document"
+ << QString::fromUtf8(uri) << "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) << "skippig update of current doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ if (vDoc) {
+ if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) {
+ doc.snapshot.validDocVersion = version;
+ doc.snapshot.validDoc = vDoc;
+ } else {
+ qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version"
+ << version << "of document" << QString::fromUtf8(uri);
+ }
+ } else {
+ qCWarning(lspServerLog)
+ << "avoid update of validDoc to " << version << "of document"
+ << QString::fromUtf8(uri) << "as it is invalid";
+ }
+ }
+ }
+ if (codeModelLog().isDebugEnabled()) {
+ qCDebug(codeModelLog) << "finished update doc of " << uri << "to version" << version;
+ snapshotByUri(uri).dump(qDebug() << "postSnapshot",
+ OpenDocumentSnapshot::DumpOption::AllCode);
+ }
+ // we should update the scope in the future thus call addOpen(uri)
+ emit updatedSnapshot(uri);
+}
+
+void QQmlCodeModel::closeOpenFile(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocuments.remove(uri);
+}
+
+void QQmlCodeModel::openUpdate(const QByteArray &uri)
+{
+ 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[uri];
+ 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(uri, *rNow, docText);
+ }
+ if (updateScope) {
+ // to do
+ }
+}
+
+void QQmlCodeModel::addOpenToUpdate(const QByteArray &uri)
+{
+ QMutexLocker l(&m_mutex);
+ m_openDocumentsToUpdate.insert(uri);
+}
+
+QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
+{
+ dbg.noquote().nospace() << "{";
+ dbg << " uri:" << QString::fromUtf8(uri) << "\n";
+ dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::LatestCode) {
+ dbg << " doc: ------------\n"
+ << doc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" doc:"
+ << (doc ? u"%1chars"_qs.arg(doc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " validDocVersion:"
+ << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_qs) << "\n";
+ if (options & DumpOption::ValidCode) {
+ dbg << " validDoc: ------------\n"
+ << validDoc.field(Fields::code).value().toString() << "\n==========\n";
+ } else {
+ dbg << u" validDoc:"
+ << (validDoc ? u"%1chars"_qs.arg(
+ validDoc.field(Fields::code).value().toString().length())
+ : u"*none*"_qs)
+ << "\n";
+ }
+ dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_qs)
+ << "\n";
+ dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
+ dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
+ dbg << "}";
+ return dbg;
+}
+
+} // namespace QmlLsp
+
+QT_END_NAMESPACE