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 }
603 // we should update the scope in the future thus call addOpen(url)
604 emit updatedSnapshot(url, policy);
605}
606
607void QQmlCodeModel::closeOpenFile(const QByteArray &url)
608{
609 QMutexLocker l(&m_mutex);
610 m_openDocuments.remove(url);
611}
612
614{
615 QMutexLocker l(&m_mutex);
616 return m_rootUrl;
617}
618
620{
621 QMutexLocker l(&m_mutex);
622 return m_buildPaths;
623}
624
626{
627 QStringList result = importPaths();
628
629 // fallback for projects targeting Qt < 6.10, that don't have .qmlls.build.ini files
630 if (result.isEmpty() || result == QLibraryInfo::paths(QLibraryInfo::QmlImportsPath))
631 result << buildPathsForFileUrl(url);
632
633 const QString importPathsKey = u"importPaths"_s;
634 const QString fileName = url2Path(url);
635 if (m_settings && m_settings->search(fileName, { QString(), verbose() }).isValid()
636 && m_settings->isSet(importPathsKey)) {
637 result.append(m_settings->valueAsAbsolutePathList(importPathsKey, fileName));
638 }
639 return result;
640}
641
642void QQmlCodeModel::setImportPaths(const QStringList &importPaths)
643{
644 QMutexLocker guard(&m_mutex);
645 m_importPaths = importPaths;
646
647 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
648 env->setLoadPaths(importPaths);
649 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
650 env->setLoadPaths(importPaths);
651}
652
654{
655 QMutexLocker guard(&m_mutex);
656 return m_resourceFiles;
657}
658
659void QQmlCodeModel::setResourceFiles(const QStringList &resourceFiles)
660{
661 QMutexLocker guard(&m_mutex);
662 m_resourceFiles = resourceFiles;
663
664 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
665 env->setResourceFiles(resourceFiles);
666 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
667 env->setResourceFiles(resourceFiles);
668}
669
671{
672 QMutexLocker guard(&m_mutex);
673 return m_importPaths;
674}
675
676static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
677{
678 // add dependent build directories
679 QStringList res;
680 std::reverse(buildPaths.begin(), buildPaths.end());
681 const int maxDeps = 4;
682 while (!buildPaths.isEmpty()) {
683 res += buildPaths.takeLast();
684 const QString &bPath = res.constLast();
685 const QString bPathExtended = bPath + u"/_deps";
686 if (QFile::exists(bPathExtended) && bPath.count(u"/_deps/"_s) < maxDeps) {
687 for (const auto &fileInfo :
688 QDirListing{ bPathExtended, QDirListing::IteratorFlag::DirsOnly }) {
689 buildPaths.append(fileInfo.absoluteFilePath());
690 }
691 }
692 }
693 return res;
694}
695
697{
698 if (QStringList result = buildPaths(); !result.isEmpty())
699 return withDependentBuildDirectories(std::move(result));
700
701 // fallback: look in the user settings (.qmlls.ini files in the source directory)
702 if (!m_settings || !m_settings->search(url2Path(url), { QString(), verbose() }).isValid())
703 return {};
704
705 constexpr QLatin1String buildDir = "buildDir"_L1;
706 if (!m_settings->isSet(buildDir))
707 return {};
708
709 const QString fileName = url2Path(url);
710 return withDependentBuildDirectories(m_settings->valueAsAbsolutePathList(buildDir, fileName));
711}
712
713void QQmlCodeModel::setDocumentationRootPath(const QString &path)
714{
715 {
716 QMutexLocker l(&m_mutex);
717 if (m_documentationRootPath == path)
718 return;
719 m_documentationRootPath = path;
720 }
721 m_helpManager.setDocumentationRootPath(path);
722}
723
724void QQmlCodeModel::setBuildPaths(const QStringList &paths)
725{
726 QMutexLocker l(&m_mutex);
727 m_buildPaths = paths;
728}
729
730void QQmlCodeModel::openUpdate(const QByteArray &url, UpdatePolicy policy)
731{
732 std::optional<int> rNow = 0;
733 QString docText;
734
735 {
736 QMutexLocker l(&m_mutex);
737 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
738 OpenDocument &doc = m_openDocuments[url];
739 std::shared_ptr<Utils::TextDocument> document = doc.textDocument;
740 if (!document)
741 return;
742 {
743 QMutexLocker l2(document->mutex());
744 rNow = document->version();
745 }
746 if (!rNow
747 || (policy != ForceUpdate && doc.snapshot.docVersion
748 && *doc.snapshot.docVersion == *rNow)) {
749 return;
750 }
751
752 {
753 QMutexLocker l2(doc.textDocument->mutex());
754 rNow = doc.textDocument->version();
755 docText = doc.textDocument->toPlainText();
756 }
757 }
758 newDocForOpenFile(url, *rNow, docText, policy);
759}
760
761void QQmlCodeModel::addOpenToUpdate(const QByteArray &url, UpdatePolicy policy)
762{
763 {
764 QMutexLocker l(&m_mutex);
765 m_openDocumentsToUpdate[url] = policy;
766 }
767 openNeedUpdate();
768}
769
771{
772 dbg.noquote().nospace() << "{";
773 dbg << " url:" << QString::fromUtf8(url) << "\n";
774 dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
775 dbg << u" doc:"
776 << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size()) : u"*none*"_s)
777 << "\n";
778 dbg << " validDocVersion:"
779 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
780 dbg << u" validDoc:"
781 << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
782 : u"*none*"_s)
783 << "\n";
784 dbg << "}";
785 return dbg;
786}
787
788static ModuleSetting *moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings,
789 UpdatePolicy policy)
790{
791 if (policy != ForceUpdate)
792 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
793
794 auto it = std::find_if(
795 moduleSettings->begin(), moduleSettings->end(),
796 [&sourceFolder](const ModuleSetting s) { return s.sourceFolder == sourceFolder; });
797 if (it == moduleSettings->end())
798 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
799 return &*it;
800}
801
802static QStringList asStringList(const QVariant &variant)
803{
804 return variant.toString().split(QDir::listSeparator(), Qt::SkipEmptyParts);
805}
806
807// hotfix for projects targeting Qt 6.11, where the .ini array starts at 0 instead of 1
808static void handleArrayStartingAt0(QSettings *settings, ModuleSettings *moduleSettings,
809 UpdatePolicy policy)
810{
811 static constexpr QLatin1String sourcePathKey = "workspaces/0/sourcePath"_L1;
812 if (!settings->contains(sourcePathKey))
813 return;
814
815 ModuleSetting *moduleSetting =
816 moduleSettingFor(settings->value(sourcePathKey).toString(), moduleSettings, policy);
817 moduleSetting->importPaths = asStringList(settings->value("workspaces/0/importPaths"_L1));
818 moduleSetting->resourceFiles = asStringList(settings->value("workspaces/0/resourceFiles"_L1));
819}
820
821static void loadWorkspacesV2(QSettings *settings, ModuleSettings *moduleSettings,
822 UpdatePolicy policy)
823{
824 handleArrayStartingAt0(settings, moduleSettings, policy);
825
826 const int entries = settings->beginReadArray("workspaces"_L1);
827 for (int i = 0; i < entries; ++i) {
828 settings->setArrayIndex(i);
829
830 if (!settings->contains("sourcePath"_L1))
831 continue;
832
833 ModuleSetting *moduleSetting =
834 moduleSettingFor(settings->value("sourcePath").toString(), moduleSettings, policy);
835 moduleSetting->importPaths = asStringList(settings->value("importPaths"_L1));
836 moduleSetting->resourceFiles = asStringList(settings->value("resourceFiles"_L1));
837 }
838 settings->endArray();
839}
840
841void QQmllsBuildInformation::loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy)
842{
843#if QT_CONFIG(settings)
844 for (const QString &path : buildPaths) {
845 if (policy != ForceUpdate && m_seenSettings.contains(path))
846 continue;
847 m_seenSettings.insert(path);
848
849 const QString iniPath = QString(path).append("/.qt/.qmlls.build.ini"_L1);
850 if (!QFile::exists(iniPath))
851 continue;
852
853 QSettings settings(iniPath, QSettings::IniFormat);
854 m_docDir = settings.value("docDir"_L1).toString();
855 const qsizetype version = settings.value("version"_L1, "1"_L1).toString().toInt();
856 switch (version) {
857 case 2: {
858 loadWorkspacesV2(&settings, &m_moduleSettings, policy);
859 break;
860 }
861 case 1:
862 default: {
863 for (const QString &group : settings.childGroups()) {
864 settings.beginGroup(group);
865
866 ModuleSetting *moduleSetting = moduleSettingFor(
867 QString(group).replace("<SLASH>"_L1, "/"_L1), &m_moduleSettings, policy);
868 moduleSetting->importPaths = asStringList(settings.value("importPaths"_L1));
869 moduleSetting->resourceFiles = asStringList(settings.value("resourceFiles"_L1));
870 settings.endGroup();
871 }
872 break;
873 }
874 }
875 }
876#else
877 Q_UNUSED(buildPaths);
878#endif
879}
880
882{
883 return settingFor(filePath).importPaths;
884}
885
887{
888 return settingFor(filePath).resourceFiles;
889}
890
892{
893 ModuleSetting result;
894 qsizetype longestMatch = 0;
895 for (const ModuleSetting &setting : m_moduleSettings) {
896 const qsizetype matchLength = setting.sourceFolder.size();
897 if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
898 result = setting;
899 longestMatch = matchLength;
900 }
901 }
902 QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
903 return result;
904}
905
907{
908 m_moduleSettings.append(moduleSetting);
909}
910
911void QQmllsBuildInformation::writeQmllsBuildIniContent(const QString &file) const
912{
913#if QT_CONFIG(settings)
914 QSettings settings(file, QSettings::IniFormat);
915 settings.setValue("docDir"_L1, m_docDir);
916 settings.setValue("version"_L1, "2"_L1);
917 settings.beginWriteArray("workspaces"_L1, m_moduleSettings.size());
918 for (int i = 0; i < m_moduleSettings.size(); ++i) {
919 settings.setArrayIndex(i);
920 settings.setValue("sourcePath"_L1, m_moduleSettings[i].sourceFolder);
921 settings.setValue("importPaths"_L1,
922 m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
923 settings.setValue("resourceFiles"_L1,
924 m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
925 }
926 settings.endArray();
927
928#else
929 Q_UNUSED(file);
930#endif
931}
932
934
935} // namespace QmlLsp
936
937QT_END_NAMESPACE
QDebug & dump(QDebug &dbg)
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)