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