1// Copyright (C) 2024 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 "qqmllshelputils_p.h"
5
6#include <QtQmlLS/private/qqmllsutils_p.h>
7#include <QtCore/private/qfactoryloader_p.h>
8#include <QtCore/qlibraryinfo.h>
9#include <QtCore/qdiriterator.h>
10#include <QtCore/qdir.h>
11#include <QtQmlCompiler/private/qqmljstyperesolver_p.h>
12#include <optional>
13
14QT_BEGIN_NAMESPACE
15
16Q_LOGGING_CATEGORY(QQmlLSHelpUtilsLog, "qt.languageserver.helpUtils")
17
18using namespace QQmlJS::Dom;
19
20static QStringList documentationFiles(const QString &qtInstallationPath)
21{
22 QStringList result;
23 QDirIterator dirIterator(qtInstallationPath, QStringList{ "*.qch"_L1 }, QDir::Files);
24 while (dirIterator.hasNext()) {
25 const auto fileInfo = dirIterator.nextFileInfo();
26 result << fileInfo.absoluteFilePath();
27 }
28 return result;
29}
30
31HelpManager::HelpManager()
32{
33 const QFactoryLoader pluginLoader(QQmlLSHelpPluginInterface_iid, u"/help"_s);
34 const auto keys = pluginLoader.metaDataKeys();
35 for (qsizetype i = 0; i < keys.size(); ++i) {
36 auto instance = qobject_cast<QQmlLSHelpPluginInterface *>(object: pluginLoader.instance(index: i));
37 if (instance) {
38 m_helpPlugin =
39 instance->initialize(collectionFile: QDir::tempPath() + "/collectionFile.qhc"_L1, parent: nullptr);
40 break;
41 }
42 }
43}
44
45void HelpManager::setDocumentationRootPath(const QString &path)
46{
47 if (m_docRootPath == path)
48 return;
49 m_docRootPath = path;
50
51 const auto foundQchFiles = documentationFiles(qtInstallationPath: path);
52 if (foundQchFiles.isEmpty()) {
53 qCWarning(QQmlLSHelpUtilsLog)
54 << "No documentation files found in the Qt doc installation path: " << path;
55 return;
56 }
57
58 return registerDocumentations(docs: foundQchFiles);
59}
60
61QString HelpManager::documentationRootPath() const
62{
63 return m_docRootPath;
64}
65
66void HelpManager::registerDocumentations(const QStringList &docs) const
67{
68 if (!m_helpPlugin)
69 return;
70 std::for_each(first: docs.cbegin(), last: docs.cend(),
71 f: [this](const auto &file) { m_helpPlugin->registerDocumentation(documentationFileName: file); });
72}
73
74std::optional<QByteArray> HelpManager::extractDocumentation(const DomItem &item) const
75{
76 if (item.internalKind() == DomType::ScriptIdentifierExpression) {
77 const auto resolvedType =
78 QQmlLSUtils::resolveExpressionType(item, QQmlLSUtils::ResolveOwnerType);
79 if (!resolvedType)
80 return std::nullopt;
81 return extractDocumentationForIdentifiers(item, expr: resolvedType.value());
82 } else {
83 return extractDocumentationForDomElements(item);
84 }
85
86 Q_UNREACHABLE_RETURN(std::nullopt);
87}
88
89std::optional<QByteArray>
90HelpManager::extractDocumentationForIdentifiers(const DomItem &item,
91 QQmlLSUtils::ExpressionType expr) const
92{
93 const auto links = collectDocumentationLinks(item, scope: expr.semanticScope, name: expr.name.value_or(u: item.name()));
94 if (links.empty())
95 return std::nullopt;
96 switch (expr.type) {
97 case QQmlLSUtils::QmlObjectIdIdentifier:
98 case QQmlLSUtils::JavaScriptIdentifier:
99 case QQmlLSUtils::GroupedPropertyIdentifier:
100 case QQmlLSUtils::PropertyIdentifier: {
101 ExtractDocumentation extractor(DomType::PropertyDefinition);
102 return tryExtract(extractor, links, name: expr.name.value());
103 }
104 case QQmlLSUtils::PropertyChangedSignalIdentifier:
105 case QQmlLSUtils::PropertyChangedHandlerIdentifier:
106 case QQmlLSUtils::SignalIdentifier:
107 case QQmlLSUtils::SignalHandlerIdentifier:
108 case QQmlLSUtils::MethodIdentifier: {
109 ExtractDocumentation extractor(DomType::MethodInfo);
110 return tryExtract(extractor, links, name: expr.name.value());
111 }
112 case QQmlLSUtils::SingletonIdentifier:
113 case QQmlLSUtils::AttachedTypeIdentifier:
114 case QQmlLSUtils::QmlComponentIdentifier: {
115 const auto &keyword = item.field(name: Fields::identifier).value().toString();
116 // The keyword is a qmlobject. Keyword search should be sufficient.
117 // TODO: Still there can be multiple qmlobject documentation, with
118 // different Qt versions. We should pick the best one.
119 ExtractDocumentation extractor(DomType::QmlObject);
120 return tryExtract(extractor, links: m_helpPlugin->documentsForKeyword(keyword), name: keyword);
121 }
122
123 // Not implemented yet
124 case QQmlLSUtils::EnumeratorIdentifier:
125 case QQmlLSUtils::EnumeratorValueIdentifier:
126 default:
127 qCDebug(QQmlLSHelpUtilsLog)
128 << "Documentation extraction for" << expr.name.value() << "was not implemented";
129 return std::nullopt;
130 }
131 Q_UNREACHABLE_RETURN(std::nullopt);
132}
133
134std::optional<QByteArray> HelpManager::extractDocumentationForDomElements(const DomItem &item) const
135{
136 const auto qmlFile = item.containingFile().as<QmlFile>();
137 if (!qmlFile)
138 return std::nullopt;
139
140 const auto name = item.field(name: Fields::name).value().toString();
141 std::vector<QQmlLSHelpProviderBase::DocumentLink> links;
142 switch (item.internalKind()) {
143 case DomType::QmlObject: {
144 links = collectDocumentationLinks(item, scope: item.nearestSemanticScope(), name);
145 break;
146 }
147 case DomType::PropertyDefinition: {
148 links = collectDocumentationLinks(
149 item, scope: QQmlLSUtils::findDefiningScopeForProperty(referrerScope: item.nearestSemanticScope(), nameToCheck: name),
150 name);
151 break;
152 }
153 case DomType::Binding: {
154 links = collectDocumentationLinks(
155 item, scope: QQmlLSUtils::findDefiningScopeForBinding(referrerScope: item.nearestSemanticScope(), nameToCheck: name),
156 name);
157 break;
158 }
159 case DomType::MethodInfo: {
160 links = collectDocumentationLinks(
161 item, scope: QQmlLSUtils::findDefiningScopeForMethod(referrerScope: item.nearestSemanticScope(), nameToCheck: name),
162 name);
163 break;
164 }
165 default:
166 qCDebug(QQmlLSHelpUtilsLog)
167 << item.internalKindStr() << "was not implemented for documentation extraction";
168 return std::nullopt;
169 }
170
171 ExtractDocumentation extractor(item.internalKind());
172 return tryExtract(extractor, links, name);
173}
174
175std::optional<QByteArray>
176HelpManager::tryExtract(ExtractDocumentation &extractor,
177 const std::vector<QQmlLSHelpProviderBase::DocumentLink> &links,
178 const QString &name) const
179{
180 if (!m_helpPlugin)
181 return std::nullopt;
182
183 for (auto &&link : links) {
184 const auto fileData = m_helpPlugin->fileData(url: link.url);
185 if (fileData.isEmpty()) {
186 qCDebug(QQmlLSHelpUtilsLog) << "No documentation found for" << link.url;
187 continue;
188 }
189 const auto &documentation = extractor.execute(code: QString::fromUtf8(ba: fileData), keyword: name,
190 mode: HtmlExtractor::ExtractionMode::Simplified);
191 if (documentation.isEmpty())
192 continue;
193 return documentation.toUtf8();
194 }
195
196 return std::nullopt;
197}
198
199std::optional<QByteArray>
200HelpManager::documentationForItem(const DomItem &file, QLspSpecification::Position position)
201{
202 if (!m_helpPlugin)
203 return std::nullopt;
204
205 if (m_helpPlugin->registeredNamespaces().empty())
206 return std::nullopt;
207
208 // Prepare Cpp types to Qml types mapping.
209 const auto fileItem = file.containingFile().as<QmlFile>();
210 if (!fileItem)
211 return std::nullopt;
212 const auto typeResolver = fileItem->typeResolver();
213 if (typeResolver) {
214 const auto &names = typeResolver->importedNames();
215 for (auto &&[scope, qmlName] : names.asKeyValueRange()) {
216 auto sc = scope;
217 // in some situations, scope->internalName() could be the same
218 // as qmlName. In those cases, the key we are looking for is the
219 // first scope which is non-composite type.
220 // This is mostly the case for templated controls.
221 // Popup <-> Popup
222 // T.Popup <-> Popup
223 // QQuickPopup <-> Popup
224 if (sc && sc->internalName() == qmlName) {
225 while (sc && sc->isComposite())
226 sc = sc->baseType();
227 }
228 if (sc && !m_cppTypesToQmlTypes.contains(key: sc->internalName()))
229 m_cppTypesToQmlTypes.insert(key: sc->internalName(), value: qmlName);
230 }
231 }
232
233 std::optional<QByteArray> result;
234 const auto [line, character] = position;
235 const auto itemLocations = QQmlLSUtils::itemsFromTextLocation(file, line, character);
236 // Process found item's internalKind and fetch its documentation.
237 for (const auto &entry : itemLocations) {
238 result = extractDocumentation(item: entry.domItem);
239 if (result.has_value())
240 break;
241 }
242
243 return result;
244}
245
246/*
247 * Returns the list of potential documentation links for the given item.
248 * A keyword is not necessarily a unique name, so we need to find the scope where
249 * the keyword is defined. If the item is a property, method or binding, it will
250 * search for the defining scope and return the documentation links by looking at
251 * the imported names. If the item is a QmlObject, it will return the documentation
252 * links for qmlobject name.
253 */
254std::vector<QQmlLSHelpProviderBase::DocumentLink>
255HelpManager::collectDocumentationLinks(const DomItem &item, const QQmlJSScope::ConstPtr &definingScope,
256 const QString &name) const
257{
258 if (!(m_helpPlugin && definingScope))
259 return {};
260 const auto &qmlFile = item.containingFile().as<QmlFile>();
261 if (!qmlFile)
262 return {};
263 const auto typeResolver = qmlFile->typeResolver();
264 if (!typeResolver)
265 return {};
266
267 std::vector<QQmlLSHelpProviderBase::DocumentLink> links;
268 const auto &foundScopeName = definingScope->internalName();
269 if (m_cppTypesToQmlTypes.contains(key: foundScopeName)) {
270 const QString id = m_cppTypesToQmlTypes.value(key: foundScopeName) + u"::"_s + name;
271 links = m_helpPlugin->documentsForIdentifier(id);
272 if (!links.empty())
273 return links;
274 }
275
276 const auto &containingObjectName = item.qmlObject().name();
277 auto scope = item.nearestSemanticScope();
278 while (scope && scope->isComposite()) {
279 const QString id = containingObjectName + u"::"_s + name;
280 links = m_helpPlugin->documentsForIdentifier(id);
281 if (!links.empty())
282 return links;
283 scope = scope->baseType();
284 }
285
286 while (scope && !m_cppTypesToQmlTypes.contains(key: scope->internalName())) {
287 const QString id = m_cppTypesToQmlTypes.value(key: scope->internalName()) + u"::"_s + name;
288 links = m_helpPlugin->documentsForIdentifier(id);
289 if (!links.empty())
290 return links;
291 scope = scope->baseType();
292 }
293
294 return m_helpPlugin->documentsForKeyword(keyword: name);
295}
296
297QT_END_NAMESPACE
298

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtdeclarative/src/qmlls/qqmllshelputils.cpp