1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlcodemodel_p.h"
5#include "qqmllsplugin_p.h"
6#include "qtextdocument_p.h"
7#include "qqmllsutils_p.h"
8
9#include <QtCore/qfileinfo.h>
10#include <QtCore/qdir.h>
11#include <QtCore/qthreadpool.h>
12#include <QtCore/qlibraryinfo.h>
13#include <QtCore/qprocess.h>
14#include <QtCore/qdiriterator.h>
15#include <QtQmlDom/private/qqmldomtop_p.h>
16
17#include <memory>
18#include <algorithm>
19
20QT_BEGIN_NAMESPACE
21
22namespace QmlLsp {
23
24Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
25
26using namespace QQmlJS::Dom;
27using namespace Qt::StringLiterals;
28
29/*!
30\internal
31\class QQmlCodeModel
32
33The code model offers a view of the current state of the current files, and traks open files.
34All methods are threadsafe, and generally return immutable or threadsafe objects that can be
35worked on from any thread (unless otherwise noted).
36The idea is the let all other operations be as lock free as possible, concentrating all tricky
37synchronization here.
38
39\section2 Global views
40\list
41\li currentEnv() offers a view that contains the latest version of all the loaded files
42\li validEnv() is just like current env but stores only the valid (meaning correctly parsed,
43 not necessarily without errors) version of a file, it is normally a better choice to load the
44 dependencies/symbol information from
45\endlist
46
47\section2 OpenFiles
48\list
49\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the
50 document, its latest valid version, scope, all connected to a specific version of the document
51 and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for
52 every partial change: document change, validDocument change, scope change).
53\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These
54 contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the
55 current version of the document, and has line by line support.
56 Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex()
57 before *any* read or write/modification to it.
58 It has a version nuber which is supposed to always change and increase.
59 It is mainly used for highlighting/indenting, and is immediately updated when the user edits a
60 document. Its use should be avoided if possible, preferring the snapshots.
61\endlist
62
63\section2 Parallelism/Theading
64Most operations are not parallel and usually take place in the main thread (but are still thread
65safe).
66There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate.
67Indexing is meant to keep the global view up to date.
68OpenDocumentUpdate keeps the snapshots of the open documents up to date.
69
70There is always a tension between being responsive, using all threads available, and avoid to hog
71too many resources. One can choose different parallelization strategies, we went with a flexiable
72approach.
73We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These
74do all locking needed, get some work, do it without locks, and at the end update the state of the
75code model. If there is more work, then they return true. Thus while (xxxSome()); works until there
76is no work left.
77
78addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do.
79
80indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a
81worker thread (or more) that work on it exist.
82*/
83
84QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings)
85 : QObject { parent },
86 m_importPaths(QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)),
87 m_currentEnv(std::make_shared<DomEnvironment>(
88 args&: m_importPaths, args: DomEnvironment::Option::SingleThreaded,
89 args: DomCreationOptions{} | DomCreationOption::WithRecovery
90 | DomCreationOption::WithScriptExpressions
91 | DomCreationOption::WithSemanticAnalysis)),
92 m_validEnv(std::make_shared<DomEnvironment>(
93 args&: m_importPaths, args: DomEnvironment::Option::SingleThreaded,
94 args: DomCreationOptions{} | DomCreationOption::WithRecovery
95 | DomCreationOption::WithScriptExpressions
96 | DomCreationOption::WithSemanticAnalysis)),
97 m_settings(settings),
98 m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s)
99{
100}
101
102/*!
103\internal
104Disable the functionality that uses CMake, and remove the already watched paths if there are some.
105*/
106void QQmlCodeModel::disableCMakeCalls()
107{
108 m_cmakeStatus = DoesNotHaveCMake;
109 m_cppFileWatcher.removePaths(files: m_cppFileWatcher.files());
110 QObject::disconnect(sender: &m_cppFileWatcher, signal: &QFileSystemWatcher::fileChanged, receiver: nullptr, zero: nullptr);
111}
112
113QQmlCodeModel::~QQmlCodeModel()
114{
115 QObject::disconnect(sender: &m_cppFileWatcher, signal: &QFileSystemWatcher::fileChanged, receiver: nullptr, zero: nullptr);
116 while (true) {
117 bool shouldWait;
118 {
119 QMutexLocker l(&m_mutex);
120 m_state = State::Stopping;
121 m_openDocumentsToUpdate.clear();
122 shouldWait = m_nIndexInProgress != 0 || m_nUpdateInProgress != 0;
123 }
124 if (!shouldWait)
125 break;
126 QThread::yieldCurrentThread();
127 }
128}
129
130OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url)
131{
132 return openDocumentByUrl(url).snapshot;
133}
134
135int QQmlCodeModel::indexEvalProgress() const
136{
137 Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
138 const int dirCost = 10;
139 int costToDo = 1;
140 for (const ToIndex &el : std::as_const(t: m_toIndex))
141 costToDo += dirCost * el.leftDepth;
142 costToDo += m_indexInProgressCost;
143 return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost);
144}
145
146void QQmlCodeModel::indexStart()
147{
148 Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
149 qCDebug(codeModelLog) << "indexStart";
150}
151
152void QQmlCodeModel::indexEnd()
153{
154 Q_ASSERT(!m_mutex.tryLock()); // should be called while locked
155 qCDebug(codeModelLog) << "indexEnd";
156 m_lastIndexProgress = 0;
157 m_nIndexInProgress = 0;
158 m_toIndex.clear();
159 m_indexInProgressCost = 0;
160 m_indexDoneCost = 0;
161}
162
163void QQmlCodeModel::indexSendProgress(int progress)
164{
165 if (progress <= m_lastIndexProgress)
166 return;
167 m_lastIndexProgress = progress;
168 // ### actually send progress
169}
170
171bool QQmlCodeModel::indexCancelled()
172{
173 QMutexLocker l(&m_mutex);
174 if (m_state == State::Stopping)
175 return true;
176 return false;
177}
178
179void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft)
180{
181 if (indexCancelled())
182 return;
183 QDir dir(path);
184 if (depthLeft > 1) {
185 const QStringList dirs =
186 dir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
187 for (const QString &child : dirs)
188 addDirectory(path: dir.filePath(fileName: child), leftDepth: --depthLeft);
189 }
190 const QStringList qmljs =
191 dir.entryList(nameFilters: QStringList({ u"*.qml"_s, u"*.js"_s, u"*.mjs"_s }), filters: QDir::Files);
192 int progress = 0;
193 {
194 QMutexLocker l(&m_mutex);
195 m_indexInProgressCost += qmljs.size();
196 progress = indexEvalProgress();
197 }
198 indexSendProgress(progress);
199 if (qmljs.isEmpty())
200 return;
201 DomItem newCurrent = m_currentEnv.makeCopy(option: DomItem::CopyOption::EnvConnected).item();
202 for (const QString &file : qmljs) {
203 if (indexCancelled())
204 return;
205 QString fPath = dir.filePath(fileName: file);
206 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
207 FileToLoad fileToLoad = FileToLoad::fromFileSystem(environment: newCurrentPtr, canonicalPath: fPath);
208 if (!fileToLoad.canonicalPath().isEmpty()) {
209 newCurrentPtr->loadBuiltins();
210 newCurrentPtr->loadFile(file: fileToLoad, callback: [](Path, const DomItem &, const DomItem &) {});
211 newCurrentPtr->loadPendingDependencies();
212 newCurrent.commitToBase(validPtr: m_validEnv.ownerAs<DomEnvironment>());
213 }
214 {
215 QMutexLocker l(&m_mutex);
216 ++m_indexDoneCost;
217 --m_indexInProgressCost;
218 progress = indexEvalProgress();
219 }
220 indexSendProgress(progress);
221 }
222}
223
224void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server)
225{
226 Q_UNUSED(server);
227 // ### create progress, &scan in a separate instance
228 const int maxDepth = 5;
229 for (const auto &path : paths)
230 addDirectory(path, leftDepth: maxDepth);
231 indexNeedsUpdate();
232}
233
234void QQmlCodeModel::addDirectory(const QString &path, int depthLeft)
235{
236 if (depthLeft < 1)
237 return;
238 {
239 QMutexLocker l(&m_mutex);
240 for (auto it = m_toIndex.begin(); it != m_toIndex.end();) {
241 if (it->path.startsWith(s: path)) {
242 if (it->path.size() == path.size())
243 return;
244 if (it->path.at(i: path.size()) == u'/') {
245 it = m_toIndex.erase(pos: it);
246 continue;
247 }
248 } else if (path.startsWith(s: it->path) && path.at(i: it->path.size()) == u'/')
249 return;
250 ++it;
251 }
252 m_toIndex.append(t: { .path: path, .leftDepth: depthLeft });
253 }
254}
255
256void QQmlCodeModel::removeDirectory(const QString &path)
257{
258 {
259 QMutexLocker l(&m_mutex);
260 auto toRemove = [path](const QString &p) {
261 return p.startsWith(s: path) && (p.size() == path.size() || p.at(i: path.size()) == u'/');
262 };
263 auto it = m_toIndex.begin();
264 auto end = m_toIndex.end();
265 while (it != end) {
266 if (toRemove(it->path))
267 it = m_toIndex.erase(pos: it);
268 else
269 ++it;
270 }
271 }
272 if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
273 validEnvPtr->removePath(path);
274 if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
275 currentEnvPtr->removePath(path);
276}
277
278QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options)
279{
280 QString res;
281 {
282 QMutexLocker l(&m_mutex);
283 res = m_url2path.value(key: url);
284 }
285 if (!res.isEmpty() && options == UrlLookup::Caching)
286 return res;
287 QUrl qurl(QString::fromUtf8(ba: url));
288 QFileInfo f(qurl.toLocalFile());
289 QString cPath = f.canonicalFilePath();
290 if (cPath.isEmpty())
291 cPath = f.filePath();
292 {
293 QMutexLocker l(&m_mutex);
294 if (!res.isEmpty() && res != cPath)
295 m_path2url.remove(key: res);
296 m_url2path.insert(key: url, value: cPath);
297 m_path2url.insert(key: cPath, value: url);
298 }
299 return cPath;
300}
301
302void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText)
303{
304 {
305 QMutexLocker l(&m_mutex);
306 auto &openDoc = m_openDocuments[url];
307 if (!openDoc.textDocument)
308 openDoc.textDocument = std::make_shared<Utils::TextDocument>();
309 QMutexLocker l2(openDoc.textDocument->mutex());
310 openDoc.textDocument->setVersion(version);
311 openDoc.textDocument->setPlainText(docText);
312 }
313 addOpenToUpdate(url);
314 openNeedUpdate();
315}
316
317OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url)
318{
319 QMutexLocker l(&m_mutex);
320 return m_openDocuments.value(key: url);
321}
322
323RegisteredSemanticTokens &QQmlCodeModel::registeredTokens()
324{
325 QMutexLocker l(&m_mutex);
326 return m_tokens;
327}
328
329const RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() const
330{
331 QMutexLocker l(&m_mutex);
332 return m_tokens;
333}
334
335void QQmlCodeModel::indexNeedsUpdate()
336{
337 const int maxIndexThreads = 1;
338 {
339 QMutexLocker l(&m_mutex);
340 if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads)
341 return;
342 if (++m_nIndexInProgress == 1)
343 indexStart();
344 }
345 QThreadPool::globalInstance()->start(functionToRun: [this]() {
346 while (indexSome()) { }
347 });
348}
349
350bool QQmlCodeModel::indexSome()
351{
352 qCDebug(codeModelLog) << "indexSome";
353 ToIndex toIndex;
354 {
355 QMutexLocker l(&m_mutex);
356 if (m_toIndex.isEmpty()) {
357 if (--m_nIndexInProgress == 0)
358 indexEnd();
359 return false;
360 }
361 toIndex = m_toIndex.last();
362 m_toIndex.removeLast();
363 }
364 bool hasMore = false;
365 {
366 auto guard = qScopeGuard(f: [this, &hasMore]() {
367 QMutexLocker l(&m_mutex);
368 if (m_toIndex.isEmpty()) {
369 if (--m_nIndexInProgress == 0)
370 indexEnd();
371 hasMore = false;
372 } else {
373 hasMore = true;
374 }
375 });
376 indexDirectory(path: toIndex.path, depthLeft: toIndex.leftDepth);
377 }
378 return hasMore;
379}
380
381void QQmlCodeModel::openNeedUpdate()
382{
383 qCDebug(codeModelLog) << "openNeedUpdate";
384 const int maxIndexThreads = 1;
385 {
386 QMutexLocker l(&m_mutex);
387 if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads)
388 return;
389 if (++m_nUpdateInProgress == 1)
390 openUpdateStart();
391 }
392 QThreadPool::globalInstance()->start(functionToRun: [this]() {
393 while (openUpdateSome()) { }
394 });
395}
396
397bool QQmlCodeModel::openUpdateSome()
398{
399 qCDebug(codeModelLog) << "openUpdateSome start";
400 QByteArray toUpdate;
401 {
402 QMutexLocker l(&m_mutex);
403 if (m_openDocumentsToUpdate.isEmpty()) {
404 if (--m_nUpdateInProgress == 0)
405 openUpdateEnd();
406 return false;
407 }
408 auto it = m_openDocumentsToUpdate.find(value: m_lastOpenDocumentUpdated);
409 auto end = m_openDocumentsToUpdate.end();
410 if (it == end)
411 it = m_openDocumentsToUpdate.begin();
412 else if (++it == end)
413 it = m_openDocumentsToUpdate.begin();
414 toUpdate = *it;
415 m_openDocumentsToUpdate.erase(i: it);
416 }
417 bool hasMore = false;
418 {
419 auto guard = qScopeGuard(f: [this, &hasMore]() {
420 QMutexLocker l(&m_mutex);
421 if (m_openDocumentsToUpdate.isEmpty()) {
422 if (--m_nUpdateInProgress == 0)
423 openUpdateEnd();
424 hasMore = false;
425 } else {
426 hasMore = true;
427 }
428 });
429 openUpdate(toUpdate);
430 }
431 return hasMore;
432}
433
434void QQmlCodeModel::openUpdateStart()
435{
436 qCDebug(codeModelLog) << "openUpdateStart";
437}
438
439void QQmlCodeModel::openUpdateEnd()
440{
441 qCDebug(codeModelLog) << "openUpdateEnd";
442}
443
444/*!
445\internal
446Performs initialization for m_cmakeStatus, including testing for CMake on the current system.
447*/
448void QQmlCodeModel::initializeCMakeStatus(const QString &pathForSettings)
449{
450 if (m_settings) {
451 const QString cmakeCalls = u"no-cmake-calls"_s;
452 m_settings->search(path: pathForSettings);
453 if (m_settings->isSet(name: cmakeCalls) && m_settings->value(name: cmakeCalls).toBool()) {
454 qWarning() << "Disabling CMake calls via .qmlls.ini setting.";
455 m_cmakeStatus = DoesNotHaveCMake;
456 return;
457 }
458 }
459
460 QProcess process;
461 process.setProgram(u"cmake"_s);
462 process.setArguments({ u"--version"_s });
463 process.start();
464 process.waitForFinished();
465 m_cmakeStatus = process.exitCode() == 0 ? HasCMake : DoesNotHaveCMake;
466
467 if (m_cmakeStatus == DoesNotHaveCMake) {
468 qWarning() << "Disabling CMake calls because CMake was not found.";
469 return;
470 }
471
472 QObject::connect(sender: &m_cppFileWatcher, signal: &QFileSystemWatcher::fileChanged, context: this,
473 slot: &QQmlCodeModel::onCppFileChanged);
474}
475
476/*!
477\internal
478For each build path that is a also a CMake build path, call CMake with \l cmakeBuildCommand to
479generate/update the .qmltypes, qmldir and .qrc files.
480It is assumed here that the number of build folders is usually no more than one, so execute the
481CMake builds one at a time.
482
483If CMake cannot be executed, false is returned. This may happen when CMake does not exist on the
484current system, when the target executed by CMake does not exist (for example when something else
485than qt_add_qml_module is used to setup the module in CMake), or the when the CMake build itself
486fails.
487*/
488bool QQmlCodeModel::callCMakeBuild(const QStringList &buildPaths)
489{
490 bool success = true;
491 for (const auto &path : buildPaths) {
492 if (!QFileInfo::exists(file: path + u"/.cmake"_s))
493 continue;
494
495 QProcess process;
496 const auto command = QQmlLSUtils::cmakeBuildCommand(path);
497 process.setProgram(command.first);
498 process.setArguments(command.second);
499 qCDebug(codeModelLog) << "Running" << process.program() << process.arguments();
500 process.start();
501
502 // TODO: run process concurrently instead of blocking qmlls
503 success &= process.waitForFinished();
504 success &= (process.exitCode() == 0);
505 qCDebug(codeModelLog) << process.program() << process.arguments() << "terminated with"
506 << process.exitCode();
507 }
508 return success;
509}
510
511/*!
512\internal
513Iterate the entire source directory to find all C++ files that have their names in fileNames, and
514return all the found file paths.
515
516This is an overapproximation and might find unrelated files with the same name.
517*/
518QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &_fileNamesToSearch)
519{
520 QStringList fileNamesToSearch{ _fileNamesToSearch };
521
522 // ignore files that were not found last time
523 fileNamesToSearch.erase(abegin: std::remove_if(first: fileNamesToSearch.begin(), last: fileNamesToSearch.end(),
524 pred: [this](const QString &fileName) {
525 return m_ignoreForWatching.contains(value: fileName);
526 }),
527 aend: fileNamesToSearch.end());
528
529 // early return:
530 if (fileNamesToSearch.isEmpty())
531 return {};
532
533 QSet<QString> foundFiles;
534 foundFiles.reserve(asize: fileNamesToSearch.size());
535
536 QStringList result;
537
538 for (const auto &rootUrl : m_rootUrls) {
539 const QString rootDir = QUrl(QString::fromUtf8(ba: rootUrl)).toLocalFile();
540
541 if (rootDir.isEmpty())
542 continue;
543
544 qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;
545 QDirIterator it(rootDir, fileNamesToSearch, QDir::Files, QDirIterator::Subdirectories);
546 while (it.hasNext()) {
547 const QFileInfo info = it.nextFileInfo();
548 const QString fileName = info.fileName();
549 foundFiles.insert(value: fileName);
550 result << info.absoluteFilePath();
551 }
552 }
553
554 for (const auto& fileName: fileNamesToSearch) {
555 if (!foundFiles.contains(value: fileName))
556 m_ignoreForWatching.insert(value: fileName);
557 }
558 return result;
559}
560
561/*!
562\internal
563Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
564of this method) that this qmlFile relies on.
565*/
566QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
567{
568 const QmlFile *file = qmlFile.as<QmlFile>();
569 if (!file)
570 return {};
571
572 auto resolver = file->typeResolver();
573 if (!resolver)
574 return {};
575
576 auto types = resolver->importedTypes();
577
578 QStringList result;
579 for (const auto &type : types) {
580 if (!type.scope)
581 continue;
582 // note: the factory only loads composite types
583 const bool isComposite = type.scope.factory() || type.scope->isComposite();
584 if (isComposite)
585 continue;
586
587 const QString filePath = QFileInfo(type.scope->filePath()).fileName();
588 if (!filePath.isEmpty())
589 result << filePath;
590 }
591
592 std::sort(first: result.begin(), last: result.end());
593 result.erase(abegin: std::unique(first: result.begin(), last: result.end()), aend: result.end());
594
595 return result;
596}
597
598/*!
599\internal
600Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
601are modified.
602*/
603void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
604{
605 const auto filesToWatch = fileNamesToWatch(qmlFile);
606 const QStringList filepathsToWatch = findFilePathsFromFileNames(fileNamesToSearch: filesToWatch);
607 const auto unwatchedPaths = m_cppFileWatcher.addPaths(files: filepathsToWatch);
608 if (!unwatchedPaths.isEmpty()) {
609 qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
610 << filepathsToWatch;
611 }
612}
613
614void QQmlCodeModel::onCppFileChanged(const QString &)
615{
616 m_rebuildRequired = true;
617}
618
619void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText)
620{
621 qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
622 << docText.size() << "chars)";
623
624 const QString fPath = url2Path(url, options: UrlLookup::ForceLookup);
625 if (m_cmakeStatus == RequiresInitialization)
626 initializeCMakeStatus(pathForSettings: fPath);
627
628 DomItem newCurrent = m_currentEnv.makeCopy(option: DomItem::CopyOption::EnvConnected).item();
629 QStringList loadPaths = buildPathsForFileUrl(url);
630
631 if (m_cmakeStatus == HasCMake && !loadPaths.isEmpty() && m_rebuildRequired) {
632 callCMakeBuild(buildPaths: loadPaths);
633 m_rebuildRequired = false;
634 }
635
636 loadPaths.append(l: m_importPaths);
637 if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) {
638 newCurrentPtr->setLoadPaths(loadPaths);
639 }
640
641 // if the documentation root path is not set through the commandline,
642 // try to set it from the settings file (.qmlls.ini file)
643 if (m_documentationRootPath.isEmpty()) {
644 QString path = url2Path(url);
645 if (m_settings && m_settings->search(path)) {
646 QString docDir = QStringLiteral(u"docDir");
647 if (m_settings->isSet(name: docDir))
648 setDocumentationRootPath(m_settings->value(name: docDir).toString());
649 }
650 }
651
652 Path p;
653 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
654 newCurrentPtr->loadFile(file: FileToLoad::fromMemory(environment: newCurrentPtr, path: fPath, data: docText),
655 callback: [&p, this](Path, const DomItem &, const DomItem &newValue) {
656 const DomItem file = newValue.fileObject();
657 p = file.canonicalPath();
658 if (m_cmakeStatus == HasCMake)
659 addFileWatches(qmlFile: file);
660 });
661 newCurrentPtr->loadPendingDependencies();
662 if (p) {
663 newCurrent.commitToBase(validPtr: m_validEnv.ownerAs<DomEnvironment>());
664 DomItem item = m_currentEnv.path(p);
665 {
666 QMutexLocker l(&m_mutex);
667 OpenDocument &doc = m_openDocuments[url];
668 if (!doc.textDocument) {
669 qCWarning(lspServerLog)
670 << "ignoring update to closed document" << QString::fromUtf8(ba: url);
671 return;
672 } else {
673 QMutexLocker l(doc.textDocument->mutex());
674 if (doc.textDocument->version() && *doc.textDocument->version() > version) {
675 qCWarning(lspServerLog)
676 << "docUpdate: version" << version << "of document"
677 << QString::fromUtf8(ba: url) << "is not the latest anymore";
678 return;
679 }
680 }
681 if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) {
682 doc.snapshot.docVersion = version;
683 doc.snapshot.doc = item;
684 } else {
685 qCWarning(lspServerLog) << "skipping update of current doc to obsolete version"
686 << version << "of document" << QString::fromUtf8(ba: url);
687 }
688 if (item.field(name: Fields::isValid).value().toBool(defaultValue: false)) {
689 if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) {
690 DomItem vDoc = m_validEnv.path(p);
691 doc.snapshot.validDocVersion = version;
692 doc.snapshot.validDoc = vDoc;
693 } else {
694 qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version"
695 << version << "of document" << QString::fromUtf8(ba: url);
696 }
697 } else {
698 qCWarning(lspServerLog)
699 << "avoid update of validDoc to " << version << "of document"
700 << QString::fromUtf8(ba: url) << "as it is invalid";
701 }
702 }
703 }
704 if (codeModelLog().isDebugEnabled()) {
705 qCDebug(codeModelLog) << "finished update doc of " << url << "to version" << version;
706 snapshotByUrl(url).dump(qDebug() << "postSnapshot",
707 dump: OpenDocumentSnapshot::DumpOption::AllCode);
708 }
709 // we should update the scope in the future thus call addOpen(url)
710 emit updatedSnapshot(url);
711}
712
713void QQmlCodeModel::closeOpenFile(const QByteArray &url)
714{
715 QMutexLocker l(&m_mutex);
716 m_openDocuments.remove(key: url);
717}
718
719void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls)
720{
721 QMutexLocker l(&m_mutex);
722 m_rootUrls = urls;
723}
724
725void QQmlCodeModel::addRootUrls(const QList<QByteArray> &urls)
726{
727 QMutexLocker l(&m_mutex);
728 for (const QByteArray &url : urls) {
729 if (!m_rootUrls.contains(t: url))
730 m_rootUrls.append(t: url);
731 }
732}
733
734void QQmlCodeModel::removeRootUrls(const QList<QByteArray> &urls)
735{
736 QMutexLocker l(&m_mutex);
737 for (const QByteArray &url : urls)
738 m_rootUrls.removeOne(t: url);
739}
740
741QList<QByteArray> QQmlCodeModel::rootUrls() const
742{
743 QMutexLocker l(&m_mutex);
744 return m_rootUrls;
745}
746
747QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url)
748{
749 QMutexLocker l(&m_mutex);
750 return m_buildPathsForRootUrl.value(key: url);
751}
752
753static bool isNotSeparator(char c)
754{
755 return c != '/';
756}
757
758QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url)
759{
760 QList<QByteArray> roots;
761 {
762 QMutexLocker l(&m_mutex);
763 roots = m_buildPathsForRootUrl.keys();
764 }
765 // we want to longest match to be first, as it should override shorter matches
766 std::sort(first: roots.begin(), last: roots.end(), comp: [](const QByteArray &el1, const QByteArray &el2) {
767 if (el1.size() > el2.size())
768 return true;
769 if (el1.size() < el2.size())
770 return false;
771 return el1 < el2;
772 });
773 QStringList buildPaths;
774 QStringList defaultValues;
775 if (!roots.isEmpty() && roots.last().isEmpty())
776 roots.removeLast();
777 QByteArray urlSlash(url);
778 if (!urlSlash.isEmpty() && isNotSeparator(c: urlSlash.at(i: urlSlash.size() - 1)))
779 urlSlash.append(c: '/');
780 // look if the file has a know prefix path
781 for (const QByteArray &root : roots) {
782 if (urlSlash.startsWith(bv: root)) {
783 buildPaths += buildPathsForRootUrl(url: root);
784 break;
785 }
786 }
787 QString path = url2Path(url);
788
789 // fallback to the empty root, if is has an entry.
790 // This is the buildPath that is passed to qmlls via --build-dir.
791 if (buildPaths.isEmpty()) {
792 buildPaths += buildPathsForRootUrl(url: QByteArray());
793 }
794
795 // look in the QMLLS_BUILD_DIRS environment variable
796 if (buildPaths.isEmpty()) {
797 QStringList envPaths = qEnvironmentVariable(varName: "QMLLS_BUILD_DIRS")
798 .split(sep: QDir::listSeparator(), behavior: Qt::SkipEmptyParts);
799 buildPaths += envPaths;
800 }
801
802 // look in the settings.
803 // This is the one that is passed via the .qmlls.ini file.
804 if (buildPaths.isEmpty() && m_settings) {
805 m_settings->search(path);
806 QString buildDir = QStringLiteral(u"buildDir");
807 if (m_settings->isSet(name: buildDir))
808 buildPaths += m_settings->value(name: buildDir).toString().split(sep: QDir::listSeparator(),
809 behavior: Qt::SkipEmptyParts);
810 }
811
812 // heuristic to find build directory
813 if (buildPaths.isEmpty()) {
814 QDir d(path);
815 d.setNameFilters(QStringList({ u"build*"_s }));
816 const int maxDirDepth = 8;
817 int iDir = maxDirDepth;
818 QString dirName = d.dirName();
819 QDateTime lastModified;
820 while (d.cdUp() && --iDir > 0) {
821 for (const QFileInfo &fInfo : d.entryInfoList(filters: QDir::Dirs | QDir::NoDotAndDotDot)) {
822 if (fInfo.completeBaseName() == u"build"
823 || fInfo.completeBaseName().startsWith(s: u"build-%1"_s.arg(a: dirName))) {
824 if (iDir > 1)
825 iDir = 1;
826 if (!lastModified.isValid() || lastModified < fInfo.lastModified()) {
827 buildPaths.clear();
828 buildPaths.append(t: fInfo.absoluteFilePath());
829 }
830 }
831 }
832 }
833 }
834 // add dependent build directories
835 QStringList res;
836 std::reverse(first: buildPaths.begin(), last: buildPaths.end());
837 const int maxDeps = 4;
838 while (!buildPaths.isEmpty()) {
839 QString bPath = buildPaths.last();
840 buildPaths.removeLast();
841 res += bPath;
842 if (QFile::exists(fileName: bPath + u"/_deps") && bPath.split(sep: u"/_deps/"_s).size() < maxDeps) {
843 QDir d(bPath + u"/_deps");
844 for (const QFileInfo &fInfo : d.entryInfoList(filters: QDir::Dirs | QDir::NoDotAndDotDot))
845 buildPaths.append(t: fInfo.absoluteFilePath());
846 }
847 }
848 return res;
849}
850
851void QQmlCodeModel::setDocumentationRootPath(const QString &path)
852{
853 QMutexLocker l(&m_mutex);
854 if (m_documentationRootPath != path) {
855 m_documentationRootPath = path;
856 emit documentationRootPathChanged(path);
857 }
858}
859
860void QQmlCodeModel::setBuildPathsForRootUrl(QByteArray url, const QStringList &paths)
861{
862 QMutexLocker l(&m_mutex);
863 if (!url.isEmpty() && isNotSeparator(c: url.at(i: url.size() - 1)))
864 url.append(c: '/');
865 if (paths.isEmpty())
866 m_buildPathsForRootUrl.remove(key: url);
867 else
868 m_buildPathsForRootUrl.insert(key: url, value: paths);
869}
870
871void QQmlCodeModel::openUpdate(const QByteArray &url)
872{
873 bool updateDoc = false;
874 bool updateScope = false;
875 std::optional<int> rNow = 0;
876 QString docText;
877 DomItem validDoc;
878 std::shared_ptr<Utils::TextDocument> document;
879 {
880 QMutexLocker l(&m_mutex);
881 OpenDocument &doc = m_openDocuments[url];
882 document = doc.textDocument;
883 if (!document)
884 return;
885 {
886 QMutexLocker l2(document->mutex());
887 rNow = document->version();
888 }
889 if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow))
890 updateDoc = true;
891 else if (doc.snapshot.validDocVersion
892 && (!doc.snapshot.scopeVersion
893 || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion))
894 updateScope = true;
895 else
896 return;
897 if (updateDoc) {
898 QMutexLocker l2(doc.textDocument->mutex());
899 rNow = doc.textDocument->version();
900 docText = doc.textDocument->toPlainText();
901 } else {
902 validDoc = doc.snapshot.validDoc;
903 rNow = doc.snapshot.validDocVersion;
904 }
905 }
906 if (updateDoc) {
907 newDocForOpenFile(url, version: *rNow, docText);
908 }
909 if (updateScope) {
910 // to do
911 }
912}
913
914void QQmlCodeModel::addOpenToUpdate(const QByteArray &url)
915{
916 QMutexLocker l(&m_mutex);
917 m_openDocumentsToUpdate.insert(value: url);
918}
919
920QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
921{
922 dbg.noquote().nospace() << "{";
923 dbg << " url:" << QString::fromUtf8(ba: url) << "\n";
924 dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
925 if (options & DumpOption::LatestCode) {
926 dbg << " doc: ------------\n"
927 << doc.field(name: Fields::code).value().toString() << "\n==========\n";
928 } else {
929 dbg << u" doc:"
930 << (doc ? u"%1chars"_s.arg(a: doc.field(name: Fields::code).value().toString().size())
931 : u"*none*"_s)
932 << "\n";
933 }
934 dbg << " validDocVersion:"
935 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
936 if (options & DumpOption::ValidCode) {
937 dbg << " validDoc: ------------\n"
938 << validDoc.field(name: Fields::code).value().toString() << "\n==========\n";
939 } else {
940 dbg << u" validDoc:"
941 << (validDoc ? u"%1chars"_s.arg(a: validDoc.field(name: Fields::code).value().toString().size())
942 : u"*none*"_s)
943 << "\n";
944 }
945 dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_s)
946 << "\n";
947 dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
948 dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
949 dbg << "}";
950 return dbg;
951}
952
953} // namespace QmlLsp
954
955QT_END_NAMESPACE
956

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of qtdeclarative/src/qmlls/qqmlcodemodel.cpp