1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "qmljsbind.h"
27 #include "qmljsconstants.h"
28 #include "qmljsfindexportedcpptypes.h"
29 #include "qmljsinterpreter.h"
30 #include "qmljsmodelmanagerinterface.h"
31 #include "qmljsplugindumper.h"
32 #include "qmljstypedescriptionreader.h"
33 #include "qmljsdialect.h"
34 #include "qmljsviewercontext.h"
35 #include "qmljsutils.h"
36
37 #include <cplusplus/cppmodelmanagerbase.h>
38 #include <utils/algorithm.h>
39 #include <utils/hostosinfo.h>
40 #include <utils/runextensions.h>
41 #include <utils/stringutils.h>
42
43 #ifdef WITH_TESTS
44 #include <extensionsystem/pluginmanager.h>
45 #endif
46
47 #include <QDir>
48 #include <QDirIterator>
49 #include <QFile>
50 #include <QFileInfo>
51 #include <QMetaObject>
52 #include <QTextDocument>
53 #include <QTextStream>
54 #include <QTimer>
55 #include <QtAlgorithms>
56 #include <QLibraryInfo>
57
58 using namespace Utils;
59
60 namespace QmlJS {
61
62 QMLJS_EXPORT Q_LOGGING_CATEGORY(qmljsLog, "qtc.qmljs.common", QtWarningMsg)
63
64 /*!
65 \class QmlJS::ModelManagerInterface
66 \brief The ModelManagerInterface class acts as an interface to the
67 global state of the QmlJS code model.
68 \sa QmlJS::Document QmlJS::Snapshot QmlJSTools::Internal::ModelManager
69
70 The ModelManagerInterface is an interface for global state and actions in
71 the QmlJS code model. It is implemented by \l{QmlJSTools::Internal::ModelManager}
72 and the instance can be accessed through ModelManagerInterface::instance().
73
74 One of its primary concerns is to keep the Snapshots it
75 maintains up to date by parsing documents and finding QML modules.
76
77 It has a Snapshot that contains only valid Documents,
78 accessible through ModelManagerInterface::snapshot() and a Snapshot with
79 potentially more recent, but invalid documents that is exposed through
80 ModelManagerInterface::newestSnapshot().
81 */
82
83 static ModelManagerInterface *g_instance = nullptr;
84 static QMutex g_instanceMutex;
85 static const char *qtQuickUISuffix = "ui.qml";
86
maybeAddPath(ViewerContext & context,const QString & path)87 static void maybeAddPath(ViewerContext &context, const QString &path)
88 {
89 if (!path.isEmpty() && !context.paths.contains(path))
90 context.paths.append(path);
91 }
92
environmentImportPaths()93 static QStringList environmentImportPaths()
94 {
95 QStringList paths;
96
97 const QStringList importPaths = QString::fromLocal8Bit(qgetenv("QML_IMPORT_PATH")).split(
98 Utils::HostOsInfo::pathListSeparator(), Qt::SkipEmptyParts);
99
100 for (const QString &path : importPaths) {
101 const QString canonicalPath = QDir(path).canonicalPath();
102 if (!canonicalPath.isEmpty() && !paths.contains(canonicalPath))
103 paths.append(canonicalPath);
104 }
105
106 return paths;
107 }
108
ModelManagerInterface(QObject * parent)109 ModelManagerInterface::ModelManagerInterface(QObject *parent)
110 : QObject(parent),
111 m_defaultImportPaths(environmentImportPaths()),
112 m_pluginDumper(new PluginDumper(this))
113 {
114 m_futureSynchronizer.setCancelOnWait(false);
115 m_indexerDisabled = qEnvironmentVariableIsSet("QTC_NO_CODE_INDEXER");
116
117 m_updateCppQmlTypesTimer = new QTimer(this);
118 const int second = 1000;
119 m_updateCppQmlTypesTimer->setInterval(second);
120 m_updateCppQmlTypesTimer->setSingleShot(true);
121 connect(m_updateCppQmlTypesTimer, &QTimer::timeout,
122 this, &ModelManagerInterface::startCppQmlTypeUpdate);
123
124 m_asyncResetTimer = new QTimer(this);
125 const int fifteenSeconds = 15000;
126 m_asyncResetTimer->setInterval(fifteenSeconds);
127 m_asyncResetTimer->setSingleShot(true);
128 connect(m_asyncResetTimer, &QTimer::timeout, this, &ModelManagerInterface::resetCodeModel);
129
130 qRegisterMetaType<QmlJS::Document::Ptr>("QmlJS::Document::Ptr");
131 qRegisterMetaType<QmlJS::LibraryInfo>("QmlJS::LibraryInfo");
132 qRegisterMetaType<QmlJS::Dialect>("QmlJS::Dialect");
133 qRegisterMetaType<QmlJS::PathAndLanguage>("QmlJS::PathAndLanguage");
134 qRegisterMetaType<QmlJS::PathsAndLanguages>("QmlJS::PathsAndLanguages");
135
136 m_defaultProjectInfo.qtQmlPath = QFileInfo(
137 QLibraryInfo::location(QLibraryInfo::Qml2ImportsPath)).canonicalFilePath();
138 m_defaultProjectInfo.qtVersionString = QLibraryInfo::version().toString();
139
140 updateImportPaths();
141
142 QMutexLocker locker(&g_instanceMutex);
143 Q_ASSERT(! g_instance);
144 g_instance = this;
145 }
146
~ModelManagerInterface()147 ModelManagerInterface::~ModelManagerInterface()
148 {
149 Q_ASSERT(g_instance == this);
150 m_cppQmlTypesUpdater.cancel();
151 m_cppQmlTypesUpdater.waitForFinished();
152
153 while (true) {
154 joinAllThreads(true);
155 // Keep these 2 mutexes in the same order as inside instanceForFuture()
156 QMutexLocker instanceLocker(&g_instanceMutex);
157 QMutexLocker futureLocker(&m_futuresMutex);
158 if (m_futureSynchronizer.isEmpty()) {
159 g_instance = nullptr;
160 return;
161 }
162 }
163 }
164
defaultLanguageMapping()165 static QHash<QString, Dialect> defaultLanguageMapping()
166 {
167 static QHash<QString, Dialect> res{
168 {QLatin1String("js"), Dialect::JavaScript},
169 {QLatin1String("qml"), Dialect::Qml},
170 {QLatin1String("qmltypes"), Dialect::QmlTypeInfo},
171 {QLatin1String("qmlproject"), Dialect::QmlProject},
172 {QLatin1String("json"), Dialect::Json},
173 {QLatin1String("qbs"), Dialect::QmlQbs},
174 {QLatin1String(qtQuickUISuffix), Dialect::QmlQtQuick2Ui}
175 };
176 return res;
177 }
178
guessLanguageOfFile(const QString & fileName)179 Dialect ModelManagerInterface::guessLanguageOfFile(const QString &fileName)
180 {
181 QHash<QString, Dialect> lMapping;
182 if (instance())
183 lMapping = instance()->languageForSuffix();
184 else
185 lMapping = defaultLanguageMapping();
186 const QFileInfo info(fileName);
187 QString fileSuffix = info.suffix();
188
189 /*
190 * I was reluctant to use complete suffix in all cases, because it is a huge
191 * change in behaivour. But in case of .qml this should be safe.
192 */
193
194 if (fileSuffix == QLatin1String("qml"))
195 fileSuffix = info.completeSuffix();
196
197 return lMapping.value(fileSuffix, Dialect::NoLanguage);
198 }
199
globPatternsForLanguages(const QList<Dialect> & languages)200 QStringList ModelManagerInterface::globPatternsForLanguages(const QList<Dialect> &languages)
201 {
202 QStringList patterns;
203 const QHash<QString, Dialect> lMapping =
204 instance() ? instance()->languageForSuffix() : defaultLanguageMapping();
205 for (auto i = lMapping.cbegin(), end = lMapping.cend(); i != end; ++i) {
206 if (languages.contains(i.value()))
207 patterns << QLatin1String("*.") + i.key();
208 }
209 return patterns;
210 }
211
instance()212 ModelManagerInterface *ModelManagerInterface::instance()
213 {
214 return g_instance;
215 }
216
217 // If the returned instance is not null, it's guaranteed that it will be valid at least as long
218 // as the passed QFuture object isn't finished.
instanceForFuture(const QFuture<void> & future)219 ModelManagerInterface *ModelManagerInterface::instanceForFuture(const QFuture<void> &future)
220 {
221 QMutexLocker locker(&g_instanceMutex);
222 if (g_instance)
223 g_instance->addFuture(future);
224 return g_instance;
225 }
writeWarning(const QString & msg)226 void ModelManagerInterface::writeWarning(const QString &msg)
227 {
228 if (ModelManagerInterface *i = instance())
229 i->writeMessageInternal(msg);
230 else
231 qCWarning(qmljsLog) << msg;
232 }
233
workingCopy()234 ModelManagerInterface::WorkingCopy ModelManagerInterface::workingCopy()
235 {
236 if (ModelManagerInterface *i = instance())
237 return i->workingCopyInternal();
238 return WorkingCopy();
239 }
240
activateScan()241 void ModelManagerInterface::activateScan()
242 {
243 if (!m_shouldScanImports) {
244 m_shouldScanImports = true;
245 updateImportPaths();
246 }
247 }
248
languageForSuffix() const249 QHash<QString, Dialect> ModelManagerInterface::languageForSuffix() const
250 {
251 return defaultLanguageMapping();
252 }
253
writeMessageInternal(const QString & msg) const254 void ModelManagerInterface::writeMessageInternal(const QString &msg) const
255 {
256 qCDebug(qmljsLog) << msg;
257 }
258
workingCopyInternal() const259 ModelManagerInterface::WorkingCopy ModelManagerInterface::workingCopyInternal() const
260 {
261 ModelManagerInterface::WorkingCopy res;
262 return res;
263 }
264
addTaskInternal(const QFuture<void> & result,const QString & msg,const char * taskId) const265 void ModelManagerInterface::addTaskInternal(const QFuture<void> &result, const QString &msg,
266 const char *taskId) const
267 {
268 Q_UNUSED(result)
269 qCDebug(qmljsLog) << "started " << taskId << " " << msg;
270 }
271
loadQmlTypeDescriptionsInternal(const QString & resourcePath)272 void ModelManagerInterface::loadQmlTypeDescriptionsInternal(const QString &resourcePath)
273 {
274 const QDir typeFileDir(resourcePath + QLatin1String("/qml-type-descriptions"));
275 const QStringList qmlTypesExtensions = QStringList("*.qmltypes");
276 QFileInfoList qmlTypesFiles = typeFileDir.entryInfoList(
277 qmlTypesExtensions,
278 QDir::Files,
279 QDir::Name);
280
281 QStringList errors;
282 QStringList warnings;
283
284 // filter out the actual Qt builtins
285 for (int i = 0; i < qmlTypesFiles.size(); ++i) {
286 if (qmlTypesFiles.at(i).baseName() == QLatin1String("builtins")) {
287 QFileInfoList list;
288 list.append(qmlTypesFiles.at(i));
289 CppQmlTypesLoader::defaultQtObjects =
290 CppQmlTypesLoader::loadQmlTypes(list, &errors, &warnings);
291 qmlTypesFiles.removeAt(i);
292 break;
293 }
294 }
295
296 // load the fallbacks for libraries
297 const CppQmlTypesLoader::BuiltinObjects objs =
298 CppQmlTypesLoader::loadQmlTypes(qmlTypesFiles, &errors, &warnings);
299 for (auto it = objs.cbegin(); it != objs.cend(); ++it)
300 CppQmlTypesLoader::defaultLibraryObjects.insert(it.key(), it.value());
301
302 for (const QString &error : qAsConst(errors))
303 writeMessageInternal(error);
304 for (const QString &warning : qAsConst(warnings))
305 writeMessageInternal(warning);
306 }
307
setDefaultProject(const ModelManagerInterface::ProjectInfo & pInfo,ProjectExplorer::Project * p)308 void ModelManagerInterface::setDefaultProject(const ModelManagerInterface::ProjectInfo &pInfo,
309 ProjectExplorer::Project *p)
310 {
311 QMutexLocker locker(&m_mutex);
312 m_defaultProject = p;
313 m_defaultProjectInfo = pInfo;
314 }
315
snapshot() const316 Snapshot ModelManagerInterface::snapshot() const
317 {
318 QMutexLocker locker(&m_mutex);
319 return m_validSnapshot;
320 }
321
newestSnapshot() const322 Snapshot ModelManagerInterface::newestSnapshot() const
323 {
324 QMutexLocker locker(&m_mutex);
325 return m_newestSnapshot;
326 }
327
updateSourceFiles(const QStringList & files,bool emitDocumentOnDiskChanged)328 void ModelManagerInterface::updateSourceFiles(const QStringList &files,
329 bool emitDocumentOnDiskChanged)
330 {
331 if (m_indexerDisabled)
332 return;
333 refreshSourceFiles(files, emitDocumentOnDiskChanged);
334 }
335
refreshSourceFiles(const QStringList & sourceFiles,bool emitDocumentOnDiskChanged)336 QFuture<void> ModelManagerInterface::refreshSourceFiles(const QStringList &sourceFiles,
337 bool emitDocumentOnDiskChanged)
338 {
339 if (sourceFiles.isEmpty())
340 return QFuture<void>();
341
342 QFuture<void> result = Utils::runAsync(&ModelManagerInterface::parse,
343 workingCopyInternal(), sourceFiles,
344 this, Dialect(Dialect::Qml),
345 emitDocumentOnDiskChanged);
346 addFuture(result);
347
348 if (sourceFiles.count() > 1)
349 addTaskInternal(result, tr("Parsing QML Files"), Constants::TASK_INDEX);
350
351 if (sourceFiles.count() > 1 && !m_shouldScanImports) {
352 bool scan = false;
353 {
354 QMutexLocker l(&m_mutex);
355 if (!m_shouldScanImports) {
356 m_shouldScanImports = true;
357 scan = true;
358 }
359 }
360 if (scan)
361 updateImportPaths();
362 }
363
364 return result;
365 }
366
367
fileChangedOnDisk(const QString & path)368 void ModelManagerInterface::fileChangedOnDisk(const QString &path)
369 {
370 addFuture(Utils::runAsync(&ModelManagerInterface::parse,
371 workingCopyInternal(), QStringList(path),
372 this, Dialect(Dialect::AnyLanguage), true));
373 }
374
removeFiles(const QStringList & files)375 void ModelManagerInterface::removeFiles(const QStringList &files)
376 {
377 emit aboutToRemoveFiles(files);
378
379 QMutexLocker locker(&m_mutex);
380
381 for (const QString &file : files) {
382 m_validSnapshot.remove(file);
383 m_newestSnapshot.remove(file);
384 }
385 }
386
387 namespace {
pInfoLessThanActive(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)388 bool pInfoLessThanActive(const ModelManagerInterface::ProjectInfo &p1,
389 const ModelManagerInterface::ProjectInfo &p2)
390 {
391 QStringList s1 = p1.activeResourceFiles;
392 QStringList s2 = p2.activeResourceFiles;
393 if (s1.size() < s2.size())
394 return true;
395 if (s1.size() > s2.size())
396 return false;
397 for (int i = 0; i < s1.size(); ++i) {
398 if (s1.at(i) < s2.at(i))
399 return true;
400 if (s1.at(i) > s2.at(i))
401 return false;
402 }
403 return false;
404 }
405
pInfoLessThanAll(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)406 bool pInfoLessThanAll(const ModelManagerInterface::ProjectInfo &p1,
407 const ModelManagerInterface::ProjectInfo &p2)
408 {
409 QStringList s1 = p1.allResourceFiles;
410 QStringList s2 = p2.allResourceFiles;
411 if (s1.size() < s2.size())
412 return true;
413 if (s1.size() > s2.size())
414 return false;
415 for (int i = 0; i < s1.size(); ++i) {
416 if (s1.at(i) < s2.at(i))
417 return true;
418 if (s1.at(i) > s2.at(i))
419 return false;
420 }
421 return false;
422 }
423
pInfoLessThanImports(const ModelManagerInterface::ProjectInfo & p1,const ModelManagerInterface::ProjectInfo & p2)424 bool pInfoLessThanImports(const ModelManagerInterface::ProjectInfo &p1,
425 const ModelManagerInterface::ProjectInfo &p2)
426 {
427 if (p1.qtQmlPath < p2.qtQmlPath)
428 return true;
429 if (p1.qtQmlPath > p2.qtQmlPath)
430 return false;
431 const PathsAndLanguages &s1 = p1.importPaths;
432 const PathsAndLanguages &s2 = p2.importPaths;
433 if (s1.size() < s2.size())
434 return true;
435 if (s1.size() > s2.size())
436 return false;
437 for (int i = 0; i < s1.size(); ++i) {
438 if (s1.at(i) < s2.at(i))
439 return true;
440 if (s2.at(i) < s1.at(i))
441 return false;
442 }
443 return false;
444 }
445
446 }
447
iterateQrcFiles(ProjectExplorer::Project * project,QrcResourceSelector resources,const std::function<void (QrcParser::ConstPtr)> & callback)448 void ModelManagerInterface::iterateQrcFiles(
449 ProjectExplorer::Project *project, QrcResourceSelector resources,
450 const std::function<void(QrcParser::ConstPtr)> &callback)
451 {
452 QList<ProjectInfo> pInfos;
453 if (project) {
454 pInfos.append(projectInfo(project));
455 } else {
456 pInfos = projectInfos();
457 if (resources == ActiveQrcResources) // make the result predictable
458 Utils::sort(pInfos, &pInfoLessThanActive);
459 else
460 Utils::sort(pInfos, &pInfoLessThanAll);
461 }
462
463 QSet<QString> pathsChecked;
464 for (const ModelManagerInterface::ProjectInfo &pInfo : qAsConst(pInfos)) {
465 QStringList qrcFilePaths;
466 if (resources == ActiveQrcResources)
467 qrcFilePaths = pInfo.activeResourceFiles;
468 else
469 qrcFilePaths = pInfo.allResourceFiles;
470 for (const QString &qrcFilePath : qAsConst(qrcFilePaths)) {
471 if (pathsChecked.contains(qrcFilePath))
472 continue;
473 pathsChecked.insert(qrcFilePath);
474 QrcParser::ConstPtr qrcFile = m_qrcCache.parsedPath(qrcFilePath);
475 if (qrcFile.isNull())
476 continue;
477 callback(qrcFile);
478 }
479 }
480 }
481
qrcPathsForFile(const QString & file,const QLocale * locale,ProjectExplorer::Project * project,QrcResourceSelector resources)482 QStringList ModelManagerInterface::qrcPathsForFile(const QString &file, const QLocale *locale,
483 ProjectExplorer::Project *project,
484 QrcResourceSelector resources)
485 {
486 QStringList res;
487 iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
488 qrcFile->collectResourceFilesForSourceFile(file, &res, locale);
489 });
490 return res;
491 }
492
filesAtQrcPath(const QString & path,const QLocale * locale,ProjectExplorer::Project * project,QrcResourceSelector resources)493 QStringList ModelManagerInterface::filesAtQrcPath(const QString &path, const QLocale *locale,
494 ProjectExplorer::Project *project,
495 QrcResourceSelector resources)
496 {
497 QString normPath = QrcParser::normalizedQrcFilePath(path);
498 QStringList res;
499 iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
500 qrcFile->collectFilesAtPath(normPath, &res, locale);
501 });
502 return res;
503 }
504
filesInQrcPath(const QString & path,const QLocale * locale,ProjectExplorer::Project * project,bool addDirs,QrcResourceSelector resources)505 QMap<QString, QStringList> ModelManagerInterface::filesInQrcPath(const QString &path,
506 const QLocale *locale,
507 ProjectExplorer::Project *project,
508 bool addDirs,
509 QrcResourceSelector resources)
510 {
511 QString normPath = QrcParser::normalizedQrcDirectoryPath(path);
512 QMap<QString, QStringList> res;
513 iterateQrcFiles(project, resources, [&](const QrcParser::ConstPtr &qrcFile) {
514 qrcFile->collectFilesInPath(normPath, &res, addDirs, locale);
515 });
516 return res;
517 }
518
projectInfos() const519 QList<ModelManagerInterface::ProjectInfo> ModelManagerInterface::projectInfos() const
520 {
521 QMutexLocker locker(&m_mutex);
522
523 return m_projects.values();
524 }
525
containsProject(ProjectExplorer::Project * project) const526 bool ModelManagerInterface::containsProject(ProjectExplorer::Project *project) const
527 {
528 QMutexLocker locker(&m_mutex);
529 return m_projects.contains(project);
530 }
531
projectInfo(ProjectExplorer::Project * project) const532 ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfo(
533 ProjectExplorer::Project *project) const
534 {
535 QMutexLocker locker(&m_mutex);
536 return m_projects.value(project);
537 }
538
updateProjectInfo(const ProjectInfo & pinfo,ProjectExplorer::Project * p)539 void ModelManagerInterface::updateProjectInfo(const ProjectInfo &pinfo, ProjectExplorer::Project *p)
540 {
541 if (pinfo.project.isNull() || !p || m_indexerDisabled)
542 return;
543
544 Snapshot snapshot;
545 ProjectInfo oldInfo;
546 {
547 QMutexLocker locker(&m_mutex);
548 oldInfo = m_projects.value(p);
549 m_projects.insert(p, pinfo);
550 if (p == m_defaultProject)
551 m_defaultProjectInfo = pinfo;
552 snapshot = m_validSnapshot;
553 }
554
555 if (oldInfo.qmlDumpPath != pinfo.qmlDumpPath
556 || oldInfo.qmlDumpEnvironment != pinfo.qmlDumpEnvironment) {
557 m_pluginDumper->scheduleRedumpPlugins();
558 }
559
560
561 updateImportPaths();
562
563 // remove files that are no longer in the project and have been deleted
564 QStringList deletedFiles;
565 for (const QString &oldFile : qAsConst(oldInfo.sourceFiles)) {
566 if (snapshot.document(oldFile)
567 && !pinfo.sourceFiles.contains(oldFile)
568 && !QFile::exists(oldFile)) {
569 deletedFiles += oldFile;
570 }
571 }
572 removeFiles(deletedFiles);
573 for (const QString &oldFile : qAsConst(deletedFiles))
574 m_fileToProject.remove(oldFile, p);
575
576 // parse any files not yet in the snapshot
577 QStringList newFiles;
578 for (const QString &file : qAsConst(pinfo.sourceFiles)) {
579 if (!m_fileToProject.contains(file, p))
580 m_fileToProject.insert(file, p);
581 if (!snapshot.document(file))
582 newFiles += file;
583 }
584
585 updateSourceFiles(newFiles, false);
586
587 // update qrc cache
588 m_qrcContents = pinfo.resourceFileContents;
589 for (const QString &newQrc : qAsConst(pinfo.allResourceFiles))
590 m_qrcCache.addPath(newQrc, m_qrcContents.value(newQrc));
591 for (const QString &oldQrc : qAsConst(oldInfo.allResourceFiles))
592 m_qrcCache.removePath(oldQrc);
593
594 m_pluginDumper->loadBuiltinTypes(pinfo);
595 emit projectInfoUpdated(pinfo);
596 }
597
598
removeProjectInfo(ProjectExplorer::Project * project)599 void ModelManagerInterface::removeProjectInfo(ProjectExplorer::Project *project)
600 {
601 ProjectInfo info;
602 info.sourceFiles.clear();
603 // update with an empty project info to clear data
604 updateProjectInfo(info, project);
605
606 {
607 QMutexLocker locker(&m_mutex);
608 m_projects.remove(project);
609 }
610 }
611
612 /*!
613 Returns project info with summarized info for \a path
614
615 \note Project pointer will be empty
616 */
projectInfoForPath(const QString & path) const617 ModelManagerInterface::ProjectInfo ModelManagerInterface::projectInfoForPath(
618 const QString &path) const
619 {
620 ProjectInfo res;
621 const auto allProjectInfos = allProjectInfosForPath(path);
622 for (const ProjectInfo &pInfo : allProjectInfos) {
623 if (res.qtQmlPath.isEmpty()) {
624 res.qtQmlPath = pInfo.qtQmlPath;
625 res.qtVersionString = pInfo.qtVersionString;
626 }
627 res.applicationDirectories.append(pInfo.applicationDirectories);
628 for (const auto &importPath : pInfo.importPaths)
629 res.importPaths.maybeInsert(importPath);
630 auto end = pInfo.moduleMappings.cend();
631 for (auto it = pInfo.moduleMappings.cbegin(); it != end; ++it)
632 res.moduleMappings.insert(it.key(), it.value());
633 }
634 res.applicationDirectories = Utils::filteredUnique(res.applicationDirectories);
635 return res;
636 }
637
638 /*!
639 Returns list of project infos for \a path
640 */
allProjectInfosForPath(const QString & path) const641 QList<ModelManagerInterface::ProjectInfo> ModelManagerInterface::allProjectInfosForPath(
642 const QString &path) const
643 {
644 QList<ProjectExplorer::Project *> projects;
645 {
646 QMutexLocker locker(&m_mutex);
647 projects = m_fileToProject.values(path);
648 if (projects.isEmpty()) {
649 QFileInfo fInfo(path);
650 projects = m_fileToProject.values(fInfo.canonicalFilePath());
651 }
652 }
653 QList<ProjectInfo> infos;
654 for (ProjectExplorer::Project *project : qAsConst(projects)) {
655 ProjectInfo info = projectInfo(project);
656 if (!info.project.isNull())
657 infos.append(info);
658 }
659 if (infos.isEmpty()) {
660 QMutexLocker locker(&m_mutex);
661 return { m_defaultProjectInfo };
662 }
663 std::sort(infos.begin(), infos.end(), &pInfoLessThanImports);
664 return infos;
665 }
666
emitDocumentChangedOnDisk(Document::Ptr doc)667 void ModelManagerInterface::emitDocumentChangedOnDisk(Document::Ptr doc)
668 {
669 emit documentChangedOnDisk(std::move(doc));
670 }
671
updateQrcFile(const QString & path)672 void ModelManagerInterface::updateQrcFile(const QString &path)
673 {
674 m_qrcCache.updatePath(path, m_qrcContents.value(path));
675 }
676
updateDocument(const Document::Ptr & doc)677 void ModelManagerInterface::updateDocument(const Document::Ptr &doc)
678 {
679 {
680 QMutexLocker locker(&m_mutex);
681 m_validSnapshot.insert(doc);
682 m_newestSnapshot.insert(doc, true);
683 }
684 emit documentUpdated(doc);
685 }
686
updateLibraryInfo(const QString & path,const LibraryInfo & info)687 void ModelManagerInterface::updateLibraryInfo(const QString &path, const LibraryInfo &info)
688 {
689 if (!info.pluginTypeInfoError().isEmpty())
690 qCDebug(qmljsLog) << "Dumping errors for " << path << ":" << info.pluginTypeInfoError();
691
692 {
693 QMutexLocker locker(&m_mutex);
694 m_validSnapshot.insertLibraryInfo(path, info);
695 m_newestSnapshot.insertLibraryInfo(path, info);
696 }
697 // only emit if we got new useful information
698 if (info.isValid())
699 emit libraryInfoUpdated(path, info);
700 }
701
filesInDirectoryForLanguages(const QString & path,const QList<Dialect> & languages)702 static QStringList filesInDirectoryForLanguages(const QString &path,
703 const QList<Dialect> &languages)
704 {
705 const QStringList pattern = ModelManagerInterface::globPatternsForLanguages(languages);
706 QStringList files;
707
708 const QDir dir(path);
709 const auto entries = dir.entryInfoList(pattern, QDir::Files);
710 for (const QFileInfo &fi : entries)
711 files += fi.absoluteFilePath();
712
713 return files;
714 }
715
findNewImplicitImports(const Document::Ptr & doc,const Snapshot & snapshot,QStringList * importedFiles,QSet<QString> * scannedPaths)716 static void findNewImplicitImports(const Document::Ptr &doc, const Snapshot &snapshot,
717 QStringList *importedFiles, QSet<QString> *scannedPaths)
718 {
719 // scan files that could be implicitly imported
720 // it's important we also do this for JS files, otherwise the isEmpty check will fail
721 if (snapshot.documentsInDirectory(doc->path()).isEmpty()) {
722 if (!scannedPaths->contains(doc->path())) {
723 *importedFiles += filesInDirectoryForLanguages(doc->path(),
724 doc->language().companionLanguages());
725 scannedPaths->insert(doc->path());
726 }
727 }
728 }
729
findNewFileImports(const Document::Ptr & doc,const Snapshot & snapshot,QStringList * importedFiles,QSet<QString> * scannedPaths)730 static void findNewFileImports(const Document::Ptr &doc, const Snapshot &snapshot,
731 QStringList *importedFiles, QSet<QString> *scannedPaths)
732 {
733 // scan files and directories that are explicitly imported
734 const auto imports = doc->bind()->imports();
735 for (const ImportInfo &import : imports) {
736 const QString &importName = import.path();
737 if (import.type() == ImportType::File) {
738 if (! snapshot.document(importName))
739 *importedFiles += importName;
740 } else if (import.type() == ImportType::Directory) {
741 if (snapshot.documentsInDirectory(importName).isEmpty()) {
742 if (! scannedPaths->contains(importName)) {
743 *importedFiles += filesInDirectoryForLanguages(
744 importName, doc->language().companionLanguages());
745 scannedPaths->insert(importName);
746 }
747 }
748 } else if (import.type() == ImportType::QrcFile) {
749 const QStringList importPaths
750 = ModelManagerInterface::instance()->filesAtQrcPath(importName);
751 for (const QString &importPath : importPaths) {
752 if (!snapshot.document(importPath))
753 *importedFiles += importPath;
754 }
755 } else if (import.type() == ImportType::QrcDirectory) {
756 const QMap<QString, QStringList> files
757 = ModelManagerInterface::instance()->filesInQrcPath(importName);
758 for (auto qrc = files.cbegin(), end = files.cend(); qrc != end; ++qrc) {
759 if (ModelManagerInterface::guessLanguageOfFile(qrc.key()).isQmlLikeOrJsLanguage()) {
760 for (const QString &sourceFile : qrc.value()) {
761 if (!snapshot.document(sourceFile))
762 *importedFiles += sourceFile;
763 }
764 }
765 }
766 }
767 }
768 }
769
770 enum class LibraryStatus {
771 Accepted,
772 Rejected,
773 Unknown
774 };
775
libraryStatus(const QString & path,const Snapshot & snapshot,QSet<QString> * newLibraries)776 static LibraryStatus libraryStatus(const QString &path, const Snapshot &snapshot,
777 QSet<QString> *newLibraries)
778 {
779 if (path.isEmpty())
780 return LibraryStatus::Rejected;
781 // if we know there is a library, done
782 const LibraryInfo &existingInfo = snapshot.libraryInfo(path);
783 if (existingInfo.isValid())
784 return LibraryStatus::Accepted;
785 if (newLibraries->contains(path))
786 return LibraryStatus::Accepted;
787 // if we looked at the path before, done
788 return existingInfo.wasScanned()
789 ? LibraryStatus::Rejected
790 : LibraryStatus::Unknown;
791 }
792
findNewQmlApplicationInPath(const QString & path,const Snapshot & snapshot,ModelManagerInterface * modelManager,QSet<QString> * newLibraries)793 static bool findNewQmlApplicationInPath(const QString &path,
794 const Snapshot &snapshot,
795 ModelManagerInterface *modelManager,
796 QSet<QString> *newLibraries)
797 {
798 switch (libraryStatus(path, snapshot, newLibraries)) {
799 case LibraryStatus::Accepted: return true;
800 case LibraryStatus::Rejected: return false;
801 default: break;
802 }
803
804 QString qmltypesFile;
805
806 QDir dir(path);
807 QDirIterator it(path, QStringList { "*.qmltypes" }, QDir::Files);
808
809 if (!it.hasNext())
810 return false;
811
812 qmltypesFile = it.next();
813
814 LibraryInfo libraryInfo = LibraryInfo(qmltypesFile);
815 const QString libraryPath = dir.absolutePath();
816 newLibraries->insert(libraryPath);
817 modelManager->updateLibraryInfo(path, libraryInfo);
818 modelManager->loadPluginTypes(QFileInfo(libraryPath).canonicalFilePath(), libraryPath,
819 QString(), QString());
820 return true;
821 }
822
findNewQmlLibraryInPath(const QString & path,const Snapshot & snapshot,ModelManagerInterface * modelManager,QStringList * importedFiles,QSet<QString> * scannedPaths,QSet<QString> * newLibraries,bool ignoreMissing)823 static bool findNewQmlLibraryInPath(const QString &path,
824 const Snapshot &snapshot,
825 ModelManagerInterface *modelManager,
826 QStringList *importedFiles,
827 QSet<QString> *scannedPaths,
828 QSet<QString> *newLibraries,
829 bool ignoreMissing)
830 {
831 switch (libraryStatus(path, snapshot, newLibraries)) {
832 case LibraryStatus::Accepted: return true;
833 case LibraryStatus::Rejected: return false;
834 default: break;
835 }
836
837 const QDir dir(path);
838 QFile qmldirFile(dir.filePath(QLatin1String("qmldir")));
839 if (!qmldirFile.exists()) {
840 if (!ignoreMissing) {
841 LibraryInfo libraryInfo(LibraryInfo::NotFound);
842 modelManager->updateLibraryInfo(path, libraryInfo);
843 }
844 return false;
845 }
846
847 if (Utils::HostOsInfo::isWindowsHost()) {
848 // QTCREATORBUG-3402 - be case sensitive even here?
849 }
850
851 // found a new library!
852 if (!qmldirFile.open(QFile::ReadOnly))
853 return false;
854 QString qmldirData = QString::fromUtf8(qmldirFile.readAll());
855
856 QmlDirParser qmldirParser;
857 qmldirParser.parse(qmldirData);
858
859 const QString libraryPath = QFileInfo(qmldirFile).absolutePath();
860 newLibraries->insert(libraryPath);
861 modelManager->updateLibraryInfo(libraryPath, LibraryInfo(qmldirParser));
862 modelManager->loadPluginTypes(QFileInfo(libraryPath).canonicalFilePath(), libraryPath,
863 QString(), QString());
864
865 // scan the qml files in the library
866 const auto components = qmldirParser.components();
867 for (const QmlDirParser::Component &component : components) {
868 if (!component.fileName.isEmpty()) {
869 const QFileInfo componentFileInfo(dir.filePath(component.fileName));
870 const QString path = QDir::cleanPath(componentFileInfo.absolutePath());
871 if (!scannedPaths->contains(path)) {
872 *importedFiles += filesInDirectoryForLanguages(path, Dialect(Dialect::AnyLanguage)
873 .companionLanguages());
874 scannedPaths->insert(path);
875 }
876 }
877 }
878
879 return true;
880 }
881
modulePath(const ImportInfo & import,const QStringList & paths)882 static QString modulePath(const ImportInfo &import, const QStringList &paths)
883 {
884 if (!import.version().isValid())
885 return QString();
886
887 const QStringList modPaths = modulePaths(import.name(), import.version().toString(), paths);
888 return modPaths.value(0); // first is best match
889 }
890
findNewLibraryImports(const Document::Ptr & doc,const Snapshot & snapshot,ModelManagerInterface * modelManager,QStringList * importedFiles,QSet<QString> * scannedPaths,QSet<QString> * newLibraries)891 static void findNewLibraryImports(const Document::Ptr &doc, const Snapshot &snapshot,
892 ModelManagerInterface *modelManager, QStringList *importedFiles,
893 QSet<QString> *scannedPaths, QSet<QString> *newLibraries)
894 {
895 // scan current dir
896 findNewQmlLibraryInPath(doc->path(), snapshot, modelManager,
897 importedFiles, scannedPaths, newLibraries, false);
898
899 // scan dir and lib imports
900 const QStringList importPaths = modelManager->importPathsNames();
901 const auto imports = doc->bind()->imports();
902 for (const ImportInfo &import : imports) {
903 switch (import.type()) {
904 case ImportType::Directory:
905 findNewQmlLibraryInPath(import.path(), snapshot, modelManager,
906 importedFiles, scannedPaths, newLibraries, false);
907 break;
908 case ImportType::Library:
909 findNewQmlLibraryInPath(modulePath(import, importPaths), snapshot, modelManager,
910 importedFiles, scannedPaths, newLibraries, false);
911 break;
912 default:
913 break;
914 }
915 }
916 }
917
parseLoop(QSet<QString> & scannedPaths,QSet<QString> & newLibraries,const WorkingCopy & workingCopy,QStringList files,ModelManagerInterface * modelManager,Dialect mainLanguage,bool emitDocChangedOnDisk,const std::function<bool (qreal)> & reportProgress)918 void ModelManagerInterface::parseLoop(QSet<QString> &scannedPaths,
919 QSet<QString> &newLibraries,
920 const WorkingCopy &workingCopy,
921 QStringList files,
922 ModelManagerInterface *modelManager,
923 Dialect mainLanguage,
924 bool emitDocChangedOnDisk,
925 const std::function<bool(qreal)> &reportProgress)
926 {
927 for (int i = 0; i < files.size(); ++i) {
928 if (!reportProgress(qreal(i) / files.size()))
929 return;
930
931 const QString fileName = files.at(i);
932
933 Dialect language = guessLanguageOfFile(fileName);
934 if (language == Dialect::NoLanguage) {
935 if (fileName.endsWith(QLatin1String(".qrc")))
936 modelManager->updateQrcFile(fileName);
937 continue;
938 }
939 if (language == Dialect::Qml
940 && (mainLanguage == Dialect::QmlQtQuick2))
941 language = mainLanguage;
942 if (language == Dialect::Qml && mainLanguage == Dialect::QmlQtQuick2Ui)
943 language = Dialect::QmlQtQuick2;
944 if (language == Dialect::QmlTypeInfo || language == Dialect::QmlProject)
945 continue;
946 QString contents;
947 int documentRevision = 0;
948
949 if (workingCopy.contains(fileName)) {
950 QPair<QString, int> entry = workingCopy.get(fileName);
951 contents = entry.first;
952 documentRevision = entry.second;
953 } else {
954 QFile inFile(fileName);
955
956 if (inFile.open(QIODevice::ReadOnly)) {
957 QTextStream ins(&inFile);
958 contents = ins.readAll();
959 inFile.close();
960 } else {
961 continue;
962 }
963 }
964
965 Document::MutablePtr doc = Document::create(fileName, language);
966 doc->setEditorRevision(documentRevision);
967 doc->setSource(contents);
968 doc->parse();
969
970 #ifdef WITH_TESTS
971 if (ExtensionSystem::PluginManager::isScenarioRunning("TestModelManagerInterface")) {
972 ExtensionSystem::PluginManager::waitForScenarioFullyInitialized();
973 if (ExtensionSystem::PluginManager::finishScenario()) {
974 qDebug() << "Point 1: Shutdown triggered";
975 QThread::currentThread()->sleep(2);
976 qDebug() << "Point 3: If Point 2 was already reached, expect a crash now";
977 }
978 }
979 #endif
980 // get list of referenced files not yet in snapshot or in directories already scanned
981 QStringList importedFiles;
982
983 // update snapshot. requires synchronization, but significantly reduces amount of file
984 // system queries for library imports because queries are cached in libraryInfo
985 {
986 // Make sure the snapshot is destroyed before updateDocument, so that we don't trigger
987 // the copy-on-write mechanism on its internals.
988 const Snapshot snapshot = modelManager->snapshot();
989
990 findNewImplicitImports(doc, snapshot, &importedFiles, &scannedPaths);
991 findNewFileImports(doc, snapshot, &importedFiles, &scannedPaths);
992 findNewLibraryImports(doc, snapshot, modelManager, &importedFiles, &scannedPaths,
993 &newLibraries);
994 }
995
996 // add new files to parse list
997 for (const QString &file : qAsConst(importedFiles)) {
998 if (!files.contains(file))
999 files.append(file);
1000 }
1001
1002 modelManager->updateDocument(doc);
1003 if (emitDocChangedOnDisk)
1004 modelManager->emitDocumentChangedOnDisk(doc);
1005 }
1006 }
1007
1008 class FutureReporter
1009 {
1010 public:
FutureReporter(QFutureInterface<void> & future,int multiplier,int base)1011 FutureReporter(QFutureInterface<void> &future, int multiplier, int base)
1012 : future(future), multiplier(multiplier), base(base)
1013 {}
1014
operator ()(qreal val)1015 bool operator()(qreal val)
1016 {
1017 if (future.isCanceled())
1018 return false;
1019 future.setProgressValue(int(base + multiplier * val));
1020 return true;
1021 }
1022 private:
1023 QFutureInterface<void> &future;
1024 int multiplier;
1025 int base;
1026 };
1027
parse(QFutureInterface<void> & future,const WorkingCopy & workingCopy,QStringList files,ModelManagerInterface * modelManager,Dialect mainLanguage,bool emitDocChangedOnDisk)1028 void ModelManagerInterface::parse(QFutureInterface<void> &future,
1029 const WorkingCopy &workingCopy,
1030 QStringList files,
1031 ModelManagerInterface *modelManager,
1032 Dialect mainLanguage,
1033 bool emitDocChangedOnDisk)
1034 {
1035 const int progressMax = 100;
1036 FutureReporter reporter(future, progressMax, 0);
1037 future.setProgressRange(0, progressMax);
1038
1039 // paths we have scanned for files and added to the files list
1040 QSet<QString> scannedPaths;
1041 // libraries we've found while scanning imports
1042 QSet<QString> newLibraries;
1043 parseLoop(scannedPaths, newLibraries, workingCopy, std::move(files), modelManager, mainLanguage,
1044 emitDocChangedOnDisk, reporter);
1045 future.setProgressValue(progressMax);
1046 }
1047
1048 struct ScanItem {
1049 QString path;
1050 int depth = 0;
1051 Dialect language = Dialect::AnyLanguage;
1052 };
1053
importScan(QFutureInterface<void> & future,const ModelManagerInterface::WorkingCopy & workingCopy,const PathsAndLanguages & paths,ModelManagerInterface * modelManager,bool emitDocChangedOnDisk,bool libOnly,bool forceRescan)1054 void ModelManagerInterface::importScan(QFutureInterface<void> &future,
1055 const ModelManagerInterface::WorkingCopy &workingCopy,
1056 const PathsAndLanguages &paths,
1057 ModelManagerInterface *modelManager,
1058 bool emitDocChangedOnDisk, bool libOnly, bool forceRescan)
1059 {
1060 // paths we have scanned for files and added to the files list
1061 QSet<QString> scannedPaths;
1062 {
1063 QMutexLocker l(&modelManager->m_mutex);
1064 scannedPaths = modelManager->m_scannedPaths;
1065 }
1066 // libraries we've found while scanning imports
1067 QSet<QString> newLibraries;
1068
1069 QVector<ScanItem> pathsToScan;
1070 pathsToScan.reserve(paths.size());
1071 {
1072 QMutexLocker l(&modelManager->m_mutex);
1073 for (const auto &path : paths) {
1074 QString cPath = QDir::cleanPath(path.path().toString());
1075 if (!forceRescan && modelManager->m_scannedPaths.contains(cPath))
1076 continue;
1077 pathsToScan.append({cPath, 0, path.language()});
1078 modelManager->m_scannedPaths.insert(cPath);
1079 }
1080 }
1081 const int maxScanDepth = 5;
1082 int progressRange = pathsToScan.size() * (1 << (2 + maxScanDepth));
1083 int totalWork = progressRange;
1084 int workDone = 0;
1085 future.setProgressRange(0, progressRange); // update max length while iterating?
1086 const Snapshot snapshot = modelManager->snapshot();
1087 bool isCanceled = future.isCanceled();
1088 while (!pathsToScan.isEmpty() && !isCanceled) {
1089 ScanItem toScan = pathsToScan.last();
1090 pathsToScan.pop_back();
1091 int pathBudget = (1 << (maxScanDepth + 2 - toScan.depth));
1092 if (forceRescan || !scannedPaths.contains(toScan.path)) {
1093 QStringList importedFiles;
1094 if (forceRescan ||
1095 (!findNewQmlLibraryInPath(toScan.path, snapshot, modelManager, &importedFiles,
1096 &scannedPaths, &newLibraries, true)
1097 && !libOnly && snapshot.documentsInDirectory(toScan.path).isEmpty())) {
1098 importedFiles += filesInDirectoryForLanguages(toScan.path,
1099 toScan.language.companionLanguages());
1100 }
1101 workDone += 1;
1102 future.setProgressValue(progressRange * workDone / totalWork);
1103 if (!importedFiles.isEmpty()) {
1104 FutureReporter reporter(future, progressRange * pathBudget / (4 * totalWork),
1105 progressRange * workDone / totalWork);
1106 parseLoop(scannedPaths, newLibraries, workingCopy, importedFiles, modelManager,
1107 toScan.language, emitDocChangedOnDisk, reporter); // run in parallel??
1108 importedFiles.clear();
1109 }
1110 workDone += pathBudget / 4 - 1;
1111 future.setProgressValue(progressRange * workDone / totalWork);
1112 } else {
1113 workDone += pathBudget / 4;
1114 }
1115 // always descend tree, as we might have just scanned with a smaller depth
1116 if (toScan.depth < maxScanDepth) {
1117 QDir dir(toScan.path);
1118 QStringList subDirs(dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot));
1119 workDone += 1;
1120 totalWork += pathBudget / 2 * subDirs.size() - pathBudget * 3 / 4 + 1;
1121 for (const QString &path : qAsConst(subDirs))
1122 pathsToScan.append({dir.absoluteFilePath(path), toScan.depth + 1, toScan.language});
1123 } else {
1124 workDone += pathBudget * 3 / 4;
1125 }
1126 future.setProgressValue(progressRange * workDone / totalWork);
1127 isCanceled = future.isCanceled();
1128 }
1129 future.setProgressValue(progressRange);
1130 if (isCanceled) {
1131 // assume no work has been done
1132 QMutexLocker l(&modelManager->m_mutex);
1133 for (const auto &path : paths)
1134 modelManager->m_scannedPaths.remove(path.path().toString());
1135 }
1136 }
1137
importPathsNames() const1138 QStringList ModelManagerInterface::importPathsNames() const
1139 {
1140 QStringList names;
1141 QMutexLocker l(&m_mutex);
1142 names.reserve(m_allImportPaths.size());
1143 for (const PathAndLanguage &x: m_allImportPaths)
1144 names << x.path().toString();
1145 return names;
1146 }
1147
activeBundles() const1148 QmlLanguageBundles ModelManagerInterface::activeBundles() const
1149 {
1150 QMutexLocker l(&m_mutex);
1151 return m_activeBundles;
1152 }
1153
extendedBundles() const1154 QmlLanguageBundles ModelManagerInterface::extendedBundles() const
1155 {
1156 QMutexLocker l(&m_mutex);
1157 return m_extendedBundles;
1158 }
1159
maybeScan(const PathsAndLanguages & importPaths)1160 void ModelManagerInterface::maybeScan(const PathsAndLanguages &importPaths)
1161 {
1162 if (m_indexerDisabled)
1163 return;
1164 PathsAndLanguages pathToScan;
1165 {
1166 QMutexLocker l(&m_mutex);
1167 for (const PathAndLanguage &importPath : importPaths)
1168 if (!m_scannedPaths.contains(importPath.path().toString()))
1169 pathToScan.maybeInsert(importPath);
1170 }
1171
1172 if (pathToScan.length() >= 1) {
1173 QFuture<void> result = Utils::runAsync(&ModelManagerInterface::importScan,
1174 workingCopyInternal(), pathToScan,
1175 this, true, true, false);
1176 addFuture(result);
1177 addTaskInternal(result, tr("Scanning QML Imports"), Constants::TASK_IMPORT_SCAN);
1178 }
1179 }
1180
updateImportPaths()1181 void ModelManagerInterface::updateImportPaths()
1182 {
1183 if (m_indexerDisabled)
1184 return;
1185 PathsAndLanguages allImportPaths;
1186 QStringList allApplicationDirectories;
1187 QmlLanguageBundles activeBundles;
1188 QmlLanguageBundles extendedBundles;
1189 for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1190 for (const auto &importPath : pInfo.importPaths) {
1191 const QString canonicalPath = importPath.path().toFileInfo().canonicalFilePath();
1192 if (!canonicalPath.isEmpty()) {
1193 allImportPaths.maybeInsert(Utils::FilePath::fromString(canonicalPath),
1194 importPath.language());
1195 }
1196 }
1197 allApplicationDirectories.append(pInfo.applicationDirectories);
1198 }
1199
1200 for (const ViewerContext &vContext : qAsConst(m_defaultVContexts)) {
1201 for (const QString &path : vContext.paths)
1202 allImportPaths.maybeInsert(Utils::FilePath::fromString(path), vContext.language);
1203 allApplicationDirectories.append(vContext.applicationDirectories);
1204 }
1205
1206 for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1207 activeBundles.mergeLanguageBundles(pInfo.activeBundle);
1208 const auto languages = pInfo.activeBundle.languages();
1209 for (Dialect l : languages) {
1210 const auto paths = pInfo.activeBundle.bundleForLanguage(l).searchPaths().stringList();
1211 for (const QString &path : paths) {
1212 const QString canonicalPath = QFileInfo(path).canonicalFilePath();
1213 if (!canonicalPath.isEmpty())
1214 allImportPaths.maybeInsert(Utils::FilePath::fromString(canonicalPath), l);
1215 }
1216 }
1217 }
1218
1219 for (const ProjectInfo &pInfo : qAsConst(m_projects)) {
1220 if (!pInfo.qtQmlPath.isEmpty()) {
1221 allImportPaths.maybeInsert(Utils::FilePath::fromString(pInfo.qtQmlPath),
1222 Dialect::QmlQtQuick2);
1223 }
1224 }
1225
1226 {
1227 const QString pathAtt = defaultProjectInfo().qtQmlPath;
1228 if (!pathAtt.isEmpty())
1229 allImportPaths.maybeInsert(Utils::FilePath::fromString(pathAtt), Dialect::QmlQtQuick2);
1230 }
1231
1232 for (const auto &importPath : defaultProjectInfo().importPaths) {
1233 allImportPaths.maybeInsert(importPath);
1234 }
1235
1236 for (const QString &path : qAsConst(m_defaultImportPaths))
1237 allImportPaths.maybeInsert(Utils::FilePath::fromString(path), Dialect::Qml);
1238 allImportPaths.compact();
1239 allApplicationDirectories = Utils::filteredUnique(allApplicationDirectories);
1240
1241 {
1242 QMutexLocker l(&m_mutex);
1243 m_allImportPaths = allImportPaths;
1244 m_activeBundles = activeBundles;
1245 m_extendedBundles = extendedBundles;
1246 }
1247
1248
1249 // check if any file in the snapshot imports something new in the new paths
1250 Snapshot snapshot = m_validSnapshot;
1251 QStringList importedFiles;
1252 QSet<QString> scannedPaths;
1253 QSet<QString> newLibraries;
1254 for (const Document::Ptr &doc : qAsConst(snapshot))
1255 findNewLibraryImports(doc, snapshot, this, &importedFiles, &scannedPaths, &newLibraries);
1256 for (const QString &path : qAsConst(allApplicationDirectories))
1257 findNewQmlApplicationInPath(path, snapshot, this, &newLibraries);
1258
1259 updateSourceFiles(importedFiles, true);
1260
1261 if (!m_shouldScanImports)
1262 return;
1263 maybeScan(allImportPaths);
1264 }
1265
loadPluginTypes(const QString & libraryPath,const QString & importPath,const QString & importUri,const QString & importVersion)1266 void ModelManagerInterface::loadPluginTypes(const QString &libraryPath, const QString &importPath,
1267 const QString &importUri, const QString &importVersion)
1268 {
1269 m_pluginDumper->loadPluginTypes(libraryPath, importPath, importUri, importVersion);
1270 }
1271
1272 // is called *inside a c++ parsing thread*, to allow hanging on to source and ast
maybeQueueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr & doc)1273 void ModelManagerInterface::maybeQueueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc)
1274 {
1275 // avoid scanning documents without source code available
1276 doc->keepSourceAndAST();
1277 if (doc->utf8Source().isEmpty()) {
1278 doc->releaseSourceAndAST();
1279 return;
1280 }
1281
1282 // keep source and AST alive if we want to scan for register calls
1283 const bool scan = FindExportedCppTypes::maybeExportsTypes(doc);
1284 if (!scan)
1285 doc->releaseSourceAndAST();
1286
1287 QMutexLocker locker(&g_instanceMutex);
1288 if (g_instance) // delegate actual queuing to the gui thread
1289 QMetaObject::invokeMethod(g_instance, [=] { queueCppQmlTypeUpdate(doc, scan); });
1290 }
1291
queueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr & doc,bool scan)1292 void ModelManagerInterface::queueCppQmlTypeUpdate(const CPlusPlus::Document::Ptr &doc, bool scan)
1293 {
1294 QPair<CPlusPlus::Document::Ptr, bool> prev = m_queuedCppDocuments.value(doc->fileName());
1295 if (prev.first && prev.second)
1296 prev.first->releaseSourceAndAST();
1297 m_queuedCppDocuments.insert(doc->fileName(), {doc, scan});
1298 m_updateCppQmlTypesTimer->start();
1299 }
1300
startCppQmlTypeUpdate()1301 void ModelManagerInterface::startCppQmlTypeUpdate()
1302 {
1303 // if a future is still running, delay
1304 if (m_cppQmlTypesUpdater.isRunning()) {
1305 m_updateCppQmlTypesTimer->start();
1306 return;
1307 }
1308
1309 CPlusPlus::CppModelManagerBase *cppModelManager =
1310 CPlusPlus::CppModelManagerBase::instance();
1311 if (!cppModelManager)
1312 return;
1313
1314 m_cppQmlTypesUpdater = Utils::runAsync(&ModelManagerInterface::updateCppQmlTypes,
1315 this, cppModelManager->snapshot(), m_queuedCppDocuments);
1316 m_queuedCppDocuments.clear();
1317 }
1318
asyncReset()1319 void ModelManagerInterface::asyncReset()
1320 {
1321 m_asyncResetTimer->start();
1322 }
1323
rescanExports(const QString & fileName,FindExportedCppTypes & finder,ModelManagerInterface::CppDataHash & newData)1324 bool rescanExports(const QString &fileName, FindExportedCppTypes &finder,
1325 ModelManagerInterface::CppDataHash &newData)
1326 {
1327 bool hasNewInfo = false;
1328
1329 QList<LanguageUtils::FakeMetaObject::ConstPtr> exported = finder.exportedTypes();
1330 QHash<QString, QString> contextProperties = finder.contextProperties();
1331 if (exported.isEmpty() && contextProperties.isEmpty()) {
1332 hasNewInfo = hasNewInfo || newData.remove(fileName) > 0;
1333 } else {
1334 ModelManagerInterface::CppData &data = newData[fileName];
1335 if (!hasNewInfo && (data.exportedTypes.size() != exported.size()
1336 || data.contextProperties != contextProperties)) {
1337 hasNewInfo = true;
1338 }
1339 if (!hasNewInfo) {
1340 QHash<QString, QByteArray> newFingerprints;
1341 for (const auto &newType : qAsConst(exported))
1342 newFingerprints[newType->className()]=newType->fingerprint();
1343 for (const auto &oldType : qAsConst(data.exportedTypes)) {
1344 if (newFingerprints.value(oldType->className()) != oldType->fingerprint()) {
1345 hasNewInfo = true;
1346 break;
1347 }
1348 }
1349 }
1350 data.exportedTypes = exported;
1351 data.contextProperties = contextProperties;
1352 }
1353 return hasNewInfo;
1354 }
1355
updateCppQmlTypes(QFutureInterface<void> & futureInterface,ModelManagerInterface * qmlModelManager,const CPlusPlus::Snapshot & snapshot,const QHash<QString,QPair<CPlusPlus::Document::Ptr,bool>> & documents)1356 void ModelManagerInterface::updateCppQmlTypes(
1357 QFutureInterface<void> &futureInterface, ModelManagerInterface *qmlModelManager,
1358 const CPlusPlus::Snapshot &snapshot,
1359 const QHash<QString, QPair<CPlusPlus::Document::Ptr, bool>> &documents)
1360 {
1361 futureInterface.setProgressRange(0, documents.size());
1362 futureInterface.setProgressValue(0);
1363
1364 CppDataHash newData;
1365 QHash<QString, QList<CPlusPlus::Document::Ptr>> newDeclarations;
1366 {
1367 QMutexLocker locker(&qmlModelManager->m_cppDataMutex);
1368 newData = qmlModelManager->m_cppDataHash;
1369 newDeclarations = qmlModelManager->m_cppDeclarationFiles;
1370 }
1371
1372 FindExportedCppTypes finder(snapshot);
1373
1374 bool hasNewInfo = false;
1375 using DocScanPair = QPair<CPlusPlus::Document::Ptr, bool>;
1376 for (const DocScanPair &pair : documents) {
1377 if (futureInterface.isCanceled())
1378 return;
1379 futureInterface.setProgressValue(futureInterface.progressValue() + 1);
1380
1381 CPlusPlus::Document::Ptr doc = pair.first;
1382 const bool scan = pair.second;
1383 const QString fileName = doc->fileName();
1384 if (!scan) {
1385 hasNewInfo = newData.remove(fileName) > 0 || hasNewInfo;
1386 const auto savedDocs = newDeclarations.value(fileName);
1387 for (const CPlusPlus::Document::Ptr &savedDoc : savedDocs) {
1388 finder(savedDoc);
1389 hasNewInfo = rescanExports(savedDoc->fileName(), finder, newData) || hasNewInfo;
1390 }
1391 continue;
1392 }
1393
1394 for (auto it = newDeclarations.begin(), end = newDeclarations.end(); it != end;) {
1395 for (auto docIt = it->begin(), endDocIt = it->end(); docIt != endDocIt;) {
1396 const CPlusPlus::Document::Ptr &savedDoc = *docIt;
1397 if (savedDoc->fileName() == fileName) {
1398 savedDoc->releaseSourceAndAST();
1399 it->erase(docIt);
1400 break;
1401 }
1402 ++docIt;
1403 }
1404 if (it->isEmpty())
1405 it = newDeclarations.erase(it);
1406 else
1407 ++it;
1408 }
1409
1410 const auto found = finder(doc);
1411 for (const QString &declarationFile : found) {
1412 newDeclarations[declarationFile].append(doc);
1413 doc->keepSourceAndAST(); // keep for later reparsing when dependent doc changes
1414 }
1415
1416 hasNewInfo = rescanExports(fileName, finder, newData) || hasNewInfo;
1417 doc->releaseSourceAndAST();
1418 }
1419
1420 QMutexLocker locker(&qmlModelManager->m_cppDataMutex);
1421 qmlModelManager->m_cppDataHash = newData;
1422 qmlModelManager->m_cppDeclarationFiles = newDeclarations;
1423 if (hasNewInfo)
1424 // one could get away with re-linking the cpp types...
1425 QMetaObject::invokeMethod(qmlModelManager, &ModelManagerInterface::asyncReset);
1426 }
1427
cppData() const1428 ModelManagerInterface::CppDataHash ModelManagerInterface::cppData() const
1429 {
1430 QMutexLocker locker(&m_cppDataMutex);
1431 return m_cppDataHash;
1432 }
1433
builtins(const Document::Ptr & doc) const1434 LibraryInfo ModelManagerInterface::builtins(const Document::Ptr &doc) const
1435 {
1436 const ProjectInfo info = projectInfoForPath(doc->fileName());
1437 if (!info.qtQmlPath.isEmpty())
1438 return m_validSnapshot.libraryInfo(info.qtQmlPath);
1439 return LibraryInfo();
1440 }
1441
completeVContext(const ViewerContext & vCtx,const Document::Ptr & doc) const1442 ViewerContext ModelManagerInterface::completeVContext(const ViewerContext &vCtx,
1443 const Document::Ptr &doc) const
1444 {
1445 return getVContext(vCtx, doc, false);
1446 }
1447
getVContext(const ViewerContext & vCtx,const Document::Ptr & doc,bool limitToProject) const1448 ViewerContext ModelManagerInterface::getVContext(const ViewerContext &vCtx,
1449 const Document::Ptr &doc,
1450 bool limitToProject) const
1451 {
1452 ViewerContext res = vCtx;
1453
1454 if (!doc.isNull()
1455 && ((vCtx.language == Dialect::AnyLanguage && doc->language() != Dialect::NoLanguage)
1456 || (vCtx.language == Dialect::Qml
1457 && (doc->language() == Dialect::QmlQtQuick2
1458 || doc->language() == Dialect::QmlQtQuick2Ui))))
1459 res.language = doc->language();
1460 ProjectInfo info;
1461 if (!doc.isNull())
1462 info = projectInfoForPath(doc->fileName());
1463 ViewerContext defaultVCtx = defaultVContext(res.language, Document::Ptr(nullptr), false);
1464 ProjectInfo defaultInfo = defaultProjectInfo();
1465 if (info.qtQmlPath.isEmpty()) {
1466 info.qtQmlPath = defaultInfo.qtQmlPath;
1467 info.qtVersionString = defaultInfo.qtVersionString;
1468 }
1469 if (info.qtQmlPath.isEmpty() && info.importPaths.size() == 0)
1470 info.importPaths = defaultInfo.importPaths;
1471 info.applicationDirectories = Utils::filteredUnique(info.applicationDirectories
1472 + defaultInfo.applicationDirectories);
1473 switch (res.flags) {
1474 case ViewerContext::Complete:
1475 break;
1476 case ViewerContext::AddAllPathsAndDefaultSelectors:
1477 res.selectors.append(defaultVCtx.selectors);
1478 Q_FALLTHROUGH();
1479 case ViewerContext::AddAllPaths:
1480 {
1481 for (const QString &path : qAsConst(defaultVCtx.paths))
1482 maybeAddPath(res, path);
1483 switch (res.language.dialect()) {
1484 case Dialect::AnyLanguage:
1485 case Dialect::Qml:
1486 maybeAddPath(res, info.qtQmlPath);
1487 Q_FALLTHROUGH();
1488 case Dialect::QmlQtQuick2:
1489 case Dialect::QmlQtQuick2Ui:
1490 {
1491 if (res.language == Dialect::QmlQtQuick2 || res.language == Dialect::QmlQtQuick2Ui)
1492 maybeAddPath(res, info.qtQmlPath);
1493
1494 QList<Dialect> languages = res.language.companionLanguages();
1495 auto addPathsOnLanguageMatch = [&](const PathsAndLanguages &importPaths) {
1496 for (const auto &importPath : importPaths) {
1497 if (languages.contains(importPath.language())
1498 || importPath.language().companionLanguages().contains(res.language)) {
1499 maybeAddPath(res, importPath.path().toString());
1500 }
1501 }
1502 };
1503 if (limitToProject) {
1504 addPathsOnLanguageMatch(info.importPaths);
1505 } else {
1506 QList<ProjectInfo> allProjects;
1507 {
1508 QMutexLocker locker(&m_mutex);
1509 allProjects = m_projects.values();
1510 }
1511 std::sort(allProjects.begin(), allProjects.end(), &pInfoLessThanImports);
1512 for (const ProjectInfo &pInfo : qAsConst(allProjects))
1513 addPathsOnLanguageMatch(pInfo.importPaths);
1514 }
1515 const auto environmentPaths = environmentImportPaths();
1516 for (const QString &path : environmentPaths)
1517 maybeAddPath(res, path);
1518 break;
1519 }
1520 case Dialect::NoLanguage:
1521 case Dialect::JavaScript:
1522 case Dialect::QmlTypeInfo:
1523 case Dialect::Json:
1524 case Dialect::QmlQbs:
1525 case Dialect::QmlProject:
1526 break;
1527 }
1528 break;
1529 }
1530 case ViewerContext::AddDefaultPathsAndSelectors:
1531 res.selectors.append(defaultVCtx.selectors);
1532 Q_FALLTHROUGH();
1533 case ViewerContext::AddDefaultPaths:
1534 for (const QString &path : qAsConst(defaultVCtx.paths))
1535 maybeAddPath(res, path);
1536 if (res.language == Dialect::AnyLanguage || res.language == Dialect::Qml)
1537 maybeAddPath(res, info.qtQmlPath);
1538 if (res.language == Dialect::AnyLanguage || res.language == Dialect::Qml
1539 || res.language == Dialect::QmlQtQuick2 || res.language == Dialect::QmlQtQuick2Ui) {
1540 const auto environemntPaths = environmentImportPaths();
1541 for (const QString &path : environemntPaths)
1542 maybeAddPath(res, path);
1543 }
1544 break;
1545 }
1546 res.flags = ViewerContext::Complete;
1547 res.applicationDirectories = info.applicationDirectories;
1548 return res;
1549 }
1550
defaultVContext(Dialect language,const Document::Ptr & doc,bool autoComplete) const1551 ViewerContext ModelManagerInterface::defaultVContext(Dialect language,
1552 const Document::Ptr &doc,
1553 bool autoComplete) const
1554 {
1555 if (!doc.isNull()) {
1556 if (language == Dialect::AnyLanguage && doc->language() != Dialect::NoLanguage)
1557 language = doc->language();
1558 else if (language == Dialect::Qml &&
1559 (doc->language() == Dialect::QmlQtQuick2
1560 || doc->language() == Dialect::QmlQtQuick2Ui))
1561 language = doc->language();
1562 }
1563 ViewerContext defaultCtx;
1564 {
1565 QMutexLocker locker(&m_mutex);
1566 defaultCtx = m_defaultVContexts.value(language);
1567 }
1568 defaultCtx.language = language;
1569 return autoComplete ? completeVContext(defaultCtx, doc) : defaultCtx;
1570 }
1571
projectVContext(Dialect language,const Document::Ptr & doc) const1572 ViewerContext ModelManagerInterface::projectVContext(Dialect language, const Document::Ptr &doc) const
1573 {
1574 // Returns context limited to the project the file belongs to
1575 ViewerContext defaultCtx = defaultVContext(language, doc, false);
1576 return getVContext(defaultCtx, doc, true);
1577 }
1578
defaultProjectInfo() const1579 ModelManagerInterface::ProjectInfo ModelManagerInterface::defaultProjectInfo() const
1580 {
1581 QMutexLocker locker(&m_mutex);
1582 return m_defaultProjectInfo;
1583 }
1584
defaultProjectInfoForProject(ProjectExplorer::Project *) const1585 ModelManagerInterface::ProjectInfo ModelManagerInterface::defaultProjectInfoForProject(
1586 ProjectExplorer::Project *) const
1587 {
1588 return ModelManagerInterface::ProjectInfo();
1589 }
1590
setDefaultVContext(const ViewerContext & vContext)1591 void ModelManagerInterface::setDefaultVContext(const ViewerContext &vContext)
1592 {
1593 QMutexLocker locker(&m_mutex);
1594 m_defaultVContexts[vContext.language] = vContext;
1595 }
1596
joinAllThreads(bool cancelOnWait)1597 void ModelManagerInterface::joinAllThreads(bool cancelOnWait)
1598 {
1599 while (true) {
1600 FutureSynchronizer futureSynchronizer;
1601 {
1602 QMutexLocker locker(&m_futuresMutex);
1603 futureSynchronizer = m_futureSynchronizer;
1604 m_futureSynchronizer.clearFutures();
1605 }
1606 futureSynchronizer.setCancelOnWait(cancelOnWait);
1607 if (futureSynchronizer.isEmpty())
1608 return;
1609 }
1610 }
1611
test_joinAllThreads()1612 void ModelManagerInterface::test_joinAllThreads()
1613 {
1614 while (true) {
1615 joinAllThreads();
1616 // In order to process all onFinished handlers of finished futures
1617 QCoreApplication::processEvents();
1618 QMutexLocker lock(&m_futuresMutex);
1619 // If handlers created new futures, repeat the loop
1620 if (m_futureSynchronizer.isEmpty())
1621 return;
1622 }
1623 }
1624
addFuture(const QFuture<void> & future)1625 void ModelManagerInterface::addFuture(const QFuture<void> &future)
1626 {
1627 QMutexLocker lock(&m_futuresMutex);
1628 m_futureSynchronizer.addFuture(future);
1629 }
1630
ensuredGetDocumentForPath(const QString & filePath)1631 Document::Ptr ModelManagerInterface::ensuredGetDocumentForPath(const QString &filePath)
1632 {
1633 QmlJS::Document::Ptr document = newestSnapshot().document(filePath);
1634 if (!document) {
1635 document = QmlJS::Document::create(filePath, QmlJS::Dialect::Qml);
1636 QMutexLocker lock(&m_mutex);
1637
1638 m_newestSnapshot.insert(document);
1639 }
1640
1641 return document;
1642 }
1643
resetCodeModel()1644 void ModelManagerInterface::resetCodeModel()
1645 {
1646 QStringList documents;
1647
1648 {
1649 QMutexLocker locker(&m_mutex);
1650
1651 // find all documents currently in the code model
1652 for (const Document::Ptr &doc : qAsConst(m_validSnapshot))
1653 documents.append(doc->fileName());
1654
1655 // reset the snapshot
1656 m_validSnapshot = Snapshot();
1657 m_newestSnapshot = Snapshot();
1658 m_scannedPaths.clear();
1659 }
1660
1661 // start a reparse thread
1662 updateSourceFiles(documents, false);
1663
1664 // rescan import directories
1665 m_shouldScanImports = true;
1666 updateImportPaths();
1667 }
1668
1669 } // namespace QmlJS
1670