diff --git a/src/plugins/clangcodemodel/clangdclient.cpp b/src/plugins/clangcodemodel/clangdclient.cpp index c9c99c81869..4588e14e2b4 100644 --- a/src/plugins/clangcodemodel/clangdclient.cpp +++ b/src/plugins/clangcodemodel/clangdclient.cpp @@ -261,7 +261,9 @@ public: QString searchTermFromCursor(const QTextCursor &cursor) const; QTextCursor adjustedCursor(const QTextCursor &cursor, const TextDocument *doc); - void setHelpItemForTooltip(const MessageId &token, const QString &fqn = {}, + void setHelpItemForTooltip(const MessageId &token, + const DocumentUri &uri, + const QString &fqn = {}, HelpItem::Category category = HelpItem::Unknown, const QString &type = {}); @@ -928,7 +930,10 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR if (closingQuoteIndex != -1) { const QString macroName = markupString.mid(nameStart, closingQuoteIndex - nameStart); - d->setHelpItemForTooltip(hoverResponse.id(), macroName, HelpItem::Macro); + d->setHelpItemForTooltip(hoverResponse.id(), + uri, + macroName, + HelpItem::Macro); return; } } @@ -941,6 +946,7 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR const auto filePath = Utils::FilePath::fromUserInput(lines.last().simplified()); if (filePath.exists()) { d->setHelpItemForTooltip(hoverResponse.id(), + uri, filePath.fileName(), HelpItem::Brief); return; @@ -961,7 +967,7 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR } const ClangdAstPath path = getAstPath(ast, range); if (path.isEmpty()) { - d->setHelpItemForTooltip(id); + d->setHelpItemForTooltip(id, uri); return; } ClangdAstNode node = path.last(); @@ -985,7 +991,7 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR type = type.left(angleBracketIndex); }; - if (gatherMemberFunctionOverrideHelpItemForTooltip(id, path)) + if (gatherMemberFunctionOverrideHelpItemForTooltip(id, uri, path)) return; const bool isMemberFunction = node.role() == "expression" && node.kind() == "Member" @@ -993,8 +999,9 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR const bool isFunction = node.role() == "expression" && node.kind() == "DeclRef" && type.contains('('); if (isMemberFunction || isFunction) { - const auto symbolInfoHandler = [this, id, type, isFunction] - (const QString &name, const QString &prefix, const MessageId &) { + const auto symbolInfoHandler = [this, id, uri, type, isFunction](const QString &name, + const QString &prefix, + const MessageId &) { qCDebug(clangdLog) << "handling symbol info reply"; const QString fqn = prefix + name; @@ -1003,7 +1010,11 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR // But since HtmlDocExtractor::getFunctionDescription() is always called // with mainOverload = true, such information would get ignored anyway. if (!fqn.isEmpty()) - d->setHelpItemForTooltip(id, fqn, HelpItem::Function, isFunction ? type : "()"); + d->setHelpItemForTooltip(id, + uri, + fqn, + HelpItem::Function, + isFunction ? type : "()"); }; requestSymbolInfo(uri.toFilePath(), range.start(), symbolInfoHandler); return; @@ -1013,8 +1024,11 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR && (node.kind() == "Var" || node.kind() == "ParmVar" || node.kind() == "Field"))) { if (node.arcanaContains("EnumConstant")) { - d->setHelpItemForTooltip(id, node.detail().value_or(QString()), - HelpItem::Enum, type); + d->setHelpItemForTooltip(id, + uri, + node.detail().value_or(QString()), + HelpItem::Enum, + type); return; } stripTemplatePartOffType(); @@ -1025,10 +1039,13 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR && type != "char" && !type.contains(" char") && type != "double" && !type.contains(" double") && type != "float" && type != "bool") { - d->setHelpItemForTooltip(id, type, node.qdocCategoryForDeclaration( + d->setHelpItemForTooltip(id, + uri, + type, + node.qdocCategoryForDeclaration( HelpItem::ClassOrNamespace)); } else { - d->setHelpItemForTooltip(id); + d->setHelpItemForTooltip(id, uri); } return; } @@ -1041,19 +1058,19 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR ns.prepend("::").prepend(name); } } - d->setHelpItemForTooltip(hoverResponse.id(), ns, HelpItem::ClassOrNamespace); + d->setHelpItemForTooltip(id, uri, ns, HelpItem::ClassOrNamespace); return; } if (node.role() == "type") { if (node.kind() == "Enum") { - d->setHelpItemForTooltip(id, node.detail().value_or(QString()), HelpItem::Enum); + d->setHelpItemForTooltip(id, uri, node.detail().value_or(QString()), HelpItem::Enum); } else if (node.kind() == "Record" || node.kind() == "TemplateSpecialization") { stripTemplatePartOffType(); - d->setHelpItemForTooltip(id, type, HelpItem::ClassOrNamespace); + d->setHelpItemForTooltip(id, uri, type, HelpItem::ClassOrNamespace); } else if (node.kind() == "Typedef") { - d->setHelpItemForTooltip(id, type, HelpItem::Typedef); + d->setHelpItemForTooltip(id, uri, type, HelpItem::Typedef); } else { - d->setHelpItemForTooltip(id); + d->setHelpItemForTooltip(id, uri); } return; } @@ -1061,21 +1078,24 @@ void ClangdClient::gatherHelpItemForTooltip(const HoverRequest::Response &hoverR const QString name = node.detail().value_or(QString()); if (!name.isEmpty()) type = name; - d->setHelpItemForTooltip(id, type, HelpItem::ClassOrNamespace); + d->setHelpItemForTooltip(id, uri, type, HelpItem::ClassOrNamespace); } if (node.role() == "specifier" && node.kind() == "NamespaceAlias") { - d->setHelpItemForTooltip(id, node.detail().value_or(QString()).chopped(2), + d->setHelpItemForTooltip(id, + uri, + node.detail().value_or(QString()).chopped(2), HelpItem::ClassOrNamespace); return; } - d->setHelpItemForTooltip(id); + d->setHelpItemForTooltip(id, uri); }; d->getAndHandleAst(doc, astHandler, AstCallbackMode::SyncIfPossible); } bool ClangdClient::gatherMemberFunctionOverrideHelpItemForTooltip( - const LanguageServerProtocol::MessageId &token, - const QList &path) + const LanguageServerProtocol::MessageId &token, + const DocumentUri &uri, + const QList &path) { // Heuristic: If we encounter a member function re-declaration, continue under the // assumption that the base class holds the documentation. @@ -1112,8 +1132,11 @@ bool ClangdClient::gatherMemberFunctionOverrideHelpItemForTooltip( const ClangdAstNode baseClassNode = baseTypeNodeChildren->first(); if (!baseClassNode.detail()) return false; - d->setHelpItemForTooltip(token, *baseClassNode.detail() + "::" + *methodNode.detail(), - HelpItem::Function, "()"); + d->setHelpItemForTooltip(token, + uri, + *baseClassNode.detail() + "::" + *methodNode.detail(), + HelpItem::Function, + "()"); return true; } @@ -1233,7 +1256,9 @@ QTextCursor ClangdClient::Private::adjustedCursor(const QTextCursor &cursor, return cursor; } -void ClangdClient::Private::setHelpItemForTooltip(const MessageId &token, const QString &fqn, +void ClangdClient::Private::setHelpItemForTooltip(const MessageId &token, + const DocumentUri &uri, + const QString &fqn, HelpItem::Category category, const QString &type) { @@ -1256,7 +1281,7 @@ void ClangdClient::Private::setHelpItemForTooltip(const MessageId &token, const if (category == HelpItem::Enum && !type.isEmpty()) mark = type; - HelpItem helpItem(helpIds, mark, category); + const HelpItem helpItem(helpIds, uri.toFilePath(), mark, category); if (isTesting) emit q->helpItemGathered(helpItem); else diff --git a/src/plugins/clangcodemodel/clangdclient.h b/src/plugins/clangcodemodel/clangdclient.h index ff0c5b1b1dc..41d6edec22e 100644 --- a/src/plugins/clangcodemodel/clangdclient.h +++ b/src/plugins/clangcodemodel/clangdclient.h @@ -70,8 +70,9 @@ public: const LanguageServerProtocol::HoverRequest::Response &hoverResponse, const LanguageServerProtocol::DocumentUri &uri); bool gatherMemberFunctionOverrideHelpItemForTooltip( - const LanguageServerProtocol::MessageId &token, - const QList &path); + const LanguageServerProtocol::MessageId &token, + const LanguageServerProtocol::DocumentUri &uri, + const QList &path); void setVirtualRanges(const Utils::FilePath &filePath, const QList &ranges, int revision); diff --git a/src/plugins/cmakeprojectmanager/cmakeeditor.cpp b/src/plugins/cmakeprojectmanager/cmakeeditor.cpp index 408b52335a2..53dc1f30c60 100644 --- a/src/plugins/cmakeprojectmanager/cmakeeditor.cpp +++ b/src/plugins/cmakeprojectmanager/cmakeeditor.cpp @@ -70,8 +70,10 @@ void CMakeEditor::contextHelp(const HelpCallback &callback) const } const QString id = "command/" + textAt(begin, end - begin).toLower(); - callback( - {{id, Utils::Text::wordUnderCursor(editorWidget()->textCursor())}, {}, HelpItem::Unknown}); + callback({{id, Utils::Text::wordUnderCursor(editorWidget()->textCursor())}, + {}, + {}, + HelpItem::Unknown}); } // diff --git a/src/plugins/coreplugin/helpitem.cpp b/src/plugins/coreplugin/helpitem.cpp index 7262346d22d..b77b32f55ec 100644 --- a/src/plugins/coreplugin/helpitem.cpp +++ b/src/plugins/coreplugin/helpitem.cpp @@ -11,14 +11,16 @@ using namespace Core; +Q_GLOBAL_STATIC(HelpItem::LinkNarrower, m_linkNarrower); + HelpItem::HelpItem() = default; HelpItem::HelpItem(const char *helpId) - : HelpItem(QStringList(QString::fromUtf8(helpId)), {}, Unknown) + : HelpItem(QStringList(QString::fromUtf8(helpId)), {}, {}, Unknown) {} HelpItem::HelpItem(const QString &helpId) - : HelpItem(QStringList(helpId), {}, Unknown) + : HelpItem(QStringList(helpId), {}, {}, Unknown) {} HelpItem::HelpItem(const QUrl &url) @@ -31,13 +33,20 @@ HelpItem::HelpItem(const QUrl &url, const QString &docMark, HelpItem::Category c , m_category(category) {} -HelpItem::HelpItem(const QString &helpId, const QString &docMark, Category category) - : HelpItem(QStringList(helpId), docMark, category) +HelpItem::HelpItem(const QString &helpId, + const Utils::FilePath &filePath, + const QString &docMark, + Category category) + : HelpItem(QStringList(helpId), filePath, docMark, category) {} -HelpItem::HelpItem(const QStringList &helpIds, const QString &docMark, Category category) +HelpItem::HelpItem(const QStringList &helpIds, + const Utils::FilePath &filePath, + const QString &docMark, + Category category) : m_docMark(docMark) , m_category(category) + , m_filePath(filePath) { setHelpIds(helpIds); } @@ -75,6 +84,24 @@ void HelpItem::setCategory(Category cat) HelpItem::Category HelpItem::category() const { return m_category; } +/*! + Sets the \a filePath that this help item originates from, for example + in case of context help. +*/ +void HelpItem::setFilePath(const Utils::FilePath &filePath) +{ + m_filePath = filePath; +} + +/*! + Returns the filePath that this help item originates from, for example + in case of context help. +*/ +Utils::FilePath HelpItem::filePath() const +{ + return m_filePath; +} + bool HelpItem::isEmpty() const { return m_helpUrl.isEmpty() && m_helpIds.isEmpty(); @@ -184,7 +211,7 @@ static QVersionNumber qtVersionHeuristic(const QString &digits) return {}; } -static std::pair extractVersion(const QUrl &url) +std::pair HelpItem::extractQtVersionNumber(const QUrl &url) { const QString host = url.host(); const QStringList hostParts = host.split('.'); @@ -203,8 +230,8 @@ static std::pair extractVersion(const QUrl &url) // sort primary by "url without version" and seconday by "version" static bool helpUrlLessThan(const QUrl &a, const QUrl &b) { - const std::pair va = extractVersion(a); - const std::pair vb = extractVersion(b); + const std::pair va = HelpItem::extractQtVersionNumber(a); + const std::pair vb = HelpItem::extractQtVersionNumber(b); const QString sa = va.first.toString(); const QString sb = vb.first.toString(); if (sa == sb) @@ -258,7 +285,7 @@ static const HelpItem::Links getBestLinks(const HelpItem::Links &links) HelpItem::Links bestLinks; QUrl currentUnversionedUrl; for (const HelpItem::Link &link : links) { - const QUrl unversionedUrl = extractVersion(link.second).first; + const QUrl unversionedUrl = HelpItem::extractQtVersionNumber(link.second).first; if (unversionedUrl != currentUnversionedUrl) { currentUnversionedUrl = unversionedUrl; bestLinks.push_back(link); @@ -279,7 +306,7 @@ static const HelpItem::Links getBestLink(const HelpItem::Links &links) // Default to first link if version extraction failed, possibly because it is not a Qt doc link HelpItem::Link bestLink = links.front(); for (const HelpItem::Link &link : links) { - const QVersionNumber version = extractVersion(link.second).second; + const QVersionNumber version = HelpItem::extractQtVersionNumber(link.second).second; if (version > highestVersion) { highestVersion = version; bestLink = link; @@ -292,7 +319,8 @@ const HelpItem::Links HelpItem::bestLinks() const { if (isFuzzyMatch()) return getBestLinks(links()); - return getBestLink(links()); + const Links filteredLinks = *m_linkNarrower ? (*m_linkNarrower)(*this, links()) : links(); + return getBestLink(filteredLinks); } const QString HelpItem::keyword() const @@ -306,3 +334,8 @@ bool HelpItem::isFuzzyMatch() const links(); return m_isFuzzyMatch; } + +void HelpItem::setLinkNarrower(const LinkNarrower &narrower) +{ + *m_linkNarrower = narrower; +} diff --git a/src/plugins/coreplugin/helpitem.h b/src/plugins/coreplugin/helpitem.h index 8efab6467d9..8624bab38a1 100644 --- a/src/plugins/coreplugin/helpitem.h +++ b/src/plugins/coreplugin/helpitem.h @@ -5,6 +5,8 @@ #include "core_global.h" +#include + #include #include #include @@ -12,6 +14,10 @@ #include #include +QT_BEGIN_NAMESPACE +class QVersionNumber; +QT_END_NAMESPACE + namespace Core { class CORE_EXPORT HelpItem @@ -19,6 +25,7 @@ class CORE_EXPORT HelpItem public: using Link = std::pair; using Links = std::vector; + using LinkNarrower = std::function; enum Category { ClassOrNamespace, @@ -36,8 +43,14 @@ public: HelpItem(); HelpItem(const char *helpId); HelpItem(const QString &helpId); - HelpItem(const QString &helpId, const QString &docMark, Category category); - HelpItem(const QStringList &helpIds, const QString &docMark, Category category); + HelpItem(const QString &helpId, + const Utils::FilePath &filePath, + const QString &docMark, + Category category); + HelpItem(const QStringList &helpIds, + const Utils::FilePath &filePath, + const QString &docMark, + Category category); explicit HelpItem(const QUrl &url); HelpItem(const QUrl &url, const QString &docMark, Category category); @@ -53,6 +66,9 @@ public: void setCategory(Category cat); Category category() const; + void setFilePath(const Utils::FilePath &filePath); + Utils::FilePath filePath() const; + bool isEmpty() const; bool isValid() const; @@ -62,6 +78,10 @@ public: const QString keyword() const; bool isFuzzyMatch() const; + // used by QtSupport to narrow to "best" Qt version + static void setLinkNarrower(const LinkNarrower &narrower); + static std::pair extractQtVersionNumber(const QUrl &url); + private: QString extractContent(bool extended) const; @@ -69,6 +89,7 @@ private: QStringList m_helpIds; QString m_docMark; Category m_category = Unknown; + Utils::FilePath m_filePath; mutable std::optional m_helpLinks; // cached help links mutable std::optional m_firstParagraph; mutable QString m_keyword; diff --git a/src/plugins/cppeditor/cppbuiltinmodelmanagersupport.cpp b/src/plugins/cppeditor/cppbuiltinmodelmanagersupport.cpp index 8521a9e6bb9..eade8005c37 100644 --- a/src/plugins/cppeditor/cppbuiltinmodelmanagersupport.cpp +++ b/src/plugins/cppeditor/cppbuiltinmodelmanagersupport.cpp @@ -50,16 +50,20 @@ private: tip += evaluator.diagnosis(); setPriority(Priority_Diagnostic); } + const Utils::FilePath filePath = editorWidget->textDocument()->filePath(); const QStringList fallback = identifierWordsUnderCursor(tc); if (evaluator.identifiedCppElement()) { const QSharedPointer &cppElement = evaluator.cppElement(); const QStringList candidates = cppElement->helpIdCandidates; - const HelpItem helpItem(candidates + fallback, cppElement->helpMark, cppElement->helpCategory); + const HelpItem helpItem(candidates + fallback, + filePath, + cppElement->helpMark, + cppElement->helpCategory); setLastHelpItemIdentified(helpItem); if (!helpItem.isValid()) tip += cppElement->tooltip; } else { - setLastHelpItemIdentified({fallback, {}, HelpItem::Unknown}); + setLastHelpItemIdentified({fallback, filePath, {}, HelpItem::Unknown}); } setToolTip(tip); } diff --git a/src/plugins/qmljseditor/qmljshoverhandler.cpp b/src/plugins/qmljseditor/qmljshoverhandler.cpp index 1acae66b18a..59e69192d4a 100644 --- a/src/plugins/qmljseditor/qmljshoverhandler.cpp +++ b/src/plugins/qmljseditor/qmljshoverhandler.cpp @@ -143,7 +143,10 @@ bool QmlJSHoverHandler::setQmlTypeHelp(const ScopeChain &scopeChain, const Docum helpIdPieces.removeAt(1); helpIdCandidates += helpIdPieces.join('.'); - const HelpItem helpItem(helpIdCandidates, qName.join('.'), HelpItem::QmlComponent); + const HelpItem helpItem(helpIdCandidates, + qmlDocument->fileName(), + qName.join('.'), + HelpItem::QmlComponent); const HelpItem::Links links = helpItem.links(); // Check if the module name contains a major version. @@ -474,7 +477,10 @@ bool QmlJSHoverHandler::setQmlHelpItem(const ScopeChain &scopeChain, + "::" + name, "QML." + className + "::" + name, className + "::" + name}; - const HelpItem helpItem(helpIdCandidates, name, HelpItem::QmlProperty); + const HelpItem helpItem(helpIdCandidates, + qmlDocument->fileName(), + name, + HelpItem::QmlProperty); if (helpItem.isValid()) { setLastHelpItemIdentified(helpItem); return true; diff --git a/src/plugins/qtsupport/qtsupportplugin.cpp b/src/plugins/qtsupport/qtsupportplugin.cpp index b6b033dbd90..2035b75f9a4 100644 --- a/src/plugins/qtsupport/qtsupportplugin.cpp +++ b/src/plugins/qtsupport/qtsupportplugin.cpp @@ -28,10 +28,12 @@ #include #include +#include #include #include using namespace Core; +using namespace Utils; using namespace ProjectExplorer; namespace QtSupport { @@ -180,6 +182,53 @@ void QtSupportPlugin::extensionsInitialized() return qt ? qt->hostLibexecPath().toUserOutput() : QString(); }); + HelpItem::setLinkNarrower([](const HelpItem &item, const HelpItem::Links &links) { + const FilePath filePath = item.filePath(); + if (filePath.isEmpty()) + return links; + const Project *project = SessionManager::projectForFile(filePath); + Target *target = project ? project->activeTarget() : nullptr; + QtVersion *qt = target ? QtKitAspect::qtVersion(target->kit()) : nullptr; + if (!qt) + return links; + + // Find best-suited documentation version, so + // sort into buckets of links with exact, same minor, and same major, and return the first + // that has entries. + const QVersionNumber qtVersion = qt->qtVersion(); + HelpItem::Links exactVersion; + HelpItem::Links sameMinor; + HelpItem::Links sameMajor; + bool hasExact = false; + bool hasSameMinor = false; + bool hasSameMajor = false; + for (const HelpItem::Link &link : links) { + const QUrl url = link.second; + const QVersionNumber version = HelpItem::extractQtVersionNumber(url).second; + // version.isNull() means it's not a Qt documentation URL, so include regardless + if (version.isNull() || version.majorVersion() == qtVersion.majorVersion()) { + sameMajor.push_back(link); + hasSameMajor = true; + if (version.isNull() || version.minorVersion() == qtVersion.minorVersion()) { + sameMinor.push_back(link); + hasSameMinor = true; + if (version.isNull() || version.microVersion() == qtVersion.microVersion()) { + exactVersion.push_back(link); + hasExact = true; + } + } + } + } + // HelpItem itself finds the highest version within sameMinor/Major/etc itself + if (hasExact) + return exactVersion; + if (hasSameMinor) + return sameMinor; + if (hasSameMajor) + return sameMajor; + return links; + }); + askAboutQtInstallation(); }