Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmlcodemodelmanager.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 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// Qt-Security score:significant reason:trusted-sources
4
7
8#include <QtQmlCompiler/private/qqmljsutils_p.h>
9
10#include <memory>
11
12QT_BEGIN_NAMESPACE
13
14namespace QmlLsp {
15
16using namespace QQmlJS::Dom;
17using namespace Qt::StringLiterals;
18
19void QQmlCodeModelManager::onCMakeProberFinished(int exitCode, QProcess::ExitStatus exitStatus)
20{
21 if (m_cmakeStatus == DoesNotHaveCMake)
22 return;
23
24 if (exitStatus != QProcess::NormalExit || exitCode != 0) {
25 disableCMakeCalls();
26 return;
27 }
28 m_cmakeStatus = HasCMake;
29 for (const auto &ws : m_workspaces)
30 ws.codeModel->tryEnableCMakeCalls(&m_processScheduler);
31}
32
33/*!
34\internal
35Enable and initialize the functionality that uses CMake, if CMake exists.
36
37\note Set the buildpaths before calling this method!
38*/
39void QQmlCodeModelManager::tryEnableCMakeCalls()
40{
41 m_cmakeStatus = IsProbingCMake;
42
43 m_cmakeProber.setProgram(u"cmake"_s);
44 m_cmakeProber.setArguments({ u"--version"_s });
45 QObject::connect(&m_cmakeProber, &QProcess::finished, this,
46 &QQmlCodeModelManager::onCMakeProberFinished);
47 QObject::connect(&m_cmakeProber, &QProcess::errorOccurred, this,
48 &QQmlCodeModelManager::disableCMakeCalls);
49
50 m_cmakeProber.start();
51}
52
53QQmlCodeModelManager::QQmlCodeModelManager(QObject *parent, QQmlToolingSharedSettings *settings)
54 : QObject{ parent }, m_settings(settings), m_pluginLoader(QmlLSPluginInterface_iid, u"/qmlls"_s)
55{
56 const QByteArray defaultCodeModel;
57 appendWorkspace(defaultCodeModel, ManagedByServer);
58 connect(&m_processScheduler, &QProcessScheduler::done, this,
59 &QQmlCodeModelManager::onBuildFinished);
60
61 connect(&m_processScheduler, &QProcessScheduler::done, this,
62 &QQmlCodeModelManager::backgroundBuildFinished);
63 connect(&m_processScheduler, &QProcessScheduler::started, this,
64 &QQmlCodeModelManager::backgroundBuildStarted);
65 connect(&m_processScheduler, &QProcessScheduler::cancelled, this,
66 &QQmlCodeModelManager::backgroundBuildCancelled);
67}
68
69void QQmlCodeModelManager::onBuildFinished(const QByteArray &url)
70{
71 auto it = findWorkspace(url);
72 if (it == m_workspaces.end())
73 return;
74
75 // refresh information obtained from build folder, like .qmlls.build.ini or resource files
76 const QStringList buildPaths = it->codeModel->buildPaths();
77 m_buildInformation.loadSettingsFrom(buildPaths, ForceUpdate);
78 setBuildPathsOn(&*it, buildPaths, DontAppendPathsFromFallback);
79}
80
81void QQmlCodeModelManager::prepareForShutdown()
82{
83 for (auto it = m_workspaces.begin(), end = m_workspaces.end(); it != end; ++it)
84 it->codeModel->prepareForShutdown();
85}
86
87QQmlCodeModelManager::~QQmlCodeModelManager()
88{
89 m_cmakeProber.kill();
90 m_cmakeProber.waitForFinished();
91 prepareForShutdown();
92}
93
94QQmlCodeModelManager::WorkspaceIterator
95QQmlCodeModelManager::findWorkspaceForFile(const QByteArray &url)
96{
97 Q_ASSERT(!m_workspaces.empty());
98 // if file was already opened before: re-use same CodeModel as last time
99 if (auto it = m_file2CodeModel.find(url); it != m_file2CodeModel.end()) {
100 const auto result = findWorkspace(it->second);
101 Q_ASSERT(result != m_workspaces.end());
102 return result;
103 }
104
105 long longestRootUrl = 0;
106 WorkspaceIterator result = m_workspaces.begin();
107 for (auto it = m_workspaces.begin(), end = m_workspaces.end(); it != end; ++it) {
108 if (it->toBeClosed)
109 continue;
110 const QByteArray rootUrl = it->url;
111 if (!url.startsWith(rootUrl))
112 continue;
113
114 if (rootUrl.size() == url.size())
115 return it;
116
117 const long rootUrlLength = rootUrl.length();
118 if (rootUrlLength > longestRootUrl) {
119 longestRootUrl = rootUrlLength;
120 result = it;
121 }
122 }
123
124 // check .qmlls.build.ini for a potentially better match
125 if (const ModuleSetting moduleSetting =
126 m_buildInformation.settingFor(QUrl::fromEncoded(url).toLocalFile());
127 !moduleSetting.importPaths.isEmpty()) {
128 const QByteArray rootUrl = QUrl::fromLocalFile(moduleSetting.sourceFolder).toEncoded();
129 if (longestRootUrl < rootUrl.size()) {
130 appendWorkspace(rootUrl, ManagedByServer);
131 return --m_workspaces.end();
132 }
133 }
134 Q_ASSERT(result != m_workspaces.end());
135 return result;
136}
137
138QQmlCodeModel *QQmlCodeModelManager::findCodeModelForFile(const QByteArray &url)
139{
140 return findWorkspaceForFile(url)->codeModel.get();
141}
142
143QQmlCodeModelManager::WorkspaceMutableIterator
144QQmlCodeModelManager::findWorkspace(const QByteArray &url)
145{
146 return std::find_if(m_workspaces.begin(), m_workspaces.end(),
147 [&url](const QQmlWorkspace &ws) { return ws.url == url; });
148}
149
150void QQmlCodeModelManager::setBuildPathsOn(const QQmlWorkspace *ws, const QStringList &buildFolder,
151 SetBuildPathOption option)
152{
153 ws->codeModel->setBuildPaths(
154 buildFolder
155 + (option == DontAppendPathsFromFallback ? QStringList{} : defaultBuildPaths()));
156
157 const QString file = QUrl::fromEncoded(ws->url).toLocalFile();
158 ws->codeModel->setImportPaths(
159 m_buildInformation.importPathsFor(file)
160 + (option == DontAppendPathsFromFallback ? QStringList{} : defaultImportPaths()));
161
162 if (const QStringList resourceFiles = m_buildInformation.resourceFilesFor(file);
163 !resourceFiles.isEmpty()) {
164 ws->codeModel->setResourceFiles(resourceFiles);
165 return;
166 }
167 // fallback for qt projects < 6.11 without resource files in their .qmlls.build.ini
168 const QStringList resourceFiles =
169 QQmlJSUtils::resourceFilesFromBuildFolders(ws->codeModel->buildPaths());
170 ws->codeModel->setResourceFiles(
171 resourceFiles
172 + (option == DontAppendPathsFromFallback ? QStringList{} : defaultResourceFiles()));
173}
174
175void QQmlCodeModelManager::appendWorkspace(const QByteArray &url, ManagedBy managedBy)
176{
177 QQmlWorkspace ws;
178 ws.url = url;
179 ws.codeModel = std::make_unique<QQmlCodeModel>(url, this, m_settings);
180
181 // the non-fallback codemodel inherits the default values from the fallback codemodel
182 if (!url.isEmpty()) {
183 ws.codeModel->setCMakeJobs(defaultCMakeJobs());
184 ws.codeModel->setDocumentationRootPath(defaultDocumentationRootPath());
185
186 setBuildPathsOn(&ws, {}, AppendPathsFromFallback);
187 }
188
189 QObject::connect(ws.codeModel.get(), &QQmlCodeModel::updatedSnapshot, this,
190 &QQmlCodeModelManager::updatedSnapshot);
191 ws.managedByClient = managedBy == ManagedByClient;
192
193 switch (m_cmakeStatus) {
194 case DoesNotHaveCMake:
195 ws.codeModel->disableCMakeCalls();
196 break;
197 case HasCMake:
198 ws.codeModel->tryEnableCMakeCalls(&m_processScheduler);
199 break;
200 case IsProbingCMake:
201 // will be enabled once the CMake probing process finishes
202 break;
203 }
204 m_workspaces.emplace_back(std::move(ws));
205}
206
207QQmlCodeModelManager::WorkspaceIterator
208QQmlCodeModelManager::workspaceFromBuildFolder(const QString &fileName,
209 const QStringList &buildFolders)
210{
211 m_buildInformation.loadSettingsFrom(buildFolders);
212 const ModuleSetting setting = m_buildInformation.settingFor(fileName);
213 QByteArray url = QUrl::fromLocalFile(setting.sourceFolder).toEncoded();
214 if (auto it = findWorkspace(url); it != m_workspaces.end())
215 return it;
216 appendWorkspace(url, ManagedByServer);
217 return --m_workspaces.end();
218}
219
220void QQmlCodeModelManager::disableCMakeCalls()
221{
222 m_cmakeStatus = DoesNotHaveCMake;
223 for (const auto &ws : m_workspaces)
224 ws.codeModel->disableCMakeCalls();
225}
226
227void QQmlCodeModelManager::cancelBackgroundBuild(const QByteArray &uri)
228{
229 m_processScheduler.cancel(uri);
230}
231
232OpenDocumentSnapshot QQmlCodeModelManager::snapshotByUrl(const QByteArray &url)
233{
234 return findCodeModelForFile(url)->snapshotByUrl(url);
235}
236
237void QQmlCodeModelManager::removeDirectory(const QByteArray &url)
238{
239 findCodeModelForFile(url)->removeDirectory(url);
240}
241
242void QQmlCodeModelManager::newOpenFile(const QByteArray &url, int version, const QString &docText)
243{
244 const auto ws = findWorkspaceForFile(url);
245 m_file2CodeModel[url] = ws->url;
246 ws->codeModel->newOpenFile(url, version, docText);
247}
248
249OpenDocument QQmlCodeModelManager::openDocumentByUrl(const QByteArray &url)
250{
251 return findCodeModelForFile(url)->openDocumentByUrl(url);
252}
253
254RegisteredSemanticTokens &QQmlCodeModelManager::registeredTokens(const QByteArray &url)
255{
256 return findCodeModelForFile(url)->registeredTokens();
257}
258
259void QQmlCodeModelManager::closeOpenFile(const QByteArray &url)
260{
261 m_file2CodeModel.erase(url);
262 const auto it = findWorkspaceForFile(url);
263 it->codeModel->closeOpenFile(url);
264
265 // don't close the default workspace
266 if (it->url.isEmpty())
267 return;
268
269 // close empty WS when managed by server or when client marked ws as toBeClosed.
270 if ((it->managedByClient && it->toBeClosed) || !it->managedByClient) {
271 if (it->codeModel->isEmpty())
272 m_workspaces.erase(it);
273 }
274}
275
276QList<QByteArray> QQmlCodeModelManager::rootUrls() const
277{
278 QList<QByteArray> result;
279 result.reserve(m_workspaces.size());
280 for (const QQmlWorkspace &ws : m_workspaces) {
281 result << ws.url;
282 }
283 return result;
284}
285
286void QQmlCodeModelManager::addRootUrls(const QList<QByteArray> &urls)
287{
288 for (const QByteArray &url : urls) {
289 if (const auto it = findWorkspace(url); it != m_workspaces.end()) {
290 it->toBeClosed = false;
291 continue;
292 }
293
294 appendWorkspace(url, ManagedByClient);
295 }
296}
297
298void QQmlCodeModelManager::removeRootUrls(const QList<QByteArray> &urls)
299{
300 for (const QByteArray &url : urls) {
301 if (auto it = findWorkspace(url); it != m_workspaces.end() && it->managedByClient)
302 it->toBeClosed = true;
303 }
304}
305
306QStringList QQmlCodeModelManager::importPathsForUrl(const QByteArray &url)
307{
308 return findCodeModelForFile(url)->importPathsForUrl(url);
309}
310
311QStringList QQmlCodeModelManager::buildPathsForFileUrl(const QByteArray &url)
312{
313 return findCodeModelForFile(url)->buildPathsForFileUrl(url);
314}
315
316QStringList QQmlCodeModelManager::resourceFilesForFileUrl(const QByteArray &url)
317{
318 if (const QStringList result =
319 m_buildInformation.resourceFilesFor(QUrl::fromEncoded(url).toLocalFile());
320 !result.isEmpty()) {
321 return result;
322 }
323
324 // fallback, for standalone qmlls on projects targeting qt < 6.11
325 return findCodeModelForFile(url)->resourceFiles();
326}
327
328QByteArray QQmlCodeModelManager::shortestRootUrlForFile(const QByteArray &fileUrl) const
329{
330 // fallback value for candidate is the empty url of the default workspace
331 QByteArray candidate;
332
333 // ignore the default workspace which is at the front of m_workspaces
334 Q_ASSERT(m_workspaces.size() > 0);
335 Q_ASSERT(m_workspaces.front().url.isEmpty());
336 auto it = std::find_if(
337 ++m_workspaces.cbegin(), m_workspaces.cend(),
338 [&fileUrl](const QQmlWorkspace &ws) { return fileUrl.startsWith(ws.url); });
339
340 if (it != m_workspaces.cend())
341 candidate = it->url;
342
343 for (; it != m_workspaces.cend(); ++it) {
344 if (it->url.length() < candidate.length() && fileUrl.startsWith(it->url))
345 candidate = it->url;
346 }
347 return candidate;
348}
349
350void QQmlCodeModelManager::setDocumentationRootPath(const QString &path)
351{
352 // Note: this function can only be called after the fallback workspace was created but before
353 // all other potential workspaces are created, for example when setting the import paths set via
354 // commandline option or environment variable.
355 Q_ASSERT(m_workspaces.size() == 1);
356 for (const auto &ws : m_workspaces)
357 ws.codeModel->setDocumentationRootPath(path);
358}
359
360void QQmlCodeModelManager::setVerbose(bool verbose)
361{
362 m_verbose = verbose;
363 for (const auto &ws : m_workspaces)
364 ws.codeModel->setVerbose(verbose);
365}
366
367void QQmlCodeModelManager::setCMakeJobs(int jobs)
368{
369 for (const auto &ws : m_workspaces)
370 ws.codeModel->setCMakeJobs(jobs);
371}
372
373void QQmlCodeModelManager::setBuildPathsForRootUrl(const QByteArray &url, const QStringList &paths)
374{
375 m_buildInformation.loadSettingsFrom(paths);
376
377 // build paths passed by -b have an empty url and apply to all workspaces
378 if (url.isEmpty()) {
379 setBuildPathsOn(&*fallbackWorkspace(), paths, DontAppendPathsFromFallback);
380 // make non-fallback workspaces get the fallback build path
381 for (auto it = beginNonFallbackWorkspace(), end = endNonFallbackWorkspace(); it != end;
382 ++it) {
383 setBuildPathsOn(&*it, {}, AppendPathsFromFallback);
384 }
385 return;
386 }
387
388 auto ws = findWorkspaceForFile(url);
389 setBuildPathsOn(&*ws, paths, AppendPathsFromFallback);
390}
391
392void QQmlCodeModelManager::addOpenToUpdate(const QByteArray &url)
393{
394 findCodeModelForFile(url)->addOpenToUpdate(url, NormalUpdate);
395}
396
397void QQmlCodeModelManager::setImportPaths(const QStringList &paths)
398{
399 // Note: this function can only be called after the fallback workspace was created but before
400 // all other potential workspaces are created, for example when setting the import paths set via
401 // commandline option or environment variable.
402 Q_ASSERT(m_workspaces.size() == 1);
403 for (const auto &ws : m_workspaces)
404 ws.codeModel->setImportPaths(paths);
405}
406
407HelpManager *QQmlCodeModelManager::helpManagerForUrl(const QByteArray &url)
408{
409 return findCodeModelForFile(url)->helpManager();
410}
411
412} // namespace QmlLsp
413
414QT_END_NAMESPACE
#define QmlLSPluginInterface_iid