| 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_STATIC_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 | |