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(scheduler, &QProcessScheduler::done, this,
124 &QQmlCodeModel::onCMakeProcessFinished);
125 QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, scheduler,
126 [this, scheduler] { callCMakeBuild(scheduler); });
127 setCMakeStatus(HasCMake);
128 callCMakeBuild(scheduler);
129}
130
131/*!
132\internal
133Disable the functionality that uses CMake, and remove the already watched paths if there are some.
134*/
136{
137 QMutexLocker guard(&m_mutex);
138 m_cmakeStatus = DoesNotHaveCMake;
139 if (const QStringList toRemove = m_cppFileWatcher.files(); !toRemove.isEmpty())
140 m_cppFileWatcher.removePaths(toRemove);
141 QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
142}
143
145{
146 while (true) {
147 bool shouldWait;
148 {
149 QMutexLocker l(&m_mutex);
150 m_openDocumentsToUpdate.clear();
151 m_state = State::Stopping;
152 shouldWait = m_nUpdateInProgress != 0;
153 }
154 if (!shouldWait)
155 break;
156 QThread::yieldCurrentThread();
157 }
158}
159
161{
162 QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, nullptr, nullptr);
164}
165
167{
168 return openDocumentByUrl(url).snapshot;
169}
170
171void QQmlCodeModel::removeDirectory(const QByteArray &url)
172{
173 const QString path = url2Path(url);
174 if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>())
175 validEnvPtr->removePath(path);
176 if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>())
177 currentEnvPtr->removePath(path);
178}
179
180QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options)
181{
182 QString res;
183 {
184 QMutexLocker l(&m_mutex);
185 res = m_url2path.value(url);
186 }
187 if (!res.isEmpty() && options == UrlLookup::Caching)
188 return res;
189 QUrl qurl(QString::fromUtf8(url));
190 QFileInfo f(qurl.toLocalFile());
191 QString cPath = f.canonicalFilePath();
192 if (cPath.isEmpty())
193 cPath = f.filePath();
194 {
195 QMutexLocker l(&m_mutex);
196 if (!res.isEmpty() && res != cPath)
197 m_path2url.remove(res);
198 m_url2path.insert(url, cPath);
199 m_path2url.insert(cPath, url);
200 }
201 return cPath;
202}
203
204void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText)
205{
206 {
207 QMutexLocker l(&m_mutex);
208 auto &openDoc = m_openDocuments[url];
209 if (!openDoc.textDocument)
210 openDoc.textDocument = std::make_shared<Utils::TextDocument>();
211 QMutexLocker l2(openDoc.textDocument->mutex());
212 openDoc.textDocument->setVersion(version);
213 openDoc.textDocument->setPlainText(docText);
214 }
215 addOpenToUpdate(url, NormalUpdate);
216 openNeedUpdate();
217}
218
220{
221 QMutexLocker l(&m_mutex);
222 return m_openDocuments.value(url);
223}
224
226{
227 QMutexLocker l(&m_mutex);
228 return m_openDocuments.isEmpty();
229}
230
232{
233 QMutexLocker l(&m_mutex);
234 return m_tokens;
235}
236
238{
239 QMutexLocker l(&m_mutex);
240 return m_tokens;
241}
242
243void QQmlCodeModel::openNeedUpdate()
244{
245 qCDebug(codeModelLog) << "openNeedUpdate";
246 const int maxThreads = 1;
247 {
248 QMutexLocker l(&m_mutex);
249 if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxThreads
250 || m_state == State::Stopping) {
251 return;
252 }
253 if (++m_nUpdateInProgress == 1)
254 openUpdateStart();
255 }
256 QThreadPool::globalInstance()->start([this]() {
257 QScopedValueRollback thread(m_openUpdateThread, QThread::currentThread());
258 while (openUpdateSome()) { }
259 emit openUpdateThreadFinished();
260 });
261}
262
263bool QQmlCodeModel::openUpdateSome()
264{
265 qCDebug(codeModelLog) << "openUpdateSome start";
266 QByteArray toUpdate;
267 UpdatePolicy policy;
268 {
269 QMutexLocker l(&m_mutex);
270 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
271
272 if (m_openDocumentsToUpdate.isEmpty()) {
273 if (--m_nUpdateInProgress == 0)
274 openUpdateEnd();
275 return false;
276 }
277 const auto it = m_openDocumentsToUpdate.begin();
278 toUpdate = it.key();
279 policy = it.value();
280 m_openDocumentsToUpdate.erase(it);
281 }
282 bool hasMore = false;
283 {
284 auto guard = qScopeGuard([this, &hasMore]() {
285 QMutexLocker l(&m_mutex);
286 if (m_openDocumentsToUpdate.isEmpty()) {
287 if (--m_nUpdateInProgress == 0)
288 openUpdateEnd();
289 hasMore = false;
290 } else {
291 hasMore = true;
292 }
293 });
294 openUpdate(toUpdate, policy);
295 }
296 return hasMore;
297}
298
299void QQmlCodeModel::openUpdateStart()
300{
301 qCDebug(codeModelLog) << "openUpdateStart";
302}
303
304void QQmlCodeModel::openUpdateEnd()
305{
306 qCDebug(codeModelLog) << "openUpdateEnd";
307}
308
310{
311 QStringList result;
312
313 std::vector<QByteArray> urls = { m_rootUrl };
314 {
315 QMutexLocker guard(&m_mutex);
316 for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
317 urls.push_back(it.key());
318 }
319
320 for (const auto &url : urls)
321 result.append(buildPathsForFileUrl(url));
322
323 // remove duplicates
324 std::sort(result.begin(), result.end());
325 result.erase(std::unique(result.begin(), result.end()), result.end());
326 return result;
327}
328
329static int cmakeJobsFromSettings(QQmlToolingSharedSettings *settings, const QString &rootPath,
330 int defaultValue)
331{
332 if (!settings)
333 return defaultValue;
334 const auto result = settings->search(rootPath);
335 if (!result.isValid())
336 return defaultValue;
337
338 bool ok = false;
339 const QString valueString = settings->value("CMakeJobs"_L1).toString();
340 if (valueString == QQmlCodeModel::s_maxCMakeJobs)
341 return QThread::idealThreadCount();
342
343 const int cmakeJobs = settings->value("CMakeJobs"_L1).toInt(&ok);
344 if (!ok || cmakeJobs < 1)
345 return defaultValue;
346 return cmakeJobs;
347}
348
349void QQmlCodeModel::callCMakeBuild(QProcessScheduler *scheduler)
350{
351 const QStringList buildPaths = buildPathsForOpenedFiles();
352 const int cmakeJobs = cmakeJobsFromSettings(m_settings, url2Path(m_rootUrl), m_cmakeJobs);
353
354 QList<QProcessScheduler::Command> commands;
355 for (const auto &path : buildPaths) {
356 if (!QFileInfo::exists(path + u"/.cmake"_s))
357 continue;
358
359 auto [program, arguments] = QQmlLSUtils::cmakeBuildCommand(path, cmakeJobs);
360 commands.append({ std::move(program), std::move(arguments) });
361 }
362 if (commands.isEmpty())
363 return;
364 scheduler->schedule(commands, m_rootUrl);
365}
366
367void QQmlCodeModel::onCMakeProcessFinished(const QByteArray &id)
368{
369 if (id != m_rootUrl)
370 return;
371
372 QMutexLocker guard(&m_mutex);
373 for (auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
374 m_openDocumentsToUpdate[it.key()] = ForceUpdate;
375 guard.unlock();
376 openNeedUpdate();
377}
378
379/*!
380\internal
381Iterate the entire source directory to find all C++ files that have their names in fileNames, and
382return all the found file paths.
383
384This is an overapproximation and might find unrelated files with the same name.
385*/
386QStringList QQmlCodeModel::findFilePathsFromFileNames(const QStringList &_fileNamesToSearch,
387 const QSet<QString> &ignoredFilePaths)
388{
389 Q_ASSERT(!m_rootUrl.isEmpty());
390 QStringList fileNamesToSearch{ _fileNamesToSearch };
391
392 {
393 QMutexLocker guard(&m_mutex);
394 // ignore files that were not found last time
395 fileNamesToSearch.erase(std::remove_if(fileNamesToSearch.begin(), fileNamesToSearch.end(),
396 [this](const QString &fileName) {
397 return m_ignoreForWatching.contains(fileName);
398 }),
399 fileNamesToSearch.end());
400 }
401
402 const QString rootDir = QUrl(QString::fromUtf8(m_rootUrl)).toLocalFile();
403 qCDebug(codeModelLog) << "Searching for files to watch in workspace folder" << rootDir;
404
405 const QStringList result =
406 QQmlLSUtils::findFilePathsFromFileNames(rootDir, fileNamesToSearch, ignoredFilePaths);
407
408 QMutexLocker guard(&m_mutex);
409 for (const auto &fileName : fileNamesToSearch) {
410 if (std::none_of(result.begin(), result.end(),
411 [&fileName](const QString &path) { return path.endsWith(fileName); })) {
412 m_ignoreForWatching.insert(fileName);
413 }
414 }
415 return result;
416}
417
418/*!
419\internal
420Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result
421of this method) that this qmlFile relies on.
422*/
423QStringList QQmlCodeModel::fileNamesToWatch(const DomItem &qmlFile)
424{
425 const QmlFile *file = qmlFile.as<QmlFile>();
426 if (!file)
427 return {};
428
429 auto resolver = file->typeResolver();
430 if (!resolver)
431 return {};
432
433 auto types = resolver->importedTypes();
434
435 QStringList result;
436 for (const auto &type : types) {
437 if (!type.scope)
438 continue;
439 // note: the factory only loads composite types
440 const bool isComposite = type.scope.factory() || type.scope->isComposite();
441 if (isComposite)
442 continue;
443
444 const QString filePath = QFileInfo(type.scope->filePath()).fileName();
445 if (!filePath.isEmpty())
446 result << filePath;
447 }
448
449 std::sort(result.begin(), result.end());
450 result.erase(std::unique(result.begin(), result.end()), result.end());
451
452 return result;
453}
454
455/*!
456\internal
457Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they
458are modified. Is a no op if this is the fallback codemodel with empty root url.
459*/
460void QQmlCodeModel::addFileWatches(const DomItem &qmlFile)
461{
462 if (m_rootUrl.isEmpty())
463 return;
464 const auto filesToWatch = fileNamesToWatch(qmlFile);
465
466 // remove already watched files to avoid a warning later on
467 const QStringList alreadyWatchedFiles = m_cppFileWatcher.files();
468 const QSet<QString> alreadyWatchedFilesSet(alreadyWatchedFiles.begin(),
469 alreadyWatchedFiles.end());
470 QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch, alreadyWatchedFilesSet);
471
472 if (filepathsToWatch.isEmpty())
473 return;
474
475 QMutexLocker guard(&m_mutex);
476 const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
477 if (!unwatchedPaths.isEmpty()) {
478 qCDebug(codeModelLog) << "Cannot watch paths" << unwatchedPaths << "from requested"
479 << filepathsToWatch;
480 }
481}
482
489
494
496{
497 if (!doc.textDocument)
498 return ClosedDocument;
499
500 {
501 QMutexLocker guard2(doc.textDocument->mutex());
502 if (doc.textDocument->version() && *doc.textDocument->version() > version)
504 }
505
506 if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= version)
508
509 return VersionOk;
510}
511
513 int version)
514{
515 if (doc.snapshot.validDocVersion && *doc.snapshot.validDocVersion >= version)
517
519}
520
521static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem,
522 const QByteArray &url, OpenDocument *doc, int version,
523 UpdatePolicy policy)
524{
525 switch (policy == ForceUpdate ? VersionOk : checkVersion(*doc, version)) {
526 case ClosedDocument:
527 qCWarning(lspServerLog) << "Ignoring update to closed document" << QString::fromUtf8(url);
528 return;
529 case VersionLowerThanDocument:
530 qCWarning(lspServerLog) << "Version" << version << "of document" << QString::fromUtf8(url)
531 << "is not the latest anymore";
532 return;
533 case VersionLowerThanSnapshot:
534 qCWarning(lspServerLog) << "Skipping update of current doc to obsolete version" << version
535 << "of document" << QString::fromUtf8(url);
536 return;
537 case VersionOk:
538 doc->snapshot.docVersion = version;
539 doc->snapshot.doc = item;
540 break;
541 }
542
543 if (!item.field(Fields::isValid).value().toBool(false)) {
544 qCWarning(lspServerLog) << "avoid update of validDoc to " << version << "of document"
545 << QString::fromUtf8(url) << "as it is invalid";
546 return;
547 }
548
549 switch (policy == ForceUpdate ? VersionOkForValidDocument
550 : checkVersionForValidDocument(*doc, version)) {
551 case VersionLowerThanValidSnapshot:
552 qCWarning(lspServerLog) << "Skipping update of valid doc to obsolete version" << version
553 << "of document" << QString::fromUtf8(url);
554 return;
556 doc->snapshot.validDocVersion = version;
557 doc->snapshot.validDoc = validItem;
558 break;
559 }
560}
561
562void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText,
563 QmlLsp::UpdatePolicy policy)
564{
565 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
566 qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "("
567 << docText.size() << "chars)";
568
569 const QString fPath = url2Path(url, UrlLookup::ForceLookup);
570 DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
571
572 // if the documentation root path is not set through the commandline,
573 // try to set it from the settings file (.qmlls.ini file)
574 if (documentationRootPath().isEmpty() && m_settings) {
575 // note: settings already searched current file in importPathsForFile() call above
576 const QString docDir = QStringLiteral(u"docDir");
577 if (m_settings->isSet(docDir))
578 setDocumentationRootPath(m_settings->value(docDir).toString());
579 }
580
581 Path p;
582 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
583 newCurrentPtr->setLoadPaths(importPathsForUrl(url));
584 newCurrentPtr->setResourceFiles(m_resourceFiles);
585 newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
586 [&p, this](Path, const DomItem &, const DomItem &newValue) {
587 const DomItem file = newValue.fileObject();
588 p = file.canonicalPath();
589 // Force population of the file by accessing isValid field. We
590 // don't want to populate the file after adding the file to the
591 // snapshot in updateItemInSnapshot.
592 file.field(Fields::isValid);
593 if (cmakeStatus() == HasCMake)
594 addFileWatches(file);
595 });
596 newCurrentPtr->loadPendingDependencies();
597 if (p) {
598 newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
599 QMutexLocker l(&m_mutex);
600 updateItemInSnapshot(m_currentEnv.path(p), m_validEnv.path(p), url, &m_openDocuments[url],
601 version, policy);
602 }
603 if (codeModelLog().isDebugEnabled()) {
604 qCDebug(codeModelLog) << "Finished update doc of " << url << "to version" << version;
605 snapshotByUrl(url).dump(qDebug() << "postSnapshot",
606 OpenDocumentSnapshot::DumpOption::AllCode);
607 }
608 // we should update the scope in the future thus call addOpen(url)
609 emit updatedSnapshot(url, policy);
610}
611
612void QQmlCodeModel::closeOpenFile(const QByteArray &url)
613{
614 QMutexLocker l(&m_mutex);
615 m_openDocuments.remove(url);
616}
617
619{
620 QMutexLocker l(&m_mutex);
621 return m_rootUrl;
622}
623
625{
626 QMutexLocker l(&m_mutex);
627 return m_buildPaths;
628}
629
631{
632 QStringList result = importPaths();
633
634 // fallback for projects targeting Qt < 6.10, that don't have .qmlls.build.ini files
635 if (result.isEmpty() || result == QLibraryInfo::paths(QLibraryInfo::QmlImportsPath))
636 result << buildPathsForFileUrl(url);
637
638 const QString importPathsKey = u"importPaths"_s;
639 const QString fileName = url2Path(url);
640 if (m_settings && m_settings->search(fileName, { QString(), verbose() }).isValid()
641 && m_settings->isSet(importPathsKey)) {
642 result.append(m_settings->valueAsAbsolutePathList(importPathsKey, fileName));
643 }
644 return result;
645}
646
647void QQmlCodeModel::setImportPaths(const QStringList &importPaths)
648{
649 QMutexLocker guard(&m_mutex);
650 m_importPaths = importPaths;
651
652 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
653 env->setLoadPaths(importPaths);
654 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
655 env->setLoadPaths(importPaths);
656}
657
659{
660 QMutexLocker guard(&m_mutex);
661 return m_resourceFiles;
662}
663
664void QQmlCodeModel::setResourceFiles(const QStringList &resourceFiles)
665{
666 QMutexLocker guard(&m_mutex);
667 m_resourceFiles = resourceFiles;
668
669 if (const auto &env = m_currentEnv.ownerAs<DomEnvironment>())
670 env->setResourceFiles(resourceFiles);
671 if (const auto &env = m_validEnv.ownerAs<DomEnvironment>())
672 env->setResourceFiles(resourceFiles);
673}
674
676{
677 QMutexLocker guard(&m_mutex);
678 return m_importPaths;
679}
680
681static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
682{
683 // add dependent build directories
684 QStringList res;
685 std::reverse(buildPaths.begin(), buildPaths.end());
686 const int maxDeps = 4;
687 while (!buildPaths.isEmpty()) {
688 res += buildPaths.takeLast();
689 const QString &bPath = res.constLast();
690 const QString bPathExtended = bPath + u"/_deps";
691 if (QFile::exists(bPathExtended) && bPath.count(u"/_deps/"_s) < maxDeps) {
692 for (const auto &fileInfo :
693 QDirListing{ bPathExtended, QDirListing::IteratorFlag::DirsOnly }) {
694 buildPaths.append(fileInfo.absoluteFilePath());
695 }
696 }
697 }
698 return res;
699}
700
702{
703 if (QStringList result = buildPaths(); !result.isEmpty())
704 return withDependentBuildDirectories(std::move(result));
705
706 // fallback: look in the user settings (.qmlls.ini files in the source directory)
707 if (!m_settings || !m_settings->search(url2Path(url), { QString(), verbose() }).isValid())
708 return {};
709
710 constexpr QLatin1String buildDir = "buildDir"_L1;
711 if (!m_settings->isSet(buildDir))
712 return {};
713
714 const QString fileName = url2Path(url);
715 return withDependentBuildDirectories(m_settings->valueAsAbsolutePathList(buildDir, fileName));
716}
717
718void QQmlCodeModel::setDocumentationRootPath(const QString &path)
719{
720 {
721 QMutexLocker l(&m_mutex);
722 if (m_documentationRootPath == path)
723 return;
724 m_documentationRootPath = path;
725 }
726 m_helpManager.setDocumentationRootPath(path);
727}
728
729void QQmlCodeModel::setBuildPaths(const QStringList &paths)
730{
731 QMutexLocker l(&m_mutex);
732 m_buildPaths = paths;
733}
734
735void QQmlCodeModel::openUpdate(const QByteArray &url, UpdatePolicy policy)
736{
737 std::optional<int> rNow = 0;
738 QString docText;
739
740 {
741 QMutexLocker l(&m_mutex);
742 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
743 OpenDocument &doc = m_openDocuments[url];
744 std::shared_ptr<Utils::TextDocument> document = doc.textDocument;
745 if (!document)
746 return;
747 {
748 QMutexLocker l2(document->mutex());
749 rNow = document->version();
750 }
751 if (!rNow
752 || (policy != ForceUpdate && doc.snapshot.docVersion
753 && *doc.snapshot.docVersion == *rNow)) {
754 return;
755 }
756
757 {
758 QMutexLocker l2(doc.textDocument->mutex());
759 rNow = doc.textDocument->version();
760 docText = doc.textDocument->toPlainText();
761 }
762 }
763 newDocForOpenFile(url, *rNow, docText, policy);
764}
765
766void QQmlCodeModel::addOpenToUpdate(const QByteArray &url, UpdatePolicy policy)
767{
768 {
769 QMutexLocker l(&m_mutex);
770 m_openDocumentsToUpdate[url] = policy;
771 }
772 openNeedUpdate();
773}
774
775QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options)
776{
777 dbg.noquote().nospace() << "{";
778 dbg << " url:" << QString::fromUtf8(url) << "\n";
779 dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) << "\n";
780 if (options & DumpOption::LatestCode) {
781 dbg << " doc: ------------\n"
782 << doc.field(Fields::code).value().toString() << "\n==========\n";
783 } else {
784 dbg << u" doc:"
785 << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size())
786 : u"*none*"_s)
787 << "\n";
788 }
789 dbg << " validDocVersion:"
790 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) << "\n";
791 if (options & DumpOption::ValidCode) {
792 dbg << " validDoc: ------------\n"
793 << validDoc.field(Fields::code).value().toString() << "\n==========\n";
794 } else {
795 dbg << u" validDoc:"
796 << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
797 : u"*none*"_s)
798 << "\n";
799 }
800 dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n";
801 dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n";
802 dbg << "}";
803 return dbg;
804}
805
806static ModuleSetting *moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings,
807 UpdatePolicy policy)
808{
809 if (policy != ForceUpdate)
810 return &moduleSettings->emplaceBack();
811
812 auto it = std::find_if(
813 moduleSettings->begin(), moduleSettings->end(),
814 [&sourceFolder](const ModuleSetting s) { return s.sourceFolder == sourceFolder; });
815 if (it == moduleSettings->end())
816 return &moduleSettings->emplaceBack();
817 return &*it;
818}
819
820void QQmllsBuildInformation::loadSettingsFrom(const QStringList &buildPaths, UpdatePolicy policy)
821{
822#if QT_CONFIG(settings)
823 for (const QString &path : buildPaths) {
824 if (policy != ForceUpdate && m_seenSettings.contains(path))
825 continue;
826 m_seenSettings.insert(path);
827
828 const QString iniPath = QString(path).append("/.qt/.qmlls.build.ini"_L1);
829 if (!QFile::exists(iniPath))
830 continue;
831
832 QSettings settings(iniPath, QSettings::IniFormat);
833 m_docDir = settings.value("docDir"_L1).toString();
834 const qsizetype version = settings.value("version"_L1, "1"_L1).toString().toInt();
835 switch (version) {
836 case 2: {
837 const int entries = settings.beginReadArray("workspaces"_L1);
838 for (int i = 0; i < entries; ++i) {
839 settings.setArrayIndex(i);
840
841 const QString sourceFolder = settings.value("sourcePath").toString();
842 ModuleSetting *moduleSetting =
843 moduleSettingFor(sourceFolder, &m_moduleSettings, policy);
844 moduleSetting->sourceFolder = sourceFolder;
845 moduleSetting->importPaths =
846 settings.value("importPaths"_L1)
847 .toString()
848 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
849 moduleSetting->resourceFiles =
850 settings.value("resourceFiles"_L1)
851 .toString()
852 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
853 }
854 settings.endArray();
855 break;
856 }
857 case 1:
858 default: {
859 for (const QString &group : settings.childGroups()) {
860 settings.beginGroup(group);
861
862 const QString sourceFolder = QString(group).replace("<SLASH>"_L1, "/"_L1);
863 ModuleSetting *moduleSetting =
864 moduleSettingFor(sourceFolder, &m_moduleSettings, policy);
865 moduleSetting->sourceFolder = sourceFolder;
866 moduleSetting->importPaths =
867 settings.value("importPaths"_L1)
868 .toString()
869 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
870 moduleSetting->resourceFiles =
871 settings.value("resourceFiles"_L1)
872 .toString()
873 .split(QDir::listSeparator(), Qt::SkipEmptyParts);
874 settings.endGroup();
875 }
876 break;
877 }
878 }
879 }
880#else
881 Q_UNUSED(buildPaths);
882#endif
883}
884
886{
887 return settingFor(filePath).importPaths;
888}
889
891{
892 return settingFor(filePath).resourceFiles;
893}
894
896{
897 ModuleSetting result;
898 qsizetype longestMatch = 0;
899 for (const ModuleSetting &setting : m_moduleSettings) {
900 const qsizetype matchLength = setting.sourceFolder.size();
901 if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
902 result = setting;
903 longestMatch = matchLength;
904 }
905 }
906 QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
907 return result;
908}
909
911{
912 m_moduleSettings.append(moduleSetting);
913}
914
915void QQmllsBuildInformation::writeQmllsBuildIniContent(const QString &file) const
916{
917#if QT_CONFIG(settings)
918 QSettings settings(file, QSettings::IniFormat);
919 settings.setValue("docDir"_L1, m_docDir);
920 settings.setValue("version"_L1, "2"_L1);
921 settings.beginWriteArray("workspaces"_L1, m_moduleSettings.size());
922 for (int i = 0; i < m_moduleSettings.size(); ++i) {
923 settings.setArrayIndex(i);
924 settings.setValue("sourcePath"_L1, m_moduleSettings[i].sourceFolder);
925 settings.setValue("importPaths"_L1,
926 m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
927 settings.setValue("resourceFiles"_L1,
928 m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
929 }
930 settings.endArray();
931
932#else
933 Q_UNUSED(file);
934#endif
935}
936
938
939} // namespace QmlLsp
940
941QT_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)