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