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