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>
17#if QT_CONFIG(settings)
18# include <QtCore/qsettings.h>
21#include <QtQmlDom/private/qqmldomtop_p.h>
22#include <QtQmlCompiler/private/qqmljsutils_p.h>
31Q_STATIC_LOGGING_CATEGORY(
codeModelLog,
"qt.languageserver.codemodel")
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
91 QQmlToolingSharedSettings *settings)
105
106
107
111 if (cmakeStatus() == DoesNotHaveCMake)
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()) {
123 QObject::connect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged, scheduler,
124 [
this, scheduler] { callCMakeBuild(scheduler); });
125 setCMakeStatus(HasCMake);
126 callCMakeBuild(scheduler);
130
131
132
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);
147 QMutexLocker l(&m_mutex);
148 m_openDocumentsToUpdate.clear();
150 shouldWait = m_nUpdateInProgress != 0;
154 QThread::yieldCurrentThread();
160 QObject::disconnect(&m_cppFileWatcher, &QFileSystemWatcher::fileChanged,
nullptr,
nullptr);
166 return openDocumentByUrl(url).snapshot;
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);
178QString
QQmlCodeModel::url2Path(
const QByteArray &url, UrlLookup options)
182 QMutexLocker l(&m_mutex);
183 res = m_url2path.value(url);
185 if (!res.isEmpty() && options == UrlLookup::Caching)
187 QUrl qurl(QString::fromUtf8(url));
188 QFileInfo f(qurl.toLocalFile());
189 QString cPath = f.canonicalFilePath();
191 cPath = f.filePath();
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);
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);
213 addOpenToUpdate(url, NormalUpdate);
219 QMutexLocker l(&m_mutex);
220 return m_openDocuments.value(url);
225 QMutexLocker l(&m_mutex);
226 return m_openDocuments.isEmpty();
231 QMutexLocker l(&m_mutex);
237 QMutexLocker l(&m_mutex);
243 qCDebug(codeModelLog) <<
"openNeedUpdate";
244 const int maxThreads = 1;
246 QMutexLocker l(&m_mutex);
247 if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxThreads
248 || m_state == State::Stopping) {
251 if (++m_nUpdateInProgress == 1)
254 QThreadPool::globalInstance()->start([
this]() {
255 QScopedValueRollback thread(m_openUpdateThread, QThread::currentThread());
256 while (openUpdateSome()) { }
257 emit openUpdateThreadFinished();
263 qCDebug(codeModelLog) <<
"openUpdateSome start";
267 QMutexLocker l(&m_mutex);
268 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
270 if (m_openDocumentsToUpdate.isEmpty()) {
271 if (--m_nUpdateInProgress == 0)
275 const auto it = m_openDocumentsToUpdate.begin();
278 m_openDocumentsToUpdate.erase(it);
280 bool hasMore =
false;
282 auto guard = qScopeGuard([
this, &hasMore]() {
283 QMutexLocker l(&m_mutex);
284 if (m_openDocumentsToUpdate.isEmpty()) {
285 if (--m_nUpdateInProgress == 0)
292 openUpdate(toUpdate, policy);
299 qCDebug(codeModelLog) <<
"openUpdateStart";
304 qCDebug(codeModelLog) <<
"openUpdateEnd";
311 std::vector<QByteArray> urls = { m_rootUrl };
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());
318 for (
const auto &url : urls)
319 result.append(buildPathsForFileUrl(url));
322 std::sort(result.begin(), result.end());
323 result.erase(std::unique(result.begin(), result.end()), result.end());
332 const auto result = settings->search(rootPath);
333 if (!result.isValid())
337 const QString valueString = settings->value(
"CMakeJobs"_L1).toString();
338 if (valueString == QQmlCodeModel::s_maxCMakeJobs)
339 return QThread::idealThreadCount();
341 const int cmakeJobs = settings->value(
"CMakeJobs"_L1).toInt(&ok);
342 if (!ok || cmakeJobs < 1)
347void QQmlCodeModel::callCMakeBuild(QProcessScheduler *scheduler)
349 const QStringList buildPaths = buildPathsForOpenedFiles();
350 const int cmakeJobs = cmakeJobsFromSettings(m_settings, url2Path(m_rootUrl), m_cmakeJobs);
352 QList<QProcessScheduler::Command> commands;
353 for (
const auto &path : buildPaths) {
354 if (!QFileInfo::exists(path + u"/.cmake"_s))
357 auto [program, arguments] = QQmlLSUtils::cmakeBuildCommand(path, cmakeJobs);
358 commands.append({ std::move(program), std::move(arguments) });
360 if (commands.isEmpty())
362 qCInfo(codeModelLog) <<
"Starting background build on paths" << buildPaths.join(
", "_L1);
363 scheduler->schedule(commands, m_rootUrl.isEmpty() ?
"Default workspace"_ba : m_rootUrl);
368 QMutexLocker guard(&m_mutex);
369 for (
auto it = m_openDocuments.begin(), end = m_openDocuments.end(); it != end; ++it)
370 m_openDocumentsToUpdate[it.key()] = ForceUpdate;
376
377
378
379
380
381
383 const QSet<QString> &ignoredFilePaths)
385 Q_ASSERT(!m_rootUrl.isEmpty());
386 QStringList fileNamesToSearch{ _fileNamesToSearch };
389 QMutexLocker guard(&m_mutex);
391 fileNamesToSearch.erase(std::remove_if(fileNamesToSearch.begin(), fileNamesToSearch.end(),
392 [
this](
const QString &fileName) {
393 return m_ignoreForWatching.contains(fileName);
395 fileNamesToSearch.end());
398 const QString rootDir = QUrl(QString::fromUtf8(m_rootUrl)).toLocalFile();
399 qCDebug(codeModelLog) <<
"Searching for files to watch in workspace folder" << rootDir;
401 const QStringList result =
402 QQmlLSUtils::findFilePathsFromFileNames(rootDir, fileNamesToSearch, ignoredFilePaths);
404 QMutexLocker guard(&m_mutex);
405 for (
const auto &fileName : fileNamesToSearch) {
406 if (std::none_of(result.begin(), result.end(),
407 [&fileName](
const QString &path) {
return path.endsWith(fileName); })) {
408 m_ignoreForWatching.insert(fileName);
415
416
417
418
421 const QmlFile *file = qmlFile.as<QmlFile>();
425 auto resolver = file->typeResolver();
429 auto types = resolver->importedTypes();
432 for (
const auto &type : types) {
436 const bool isComposite = type.scope.factory() || type.scope->isComposite();
440 const QString filePath = QFileInfo(type.scope->filePath()).fileName();
441 if (!filePath.isEmpty())
445 std::sort(result.begin(), result.end());
446 result.erase(std::unique(result.begin(), result.end()), result.end());
452
453
454
455
458 if (m_rootUrl.isEmpty())
460 const auto filesToWatch = fileNamesToWatch(qmlFile);
463 const QStringList alreadyWatchedFiles = m_cppFileWatcher.files();
464 const QSet<QString> alreadyWatchedFilesSet(alreadyWatchedFiles.begin(),
465 alreadyWatchedFiles.end());
466 QStringList filepathsToWatch = findFilePathsFromFileNames(filesToWatch, alreadyWatchedFilesSet);
468 if (filepathsToWatch.isEmpty())
471 QMutexLocker guard(&m_mutex);
472 const auto unwatchedPaths = m_cppFileWatcher.addPaths(filepathsToWatch);
473 if (!unwatchedPaths.isEmpty()) {
474 qCDebug(codeModelLog) <<
"Cannot watch paths" << unwatchedPaths <<
"from requested"
493 if (!doc.textDocument)
497 QMutexLocker guard2(doc.textDocument->mutex());
498 if (doc.textDocument->version() && *doc.textDocument->version() > version)
502 if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= version)
511 if (doc.snapshot.validDocVersion && *doc.snapshot.validDocVersion >= version)
523 qCWarning(lspServerLog) <<
"Ignoring update to closed document" << QString::fromUtf8(url);
525 case VersionLowerThanDocument:
526 qCWarning(lspServerLog) <<
"Version" << version <<
"of document" << QString::fromUtf8(url)
527 <<
"is not the latest anymore";
529 case VersionLowerThanSnapshot:
530 qCWarning(lspServerLog) <<
"Skipping update of current doc to obsolete version" << version
531 <<
"of document" << QString::fromUtf8(url);
534 doc->snapshot.docVersion = version;
535 doc->snapshot.doc = item;
539 if (!item.field(Fields::isValid).value().toBool(
false)) {
540 qCWarning(lspServerLog) <<
"avoid update of validDoc to " << version <<
"of document"
541 << QString::fromUtf8(url) <<
"as it is invalid";
547 case VersionLowerThanValidSnapshot:
548 qCWarning(lspServerLog) <<
"Skipping update of valid doc to obsolete version" << version
549 <<
"of document" << QString::fromUtf8(url);
552 doc->snapshot.validDocVersion = version;
553 doc->snapshot.validDoc = validItem;
558void QQmlCodeModel::newDocForOpenFile(
const QByteArray &url,
int version,
const QString &docText,
561 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
562 qCDebug(codeModelLog) <<
"updating doc" << url <<
"to version" << version <<
"("
563 << docText.size() <<
"chars)";
565 const QString fPath = url2Path(url, UrlLookup::ForceLookup);
566 DomItem newCurrent = m_currentEnv.makeCopy(DomItem::CopyOption::EnvConnected).item();
570 if (documentationRootPath().isEmpty() && m_settings) {
572 const QString docDir = QStringLiteral(u"docDir");
573 if (m_settings->isSet(docDir))
574 setDocumentationRootPath(m_settings->value(docDir).toString());
578 auto newCurrentPtr = newCurrent.ownerAs<DomEnvironment>();
579 newCurrentPtr->setLoadPaths(importPathsForUrl(url));
580 newCurrentPtr->setResourceFiles(m_resourceFiles);
581 newCurrentPtr->loadFile(FileToLoad::fromMemory(newCurrentPtr, fPath, docText),
582 [&p,
this](Path,
const DomItem &,
const DomItem &newValue) {
583 const DomItem file = newValue.fileObject();
584 p = file.canonicalPath();
588 file.field(Fields::isValid);
589 if (cmakeStatus() == HasCMake)
590 addFileWatches(file);
592 newCurrentPtr->loadPendingDependencies();
594 newCurrent.commitToBase(m_validEnv.ownerAs<DomEnvironment>());
595 QMutexLocker l(&m_mutex);
596 updateItemInSnapshot(m_currentEnv.path(p), m_validEnv.path(p), url, &m_openDocuments[url],
599 if (codeModelLog().isDebugEnabled()) {
600 qCDebug(codeModelLog) <<
"Finished update doc of " << url <<
"to version" << version;
601 snapshotByUrl(url).dump(qDebug() <<
"postSnapshot");
604 emit updatedSnapshot(url, policy);
609 QMutexLocker l(&m_mutex);
610 m_openDocuments.remove(url);
615 QMutexLocker l(&m_mutex);
621 QMutexLocker l(&m_mutex);
627 QStringList result = importPaths();
630 if (result.isEmpty() || result == QLibraryInfo::paths(QLibraryInfo::QmlImportsPath))
631 result << buildPathsForFileUrl(url);
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));
644 QMutexLocker guard(&m_mutex);
645 m_importPaths = importPaths;
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);
655 QMutexLocker guard(&m_mutex);
656 return m_resourceFiles;
661 QMutexLocker guard(&m_mutex);
662 m_resourceFiles = resourceFiles;
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);
672 QMutexLocker guard(&m_mutex);
673 return m_importPaths;
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());
698 if (QStringList result = buildPaths(); !result.isEmpty())
699 return withDependentBuildDirectories(std::move(result));
702 if (!m_settings || !m_settings->search(url2Path(url), { QString(), verbose() }).isValid())
705 constexpr QLatin1String buildDir =
"buildDir"_L1;
706 if (!m_settings->isSet(buildDir))
709 const QString fileName = url2Path(url);
710 return withDependentBuildDirectories(m_settings->valueAsAbsolutePathList(buildDir, fileName));
716 QMutexLocker l(&m_mutex);
717 if (m_documentationRootPath == path)
719 m_documentationRootPath = path;
721 m_helpManager.setDocumentationRootPath(path);
726 QMutexLocker l(&m_mutex);
727 m_buildPaths = paths;
732 std::optional<
int> rNow = 0;
736 QMutexLocker l(&m_mutex);
737 Q_ASSERT(QThread::currentThread() == m_openUpdateThread);
739 std::shared_ptr<Utils::TextDocument> document = doc.textDocument;
743 QMutexLocker l2(document->mutex());
744 rNow = document->version();
747 || (policy != ForceUpdate && doc.snapshot.docVersion
748 && *doc.snapshot.docVersion == *rNow)) {
753 QMutexLocker l2(doc.textDocument->mutex());
754 rNow = doc.textDocument->version();
755 docText = doc.textDocument->toPlainText();
758 newDocForOpenFile(url, *rNow, docText, policy);
764 QMutexLocker l(&m_mutex);
765 m_openDocumentsToUpdate[url] = policy;
772 dbg.noquote().nospace() <<
"{";
773 dbg <<
" url:" << QString::fromUtf8(url) <<
"\n";
774 dbg <<
" docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s) <<
"\n";
776 << (doc ? u"%1chars"_s.arg(doc.field(Fields::code).value().toString().size()) : u"*none*"_s)
778 dbg <<
" validDocVersion:"
779 << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s) <<
"\n";
781 << (validDoc ? u"%1chars"_s.arg(validDoc.field(Fields::code).value().toString().size())
792 return &moduleSettings->emplaceBack(
ModuleSetting {sourceFolder, {}, {}});
794 auto it = std::find_if(
795 moduleSettings->begin(), moduleSettings->end(),
796 [&sourceFolder](
const ModuleSetting s) {
return s.sourceFolder == sourceFolder; });
797 if (it == moduleSettings->end())
798 return &moduleSettings->emplaceBack(
ModuleSetting {sourceFolder, {}, {}});
804 return variant.toString().split(QDir::listSeparator(), Qt::SkipEmptyParts);
811 static constexpr QLatin1String sourcePathKey =
"workspaces/0/sourcePath"_L1;
812 if (!settings->contains(sourcePathKey))
816 moduleSettingFor(settings->value(sourcePathKey).toString(), moduleSettings, policy);
817 moduleSetting->importPaths = asStringList(settings->value(
"workspaces/0/importPaths"_L1));
818 moduleSetting->resourceFiles = asStringList(settings->value(
"workspaces/0/resourceFiles"_L1));
824 handleArrayStartingAt0(settings, moduleSettings, policy);
826 const int entries = settings->beginReadArray(
"workspaces"_L1);
827 for (
int i = 0; i < entries; ++i) {
828 settings->setArrayIndex(i);
830 if (!settings->contains(
"sourcePath"_L1))
834 moduleSettingFor(settings->value(
"sourcePath").toString(), moduleSettings, policy);
835 moduleSetting->importPaths = asStringList(settings->value(
"importPaths"_L1));
836 moduleSetting->resourceFiles = asStringList(settings->value(
"resourceFiles"_L1));
838 settings->endArray();
843#if QT_CONFIG(settings)
844 for (
const QString &path : buildPaths) {
845 if (policy != ForceUpdate && m_seenSettings.contains(path))
847 m_seenSettings.insert(path);
849 const QString iniPath = QString(path).append(
"/.qt/.qmlls.build.ini"_L1);
850 if (!QFile::exists(iniPath))
853 QSettings settings(iniPath, QSettings::IniFormat);
854 m_docDir = settings.value(
"docDir"_L1).toString();
855 const qsizetype version = settings.value(
"version"_L1,
"1"_L1).toString().toInt();
858 loadWorkspacesV2(&settings, &m_moduleSettings, policy);
863 for (
const QString &group : settings.childGroups()) {
864 settings.beginGroup(group);
866 ModuleSetting *moduleSetting = moduleSettingFor(
867 QString(group).replace(
"<SLASH>"_L1,
"/"_L1), &m_moduleSettings, policy);
868 moduleSetting->importPaths = asStringList(settings.value(
"importPaths"_L1));
869 moduleSetting->resourceFiles = asStringList(settings.value(
"resourceFiles"_L1));
877 Q_UNUSED(buildPaths);
883 return settingFor(filePath).importPaths;
888 return settingFor(filePath).resourceFiles;
894 qsizetype longestMatch = 0;
895 for (
const ModuleSetting &setting : m_moduleSettings) {
896 const qsizetype matchLength = setting.sourceFolder.size();
897 if (filePath.startsWith(setting.sourceFolder) && matchLength > longestMatch) {
899 longestMatch = matchLength;
902 QQmlToolingSettings::resolveRelativeImportPaths(filePath, &result.importPaths);
908 m_moduleSettings.append(moduleSetting);
913#if QT_CONFIG(settings)
914 QSettings settings(file, QSettings::IniFormat);
915 settings.setValue(
"docDir"_L1, m_docDir);
916 settings.setValue(
"version"_L1,
"2"_L1);
917 settings.beginWriteArray(
"workspaces"_L1, m_moduleSettings.size());
918 for (
int i = 0; i < m_moduleSettings.size(); ++i) {
919 settings.setArrayIndex(i);
920 settings.setValue(
"sourcePath"_L1, m_moduleSettings[i].sourceFolder);
921 settings.setValue(
"importPaths"_L1,
922 m_moduleSettings[i].importPaths.join(QDir::listSeparator()));
923 settings.setValue(
"resourceFiles"_L1,
924 m_moduleSettings[i].resourceFiles.join(QDir::listSeparator()));
QDebug & dump(QDebug &dbg)
void newOpenFile(const QByteArray &url, int version, const QString &docText)
void reloadAllOpenFiles()
void addOpenToUpdate(const QByteArray &, UpdatePolicy policy)
OpenDocumentSnapshot snapshotByUrl(const QByteArray &url)
void tryEnableCMakeCalls(QProcessScheduler *scheduler)
void prepareForShutdown()
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)
Combined button and popup list for selecting options.
static QStringList asStringList(const QVariant &variant)
static ModuleSetting * moduleSettingFor(const QString &sourceFolder, ModuleSettings *moduleSettings, UpdatePolicy policy)
VersionCheckResult checkVersion(const OpenDocument &doc, int version)
static QStringList withDependentBuildDirectories(QStringList &&buildPaths)
VersionCheckResultForValidDocument
@ VersionLowerThanValidSnapshot
@ VersionOkForValidDocument
static VersionCheckResultForValidDocument checkVersionForValidDocument(const OpenDocument &doc, int version)
static void loadWorkspacesV2(QSettings *settings, ModuleSettings *moduleSettings, UpdatePolicy policy)
@ VersionLowerThanSnapshot
@ VersionLowerThanDocument
static void handleArrayStartingAt0(QSettings *settings, ModuleSettings *moduleSettings, UpdatePolicy policy)
static void updateItemInSnapshot(const DomItem &item, const DomItem &validItem, const QByteArray &url, OpenDocument *doc, int version, UpdatePolicy policy)
static int cmakeJobsFromSettings(QQmlToolingSharedSettings *settings, const QString &rootPath, int defaultValue)