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 scheduler->schedule(commands, m_rootUrl);
363}
364
366{
367 QMutexLocker guard(&m_mutex);
368 for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
369 m_openDocumentsToUpdate[it.key()] = ForceUpdate;
370 guard.unlock();
371 openNeedUpdate();
372}
373
374/*!
375\internal
376Iterate the entire source directory to find all C++ files that have their names in fileNames, and
377return all the found file paths.
378
379This is an overapproximation and might find unrelated files with the same name.
380*/
381QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &_fileNamesToSearch,
382 const QSet<QString> &ignoredFilePaths)
383{
384 Q_ASSERT(!m_rootUrl.isEmpty());
385 QStringList fileNamesToSearch{ _fileNamesToSearch };
386
387 {
388 QMutexLocker guard(&m_mutex);
389 // ignore files that were not found last time
390 fileNamesToSearch.erase(std::remove_if(fileNamesToSearch.begin(), fileNamesToSearch.end(),
391 [this](const QString &fileName) {
392 return m_ignoreForWatching.contains(fileName);
393 }),
394 fileNamesToSearch.end());
395 }
396
397 const QString rootDir = QUrl(QString::fromUtf8(m_rootUrl)).toLocalFile();
398 qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;
399
400 const QStringList result =
401 QQmlLSUtils::findFilePathsFromFileNames(rootDir, fileNamesToSearch, ignoredFilePaths);
402
403 QMutexLocker guard(&m_mutex);
404 for (const auto &fileName : fileNamesToSearch) {
405 if (std::none_of(result.begin(), result.end(),
406 [&fileName](const QString &path) { return path.endsWith(fileName); })) {
407 m_ignoreForWatching.insert(fileName);
408 }
409 }
410 return result;
411}
412
413/*!
414\internal
415Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
416of this method) that this qmlFile relies on.
417*/
418QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
419{
420 const QmlFile *file = qmlFile.as<QmlFile>();
421 if (!file)
422 return {};
423
424 auto resolver = file->typeResolver();
425 if (!resolver)
426 return {};
427
428 auto types = resolver->importedTypes();
429
430 QStringList result;
431 for (const auto &type : types) {
432 if (!type.scope)
433 continue;
434 // note: the factory only loads composite types
435 const bool isComposite = type.scope.factory() || type.scope->isComposite();
436 if (isComposite)
437 continue;
438
439 const QString filePath = QFileInfo(type.scope->filePath()).fileName();
440 if (!filePath.isEmpty())
441 result << filePath;
442 }
443
444 std::sort(result.begin(), result.end());
445 result.erase(std::unique(result.begin(), result.end()), result.end());
446
447 return result;
448}
449
450/*!
451\internal
452Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
453are modified. Is a no op if this is the fallback codemodel with empty root url.
454*/
455void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
456{
457 if (m_rootUrl.isEmpty())
458 return;
459 const auto filesToWatch = fileNamesToWatch(qmlFile);
460
461 // remove already watched files to avoid a warning later on
462 const QStringList alreadyWatchedFiles = m_cppFileWatcher.files();
463 const QSet<QString> alreadyWatchedFilesSet(alreadyWatchedFiles.begin(),
464 alreadyWatchedFiles.end());
465 QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch, alreadyWatchedFilesSet);
466
467 if (filepathsToWatch.isEmpty())
468 return;
469
470 QMutexLocker guard(&m_mutex);
471 const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
472 if (!unwatchedPaths.isEmpty()) {
473 qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
474 << filepathsToWatch;
475 }
476}
477
484
489
491{
492 if (!doc.textDocument)
493 return ClosedDocument;
494
495 {
496 QMutexLocker guard2(doc.textDocument->mutex());
497 if (doc.textDocument->version() && *doc.textDocument->version() > version)
499 }
500
501 if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= version)
503
504 return VersionOk;
505}
506
508 int version)
509{
510 if (doc.snapshot.validDocVersion && *doc.snapshot.validDocVersion >= version)
512
514}
515
516static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem,
517 const QByteArray &url, OpenDocument *doc, int version,
518 UpdatePolicy policy)
519{
520 switch (policy == ForceUpdate ? VersionOk : checkVersion(*doc, version)) {
521 case ClosedDocument:
522 qCWarning(lspServerLog) << "Ignoring update to closed document" << QString::fromUtf8(url);
523 return;
524 case VersionLowerThanDocument:
525 qCWarning(lspServerLog) << "Version" << version << "of document" << QString::fromUtf8(url)
526 << "is not the latest anymore";
527 return;
528 case VersionLowerThanSnapshot:
529 qCWarning(lspServerLog) << "Skipping update of current doc to obsolete version" << version
530 << "of document" << QString::fromUtf8(url);
531 return;
532 case VersionOk:
533 doc->snapshot.docVersion = version;
534 doc->snapshot.doc = item;
535 break;
536 }
537
538 if (!item.field(Fields::isValid).value().toBool(false)) {
539 qCWarning(lspServerLog) << "avoid update of validDoc to " << version << "of document"
540 << QString::fromUtf8(url) << "as it is invalid";
541 return;
542 }
543
544 switch (policy == ForceUpdate ? VersionOkForValidDocument
545 : checkVersionForValidDocument(*doc, version)) {
546 case VersionLowerThanValidSnapshot:
547 qCWarning(lspServerLog) << "Skipping update of valid doc to obsolete version" << version
548 << "of document" << QString::fromUtf8(url);
549 return;
551 doc->snapshot.validDocVersion = version;
552 doc->snapshot.validDoc = validItem;
553 break;
554 }
555}
556
557void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText,
558 QmlLsp::UpdatePolicy policy)
559{
560 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
561 qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
562 << docText.size() << "chars)";
563
564 const QString fPath = url2Path(url, UrlLookup::ForceLookup);
565 DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
566
567 // if the documentation root path is not set through the commandline,
568 // try to set it from the settings file (.qmlls.ini file)
569 if (documentationRootPath().isEmpty() && m_settings) {
570 // note: settings already searched current file in importPathsForFile() call above
571 const QString docDir = QStringLiteral(u"docDir");
572 if (m_settings->isSet(docDir))
573 setDocumentationRootPath(m_settings->value(docDir).toString());
574 }
575
576 Path p;
577 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
578 newCurrentPtr->setLoadPaths(importPathsForUrl(url));
579 newCurrentPtr->setResourceFiles(m_resourceFiles);
580 newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
581 [&p, this](Path, const DomItem &, const DomItem &newValue) {
582 const DomItem file = newValue.fileObject();
583 p = file.canonicalPath();
584 // Force population of the file by accessing isValid field. We
585 // don't want to populate the file after adding the file to the
586 // snapshot in updateItemInSnapshot.
587 file.field(Fields::isValid);
588 if (cmakeStatus() == HasCMake)
589 addFileWatches(file);
590 });
591 newCurrentPtr->loadPendingDependencies();
592 if (p) {
593 newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
594 QMutexLocker l(&m_mutex);
595 updateItemInSnapshot(m_currentEnv.path(p), m_validEnv.path(p), url, &m_openDocuments[url],
596 version, policy);
597 }
598 if (codeModelLog().isDebugEnabled()) {
599 qCDebug(codeModelLog) << "Finished update doc of " << url << "to version" << version;
600 snapshotByUrl(url).dump(qDebug() << "postSnapshot",
601 OpenDocumentSnapshot::DumpOption::AllCode);
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
770QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
771{
772 dbg.noquote().nospace() << "{";
773 dbg << " url:" << QString::fromUtf8(url) << "\n";
774 dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
775 if (options & DumpOption::LatestCode) {
776 dbg << " doc: ------------\n"
777 << doc.field(Fields::code).value().toString() << "\n==========\n";
778 } else {
779 dbg << u" doc:"
780 << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size())
781 : u"*none*"_s)
782 << "\n";
783 }
784 dbg << " validDocVersion:"
785 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
786 if (options & DumpOption::ValidCode) {
787 dbg << " validDoc: ------------\n"
788 << validDoc.field(Fields::code).value().toString() << "\n==========\n";
789 } else {
790 dbg << u" validDoc:"
791 << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
792 : u"*none*"_s)
793 << "\n";
794 }
795 dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
796 dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
797 dbg << "}";
798 return dbg;
799}
800
801static ModuleSetting *moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings,
802 UpdatePolicy policy)
803{
804 if (policy != ForceUpdate)
805 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
806
807 auto it = std::find_if(
808 moduleSettings->begin(), moduleSettings->end(),
809 [&sourceFolder](const ModuleSetting s) { return s.sourceFolder == sourceFolder; });
810 if (it == moduleSettings->end())
811 return &moduleSettings->emplaceBack(ModuleSetting {sourceFolder, {}, {}});
812 return &*it;
813}
814
815void QQmllsBuildInformation::loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy)
816{
817#if QT_CONFIG(settings)
818 for (const QString &path : buildPaths) {
819 if (policy != ForceUpdate && m_seenSettings.contains(path))
820 continue;
821 m_seenSettings.insert(path);
822
823 const QString iniPath = QString(path).append("/.qt/.qmlls.build.ini"_L1);
824 if (!QFile::exists(iniPath))
825 continue;
826
827 QSettings settings(iniPath, QSettings::IniFormat);
828 m_docDir = settings.value("docDir"_L1).toString();
829 const qsizetype version = settings.value("version"_L1, "1"_L1).toString().toInt();
830 switch (version) {
831 case 2: {
832 const int entries = settings.beginReadArray("workspaces"_L1);
833 for (int i = 0; i < entries; ++i) {
834 settings.setArrayIndex(i);
835
836 ModuleSetting *moduleSetting = moduleSettingFor(
837 settings.value("sourcePath").toString(), &m_moduleSettings, policy);
838 moduleSetting->importPaths =
839 settings.value("importPaths"_L1)
840 .toString()
841 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
842 moduleSetting->resourceFiles =
843 settings.value("resourceFiles"_L1)
844 .toString()
845 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
846 }
847 settings.endArray();
848 break;
849 }
850 case 1:
851 default: {
852 for (const QString &group : settings.childGroups()) {
853 settings.beginGroup(group);
854
855 ModuleSetting *moduleSetting = moduleSettingFor(
856 QString(group).replace("<SLASH>"_L1, "/"_L1), &m_moduleSettings, policy);
857 moduleSetting->importPaths =
858 settings.value("importPaths"_L1)
859 .toString()
860 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
861 moduleSetting->resourceFiles =
862 settings.value("resourceFiles"_L1)
863 .toString()
864 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
865 settings.endGroup();
866 }
867 break;
868 }
869 }
870 }
871#else
872 Q_UNUSED(buildPaths);
873#endif
874}
875
877{
878 return settingFor(filePath).importPaths;
879}
880
882{
883 return settingFor(filePath).resourceFiles;
884}
885
887{
888 ModuleSetting result;
889 qsizetype longestMatch = 0;
890 for (const ModuleSetting &setting : m_moduleSettings) {
891 const qsizetype matchLength = setting.sourceFolder.size();
892 if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
893 result = setting;
894 longestMatch = matchLength;
895 }
896 }
897 QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
898 return result;
899}
900
902{
903 m_moduleSettings.append(moduleSetting);
904}
905
906void QQmllsBuildInformation::writeQmllsBuildIniContent(const QString &file) const
907{
908#if QT_CONFIG(settings)
909 QSettings settings(file, QSettings::IniFormat);
910 settings.setValue("docDir"_L1, m_docDir);
911 settings.setValue("version"_L1, "2"_L1);
912 settings.beginWriteArray("workspaces"_L1, m_moduleSettings.size());
913 for (int i = 0; i < m_moduleSettings.size(); ++i) {
914 settings.setArrayIndex(i);
915 settings.setValue("sourcePath"_L1, m_moduleSettings[i].sourceFolder);
916 settings.setValue("importPaths"_L1,
917 m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
918 settings.setValue("resourceFiles"_L1,
919 m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
920 }
921 settings.endArray();
922
923#else
924 Q_UNUSED(file);
925#endif
926}
927
929
930} // namespace QmlLsp
931
932QT_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 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)
@ VersionLowerThanSnapshot
@ VersionLowerThanDocument
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)