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

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