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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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