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