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 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | Q_LOGGING_CATEGORY(QQmlLSHelpUtilsLog, "qt.languageserver.helpUtils" ) |
17 | |
18 | using namespace QQmlJS::Dom; |
19 | |
20 | static 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 | |
31 | HelpManager::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 | |
45 | void 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 | |
61 | QString HelpManager::documentationRootPath() const |
62 | { |
63 | return m_docRootPath; |
64 | } |
65 | |
66 | void 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 | |
74 | std::optional<QByteArray> HelpManager::(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 | |
89 | std::optional<QByteArray> |
90 | HelpManager::(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 (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 (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 (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 | |
134 | std::optional<QByteArray> HelpManager::(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 (item.internalKind()); |
172 | return tryExtract(extractor, links, name); |
173 | } |
174 | |
175 | std::optional<QByteArray> |
176 | HelpManager::(ExtractDocumentation &, |
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 | |
199 | std::optional<QByteArray> |
200 | HelpManager::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 | */ |
254 | std::vector<QQmlLSHelpProviderBase::DocumentLink> |
255 | HelpManager::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 | |
297 | QT_END_NAMESPACE |
298 | |