/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of Qt Creator. ** ** Commercial License Usage ** Licensees holding valid commercial Qt licenses may use this file in ** accordance with the commercial license agreement provided with the ** Software or, alternatively, in accordance with the terms contained in ** a written agreement between you and The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/contact-us. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 3 as published by the Free Software ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-3.0.html. ** ****************************************************************************/ #include "clangdiagnostictooltipwidget.h" #include "clangfixitoperation.h" #include "clangutils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ClangCodeModel; using Internal::ClangDiagnosticWidget; using Internal::ClangFixItOperation; namespace { // CLANG-UPGRADE-CHECK: Checks/update URLs. // // Once it gets dedicated documentation pages for released versions, // use them instead of pointing to master, as checks might vanish. const char CLAZY_DOCUMENTATION_URL_TEMPLATE[] = "https://github.com/KDE/clazy/blob/master/docs/checks/README-%1.md"; const char LINK_ACTION_GOTO_LOCATION[] = "#gotoLocation"; const char LINK_ACTION_APPLY_FIX[] = "#applyFix"; QString fileNamePrefix(const QString &mainFilePath, const ClangBackEnd::SourceLocationContainer &location) { const QString filePath = location.filePath.toString(); if (filePath != mainFilePath) return QFileInfo(filePath).fileName() + QLatin1Char(':'); return QString(); } QString locationToString(const ClangBackEnd::SourceLocationContainer &location) { return QString::number(location.line) + QStringLiteral(":") + QString::number(location.column); } void openEditorAt(const ClangBackEnd::DiagnosticContainer &diagnostic) { const ClangBackEnd::SourceLocationContainer &location = diagnostic.location; Core::EditorManager::openEditorAt(location.filePath.toString(), int(location.line), int(location.column - 1)); } void applyFixit(const ClangBackEnd::DiagnosticContainer &diagnostic) { ClangFixItOperation operation(Utf8String(), diagnostic.fixIts); operation.perform(); } class WidgetFromDiagnostics { public: struct DisplayHints { bool showCategoryAndEnableOption; bool showFileNameInMainDiagnostic; bool enableClickableFixits; bool limitWidth; bool hideTooltipAfterLinkActivation; bool allowTextSelection; }; WidgetFromDiagnostics(const DisplayHints &displayHints) : m_displayHints(displayHints) { } QWidget *createWidget(const QVector &diagnostics) { const QString text = htmlText(diagnostics); auto *label = new QLabel; label->setTextFormat(Qt::RichText); label->setText(text); if (m_displayHints.allowTextSelection) { label->setTextInteractionFlags(Qt::TextBrowserInteraction); } else { label->setTextInteractionFlags(Qt::LinksAccessibleByMouse | Qt::LinksAccessibleByKeyboard); } if (m_displayHints.limitWidth) { const int limit = widthLimit(); // Using "setWordWrap(true)" alone will wrap the text already for small // widths, so do not require word wrapping until we hit limits. if (label->sizeHint().width() > limit) { label->setMaximumWidth(limit); label->setWordWrap(true); } } else { label->setWordWrap(true); } const TargetIdToDiagnosticTable table = m_targetIdsToDiagnostics; const bool hideToolTipAfterLinkActivation = m_displayHints.hideTooltipAfterLinkActivation; QObject::connect(label, &QLabel::linkActivated, [table, hideToolTipAfterLinkActivation] (const QString &action) { const ClangBackEnd::DiagnosticContainer diagnostic = table.value(action); if (diagnostic == ClangBackEnd::DiagnosticContainer()) QDesktopServices::openUrl(QUrl(action)); else if (action.startsWith(LINK_ACTION_GOTO_LOCATION)) openEditorAt(diagnostic); else if (action.startsWith(LINK_ACTION_APPLY_FIX)) applyFixit(diagnostic); else QTC_CHECK(!"Link target cannot be handled."); if (hideToolTipAfterLinkActivation) ::Utils::ToolTip::hideImmediately(); }); return label; } QString htmlText(const QVector &diagnostics) { // For debugging, add: style='border-width:1px;border-color:black' QString text = ""; foreach (const ClangBackEnd::DiagnosticContainer &diagnostic, diagnostics) text.append(tableRows(diagnostic)); text.append("
"); return text; } private: enum class IndentMode { Indent, DoNotIndent }; // Diagnostics from clazy/tidy do not have any category or option set but // we will conclude them from the diagnostic message. // // Ideally, libclang should provide the correct category/option by default. // However, tidy and clazy diagnostics use "custom diagnostic ids" and // clang's static diagnostic table does not know anything about them. // // For clazy/tidy diagnostics, we expect something like "some text [some option]", e.g.: // * clazy: "Use the static QFileInfo::exists() instead. It's documented to be faster. [-Wclazy-qfileinfo-exists]" // * tidy: "use emplace_back instead of push_back [modernize-use-emplace]" static ClangBackEnd::DiagnosticContainer supplementedDiagnostic( const ClangBackEnd::DiagnosticContainer &diagnostic) { if (!diagnostic.category.isEmpty()) return diagnostic; // OK, diagnostics from clang itself have this set. ClangBackEnd::DiagnosticContainer supplementedDiagnostic = diagnostic; using namespace ClangCodeModel::Utils; DiagnosticTextInfo info(diagnostic.text); supplementedDiagnostic.enableOption = info.option(); supplementedDiagnostic.category = info.category(); supplementedDiagnostic.text = info.textWithoutOption(); for (auto &child : supplementedDiagnostic.children) child.text = DiagnosticTextInfo(diagnostic.text.toString()).textWithoutOption(); return supplementedDiagnostic; } QString tableRows(const ClangBackEnd::DiagnosticContainer &diagnostic) { m_mainFilePath = m_displayHints.showFileNameInMainDiagnostic ? Utf8String() : diagnostic.location.filePath; const ClangBackEnd::DiagnosticContainer diag = supplementedDiagnostic(diagnostic); QString text; if (m_displayHints.showCategoryAndEnableOption) text.append(diagnosticCategoryAndEnableOptionRow(diag)); text.append(diagnosticRow(diag, IndentMode::DoNotIndent)); text.append(diagnosticRowsForChildren(diag)); return text; } static QString documentationUrlForOption(const Utf8String &optionAsUtf8String) { if (optionAsUtf8String.isEmpty()) return QString(); QString option = optionAsUtf8String.toString(); // Clazy if (ClangCodeModel::Utils::DiagnosticTextInfo::isClazyOption(option)) { option = optionAsUtf8String.mid(8); // Remove "-Wclazy-" prefix. return QString::fromUtf8(CLAZY_DOCUMENTATION_URL_TEMPLATE).arg(option); } // Clang itself if (option.startsWith("-W")) return QString(); // Clang-Tidy return QString::fromUtf8(CppTools::Constants::TIDY_DOCUMENTATION_URL_TEMPLATE).arg(option); } static QString maybeClickableOption(const Utf8String &option) { if (option.isEmpty()) return option; const QString link = documentationUrlForOption(option); if (link.isEmpty()) return option; return wrapInLink(option.toString(), link); } static QString diagnosticCategoryAndEnableOptionRow( const ClangBackEnd::DiagnosticContainer &diagnostic) { const QString text = QString::fromLatin1( " " " %1" "  %2" " ") .arg(diagnostic.category, maybeClickableOption(diagnostic.enableOption)); return text; } QString diagnosticText(const ClangBackEnd::DiagnosticContainer &diagnostic) { const bool hasFixit = m_displayHints.enableClickableFixits && !diagnostic.fixIts.isEmpty(); const QString diagnosticText = diagnostic.text.toString().toHtmlEscaped(); const QString text = QString::fromLatin1("%1: %2") .arg(clickableLocation(diagnostic, m_mainFilePath), clickableFixIt(diagnostic, diagnosticText, hasFixit)); return text; } QString diagnosticRow(const ClangBackEnd::DiagnosticContainer &diagnostic, IndentMode indentMode) { const QString text = QString::fromLatin1( " " " %2" " ") .arg(indentModeToHtmlStyle(indentMode), diagnosticText(diagnostic)); return text; } QString diagnosticRowsForChildren(const ClangBackEnd::DiagnosticContainer &diagnostic) { const QVector &children = diagnostic.children; QString text; if (children.size() <= 10) { text += diagnosticRowsForChildren(children.begin(), children.end()); } else { text += diagnosticRowsForChildren(children.begin(), children.begin() + 7); text += ellipsisRow(); text += diagnosticRowsForChildren(children.end() - 3, children.end()); } return text; } QString diagnosticRowsForChildren( const QVector::const_iterator first, const QVector::const_iterator last) { QString text; for (auto it = first; it != last; ++it) text.append(diagnosticRow(*it, IndentMode::Indent)); return text; } QString clickableLocation(const ClangBackEnd::DiagnosticContainer &diagnostic, const QString &mainFilePath) { const ClangBackEnd::SourceLocationContainer &location = diagnostic.location; const QString filePrefix = fileNamePrefix(mainFilePath, location); const QString lineColumn = locationToString(location); const QString linkText = filePrefix + lineColumn; const QString targetId = generateTargetId(LINK_ACTION_GOTO_LOCATION, diagnostic); return wrapInLink(linkText, targetId); } QString clickableFixIt(const ClangBackEnd::DiagnosticContainer &diagnostic, const QString &text, bool hasFixIt) { if (!hasFixIt) return text; QString clickableText = text; QString nonClickableCategory; const int colonPosition = text.indexOf(QStringLiteral(": ")); if (colonPosition != -1) { nonClickableCategory = text.mid(0, colonPosition + 2); clickableText = text.mid(colonPosition + 2); } const QString targetId = generateTargetId(LINK_ACTION_APPLY_FIX, diagnostic); return nonClickableCategory + wrapInLink(clickableText, targetId); } QString generateTargetId(const QString &targetPrefix, const ClangBackEnd::DiagnosticContainer &diagnostic) { const QString idAsString = QString::number(++m_targetIdCounter); const QString targetId = targetPrefix + idAsString; m_targetIdsToDiagnostics.insert(targetId, diagnostic); return targetId; } static QString wrapInLink(const QString &text, const QString &target) { return QStringLiteral("%2").arg(target, text); } static QString ellipsisRow() { return QString::fromLatin1( " " " ..." " ") .arg(indentModeToHtmlStyle(IndentMode::Indent)); } static QString indentModeToHtmlStyle(IndentMode indentMode) { return indentMode == IndentMode::Indent ? QString("padding-left:10px") : QString(); } static int widthLimit() { auto screen = QGuiApplication::screenAt(QCursor::pos()); if (!screen) screen = QGuiApplication::primaryScreen(); return screen->availableGeometry().width() / 2; } private: const DisplayHints m_displayHints; using TargetIdToDiagnosticTable = QHash; TargetIdToDiagnosticTable m_targetIdsToDiagnostics; unsigned m_targetIdCounter = 0; QString m_mainFilePath; }; WidgetFromDiagnostics::DisplayHints toHints(const ClangDiagnosticWidget::Destination &destination) { WidgetFromDiagnostics::DisplayHints hints; if (destination == ClangDiagnosticWidget::ToolTip) { hints.showCategoryAndEnableOption = true; hints.showFileNameInMainDiagnostic = false; hints.enableClickableFixits = true; hints.limitWidth = true; hints.hideTooltipAfterLinkActivation = true; hints.allowTextSelection = false; } else { // Info Bar hints.showCategoryAndEnableOption = false; hints.showFileNameInMainDiagnostic = true; // Clickable fixits might change toolchain headers, so disable. hints.enableClickableFixits = false; hints.limitWidth = false; hints.hideTooltipAfterLinkActivation = false; hints.allowTextSelection = true; } return hints; } } // anonymous namespace namespace ClangCodeModel { namespace Internal { QString ClangDiagnosticWidget::createText( const QVector &diagnostics, const ClangDiagnosticWidget::Destination &destination) { const QString htmlText = WidgetFromDiagnostics(toHints(destination)).htmlText(diagnostics); QTextDocument document; document.setHtml(htmlText); QString text = document.toPlainText(); if (text.startsWith('\n')) text = text.mid(1); if (text.endsWith('\n')) text.chop(1); return text; } QWidget *ClangDiagnosticWidget::createWidget( const QVector &diagnostics, const Destination &destination) { return WidgetFromDiagnostics(toHints(destination)).createWidget(diagnostics); } } // namespace Internal } // namespace ClangCodeModel