Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmlcodemodel.cpp
Go to the documentation of this file.
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// Qt-Security score:significant reason:trusted-sources
4
9
10#include <QtCore/qfileinfo.h>
11#include <QtCore/qdir.h>
12#include <QtCore/qthreadpool.h>
13#include <QtCore/qlibraryinfo.h>
14#include <QtCore/qprocess.h>
15#include <QtCore/qdiriterator.h>
16
17#if QT_CONFIG(settings)
18# include <QtCore/qsettings.h>
19#endif
20
21#include <QtQmlDom/private/qqmldomtop_p.h>
22#include <QtQmlCompiler/private/qqmljsutils_p.h>
23
24#include <memory>
25#include <algorithm>
26
28
29namespace QmlLsp {
30
31Q_STATIC_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel")
32
33using namespace QQmlJS::Dom;
34using namespace Qt::StringLiterals;
35
36/*!
37\internal
38\class QQmlCodeModel
39
40The code model offers a view of the current state of the current files, and traks open files.
41All methods are threadsafe, and generally return immutable or threadsafe objects that can be
42worked on from any thread (unless otherwise noted).
43The idea is the let all other operations be as lock free as possible, concentrating all tricky
44synchronization here.
45
46\section2 Global views
47\list
48\li currentEnv() offers a view that contains the latest version of all the loaded files
49\li validEnv() is just like current env but stores only the valid (meaning correctly parsed,
50 not necessarily without errors) version of a file, it is normally a better choice to load the
51 dependencies/symbol information from
52\endlist
53
54\section2 OpenFiles
55\list
56\li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the
57 document, its latest valid version, scope, all connected to a specific version of the document
58 and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for
59 every partial change: document change, validDocument change, scope change).
60\li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These
61 contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the
62 current version of the document, and has line by line support.
63 Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex()
64 before *any* read or write/modification to it.
65 It has a version nuber which is supposed to always change and increase.
66 It is mainly used for highlighting/indenting, and is immediately updated when the user edits a
67 document. Its use should be avoided if possible, preferring the snapshots.
68\endlist
69
70\section2 Parallelism/Theading
71Most operations are not parallel and usually take place in the main thread (but are still thread
72safe).
73There is one task that is executed in parallel: OpenDocumentUpdate.
74OpenDocumentUpdate keeps the snapshots of the open documents up to date.
75
76There is always a tension between being responsive, using all threads available, and avoid to hog
77too many resources. One can choose different parallelization strategies, we went with a flexiable
78approach.
79We have (private) functions that execute part of the work: openUpdateSome(). These
80do all locking needed, get some work, do it without locks, and at the end update the state of the
81code model. If there is more work, then they return true. Thus while (xxxSome()); works until there
82is no work left.
83
84The internal addOpenToUpdate() add more work to do.
85
86openNeedUpdate() checks if there is work to do, and if yes ensure that a
87worker thread (or more) that work on it exist.
88*/
89
103
104/*!
105\internal
106Enable and initialize the functionality that uses CMake, if CMake exists. Runs the build once.
107*/
108void QQmlCodeModel::tryEnableCMakeCalls(QProcessScheduler *scheduler)
109{
110 Q_ASSERT(scheduler);
111 if (cmakeStatus() == DoesNotHaveCMake)
112 return;
113
114 if (m_settings) {
115 const QString cmakeCalls = u"no-cmake-calls"_s;
116 m_settings->search(url2Path(m_rootUrl), { QString(), verbose() });
117 if (m_settings->isSet(cmakeCalls) && m_settings->value(cmakeCalls).toBool()) {
119 return;
120 }
121 }
122
123 QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, scheduler,
124 [this, scheduler] { callCMakeBuild(scheduler); });
125 setCMakeStatus(HasCMake);
126 callCMakeBuild(scheduler);
127}
128
129/*!
130\internal
131Disable the functionality that uses CMake, and remove the already watched paths if there are some.
132*/
134{
135 QMutexLocker guard(&m_mutex);
136 m_cmakeStatus = DoesNotHaveCMake;
137 if (const QStringList toRemove = m_cppFileWatcher.files(); !toRemove.isEmpty())
138 m_cppFileWatcher.removePaths(toRemove);
139 QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
140}
141
143{
144 while (true) {
145 bool shouldWait;
146 {
147 QMutexLocker l(&m_mutex);
148 m_openDocumentsToUpdate.clear();
149 m_state = State::Stopping;
150 shouldWait = m_nUpdateInProgress != 0;
151 }
152 if (!shouldWait)
153 break;
154 QThread::yieldCurrentThread();
155 }
156}
157
159{
160 QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
162}
163
165{
166 return openDocumentByUrl(url).snapshot;
167}
168
169void QQmlCodeModel::removeDirectory(const QByteArray &url)
170{
171 const QString path = url2Path(url);
172 if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
173 validEnvPtr->removePath(path);
174 if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
175 currentEnvPtr->removePath(path);
176}
177
178QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options)
179{
180 QString res;
181 {
182 QMutexLocker l(&m_mutex);
183 res = m_url2path.value(url);
184 }
185 if (!res.isEmpty() && options == UrlLookup::Caching)
186 return res;
187 QUrl qurl(QString::fromUtf8(url));
188 QFileInfo f(qurl.toLocalFile());
189 QString cPath = f.canonicalFilePath();
190 if (cPath.isEmpty())
191 cPath = f.filePath();
192 {
193 QMutexLocker l(&m_mutex);
194 if (!res.isEmpty() && res != cPath)
195 m_path2url.remove(res);
196 m_url2path.insert(url, cPath);
197 m_path2url.insert(cPath, url);
198 }
199 return cPath;
200}
201
202void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText)
203{
204 {
205 QMutexLocker l(&m_mutex);
206 auto &openDoc = m_openDocuments[url];
207 if (!openDoc.textDocument)
208 openDoc.textDocument = std::make_shared<Utils::TextDocument>();
209 QMutexLocker l2(openDoc.textDocument->mutex());
210 openDoc.textDocument->setVersion(version);
211 openDoc.textDocument->setPlainText(docText);
212 }
213 addOpenToUpdate(url, NormalUpdate);
214 openNeedUpdate();
215}
216
218{
219 QMutexLocker l(&m_mutex);
220 return m_openDocuments.value(url);
221}
222
224{
225 QMutexLocker l(&m_mutex);
226 return m_openDocuments.isEmpty();
227}
228
230{
231 QMutexLocker l(&m_mutex);
232 return m_tokens;
233}
234
236{
237 QMutexLocker l(&m_mutex);
238 return m_tokens;
239}
240
241void QQmlCodeModel::openNeedUpdate()
242{
243 qCDebug(codeModelLog) << "openNeedUpdate";
244 const int maxThreads = 1;
245 {
246 QMutexLocker l(&m_mutex);
247 if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxThreads
248 || m_state == State::Stopping) {
249 return;
250 }
251 if (++m_nUpdateInProgress == 1)
252 openUpdateStart();
253 }
254 QThreadPool::globalInstance()->start([this]() {
255 QScopedValueRollback thread(m_openUpdateThread, QThread::currentThread());
256 while (openUpdateSome()) { }
257 emit openUpdateThreadFinished();
258 });
259}
260
261bool QQmlCodeModel::openUpdateSome()
262{
263 qCDebug(codeModelLog) << "openUpdateSome start";
264 QByteArray toUpdate;
265 UpdatePolicy policy;
266 {
267 QMutexLocker l(&m_mutex);
268 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
269
270 if (m_openDocumentsToUpdate.isEmpty()) {
271 if (--m_nUpdateInProgress == 0)
272 openUpdateEnd();
273 return false;
274 }
275 const auto it = m_openDocumentsToUpdate.begin();
276 toUpdate = it.key();
277 policy = it.value();
278 m_openDocumentsToUpdate.erase(it);
279 }
280 bool hasMore = false;
281 {
282 auto guard = qScopeGuard([this, &hasMore]() {
283 QMutexLocker l(&m_mutex);
284 if (m_openDocumentsToUpdate.isEmpty()) {
285 if (--m_nUpdateInProgress == 0)
286 openUpdateEnd();
287 hasMore = false;
288 } else {
289 hasMore = true;
290 }
291 });
292 openUpdate(toUpdate, policy);
293 }
294 return hasMore;
295}
296
297void QQmlCodeModel::openUpdateStart()
298{
299 qCDebug(codeModelLog) << "openUpdateStart";
300}
301
302void QQmlCodeModel::openUpdateEnd()
303{
304 qCDebug(codeModelLog) << "openUpdateEnd";
305}
306
308{
309 QStringList result;
310
311 std::vector<QByteArray> urls = { m_rootUrl };
312 {
313 QMutexLocker guard(&m_mutex);
314 for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
315 urls.push_back(it.key());
316 }
317
318 for (const auto &url : urls)
319 result.append(buildPathsForFileUrl(url));
320
321 // remove duplicates
322 std::sort(result.begin(), result.end());
323 result.erase(std::unique(result.begin(), result.end()), result.end());
324 return result;
325}
326
327static int cmakeJobsFromSettings(QQmlToolingSharedSettings *settings, const QString &rootPath,
328 int defaultValue)
329{
330 if (!settings)
331 return defaultValue;
332 const auto result = settings->search(rootPath);
333 if (!result.isValid())
334 return defaultValue;
335
336 bool ok = false;
337 const QString valueString = settings->value("CMakeJobs"_L1).toString();
338 if (valueString == QQmlCodeModel::s_maxCMakeJobs)
339 return QThread::idealThreadCount();
340
341 const int cmakeJobs = settings->value("CMakeJobs"_L1).toInt(&ok);
342 if (!ok || cmakeJobs < 1)
343 return defaultValue;
344 return cmakeJobs;
345}
346
347void QQmlCodeModel::callCMakeBuild(QProcessScheduler *scheduler)
348{
349 const QStringList buildPaths = buildPathsForOpenedFiles();
350 const int cmakeJobs = cmakeJobsFromSettings(m_settings, url2Path(m_rootUrl), m_cmakeJobs);
351
352 QList<QProcessScheduler::Command> commands;
353 for (const auto &path : buildPaths) {
354 if (!QFileInfo::exists(path + u"/.cmake"_s))
355 continue;
356
357 auto [program, arguments] = QQmlLSUtils::cmakeBuildCommand(path, cmakeJobs);
358 commands.append({ std::move(program), std::move(arguments) });
359 }
360 if (commands.isEmpty())
361 return;
362 qCInfo(codeModelLog) << "Starting background build on paths" << buildPaths.join(", "_L1);
363 scheduler->schedule(commands, m_rootUrl.isEmpty() ? "Default workspace"_ba : m_rootUrl);
364}
365
367{
368 QMutexLocker guard(&m_mutex);
369 for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
370 m_openDocumentsToUpdate[it.key()] = ForceUpdate;
371 guard.unlock();
372 openNeedUpdate();
373}
374
375/*!
376\internal
377Iterate the entire source directory to find all C++ files that have their names in fileNames, and
378return all the found file paths.
379
380This is an overapproximation and might find unrelated files with the same name.
381*/
382QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &_fileNamesToSearch,
383 const QSet<QString> &ignoredFilePaths)
384{
385 Q_ASSERT(!m_rootUrl.isEmpty());
386 QStringList fileNamesToSearch{ _fileNamesToSearch };
387
388 {
389 QMutexLocker guard(&m_mutex);
390 // ignore files that were not found last time
391 fileNamesToSearch.erase(std::remove_if(fileNamesToSearch.begin(), fileNamesToSearch.end(),
392 [this](const QString &fileName) {
393 return m_ignoreForWatching.contains(fileName);
394 }),
395 fileNamesToSearch.end());
396 }
397
398 const QString rootDir = QUrl(QString::fromUtf8(m_rootUrl)).toLocalFile();
399 qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;
400
401 const QStringList result =
402 QQmlLSUtils::findFilePathsFromFileNames(rootDir, fileNamesToSearch, ignoredFilePaths);
403
404 QMutexLocker guard(&m_mutex);
405 for (const auto &fileName : fileNamesToSearch) {
406 if (std::none_of(result.begin(), result.end(),
407 [&fileName](const QString &path) { return path.endsWith(fileName); })) {
408 m_ignoreForWatching.insert(fileName);
409 }
410 }
411 return result;
412}
413
414/*!
415\internal
416Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
417of this method) that this qmlFile relies on.
418*/
419QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
420{
421 const QmlFile *file = qmlFile.as<QmlFile>();
422 if (!file)
423 return {};
424
425 auto resolver = file->typeResolver();
426 if (!resolver)
427 return {};
428
429 auto types = resolver->importedTypes();
430
431 QStringList result;
432 for (const auto &type : types) {
433 if (!type.scope)
434 continue;
435 // note: the factory only loads composite types
436 const bool isComposite = type.scope.factory() || type.scope->isComposite();
437 if (isComposite)
438 continue;
439
440 const QString filePath = QFileInfo(type.scope->filePath()).fileName();
441 if (!filePath.isEmpty())
442 result << filePath;
443 }
444
445 std::sort(result.begin(), result.end());
446 result.erase(std::unique(result.begin(), result.end()), result.end());
447
448 return result;
449}
450
451/*!
452\internal
453Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
454are modified. Is a no op if this is the fallback codemodel with empty root url.
455*/
456void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
457{
458 if (m_rootUrl.isEmpty())
459 return;
460 const auto filesToWatch = fileNamesToWatch(qmlFile);
461
462 // remove already watched files to avoid a warning later on
463 const QStringList alreadyWatchedFiles = m_cppFileWatcher.files();
464 const QSet<QString> alreadyWatchedFilesSet(alreadyWatchedFiles.begin(),
465 alreadyWatchedFiles.end());
466 QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch, alreadyWatchedFilesSet);
467
468 if (filepathsToWatch.isEmpty())
469 return;
470
471 QMutexLocker guard(&m_mutex);
472 const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
473 if (!unwatchedPaths.isEmpty()) {
474 qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
475 << filepathsToWatch;
476 }
477}
478
485
490
492{
493 if (!doc.textDocument)
494 return ClosedDocument;
495
496 {
497 QMutexLocker guard2(doc.textDocument->mutex());
498 if (doc.textDocument->version() && *doc.textDocument->version() > version)
500 }
501
502 if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= version)
504
505 return VersionOk;
506}
507
509 int version)
510{
511 if (doc.snapshot.validDocVersion && *doc.snapshot.validDocVersion >= version)
513
515}
516
517static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem,
518 const QByteArray &url, OpenDocument *doc, int version,
519 UpdatePolicy policy)
520{
521 switch (policy == ForceUpdate ? VersionOk : checkVersion(*doc, version)) {
522 case ClosedDocument:
523 qCWarning(lspServerLog) << "Ignoring update to closed document" << QString::fromUtf8(url);
524 return;
525 case VersionLowerThanDocument:
526 qCWarning(lspServerLog) << "Version" << version << "of document" << QString::fromUtf8(url)
527 << "is not the latest anymore";
528 return;
529 case VersionLowerThanSnapshot:
530 qCWarning(lspServerLog) << "Skipping update of current doc to obsolete version" << version
531 << "of document" << QString::fromUtf8(url);
532 return;
533 case VersionOk:
534 doc->snapshot.docVersion = version;
535 doc->snapshot.doc = item;
536 break;
537 }
538
539 if (!item.field(Fields::isValid).value().toBool(false)) {
540 qCWarning(lspServerLog) << "avoid update of validDoc to " << version << "of document"
541 << QString::fromUtf8(url) << "as it is invalid";
542 return;
543 }
544
545 switch (policy == ForceUpdate ? VersionOkForValidDocument
546 : checkVersionForValidDocument(*doc, version)) {
547 case VersionLowerThanValidSnapshot:
548 qCWarning(lspServerLog) << "Skipping update of valid doc to obsolete version" << version
549 << "of document" << QString::fromUtf8(url);
550 return;
552 doc->snapshot.validDocVersion = version;
553 doc->snapshot.validDoc = validItem;
554 break;
555 }
556}
557
558void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText,
559 QmlLsp::UpdatePolicy policy)
560{
561 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
562 qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
563 << docText.size() << "chars)";
564
565 const QString fPath = url2Path(url, UrlLookup::ForceLookup);
566 DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
567
568 // if the documentation root path is not set through the commandline,
569 // try to set it from the settings file (.qmlls.ini file)
570 if (documentationRootPath().isEmpty() && m_settings) {
571 // note: settings already searched current file in importPathsForFile() call above
572 const QString docDir = QStringLiteral(u"docDir");
573 if (m_settings->isSet(docDir))
574 setDocumentationRootPath(m_settings->value(docDir).toString());
575 }
576
577 Path p;
578 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
579 newCurrentPtr->setLoadPaths(importPathsForUrl(url));
580 newCurrentPtr->setResourceFiles(m_resourceFiles);
581 newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
582 [&p, this](Path, const DomItem &, const DomItem &newValue) {
583 const DomItem file = newValue.fileObject();
584 p = file.canonicalPath();
585 // Force population of the file by accessing isValid field. We
586 // don't want to populate the file after adding the file to the
587 // snapshot in updateItemInSnapshot.
588 file.field(Fields::isValid);
589 if (cmakeStatus() == HasCMake)
590 addFileWatches(file);
591 });
592 newCurrentPtr->loadPendingDependencies();
593 if (p) {
594 newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
595 QMutexLocker l(&m_mutex);
596 updateItemInSnapshot(m_currentEnv.path(p), m_validEnv.path(p), url, &m_openDocuments[url],
597 version, policy);
598 }
599 if (codeModelLog().isDebugEnabled()) {
600 qCDebug(codeModelLog) << "Finished update doc of " << url << "to version" << version;
601 snapshotByUrl(url).dump(qDebug() << "postSnapshot",
602 OpenDocumentSnapshot::DumpOption::AllCode);
603 }
604 // we should update the scope in the future thus call addOpen(url)
605 emit updatedSnapshot(url, policy);
606}
607
608void QQmlCodeModel::closeOpenFile(const QByteArray &url)
609{
610 QMutexLocker l(&m_mutex);
611 m_openDocuments.remove(url);
612}
613
615{
616 QMutexLocker l(&m_mutex);
617 return m_rootUrl;
618}
619
621{
622 QMutexLocker l(&m_mutex);
623 return m_buildPaths;
624}
625
627{
628 QStringList result = importPaths();
629
630 // fallback for projects targeting Qt < 6.10, that don't have .qmlls.build.ini files
631 if (result.isEmpty() || result == QLibraryInfo::paths(QLibraryInfo::QmlImportsPath))
632 result << buildPathsForFileUrl(url);
633
634 const QString importPathsKey = u"importPaths"_s;
635 const QString fileName = url2Path(url);
636 if (m_settings && m_settings->search(fileName, { QString(), verbose() }).isValid()
637 && m_settings->isSet(importPathsKey)) {
638 result.append(m_settings->valueAsAbsolutePathList(importPathsKey, fileName));
639 }
640 return result;
641}
642
643void QQmlCodeModel::setImportPaths(const QStringList &importPaths)
644{
645 QMutexLocker guard(&m_mutex);
646 m_importPaths = importPaths;
647
648 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
649 env->setLoadPaths(importPaths);
650 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
651 env->setLoadPaths(importPaths);
652}
653
655{
656 QMutexLocker guard(&m_mutex);
657 return m_resourceFiles;
658}
659
660void QQmlCodeModel::setResourceFiles(const QStringList &resourceFiles)
661{
662 QMutexLocker guard(&m_mutex);
663 m_resourceFiles = resourceFiles;
664
665 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
666 env->setResourceFiles(resourceFiles);
667 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
668 env->setResourceFiles(resourceFiles);
669}
670
672{
673 QMutexLocker guard(&m_mutex);
674 return m_importPaths;
675}
676
677static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
678{
679 // add dependent build directories
680 QStringList res;
681 std::reverse(buildPaths.begin(), buildPaths.end());
682 const int maxDeps = 4;
683 while (!buildPaths.isEmpty()) {
684 res += buildPaths.takeLast();
685 const QString &bPath = res.constLast();
686 const QString bPathExtended = bPath + u"/_deps";
687 if (QFile::exists(bPathExtended) && bPath.count(u"/_deps/"_s) < maxDeps) {
688 for (const auto &fileInfo :
689 QDirListing{ bPathExtended, QDirListing::IteratorFlag::DirsOnly }) {
690 buildPaths.append(fileInfo.absoluteFilePath());
691 }
692 }
693 }
694 return res;
695}
696
698{
699 if (QStringList result = buildPaths(); !result.isEmpty())
700 return withDependentBuildDirectories(std::move(result));
701
702 // fallback: look in the user settings (.qmlls.ini files in the source directory)
703 if (!m_settings || !m_settings->search(url2Path(url), { QString(), verbose() }).isValid())
704 return {};
705
706 constexpr QLatin1String buildDir = "buildDir"_L1;
707 if (!m_settings->isSet(buildDir))
708 return {};
709
710 const QString fileName = url2Path(url);
711 return withDependentBuildDirectories(m_settings->valueAsAbsolutePathList(buildDir, fileName));
712}
713
714void QQmlCodeModel::setDocumentationRootPath(const QString &path)
715{
716 {
717 QMutexLocker l(&m_mutex);
718 if (m_documentationRootPath == path)
719 return;
720 m_documentationRootPath = path;
721 }
722 m_helpManager.setDocumentationRootPath(path);
723}
724
725void QQmlCodeModel::setBuildPaths(const QStringList &paths)
726{
727 QMutexLocker l(&m_mutex);
728 m_buildPaths = paths;
729}
730
731void QQmlCodeModel::openUpdate(const QByteArray &url, UpdatePolicy policy)
732{
733 std::optional<int> rNow = 0;
734 QString docText;
735
736 {
737 QMutexLocker l(&m_mutex);
738 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
739 OpenDocument &doc = m_openDocuments[url];
740 std::shared_ptr<Utils::TextDocument> document = doc.textDocument;
741 if (!document)
742 return;
743 {
744 QMutexLocker l2(document->mutex());
745 rNow = document->version();
746 }
747 if (!rNow
748 || (policy != ForceUpdate && doc.snapshot.docVersion
749 && *doc.snapshot.docVersion == *rNow)) {
750 return;
751 }
752
753 {
754 QMutexLocker l2(doc.textDocument->mutex());
755 rNow = doc.textDocument->version();
756 docText = doc.textDocument->toPlainText();
757 }
758 }
759 newDocForOpenFile(url, *rNow, docText, policy);
760}
761
762void QQmlCodeModel::addOpenToUpdate(const QByteArray &url, UpdatePolicy policy)
763{
764 {
765 QMutexLocker l(&m_mutex);
766 m_openDocumentsToUpdate[url] = policy;
767 }
768 openNeedUpdate();
769}
770
771QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
772{
773 dbg.noquote().nospace() << "{";
774 dbg << " url:" << QString::fromUtf8(url) << "\n";
775 dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
776 if (options & DumpOption::LatestCode) {
777 dbg << " doc: ------------\n"
778 << doc.field(Fields::code).value().toString() << "\n==========\n";
779 } else {
780 dbg << u" doc:"
781 << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size())
782 : u"*none*"_s)
783 << "\n";
784 }
785 dbg << " validDocVersion:"
786 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
787 if (options & DumpOption::ValidCode) {
788 dbg << " validDoc: ------------\n"
789 << validDoc.field(Fields::code).value().toString() << "\n==========\n";
790 } else {
791 dbg << u" validDoc:"
792 << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
793 : u"*none*"_s)
794 << "\n";
795 }
796 dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
797 dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
798 dbg << "}";
799 return dbg;
800}
801
802static ModuleSetting *moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings,
803 UpdatePolicy policy)
804{
805 if (policy != ForceUpdate)
806 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
807
808 auto it = std::find_if(
809 moduleSettings->begin(), moduleSettings->end(),
810 [&sourceFolder](const ModuleSetting s) { return s.sourceFolder == sourceFolder; });
811 if (it == moduleSettings->end())
812 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
813 return &*it;
814}
815
816static QStringList asStringList(const QVariant &variant)
817{
818 return variant.toString().split(QDir::listSeparator(), Qt::SkipEmptyParts);
819}
820
821// hotfix for projects targeting Qt 6.11, where the .ini array starts at 0 instead of 1
822static void handleArrayStartingAt0(QSettings *settings, ModuleSettings *moduleSettings,
823 UpdatePolicy policy)
824{
825 static constexpr QLatin1String sourcePathKey = "workspaces/0/sourcePath"_L1;
826 if (!settings->contains(sourcePathKey))
827 return;
828
829 ModuleSetting *moduleSetting =
830 moduleSettingFor(settings->value(sourcePathKey).toString(), moduleSettings, policy);
831 moduleSetting->importPaths = asStringList(settings->value("workspaces/0/importPaths"_L1));
832 moduleSetting->resourceFiles = asStringList(settings->value("workspaces/0/resourceFiles"_L1));
833}
834
835static void loadWorkspacesV2(QSettings *settings, ModuleSettings *moduleSettings,
836 UpdatePolicy policy)
837{
838 handleArrayStartingAt0(settings, moduleSettings, policy);
839
840 const int entries = settings->beginReadArray("workspaces"_L1);
841 for (int i = 0; i < entries; ++i) {
842 settings->setArrayIndex(i);
843
844 if (!settings->contains("sourcePath"_L1))
845 continue;
846
847 ModuleSetting *moduleSetting =
848 moduleSettingFor(settings->value("sourcePath").toString(), moduleSettings, policy);
849 moduleSetting->importPaths = asStringList(settings->value("importPaths"_L1));
850 moduleSetting->resourceFiles = asStringList(settings->value("resourceFiles"_L1));
851 }
852 settings->endArray();
853}
854
855void QQmllsBuildInformation::loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy)
856{
857#if QT_CONFIG(settings)
858 for (const QString &path : buildPaths) {
859 if (policy != ForceUpdate && m_seenSettings.contains(path))
860 continue;
861 m_seenSettings.insert(path);
862
863 const QString iniPath = QString(path).append("/.qt/.qmlls.build.ini"_L1);
864 if (!QFile::exists(iniPath))
865 continue;
866
867 QSettings settings(iniPath, QSettings::IniFormat);
868 m_docDir = settings.value("docDir"_L1).toString();
869 const qsizetype version = settings.value("version"_L1, "1"_L1).toString().toInt();
870 switch (version) {
871 case 2: {
872 loadWorkspacesV2(&settings, &m_moduleSettings, policy);
873 break;
874 }
875 case 1:
876 default: {
877 for (const QString &group : settings.childGroups()) {
878 settings.beginGroup(group);
879
880 ModuleSetting *moduleSetting = moduleSettingFor(
881 QString(group).replace("<SLASH>"_L1, "/"_L1), &m_moduleSettings, policy);
882 moduleSetting->importPaths = asStringList(settings.value("importPaths"_L1));
883 moduleSetting->resourceFiles = asStringList(settings.value("resourceFiles"_L1));
884 settings.endGroup();
885 }
886 break;
887 }
888 }
889 }
890#else
891 Q_UNUSED(buildPaths);
892#endif
893}
894
896{
897 return settingFor(filePath).importPaths;
898}
899
901{
902 return settingFor(filePath).resourceFiles;
903}
904
906{
907 ModuleSetting result;
908 qsizetype longestMatch = 0;
909 for (const ModuleSetting &setting : m_moduleSettings) {
910 const qsizetype matchLength = setting.sourceFolder.size();
911 if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
912 result = setting;
913 longestMatch = matchLength;
914 }
915 }
916 QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
917 return result;
918}
919
921{
922 m_moduleSettings.append(moduleSetting);
923}
924
925void QQmllsBuildInformation::writeQmllsBuildIniContent(const QString &file) const
926{
927#if QT_CONFIG(settings)
928 QSettings settings(file, QSettings::IniFormat);
929 settings.setValue("docDir"_L1, m_docDir);
930 settings.setValue("version"_L1, "2"_L1);
931 settings.beginWriteArray("workspaces"_L1, m_moduleSettings.size());
932 for (int i = 0; i < m_moduleSettings.size(); ++i) {
933 settings.setArrayIndex(i);
934 settings.setValue("sourcePath"_L1, m_moduleSettings[i].sourceFolder);
935 settings.setValue("importPaths"_L1,
936 m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
937 settings.setValue("resourceFiles"_L1,
938 m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
939 }
940 settings.endArray();
941
942#else
943 Q_UNUSED(file);
944#endif
945}
946
948
949} // namespace QmlLsp
950
951QT_END_NAMESPACE
QDebug dump(QDebug dbg, DumpOptions dump=DumpOption::NoCode)
void newOpenFile(const QByteArray &url, int version, const QString &docText)
void addOpenToUpdate(const QByteArray &, UpdatePolicy policy)
OpenDocumentSnapshot snapshotByUrl(const QByteArray &url)
void tryEnableCMakeCalls(QProcessScheduler *scheduler)
void setDocumentationRootPath(const QString &path)
QByteArray rootUrl() const
QStringList importPaths() const
OpenDocument openDocumentByUrl(const QByteArray &url)
QStringList findFilePathsFromFileNames(const QStringList &fileNames, const QSet< QString > &alreadyWatchedFiles)
QQmlCodeModel(const QByteArray &rootUrl={}, QObject *parent=nullptr, QQmlToolingSharedSettings *settings=nullptr)
const RegisteredSemanticTokens & registeredTokens() const
QStringList buildPathsForFileUrl(const QByteArray &url)
QStringList importPathsForUrl(const QByteArray &)
QStringList resourceFiles() const
QStringList buildPathsForOpenedFiles()
void setBuildPaths(const QStringList &paths)
void closeOpenFile(const QByteArray &url)
RegisteredSemanticTokens & registeredTokens()
void removeDirectory(const QByteArray &)
void setImportPaths(const QStringList &paths)
void setResourceFiles(const QStringList &resourceFiles)
QStringList importPathsFor(const QString &filePath)
QStringList resourceFilesFor(const QString &filePath)
ModuleSetting settingFor(const QString &filePath)
void writeQmllsBuildIniContent(const QString &file) const
void addModuleSetting(const ModuleSetting &moduleSetting)
void loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy=NormalUpdate)
Combined button and popup list for selecting options.
static QStringList asStringList(const QVariant &variant)
static ModuleSetting * moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings, UpdatePolicy policy)
VersionCheckResult checkVersion(const OpenDocument &doc, int version)
static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
VersionCheckResultForValidDocument
@ VersionLowerThanValidSnapshot
@ VersionOkForValidDocument
static VersionCheckResultForValidDocument checkVersionForValidDocument(const OpenDocument &doc, int version)
static void loadWorkspacesV2(QSettings *settings, ModuleSettings *moduleSettings, UpdatePolicy policy)
@ VersionLowerThanSnapshot
@ VersionLowerThanDocument
static void handleArrayStartingAt0(QSettings *settings, ModuleSettings *moduleSettings, UpdatePolicy policy)
static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem, const QByteArray &url, OpenDocument *doc, int version, UpdatePolicy policy)
static int cmakeJobsFromSettings(QQmlToolingSharedSettings *settings, const QString &rootPath, int defaultValue)