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 | |
20 | QT_BEGIN_NAMESPACE |
21 | |
22 | namespace QmlLsp { |
23 | |
24 | Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel") |
25 | |
26 | using namespace QQmlJS::Dom; |
27 | using namespace Qt::StringLiterals; |
28 | |
29 | /*! |
30 | \internal |
31 | \class QQmlCodeModel |
32 | |
33 | The code model offers a view of the current state of the current files, and traks open files. |
34 | All methods are threadsafe, and generally return immutable or threadsafe objects that can be |
35 | worked on from any thread (unless otherwise noted). |
36 | The idea is the let all other operations be as lock free as possible, concentrating all tricky |
37 | synchronization 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 |
64 | Most operations are not parallel and usually take place in the main thread (but are still thread |
65 | safe). |
66 | There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate. |
67 | Indexing is meant to keep the global view up to date. |
68 | OpenDocumentUpdate keeps the snapshots of the open documents up to date. |
69 | |
70 | There is always a tension between being responsive, using all threads available, and avoid to hog |
71 | too many resources. One can choose different parallelization strategies, we went with a flexiable |
72 | approach. |
73 | We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These |
74 | do all locking needed, get some work, do it without locks, and at the end update the state of the |
75 | code model. If there is more work, then they return true. Thus while (xxxSome()); works until there |
76 | is no work left. |
77 | |
78 | addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do. |
79 | |
80 | indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a |
81 | worker thread (or more) that work on it exist. |
82 | */ |
83 | |
84 | QQmlCodeModel::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 |
98 | Disable the functionality that uses CMake, and remove the already watched paths if there are some. |
99 | */ |
100 | void 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 | |
107 | QQmlCodeModel::~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 | |
124 | OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url) |
125 | { |
126 | return openDocumentByUrl(url).snapshot; |
127 | } |
128 | |
129 | int 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 | |
140 | void QQmlCodeModel::indexStart() |
141 | { |
142 | Q_ASSERT(!m_mutex.tryLock()); // should be called while locked |
143 | qCDebug(codeModelLog) << "indexStart"; |
144 | } |
145 | |
146 | void 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 | |
157 | void QQmlCodeModel::indexSendProgress(int progress) |
158 | { |
159 | if (progress <= m_lastIndexProgress) |
160 | return; |
161 | m_lastIndexProgress = progress; |
162 | // ### actually send progress |
163 | } |
164 | |
165 | bool QQmlCodeModel::indexCancelled() |
166 | { |
167 | QMutexLocker l(&m_mutex); |
168 | if (m_state == State::Stopping) |
169 | return true; |
170 | return false; |
171 | } |
172 | |
173 | void 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 | |
218 | void 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 | |
228 | void 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 | |
250 | void 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 | |
272 | QString 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 | |
296 | void 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 | |
311 | OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url) |
312 | { |
313 | QMutexLocker l(&m_mutex); |
314 | return m_openDocuments.value(key: url); |
315 | } |
316 | |
317 | RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() |
318 | { |
319 | QMutexLocker l(&m_mutex); |
320 | return m_tokens; |
321 | } |
322 | |
323 | const RegisteredSemanticTokens &QQmlCodeModel::registeredTokens() const |
324 | { |
325 | QMutexLocker l(&m_mutex); |
326 | return m_tokens; |
327 | } |
328 | |
329 | void 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 | |
344 | bool 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 | |
375 | void 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 | |
391 | bool 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 | |
428 | void QQmlCodeModel::openUpdateStart() |
429 | { |
430 | qCDebug(codeModelLog) << "openUpdateStart"; |
431 | } |
432 | |
433 | void QQmlCodeModel::openUpdateEnd() |
434 | { |
435 | qCDebug(codeModelLog) << "openUpdateEnd"; |
436 | } |
437 | |
438 | /*! |
439 | \internal |
440 | Performs initialization for m_cmakeStatus, including testing for CMake on the current system. |
441 | */ |
442 | void 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 |
472 | For each build path that is a also a CMake build path, call CMake with \l cmakeBuildCommand to |
473 | generate/update the .qmltypes, qmldir and .qrc files. |
474 | It is assumed here that the number of build folders is usually no more than one, so execute the |
475 | CMake builds one at a time. |
476 | |
477 | If CMake cannot be executed, false is returned. This may happen when CMake does not exist on the |
478 | current system, when the target executed by CMake does not exist (for example when something else |
479 | than qt_add_qml_module is used to setup the module in CMake), or the when the CMake build itself |
480 | fails. |
481 | */ |
482 | bool 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 |
507 | Iterate the entire source directory to find all C++ files that have their names in fileNames, and |
508 | return all the found file paths. |
509 | |
510 | This is an overapproximation and might find unrelated files with the same name. |
511 | */ |
512 | QStringList 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 |
557 | Find all C++ file names (not path, for file paths call \l findFilePathsFromFileNames on the result |
558 | of this method) that this qmlFile relies on. |
559 | */ |
560 | QStringList 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 |
594 | Add watches for all C++ files that this qmlFile relies on, so a rebuild can be triggered when they |
595 | are modified. |
596 | */ |
597 | void 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 | |
608 | void QQmlCodeModel::onCppFileChanged(const QString &) |
609 | { |
610 | m_rebuildRequired = true; |
611 | } |
612 | |
613 | void 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 | |
705 | void QQmlCodeModel::closeOpenFile(const QByteArray &url) |
706 | { |
707 | QMutexLocker l(&m_mutex); |
708 | m_openDocuments.remove(key: url); |
709 | } |
710 | |
711 | void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls) |
712 | { |
713 | QMutexLocker l(&m_mutex); |
714 | m_rootUrls = urls; |
715 | } |
716 | |
717 | void 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 | |
726 | void 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 | |
733 | QList<QByteArray> QQmlCodeModel::rootUrls() const |
734 | { |
735 | QMutexLocker l(&m_mutex); |
736 | return m_rootUrls; |
737 | } |
738 | |
739 | QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url) |
740 | { |
741 | QMutexLocker l(&m_mutex); |
742 | return m_buildPathsForRootUrl.value(key: url); |
743 | } |
744 | |
745 | static bool isNotSeparator(char c) |
746 | { |
747 | return c != '/'; |
748 | } |
749 | |
750 | QStringList 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 | |
762 | QStringList 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 | |
855 | void 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 | |
864 | void 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 | |
875 | void 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 | |
918 | void QQmlCodeModel::addOpenToUpdate(const QByteArray &url) |
919 | { |
920 | QMutexLocker l(&m_mutex); |
921 | m_openDocumentsToUpdate.insert(value: url); |
922 | } |
923 | |
924 | QDebug 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 | |
959 | QT_END_NAMESPACE |
960 |
Definitions
- codeModelLog
- QQmlCodeModel
- disableCMakeCalls
- ~QQmlCodeModel
- snapshotByUrl
- indexEvalProgress
- indexStart
- indexEnd
- indexSendProgress
- indexCancelled
- indexDirectory
- addDirectoriesToIndex
- addDirectory
- removeDirectory
- url2Path
- newOpenFile
- openDocumentByUrl
- registeredTokens
- registeredTokens
- indexNeedsUpdate
- indexSome
- openNeedUpdate
- openUpdateSome
- openUpdateStart
- openUpdateEnd
- initializeCMakeStatus
- callCMakeBuild
- findFilePathsFromFileNames
- fileNamesToWatch
- addFileWatches
- onCppFileChanged
- newDocForOpenFile
- closeOpenFile
- setRootUrls
- addRootUrls
- removeRootUrls
- rootUrls
- buildPathsForRootUrl
- isNotSeparator
- importPathsForFile
- buildPathsForFileUrl
- setDocumentationRootPath
- setBuildPathsForRootUrl
- openUpdate
- addOpenToUpdate
Learn to use CMake with our Intro Training
Find out more