diff --git a/language/duchain/navigation/abstractnavigationwidget.cpp b/language/duchain/navigation/abstractnavigationwidget.cpp --- a/language/duchain/navigation/abstractnavigationwidget.cpp +++ b/language/duchain/navigation/abstractnavigationwidget.cpp @@ -40,14 +40,17 @@ resize(100, 100); } -const int maxNavigationWidgetWidth = 580; +const int maxNavigationWidgetWidth = 800; +const int maxNavigationWidgetHeight = 400; QSize AbstractNavigationWidget::sizeHint() const { if(m_browser) { updateIdealSize(); - QSize ret = QSize(qMin(m_idealTextSize.width(), maxNavigationWidgetWidth), qMin(m_idealTextSize.height(), 300)); - if(m_idealTextSize.height()>=300) { //make space for the scrollbar in case it's not fitting + QSize ret = QSize(qMin(m_idealTextSize.width(), maxNavigationWidgetWidth), + qMin(m_idealTextSize.height(), maxNavigationWidgetHeight)); + if(m_idealTextSize.height()>=maxNavigationWidgetHeight) { + //make space for the scrollbar in case it's not fitting ret.rwidth() += 17; //m_browser->verticalScrollBar()->width() returns 100, for some reason } diff --git a/language/duchain/navigation/problemnavigationcontext.h b/language/duchain/navigation/problemnavigationcontext.h --- a/language/duchain/navigation/problemnavigationcontext.h +++ b/language/duchain/navigation/problemnavigationcontext.h @@ -1,6 +1,6 @@ /* Copyright 2009 David Nolden - + This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. @@ -38,7 +38,7 @@ }; Q_DECLARE_FLAGS(Flags, Flag) - explicit ProblemNavigationContext(const IProblem::Ptr& problem, const Flags flags = {}); + explicit ProblemNavigationContext(const QVector& problems, const Flags flags = {}); ~ProblemNavigationContext() override; QString name() const override; @@ -52,11 +52,20 @@ void executeAction(int index); // TODO: Add API in base class? private: - IProblem::Ptr m_problem; + void html(IProblem::Ptr problem); + + /** + * Return HTML-ized text. Used for processing problem's description and explanation. + * Some plugins (kdev-cppcheck for example) return already HTML-ized strings, + * therefore we should make check for this case. + */ + QString escapedHtml(const QString& text) const; + + QVector m_problems; Flags m_flags; QPointer m_widget; - IAssistant::Ptr m_cachedAssistant; // cache assistant, calling IAssistant::solutionAssistant() might be expensive + QVector m_assistantsActions; }; } diff --git a/language/duchain/navigation/problemnavigationcontext.cpp b/language/duchain/navigation/problemnavigationcontext.cpp --- a/language/duchain/navigation/problemnavigationcontext.cpp +++ b/language/duchain/navigation/problemnavigationcontext.cpp @@ -70,11 +70,26 @@ } -ProblemNavigationContext::ProblemNavigationContext(const IProblem::Ptr& problem, const Flags flags) - : m_problem(problem) +ProblemNavigationContext::ProblemNavigationContext(const QVector& problems, const Flags flags) + : m_problems(problems) , m_flags(flags) , m_widget(nullptr) { + // Sort problems vector: + // 1) By severity + // 2) By sourceString, if severities are equals + std::sort(m_problems.begin(), m_problems.end(), [](const IProblem::Ptr a, const IProblem::Ptr b) { + if (a->severity() < b->severity()) + return true; + + if (a->severity() > b->severity()) + return false; + + if (a->sourceString() < b->sourceString()) + return true; + + return false; + }); } ProblemNavigationContext::~ProblemNavigationContext() @@ -97,49 +112,65 @@ return i18n("Problem"); } -QString ProblemNavigationContext::html(bool shorten) +QString ProblemNavigationContext::escapedHtml(const QString& text) const { - clear(); - m_shorten = shorten; - auto iconPath = iconForSeverity(m_problem->severity()); + static const QString htmlStart = QStringLiteral(""); + static const QString htmlEnd = QStringLiteral(""); + + QString result = text.trimmed(); + + if (!result.startsWith(htmlStart)) + return result.toHtmlEscaped(); + + result.remove(htmlStart); + result.remove(htmlEnd); + + return result; +} + +void ProblemNavigationContext::html(IProblem::Ptr problem) +{ + auto iconPath = iconForSeverity(problem->severity()); modifyHtml() += QStringLiteral(""); modifyHtml() += QStringLiteral("").arg(htmlImg(iconPath, KIconLoader::Panel)); // BEGIN: right column modifyHtml() += QStringLiteral(""); // END: right column modifyHtml() += QStringLiteral("
%1"); - modifyHtml() += i18n("Problem in %1", m_problem->sourceString()); + modifyHtml() += i18n("Problem in %1", problem->sourceString()); modifyHtml() += QStringLiteral("
"); if (m_flags & ShowLocation) { - const auto duchainProblem = dynamic_cast(m_problem.data()); - if (duchainProblem) { - modifyHtml() += labelHighlight(i18n("Location: ")); - makeLink(QStringLiteral("%1 :%2") - .arg(duchainProblem->finalLocation().document.toUrl().fileName()) - .arg(duchainProblem->rangeInCurrentRevision().start().line() + 1), - QString(), - NavigationAction(duchainProblem->finalLocation().document.toUrl(), duchainProblem->finalLocation().start()) - ); - modifyHtml() += QStringLiteral("
"); - } + modifyHtml() += labelHighlight(i18n("Location: ")); + makeLink(QStringLiteral("%1 :%2") + .arg(problem->finalLocation().document.toUrl().fileName()) + .arg(problem->finalLocation().start().line() + 1), + QString(), + NavigationAction(problem->finalLocation().document.toUrl(), problem->finalLocation().start()) + ); + + modifyHtml() += QStringLiteral("
"); } - modifyHtml() += m_problem->description().toHtmlEscaped(); - if ( !m_problem->explanation().isEmpty() ) { - modifyHtml() += "

" + m_problem->explanation().toHtmlEscaped() + "

"; - } + QString description = escapedHtml(problem->description()); + QString explanation = escapedHtml(problem->explanation()); + + modifyHtml() += description; + + // Add only non-empty explanation which differs from the problem description. + // Skip this if we have more than one problem. + if (m_problems.size() == 1 && !explanation.isEmpty() && explanation != description) + modifyHtml() += "

" + explanation + "

"; modifyHtml() += QStringLiteral("
"); - const QVector diagnostics = m_problem->diagnostics(); + auto diagnostics = problem->diagnostics(); if (!diagnostics.isEmpty()) { - DUChainReadLocker lock; for (auto diagnostic : diagnostics) { modifyHtml() += QStringLiteral("

"); @@ -155,69 +186,87 @@ makeLink(QStringLiteral("%1 :%2") .arg(declaration->url().toUrl().fileName()) .arg(declaration->rangeInCurrentRevision().start().line() + 1), - DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); - } else if (range.start().isValid()) { + DeclarationPointer(declaration), NavigationAction::NavigateDeclaration); + } + + else if (range.start().isValid()) { modifyHtml() += i18n("
See: "); const auto url = range.document.toUrl(); makeLink(QStringLiteral("%1 :%2") - .arg(url.fileName()) - .arg(range.start().line() + 1), - url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start())); + .arg(url.fileName()) + .arg(range.start().line() + 1), + url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start())); } + modifyHtml() += QStringLiteral("

"); } } - if (!m_cachedAssistant) { - m_cachedAssistant = m_problem->solutionAssistant(); - } - auto assistant = m_cachedAssistant; + auto assistant = problem->solutionAssistant(); if (assistant && !assistant->actions().isEmpty()) { modifyHtml() += QString::fromLatin1("").arg("#b3d4ff"); - modifyHtml() += QStringLiteral(""; modifyHtml() += QStringLiteral("
%1").arg(htmlImg(QStringLiteral("dialog-ok-apply"), KIconLoader::Panel)); - int index = 0; + + const int startIndex = m_assistantsActions.size(); + int currentIndex = startIndex; foreach (auto assistantAction, assistant->actions()) { - if (index != 0) { + m_assistantsActions.append(assistantAction); + + if (currentIndex != startIndex) modifyHtml() += "
"; - } - makeLink(i18n("Solution (%1)", index + 1), KEY_INVOKE_ACTION(index), - NavigationAction(KEY_INVOKE_ACTION(index))); + + makeLink(i18n("Solution (%1)", currentIndex + 1), KEY_INVOKE_ACTION( currentIndex ), + NavigationAction(KEY_INVOKE_ACTION( currentIndex ))); modifyHtml() += ": " + assistantAction->description().toHtmlEscaped(); - ++index; + + ++currentIndex; } + modifyHtml() += "
"); } +} + +QString ProblemNavigationContext::html(bool shorten) +{ + m_shorten = shorten; + + clear(); + m_assistantsActions.clear(); + + int problemIndex = 0; + foreach (auto problem, m_problems) { + html(problem); + + if (++problemIndex != m_problems.size()) + modifyHtml() += "
"; + } return currentHtml(); } NavigationContextPointer ProblemNavigationContext::executeKeyAction(QString key) { - auto assistant = m_cachedAssistant; - if (!assistant) - return {}; if (key.startsWith(QLatin1String("invoke_action_"))) { - const auto index = key.replace(QLatin1String("invoke_action_"), QString()).toInt(); + const int index = key.replace(QLatin1String("invoke_action_"), QString()).toInt(); executeAction(index); } return {}; } void ProblemNavigationContext::executeAction(int index) { - auto assistant = m_problem->solutionAssistant(); - if (!assistant) + if (index < 0 || index >= m_assistantsActions.size()) return; - auto action = assistant->actions().value(index); + auto action = m_assistantsActions.at(index); + Q_ASSERT(action); + if (action) { action->execute(); - if ( topContext() ) { + if ( topContext() ) DUChain::self()->updateContextForUrl(topContext()->url(), TopDUContext::ForceUpdate); - } } else { qCWarning(LANGUAGE()) << "No such action"; return; diff --git a/plugins/contextbrowser/CMakeLists.txt b/plugins/contextbrowser/CMakeLists.txt --- a/plugins/contextbrowser/CMakeLists.txt +++ b/plugins/contextbrowser/CMakeLists.txt @@ -11,4 +11,4 @@ qt5_add_resources(kdevcontextbrowser_PART_SRCS kdevcontextbrowser.qrc) kdevplatform_add_plugin(kdevcontextbrowser JSON kdevcontextbrowser.json SOURCES ${kdevcontextbrowser_PART_SRCS}) -target_link_libraries(kdevcontextbrowser KDev::Interfaces KDev::Util KDev::Language KDev::Sublime KF5::TextEditor KF5::Parts) +target_link_libraries(kdevcontextbrowser KDev::Interfaces KDev::Util KDev::Language KDev::Sublime KDev::Shell KF5::TextEditor KF5::Parts) diff --git a/plugins/contextbrowser/contextbrowser.h b/plugins/contextbrowser/contextbrowser.h --- a/plugins/contextbrowser/contextbrowser.h +++ b/plugins/contextbrowser/contextbrowser.h @@ -232,7 +232,7 @@ QPointer m_currentToolTip; QPointer m_currentNavigationWidget; KDevelop::IndexedDeclaration m_currentToolTipDeclaration; - KDevelop::Problem::Ptr m_currentToolTipProblem; + QVector m_currentToolTipProblems; QAction* m_findUses; QPointer m_lastInsertionDocument; diff --git a/plugins/contextbrowser/contextbrowser.cpp b/plugins/contextbrowser/contextbrowser.cpp --- a/plugins/contextbrowser/contextbrowser.cpp +++ b/plugins/contextbrowser/contextbrowser.cpp @@ -74,6 +74,9 @@ #include +#include +#include + #include #include @@ -437,34 +440,43 @@ m_currentToolTip->deleteLater(); m_currentToolTip = nullptr; m_currentNavigationWidget = nullptr; - m_currentToolTipProblem = {}; + m_currentToolTipProblems.clear(); m_currentToolTipDeclaration = {}; } } -static ProblemPointer findProblemUnderCursor(TopDUContext* topContext, KTextEditor::Cursor position) +static QVector findProblemsUnderCursor(TopDUContext* topContext, KTextEditor::Cursor position) { - const auto top = ReferencedTopDUContext(topContext); - foreach (auto problem, DUChainUtils::allProblemsForContext(top)) { - if (problem->rangeInCurrentRevision().contains(position)) { - return problem; + QVector problems; + auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); + foreach (auto modelData, modelsData) { + foreach (auto problem, modelData.model->problems(topContext->url())) { + DocumentRange problemRange = problem->finalLocation(); + if (problemRange.contains(position) || (problemRange.isEmpty() && problemRange.boundaryAtCursor(position))) + problems += problem; } } - return {}; + return problems; } -static ProblemPointer findProblemCloseToCursor(TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::View* view) +static QVector findProblemsCloseToCursor(TopDUContext* topContext, KTextEditor::Cursor position, KTextEditor::View* view) { - const auto top = ReferencedTopDUContext(topContext); - auto problems = DUChainUtils::allProblemsForContext(top); - if (problems.isEmpty()) - return {}; + QVector allProblems; + auto modelsData = ICore::self()->languageController()->problemModelSet()->models(); + foreach (auto modelData, modelsData) { + foreach (auto problem, modelData.model->problems(topContext->url())) { + allProblems += problem; + } + } - auto closestProblem = std::min_element(problems.constBegin(), problems.constEnd(), - [position](const ProblemPointer& a, const ProblemPointer& b) { - const auto aRange = a->rangeInCurrentRevision(); - const auto bRange = b->rangeInCurrentRevision(); + if (allProblems.isEmpty()) + return allProblems; + + std::sort(allProblems.begin(), allProblems.end(), + [position](const KDevelop::IProblem::Ptr a, const KDevelop::IProblem::Ptr b) { + const auto aRange = a->finalLocation(); + const auto bRange = b->finalLocation(); const auto aLineDistance = qMin(qAbs(aRange.start().line() - position.line()), qAbs(aRange.end().line() - position.line())); @@ -482,32 +494,40 @@ qAbs(bRange.end().column() - position.column()); }); - auto r = (*closestProblem)->rangeInCurrentRevision(); - if (!r.contains(position)) { - if (r.start().line() == position.line() || r.end().line() == position.line()) { - // problem is on the same line, let's use it - return *closestProblem; - } + QVector closestProblems; - // if not, only show it in case there's only whitespace - // between the current cursor position and the problem line - KTextEditor::Range dist; - KTextEditor::Cursor bound(r.start().line(), 0); - if (position < r.start()) - dist = KTextEditor::Range(position, bound); - else { - bound.setLine(r.end().line() + 1); - dist = KTextEditor::Range(bound, position); - } + // Show problems, located on the same line + foreach (auto problem, allProblems) { + auto r = problem->finalLocation(); + if (r.onSingleLine() && r.start().line() == position.line()) + closestProblems += problem; + else + break; + } - auto textBetween = view->document()->text(dist); - auto isSpace = std::all_of(textBetween.begin(), textBetween.end(), [](QChar c) { return c.isSpace(); }); - if (!isSpace) { - return {}; + // If not, only show it in case there's only whitespace + // between the current cursor position and the problem line + if (closestProblems.isEmpty()) { + foreach (auto problem, allProblems) { + auto r = problem->finalLocation(); + + KTextEditor::Range dist; + KTextEditor::Cursor bound(r.start().line(), 0); + if (position < r.start()) + dist = KTextEditor::Range(position, bound); + else { + bound.setLine(r.end().line() + 1); + dist = KTextEditor::Range(bound, position); + } + + if (view->document()->text(dist).trimmed().isEmpty()) + closestProblems += problem; + else + break; } } - return *closestProblem; + return closestProblems; } QWidget* ContextBrowserPlugin::navigationWidgetForPosition(KTextEditor::View* view, KTextEditor::Cursor position) @@ -526,15 +546,15 @@ TopDUContext* topContext = DUChainUtils::standardContextForUrl(view->document()->url()); if (topContext) { // first pass: find problems under the cursor - const auto problem = findProblemUnderCursor(topContext, position); - if (problem) { - if (problem == m_currentToolTipProblem && m_currentToolTip) { + const auto problems = findProblemsUnderCursor(topContext, position); + if (!problems.isEmpty()) { + if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } - m_currentToolTipProblem = problem; + m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; - auto context = new ProblemNavigationContext(problem); + auto context = new ProblemNavigationContext(problems); context->setTopContext(TopDUContextPointer(topContext)); widget->setContext(NavigationContextPointer(context)); return widget; @@ -559,25 +579,24 @@ if (topContext) { // second pass: find closest problem to the cursor - const auto problem = findProblemCloseToCursor(topContext, position, view); - if (problem) { - if (problem == m_currentToolTipProblem && m_currentToolTip) { + const auto problems = findProblemsCloseToCursor(topContext, position, view); + if (!problems.isEmpty()) { + if (problems == m_currentToolTipProblems && m_currentToolTip) { return nullptr; } - m_currentToolTipProblem = problem; + m_currentToolTipProblems = problems; auto widget = new AbstractNavigationWidget; // since the problem is not under cursor: show location - widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problem, ProblemNavigationContext::ShowLocation))); + widget->setContext(NavigationContextPointer(new ProblemNavigationContext(problems, ProblemNavigationContext::ShowLocation))); return widget; } } return nullptr; } void ContextBrowserPlugin::showToolTip(KTextEditor::View* view, KTextEditor::Cursor position) { - ContextBrowserView* contextView = browserViewForWidget(view); if(contextView && contextView->isVisible() && !contextView->isLocked()) return; // If the context-browser view is visible, it will care about updating by itself