1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "qqmlcodemodel_p.h" |
5 | #include "qtextdocument_p.h" |
6 | |
7 | #include <QtCore/qfileinfo.h> |
8 | #include <QtCore/qdir.h> |
9 | #include <QtCore/qthreadpool.h> |
10 | #include <QtCore/qlibraryinfo.h> |
11 | #include <QtQmlDom/private/qqmldomtop_p.h> |
12 | |
13 | #include <memory> |
14 | #include <algorithm> |
15 | |
16 | QT_BEGIN_NAMESPACE |
17 | |
18 | namespace QmlLsp { |
19 | |
20 | Q_LOGGING_CATEGORY(codeModelLog, "qt.languageserver.codemodel" ) |
21 | |
22 | using namespace QQmlJS::Dom; |
23 | using namespace Qt::StringLiterals; |
24 | |
25 | /*! |
26 | \internal |
27 | \class QQmlCodeModel |
28 | |
29 | The code model offers a view of the current state of the current files, and traks open files. |
30 | All methods are threadsafe, and generally return immutable or threadsafe objects that can be |
31 | worked on from any thread (unless otherwise noted). |
32 | The idea is the let all other operations be as lock free as possible, concentrating all tricky |
33 | synchronization here. |
34 | |
35 | \section2 Global views |
36 | \list |
37 | \li currentEnv() offers a view that contains the latest version of all the loaded files |
38 | \li validEnv() is just like current env but stores only the valid (meaning correctly parsed, |
39 | not necessarily without errors) version of a file, it is normally a better choice to load the |
40 | dependencies/symbol information from |
41 | \endlist |
42 | |
43 | \section2 OpenFiles |
44 | \list |
45 | \li snapshotByUrl() returns an OpenDocumentSnapshot of an open document. From it you can get the |
46 | document, its latest valid version, scope, all connected to a specific version of the document |
47 | and immutable. The signal updatedSnapshot() is called every time a snapshot changes (also for |
48 | every partial change: document change, validDocument change, scope change). |
49 | \li openDocumentByUrl() is a lower level and more intrusive access to OpenDocument objects. These |
50 | contains the current snapshot, and shared pointer to a Utils::TextDocument. This is *always* the |
51 | current version of the document, and has line by line support. |
52 | Working on it is more delicate and intrusive, because you have to explicitly acquire its mutex() |
53 | before *any* read or write/modification to it. |
54 | It has a version nuber which is supposed to always change and increase. |
55 | It is mainly used for highlighting/indenting, and is immediately updated when the user edits a |
56 | document. Its use should be avoided if possible, preferring the snapshots. |
57 | \endlist |
58 | |
59 | \section2 Parallelism/Theading |
60 | Most operations are not parallel and usually take place in the main thread (but are still thread |
61 | safe). |
62 | There are two main task that are executed in parallel: Indexing, and OpenDocumentUpdate. |
63 | Indexing is meant to keep the global view up to date. |
64 | OpenDocumentUpdate keeps the snapshots of the open documents up to date. |
65 | |
66 | There is always a tension between being responsive, using all threads available, and avoid to hog |
67 | too many resources. One can choose different parallelization strategies, we went with a flexiable |
68 | approach. |
69 | We have (private) functions that execute part of the work: indexSome() and openUpdateSome(). These |
70 | do all locking needed, get some work, do it without locks, and at the end update the state of the |
71 | code model. If there is more work, then they return true. Thus while (xxxSome()); works until there |
72 | is no work left. |
73 | |
74 | addDirectoriesToIndex(), the internal addDirectory() and addOpenToUpdate() add more work to do. |
75 | |
76 | indexNeedsUpdate() and openNeedUpdate(), check if there is work to do, and if yes ensure that a |
77 | worker thread (or more) that work on it exist. |
78 | */ |
79 | |
80 | QQmlCodeModel::QQmlCodeModel(QObject *parent, QQmlToolingSettings *settings) |
81 | : QObject { parent }, |
82 | m_currentEnv(std::make_shared<DomEnvironment>( |
83 | args: QStringList(QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)), |
84 | args: DomEnvironment::Option::SingleThreaded)), |
85 | m_validEnv(std::make_shared<DomEnvironment>( |
86 | args: QStringList(QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)), |
87 | args: DomEnvironment::Option::SingleThreaded)), |
88 | m_settings(settings) |
89 | { |
90 | } |
91 | |
92 | QQmlCodeModel::~QQmlCodeModel() |
93 | { |
94 | while (true) { |
95 | bool shouldWait; |
96 | { |
97 | QMutexLocker l(&m_mutex); |
98 | m_state = State::Stopping; |
99 | m_openDocumentsToUpdate.clear(); |
100 | shouldWait = m_nIndexInProgress != 0 || m_nUpdateInProgress != 0; |
101 | } |
102 | if (!shouldWait) |
103 | break; |
104 | QThread::yieldCurrentThread(); |
105 | } |
106 | } |
107 | |
108 | OpenDocumentSnapshot QQmlCodeModel::snapshotByUrl(const QByteArray &url) |
109 | { |
110 | return openDocumentByUrl(url).snapshot; |
111 | } |
112 | |
113 | int QQmlCodeModel::indexEvalProgress() const |
114 | { |
115 | Q_ASSERT(!m_mutex.tryLock()); // should be called while locked |
116 | const int dirCost = 10; |
117 | int costToDo = 1; |
118 | for (const ToIndex &el : std::as_const(t: m_toIndex)) |
119 | costToDo += dirCost * el.leftDepth; |
120 | costToDo += m_indexInProgressCost; |
121 | return m_indexDoneCost * 100 / (costToDo + m_indexDoneCost); |
122 | } |
123 | |
124 | void QQmlCodeModel::indexStart() |
125 | { |
126 | Q_ASSERT(!m_mutex.tryLock()); // should be called while locked |
127 | qCDebug(codeModelLog) << "indexStart" ; |
128 | } |
129 | |
130 | void QQmlCodeModel::indexEnd() |
131 | { |
132 | Q_ASSERT(!m_mutex.tryLock()); // should be called while locked |
133 | qCDebug(codeModelLog) << "indexEnd" ; |
134 | m_lastIndexProgress = 0; |
135 | m_nIndexInProgress = 0; |
136 | m_toIndex.clear(); |
137 | m_indexInProgressCost = 0; |
138 | m_indexDoneCost = 0; |
139 | } |
140 | |
141 | void QQmlCodeModel::indexSendProgress(int progress) |
142 | { |
143 | if (progress <= m_lastIndexProgress) |
144 | return; |
145 | m_lastIndexProgress = progress; |
146 | // ### actually send progress |
147 | } |
148 | |
149 | bool QQmlCodeModel::indexCancelled() |
150 | { |
151 | QMutexLocker l(&m_mutex); |
152 | if (m_state == State::Stopping) |
153 | return true; |
154 | return false; |
155 | } |
156 | |
157 | void QQmlCodeModel::indexDirectory(const QString &path, int depthLeft) |
158 | { |
159 | if (indexCancelled()) |
160 | return; |
161 | QDir dir(path); |
162 | if (depthLeft > 1) { |
163 | const QStringList dirs = |
164 | dir.entryList(filters: QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); |
165 | for (const QString &child : dirs) |
166 | addDirectory(path: dir.filePath(fileName: child), leftDepth: --depthLeft); |
167 | } |
168 | const QStringList qmljs = |
169 | dir.entryList(nameFilters: QStringList({ u"*.qml"_s , u"*.js"_s , u"*.mjs"_s }), filters: QDir::Files); |
170 | int progress = 0; |
171 | { |
172 | QMutexLocker l(&m_mutex); |
173 | m_indexInProgressCost += qmljs.size(); |
174 | progress = indexEvalProgress(); |
175 | } |
176 | indexSendProgress(progress); |
177 | if (qmljs.isEmpty()) |
178 | return; |
179 | DomItem newCurrent = m_currentEnv.makeCopy(option: DomItem::CopyOption::EnvConnected).item(); |
180 | for (const QString &file : qmljs) { |
181 | if (indexCancelled()) |
182 | return; |
183 | QString fPath = dir.filePath(fileName: file); |
184 | DomCreationOptions options; |
185 | options.setFlag(flag: DomCreationOption::WithScriptExpressions); |
186 | options.setFlag(flag: DomCreationOption::WithSemanticAnalysis); |
187 | FileToLoad fileToLoad = |
188 | FileToLoad::fromFileSystem(environment: newCurrent.ownerAs<DomEnvironment>(), canonicalPath: fPath, options); |
189 | if (!fileToLoad.canonicalPath().isEmpty()) { |
190 | newCurrent.loadBuiltins(); |
191 | newCurrent.loadFile(file: fileToLoad, callback: [](Path, DomItem &, DomItem &) {}, loadOptions: {}); |
192 | newCurrent.loadPendingDependencies(); |
193 | newCurrent.commitToBase(validPtr: m_validEnv.ownerAs<DomEnvironment>()); |
194 | } |
195 | { |
196 | QMutexLocker l(&m_mutex); |
197 | ++m_indexDoneCost; |
198 | --m_indexInProgressCost; |
199 | progress = indexEvalProgress(); |
200 | } |
201 | indexSendProgress(progress); |
202 | } |
203 | } |
204 | |
205 | void QQmlCodeModel::addDirectoriesToIndex(const QStringList &paths, QLanguageServer *server) |
206 | { |
207 | Q_UNUSED(server); |
208 | // ### create progress, &scan in a separate instance |
209 | const int maxDepth = 5; |
210 | for (const auto &path : paths) |
211 | addDirectory(path, leftDepth: maxDepth); |
212 | indexNeedsUpdate(); |
213 | } |
214 | |
215 | void QQmlCodeModel::addDirectory(const QString &path, int depthLeft) |
216 | { |
217 | if (depthLeft < 1) |
218 | return; |
219 | { |
220 | QMutexLocker l(&m_mutex); |
221 | for (auto it = m_toIndex.begin(); it != m_toIndex.end();) { |
222 | if (it->path.startsWith(s: path)) { |
223 | if (it->path.size() == path.size()) |
224 | return; |
225 | if (it->path.at(i: path.size()) == u'/') { |
226 | it = m_toIndex.erase(pos: it); |
227 | continue; |
228 | } |
229 | } else if (path.startsWith(s: it->path) && path.at(i: it->path.size()) == u'/') |
230 | return; |
231 | ++it; |
232 | } |
233 | m_toIndex.append(t: { .path: path, .leftDepth: depthLeft }); |
234 | } |
235 | } |
236 | |
237 | void QQmlCodeModel::removeDirectory(const QString &path) |
238 | { |
239 | { |
240 | QMutexLocker l(&m_mutex); |
241 | auto toRemove = [path](const QString &p) { |
242 | return p.startsWith(s: path) && (p.size() == path.size() || p.at(i: path.size()) == u'/'); |
243 | }; |
244 | auto it = m_toIndex.begin(); |
245 | auto end = m_toIndex.end(); |
246 | while (it != end) { |
247 | if (toRemove(it->path)) |
248 | it = m_toIndex.erase(pos: it); |
249 | else |
250 | ++it; |
251 | } |
252 | } |
253 | if (auto validEnvPtr = m_validEnv.ownerAs<DomEnvironment>()) |
254 | validEnvPtr->removePath(path); |
255 | if (auto currentEnvPtr = m_currentEnv.ownerAs<DomEnvironment>()) |
256 | currentEnvPtr->removePath(path); |
257 | } |
258 | |
259 | QString QQmlCodeModel::url2Path(const QByteArray &url, UrlLookup options) |
260 | { |
261 | QString res; |
262 | { |
263 | QMutexLocker l(&m_mutex); |
264 | res = m_url2path.value(key: url); |
265 | } |
266 | if (!res.isEmpty() && options == UrlLookup::Caching) |
267 | return res; |
268 | QUrl qurl(QString::fromUtf8(ba: url)); |
269 | QFileInfo f(qurl.toLocalFile()); |
270 | QString cPath = f.canonicalFilePath(); |
271 | if (cPath.isEmpty()) |
272 | cPath = f.filePath(); |
273 | { |
274 | QMutexLocker l(&m_mutex); |
275 | if (!res.isEmpty() && res != cPath) |
276 | m_path2url.remove(key: res); |
277 | m_url2path.insert(key: url, value: cPath); |
278 | m_path2url.insert(key: cPath, value: url); |
279 | } |
280 | return cPath; |
281 | } |
282 | |
283 | void QQmlCodeModel::newOpenFile(const QByteArray &url, int version, const QString &docText) |
284 | { |
285 | { |
286 | QMutexLocker l(&m_mutex); |
287 | auto &openDoc = m_openDocuments[url]; |
288 | if (!openDoc.textDocument) |
289 | openDoc.textDocument = std::make_shared<Utils::TextDocument>(); |
290 | QMutexLocker l2(openDoc.textDocument->mutex()); |
291 | openDoc.textDocument->setVersion(version); |
292 | openDoc.textDocument->setPlainText(docText); |
293 | } |
294 | addOpenToUpdate(url); |
295 | openNeedUpdate(); |
296 | } |
297 | |
298 | OpenDocument QQmlCodeModel::openDocumentByUrl(const QByteArray &url) |
299 | { |
300 | QMutexLocker l(&m_mutex); |
301 | return m_openDocuments.value(key: url); |
302 | } |
303 | |
304 | void QQmlCodeModel::indexNeedsUpdate() |
305 | { |
306 | const int maxIndexThreads = 1; |
307 | { |
308 | QMutexLocker l(&m_mutex); |
309 | if (m_toIndex.isEmpty() || m_nIndexInProgress >= maxIndexThreads) |
310 | return; |
311 | if (++m_nIndexInProgress == 1) |
312 | indexStart(); |
313 | } |
314 | QThreadPool::globalInstance()->start(functionToRun: [this]() { |
315 | while (indexSome()) { } |
316 | }); |
317 | } |
318 | |
319 | bool QQmlCodeModel::indexSome() |
320 | { |
321 | qCDebug(codeModelLog) << "indexSome" ; |
322 | ToIndex toIndex; |
323 | { |
324 | QMutexLocker l(&m_mutex); |
325 | if (m_toIndex.isEmpty()) { |
326 | if (--m_nIndexInProgress == 0) |
327 | indexEnd(); |
328 | return false; |
329 | } |
330 | toIndex = m_toIndex.last(); |
331 | m_toIndex.removeLast(); |
332 | } |
333 | bool hasMore = false; |
334 | { |
335 | auto guard = qScopeGuard(f: [this, &hasMore]() { |
336 | QMutexLocker l(&m_mutex); |
337 | if (m_toIndex.isEmpty()) { |
338 | if (--m_nIndexInProgress == 0) |
339 | indexEnd(); |
340 | hasMore = false; |
341 | } else { |
342 | hasMore = true; |
343 | } |
344 | }); |
345 | indexDirectory(path: toIndex.path, depthLeft: toIndex.leftDepth); |
346 | } |
347 | return hasMore; |
348 | } |
349 | |
350 | void QQmlCodeModel::openNeedUpdate() |
351 | { |
352 | qCDebug(codeModelLog) << "openNeedUpdate" ; |
353 | const int maxIndexThreads = 1; |
354 | { |
355 | QMutexLocker l(&m_mutex); |
356 | if (m_openDocumentsToUpdate.isEmpty() || m_nUpdateInProgress >= maxIndexThreads) |
357 | return; |
358 | if (++m_nUpdateInProgress == 1) |
359 | openUpdateStart(); |
360 | } |
361 | QThreadPool::globalInstance()->start(functionToRun: [this]() { |
362 | while (openUpdateSome()) { } |
363 | }); |
364 | } |
365 | |
366 | bool QQmlCodeModel::openUpdateSome() |
367 | { |
368 | qCDebug(codeModelLog) << "openUpdateSome start" ; |
369 | QByteArray toUpdate; |
370 | { |
371 | QMutexLocker l(&m_mutex); |
372 | if (m_openDocumentsToUpdate.isEmpty()) { |
373 | if (--m_nUpdateInProgress == 0) |
374 | openUpdateEnd(); |
375 | return false; |
376 | } |
377 | auto it = m_openDocumentsToUpdate.find(value: m_lastOpenDocumentUpdated); |
378 | auto end = m_openDocumentsToUpdate.end(); |
379 | if (it == end) |
380 | it = m_openDocumentsToUpdate.begin(); |
381 | else if (++it == end) |
382 | it = m_openDocumentsToUpdate.begin(); |
383 | toUpdate = *it; |
384 | m_openDocumentsToUpdate.erase(i: it); |
385 | } |
386 | bool hasMore = false; |
387 | { |
388 | auto guard = qScopeGuard(f: [this, &hasMore]() { |
389 | QMutexLocker l(&m_mutex); |
390 | if (m_openDocumentsToUpdate.isEmpty()) { |
391 | if (--m_nUpdateInProgress == 0) |
392 | openUpdateEnd(); |
393 | hasMore = false; |
394 | } else { |
395 | hasMore = true; |
396 | } |
397 | }); |
398 | openUpdate(toUpdate); |
399 | } |
400 | return hasMore; |
401 | } |
402 | |
403 | void QQmlCodeModel::openUpdateStart() |
404 | { |
405 | qCDebug(codeModelLog) << "openUpdateStart" ; |
406 | } |
407 | |
408 | void QQmlCodeModel::openUpdateEnd() |
409 | { |
410 | qCDebug(codeModelLog) << "openUpdateEnd" ; |
411 | } |
412 | |
413 | void QQmlCodeModel::newDocForOpenFile(const QByteArray &url, int version, const QString &docText) |
414 | { |
415 | qCDebug(codeModelLog) << "updating doc" << url << "to version" << version << "(" |
416 | << docText.size() << "chars)" ; |
417 | DomItem newCurrent = m_currentEnv.makeCopy(option: DomItem::CopyOption::EnvConnected).item(); |
418 | QStringList loadPaths = buildPathsForFileUrl(url); |
419 | loadPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)); |
420 | if (std::shared_ptr<DomEnvironment> newCurrentPtr = newCurrent.ownerAs<DomEnvironment>()) { |
421 | newCurrentPtr->setLoadPaths(loadPaths); |
422 | } |
423 | QString fPath = url2Path(url, options: UrlLookup::ForceLookup); |
424 | Path p; |
425 | DomCreationOptions options; |
426 | options.setFlag(flag: DomCreationOption::WithScriptExpressions); |
427 | options.setFlag(flag: DomCreationOption::WithSemanticAnalysis); |
428 | newCurrent.loadFile( |
429 | file: FileToLoad::fromMemory(environment: newCurrent.ownerAs<DomEnvironment>(), path: fPath, data: docText, options), |
430 | callback: [&p](Path, DomItem &, DomItem &newValue) { p = newValue.fileObject().canonicalPath(); }, |
431 | loadOptions: {}); |
432 | newCurrent.loadPendingDependencies(); |
433 | if (p) { |
434 | newCurrent.commitToBase(validPtr: m_validEnv.ownerAs<DomEnvironment>()); |
435 | DomItem item = m_currentEnv.path(p); |
436 | { |
437 | QMutexLocker l(&m_mutex); |
438 | OpenDocument &doc = m_openDocuments[url]; |
439 | if (!doc.textDocument) { |
440 | qCWarning(lspServerLog) |
441 | << "ignoring update to closed document" << QString::fromUtf8(ba: url); |
442 | return; |
443 | } else { |
444 | QMutexLocker l(doc.textDocument->mutex()); |
445 | if (doc.textDocument->version() && *doc.textDocument->version() > version) { |
446 | qCWarning(lspServerLog) |
447 | << "docUpdate: version" << version << "of document" |
448 | << QString::fromUtf8(ba: url) << "is not the latest anymore" ; |
449 | return; |
450 | } |
451 | } |
452 | if (!doc.snapshot.docVersion || *doc.snapshot.docVersion < version) { |
453 | doc.snapshot.docVersion = version; |
454 | doc.snapshot.doc = item; |
455 | } else { |
456 | qCWarning(lspServerLog) << "skipping update of current doc to obsolete version" |
457 | << version << "of document" << QString::fromUtf8(ba: url); |
458 | } |
459 | if (item.field(name: Fields::isValid).value().toBool(defaultValue: false)) { |
460 | if (!doc.snapshot.validDocVersion || *doc.snapshot.validDocVersion < version) { |
461 | DomItem vDoc = m_validEnv.path(p); |
462 | doc.snapshot.validDocVersion = version; |
463 | doc.snapshot.validDoc = vDoc; |
464 | } else { |
465 | qCWarning(lspServerLog) << "skippig update of valid doc to obsolete version" |
466 | << version << "of document" << QString::fromUtf8(ba: url); |
467 | } |
468 | } else { |
469 | qCWarning(lspServerLog) |
470 | << "avoid update of validDoc to " << version << "of document" |
471 | << QString::fromUtf8(ba: url) << "as it is invalid" ; |
472 | } |
473 | } |
474 | } |
475 | if (codeModelLog().isDebugEnabled()) { |
476 | qCDebug(codeModelLog) << "finished update doc of " << url << "to version" << version; |
477 | snapshotByUrl(url).dump(qDebug() << "postSnapshot" , |
478 | dump: OpenDocumentSnapshot::DumpOption::AllCode); |
479 | } |
480 | // we should update the scope in the future thus call addOpen(url) |
481 | emit updatedSnapshot(url); |
482 | } |
483 | |
484 | void QQmlCodeModel::closeOpenFile(const QByteArray &url) |
485 | { |
486 | QMutexLocker l(&m_mutex); |
487 | m_openDocuments.remove(key: url); |
488 | } |
489 | |
490 | void QQmlCodeModel::setRootUrls(const QList<QByteArray> &urls) |
491 | { |
492 | QMutexLocker l(&m_mutex); |
493 | m_rootUrls = urls; |
494 | } |
495 | |
496 | void QQmlCodeModel::addRootUrls(const QList<QByteArray> &urls) |
497 | { |
498 | QMutexLocker l(&m_mutex); |
499 | for (const QByteArray &url : urls) { |
500 | if (!m_rootUrls.contains(t: url)) |
501 | m_rootUrls.append(t: url); |
502 | } |
503 | } |
504 | |
505 | void QQmlCodeModel::removeRootUrls(const QList<QByteArray> &urls) |
506 | { |
507 | QMutexLocker l(&m_mutex); |
508 | for (const QByteArray &url : urls) |
509 | m_rootUrls.removeOne(t: url); |
510 | } |
511 | |
512 | QList<QByteArray> QQmlCodeModel::rootUrls() const |
513 | { |
514 | QMutexLocker l(&m_mutex); |
515 | return m_rootUrls; |
516 | } |
517 | |
518 | QStringList QQmlCodeModel::buildPathsForRootUrl(const QByteArray &url) |
519 | { |
520 | QMutexLocker l(&m_mutex); |
521 | return m_buildPathsForRootUrl.value(key: url); |
522 | } |
523 | |
524 | static bool isNotSeparator(char c) |
525 | { |
526 | return c != '/'; |
527 | } |
528 | |
529 | QStringList QQmlCodeModel::buildPathsForFileUrl(const QByteArray &url) |
530 | { |
531 | QList<QByteArray> roots; |
532 | { |
533 | QMutexLocker l(&m_mutex); |
534 | roots = m_buildPathsForRootUrl.keys(); |
535 | } |
536 | // we want to longest match to be first, as it should override shorter matches |
537 | std::sort(first: roots.begin(), last: roots.end(), comp: [](const QByteArray &el1, const QByteArray &el2) { |
538 | if (el1.size() > el2.size()) |
539 | return true; |
540 | if (el1.size() < el2.size()) |
541 | return false; |
542 | return el1 < el2; |
543 | }); |
544 | QStringList buildPaths; |
545 | QStringList defaultValues; |
546 | if (!roots.isEmpty() && roots.last().isEmpty()) |
547 | roots.removeLast(); |
548 | QByteArray urlSlash(url); |
549 | if (!urlSlash.isEmpty() && isNotSeparator(c: urlSlash.at(i: urlSlash.size() - 1))) |
550 | urlSlash.append(c: '/'); |
551 | // look if the file has a know prefix path |
552 | for (const QByteArray &root : roots) { |
553 | if (urlSlash.startsWith(bv: root)) { |
554 | buildPaths += buildPathsForRootUrl(url: root); |
555 | break; |
556 | } |
557 | } |
558 | QString path = url2Path(url); |
559 | |
560 | // fallback to the empty root, if is has an entry. |
561 | // This is the buildPath that is passed to qmlls via --build-dir. |
562 | if (buildPaths.isEmpty()) { |
563 | buildPaths += buildPathsForRootUrl(url: QByteArray()); |
564 | } |
565 | |
566 | // look in the QMLLS_BUILD_DIRS environment variable |
567 | if (buildPaths.isEmpty()) { |
568 | QStringList envPaths = qEnvironmentVariable(varName: "QMLLS_BUILD_DIRS" ) |
569 | .split(sep: QDir::listSeparator(), behavior: Qt::SkipEmptyParts); |
570 | buildPaths += envPaths; |
571 | } |
572 | |
573 | // look in the settings. |
574 | // This is the one that is passed via the .qmlls.ini file. |
575 | if (buildPaths.isEmpty() && m_settings) { |
576 | m_settings->search(path); |
577 | QString buildDir = QStringLiteral(u"buildDir" ); |
578 | if (m_settings->isSet(name: buildDir)) |
579 | buildPaths += m_settings->value(name: buildDir).toString().split(sep: QDir::listSeparator(), |
580 | behavior: Qt::SkipEmptyParts); |
581 | } |
582 | |
583 | // heuristic to find build directory |
584 | if (buildPaths.isEmpty()) { |
585 | QDir d(path); |
586 | d.setNameFilters(QStringList({ u"build*"_s })); |
587 | const int maxDirDepth = 8; |
588 | int iDir = maxDirDepth; |
589 | QString dirName = d.dirName(); |
590 | QDateTime lastModified; |
591 | while (d.cdUp() && --iDir > 0) { |
592 | for (const QFileInfo &fInfo : d.entryInfoList(filters: QDir::Dirs)) { |
593 | if (fInfo.completeBaseName() == u"build" |
594 | || fInfo.completeBaseName().startsWith(s: u"build-%1"_s .arg(a: dirName))) { |
595 | if (iDir > 1) |
596 | iDir = 1; |
597 | if (!lastModified.isValid() || lastModified < fInfo.lastModified()) { |
598 | buildPaths.clear(); |
599 | buildPaths.append(t: fInfo.absoluteFilePath()); |
600 | } |
601 | } |
602 | } |
603 | } |
604 | } |
605 | // add dependent build directories |
606 | QStringList res; |
607 | std::reverse(first: buildPaths.begin(), last: buildPaths.end()); |
608 | const int maxDeps = 4; |
609 | while (!buildPaths.isEmpty()) { |
610 | QString bPath = buildPaths.last(); |
611 | buildPaths.removeLast(); |
612 | res += bPath; |
613 | if (QFile::exists(fileName: bPath + u"/_deps" ) && bPath.split(sep: u"/_deps/"_s ).size() < maxDeps) { |
614 | QDir d(bPath + u"/_deps" ); |
615 | for (const QFileInfo &fInfo : d.entryInfoList(filters: QDir::Dirs)) |
616 | buildPaths.append(t: fInfo.absoluteFilePath()); |
617 | } |
618 | } |
619 | return res; |
620 | } |
621 | |
622 | void QQmlCodeModel::setBuildPathsForRootUrl(QByteArray url, const QStringList &paths) |
623 | { |
624 | QMutexLocker l(&m_mutex); |
625 | if (!url.isEmpty() && isNotSeparator(c: url.at(i: url.size() - 1))) |
626 | url.append(c: '/'); |
627 | if (paths.isEmpty()) |
628 | m_buildPathsForRootUrl.remove(key: url); |
629 | else |
630 | m_buildPathsForRootUrl.insert(key: url, value: paths); |
631 | } |
632 | |
633 | void QQmlCodeModel::openUpdate(const QByteArray &url) |
634 | { |
635 | bool updateDoc = false; |
636 | bool updateScope = false; |
637 | std::optional<int> rNow = 0; |
638 | QString docText; |
639 | DomItem validDoc; |
640 | std::shared_ptr<Utils::TextDocument> document; |
641 | { |
642 | QMutexLocker l(&m_mutex); |
643 | OpenDocument &doc = m_openDocuments[url]; |
644 | document = doc.textDocument; |
645 | if (!document) |
646 | return; |
647 | { |
648 | QMutexLocker l2(document->mutex()); |
649 | rNow = document->version(); |
650 | } |
651 | if (rNow && (!doc.snapshot.docVersion || *doc.snapshot.docVersion != *rNow)) |
652 | updateDoc = true; |
653 | else if (doc.snapshot.validDocVersion |
654 | && (!doc.snapshot.scopeVersion |
655 | || *doc.snapshot.scopeVersion != *doc.snapshot.validDocVersion)) |
656 | updateScope = true; |
657 | else |
658 | return; |
659 | if (updateDoc) { |
660 | QMutexLocker l2(doc.textDocument->mutex()); |
661 | rNow = doc.textDocument->version(); |
662 | docText = doc.textDocument->toPlainText(); |
663 | } else { |
664 | validDoc = doc.snapshot.validDoc; |
665 | rNow = doc.snapshot.validDocVersion; |
666 | } |
667 | } |
668 | if (updateDoc) { |
669 | newDocForOpenFile(url, version: *rNow, docText); |
670 | } |
671 | if (updateScope) { |
672 | // to do |
673 | } |
674 | } |
675 | |
676 | void QQmlCodeModel::addOpenToUpdate(const QByteArray &url) |
677 | { |
678 | QMutexLocker l(&m_mutex); |
679 | m_openDocumentsToUpdate.insert(value: url); |
680 | } |
681 | |
682 | QDebug OpenDocumentSnapshot::dump(QDebug dbg, DumpOptions options) |
683 | { |
684 | dbg.noquote().nospace() << "{" ; |
685 | dbg << " url:" << QString::fromUtf8(ba: url) << "\n" ; |
686 | dbg << " docVersion:" << (docVersion ? QString::number(*docVersion) : u"*none*"_s ) << "\n" ; |
687 | if (options & DumpOption::LatestCode) { |
688 | dbg << " doc: ------------\n" |
689 | << doc.field(name: Fields::code).value().toString() << "\n==========\n" ; |
690 | } else { |
691 | dbg << u" doc:" |
692 | << (doc ? u"%1chars"_s .arg(a: doc.field(name: Fields::code).value().toString().size()) |
693 | : u"*none*"_s ) |
694 | << "\n" ; |
695 | } |
696 | dbg << " validDocVersion:" |
697 | << (validDocVersion ? QString::number(*validDocVersion) : u"*none*"_s ) << "\n" ; |
698 | if (options & DumpOption::ValidCode) { |
699 | dbg << " validDoc: ------------\n" |
700 | << validDoc.field(name: Fields::code).value().toString() << "\n==========\n" ; |
701 | } else { |
702 | dbg << u" validDoc:" |
703 | << (validDoc ? u"%1chars"_s .arg(a: validDoc.field(name: Fields::code).value().toString().size()) |
704 | : u"*none*"_s ) |
705 | << "\n" ; |
706 | } |
707 | dbg << " scopeVersion:" << (scopeVersion ? QString::number(*scopeVersion) : u"*none*"_s ) |
708 | << "\n" ; |
709 | dbg << " scopeDependenciesLoadTime:" << scopeDependenciesLoadTime << "\n" ; |
710 | dbg << " scopeDependenciesChanged" << scopeDependenciesChanged << "\n" ; |
711 | dbg << "}" ; |
712 | return dbg; |
713 | } |
714 | |
715 | } // namespace QmlLsp |
716 | |
717 | QT_END_NAMESPACE |
718 | |