1 /*
2     SPDX-FileCopyrightText: 2009 David Nolden <david.nolden.kdevelop@art-master.de>
3 
4     SPDX-License-Identifier: LGPL-2.0-only
5 */
6 
7 #include "problemnavigationcontext.h"
8 
9 #include <debug.h>
10 
11 #include <QBuffer>
12 #include <QStyle>
13 #include <QApplication>
14 
15 #include <KLocalizedString>
16 
17 #include <language/duchain/declaration.h>
18 #include <language/duchain/duchainlock.h>
19 #include <language/duchain/duchainutils.h>
20 #include <language/duchain/problem.h>
21 
22 #include <language/duchain/duchain.h>
23 #include <language/editor/documentrange.h>
24 
25 using namespace KDevelop;
26 
27 namespace {
KEY_INVOKE_ACTION(int num)28 QString KEY_INVOKE_ACTION(int num) { return QStringLiteral("invoke_action_%1").arg(num); }
29 
htmlImg(const QIcon & icon,QStyle::PixelMetric metric)30 QString htmlImg(const QIcon& icon, QStyle::PixelMetric metric)
31 {
32     const int size = qApp->style()->pixelMetric(metric, nullptr, nullptr);
33     const QPixmap pixmap = icon.pixmap(size, size);
34     QByteArray pngBytes;
35     QBuffer buffer(&pngBytes);
36     buffer.open(QIODevice::WriteOnly);
37     pixmap.save(&buffer, "PNG", 100);
38 
39     const QString imgTag = QStringLiteral("<img width='%1' height='%1' src='data:image/png;base64, %2'/>")
40            .arg(size)
41            .arg(QString::fromLatin1(pngBytes.toBase64()));
42     return imgTag;
43 }
44 }
45 
ProblemNavigationContext(const QVector<IProblem::Ptr> & problems,const Flags flags)46 ProblemNavigationContext::ProblemNavigationContext(const QVector<IProblem::Ptr>& problems, const Flags flags)
47     : m_problems(problems)
48     , m_flags(flags)
49     , m_widget(nullptr)
50 {
51     // Sort problems vector:
52     // 1) By severity
53     // 2) By sourceString, if severities are equals
54     std::sort(m_problems.begin(), m_problems.end(), [](const IProblem::Ptr& a, const IProblem::Ptr& b) {
55         if (a->severity() < b->severity())
56             return true;
57 
58         if (a->severity() > b->severity())
59             return false;
60 
61         if (a->sourceString() < b->sourceString())
62             return true;
63 
64         return false;
65     });
66 }
67 
~ProblemNavigationContext()68 ProblemNavigationContext::~ProblemNavigationContext()
69 {
70     delete m_widget;
71 }
72 
widget() const73 QWidget* ProblemNavigationContext::widget() const
74 {
75     return m_widget;
76 }
77 
isWidgetMaximized() const78 bool ProblemNavigationContext::isWidgetMaximized() const
79 {
80     return false;
81 }
82 
name() const83 QString ProblemNavigationContext::name() const
84 {
85     return i18n("Problem");
86 }
87 
escapedHtml(const QString & text) const88 QString ProblemNavigationContext::escapedHtml(const QString& text) const
89 {
90     const QString htmlStart = QStringLiteral("<html>");
91     const QString htmlEnd = QStringLiteral("</html>");
92 
93     QString result = text.trimmed();
94 
95     if (!result.startsWith(htmlStart))
96         return result.toHtmlEscaped();
97 
98     result.remove(htmlStart);
99     result.remove(htmlEnd);
100 
101     return result;
102 }
103 
html(IProblem::Ptr problem)104 void ProblemNavigationContext::html(IProblem::Ptr problem)
105 {
106     modifyHtml() += QStringLiteral("<table><tr>");
107 
108     modifyHtml() += QStringLiteral("<td valign=\"middle\">%1</td>")
109                     .arg(htmlImg(IProblem::iconForSeverity(problem->severity()), QStyle::PM_LargeIconSize));
110 
111     // BEGIN: right column
112     modifyHtml() += QStringLiteral("<td>");
113 
114     modifyHtml() += i18n("Problem in <i>%1</i>", problem->sourceString());
115     modifyHtml() += QStringLiteral("<br/>");
116 
117     if (m_flags & ShowLocation) {
118         modifyHtml() += labelHighlight(i18n("Location: "));
119         makeLink(QStringLiteral("%1 :%2")
120                  .arg(problem->finalLocation().document.toUrl().fileName())
121                  .arg(problem->finalLocation().start().line() + 1),
122                  QString(),
123                  NavigationAction(problem->finalLocation().document.toUrl(), problem->finalLocation().start())
124         );
125 
126         modifyHtml() += QStringLiteral("<br/>");
127     }
128 
129     QString description = escapedHtml(problem->description());
130     QString explanation = escapedHtml(problem->explanation());
131 
132     modifyHtml() += description;
133 
134     // Add only non-empty explanation which differs from the problem description.
135     // Skip this if we have more than one problem.
136     if (m_problems.size() == 1 && !explanation.isEmpty() && explanation != description)
137         modifyHtml() += QLatin1String("<p><i style=\"white-space:pre-wrap\">") + explanation +
138                         QLatin1String("</i></p>");
139 
140     modifyHtml() += QStringLiteral("</td>");
141     // END: right column
142 
143     modifyHtml() += QStringLiteral("</tr></table>");
144 
145     const auto diagnostics = problem->diagnostics();
146     if (!diagnostics.isEmpty()) {
147         DUChainReadLocker lock;
148         for (auto diagnostic : diagnostics) {
149             modifyHtml() += QStringLiteral("<p>");
150             modifyHtml() += labelHighlight(QStringLiteral("%1: ").arg(diagnostic->severityString()));
151             modifyHtml() += escapedHtml(diagnostic->description());
152 
153             const DocumentRange range = diagnostic->finalLocation();
154             Declaration* declaration = DUChainUtils::itemUnderCursor(range.document.toUrl(), range.start()).declaration;
155             if (declaration) {
156                 modifyHtml() += i18n("<br>See: ");
157                 makeLink(declaration->toString(), DeclarationPointer(declaration),
158                          NavigationAction::NavigateDeclaration);
159                 modifyHtml() += i18n(" in ");
160                 const auto url = declaration->url().toUrl();
161                 makeLink(QStringLiteral("%1 :%2")
162                          .arg(url.fileName())
163                          .arg(declaration->rangeInCurrentRevision().start().line() + 1),
164                          url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, declaration->rangeInCurrentRevision().start()));
165             } else if (range.start().isValid()) {
166                 modifyHtml() += i18n("<br>See: ");
167                 const auto url = range.document.toUrl();
168                 makeLink(QStringLiteral("%1 :%2")
169                          .arg(url.fileName())
170                          .arg(range.start().line() + 1),
171                          url.toDisplayString(QUrl::PreferLocalFile), NavigationAction(url, range.start()));
172             }
173 
174             modifyHtml() += QStringLiteral("</p>");
175         }
176     }
177 
178     auto assistant = problem->solutionAssistant();
179     if (assistant && !assistant->actions().isEmpty()) {
180         modifyHtml() +=
181             QStringLiteral("<table width='100%' style='border: 1px solid black; background-color: %1;'>").arg(QStringLiteral(
182                                                                                                                   "#b3d4ff"));
183         modifyHtml() +=
184             QStringLiteral("<tr><td valign='middle'>%1</td><td width='100%'>")
185             .arg(htmlImg(QIcon::fromTheme(QStringLiteral("dialog-ok-apply")), QStyle::PM_LargeIconSize));
186 
187         const int startIndex = m_assistantsActions.size();
188         int currentIndex = startIndex;
189         const auto assistantActions = assistant->actions();
190         for (auto& assistantAction : assistantActions) {
191             m_assistantsActions.append(assistantAction);
192 
193             if (currentIndex != startIndex)
194                 modifyHtml() += QStringLiteral("<br/>");
195 
196             makeLink(i18n("Solution (%1)", currentIndex + 1), KEY_INVOKE_ACTION(currentIndex),
197                      NavigationAction(KEY_INVOKE_ACTION(currentIndex)));
198             modifyHtml() += QLatin1String(": ") + assistantAction->description().toHtmlEscaped();
199 
200             ++currentIndex;
201         }
202 
203         modifyHtml() += QStringLiteral("</td></tr>");
204         modifyHtml() += QStringLiteral("</table>");
205     }
206 }
207 
html(bool shorten)208 QString ProblemNavigationContext::html(bool shorten)
209 {
210     AbstractNavigationContext::html(shorten);
211 
212     clear();
213     m_assistantsActions.clear();
214 
215     int problemIndex = 0;
216     for (auto& problem : qAsConst(m_problems)) {
217         html(problem);
218 
219         if (++problemIndex != m_problems.size())
220             modifyHtml() += QStringLiteral("<hr>");
221     }
222 
223     return currentHtml();
224 }
225 
executeKeyAction(const QString & key)226 NavigationContextPointer ProblemNavigationContext::executeKeyAction(const QString& key)
227 {
228     const QLatin1String invokeActionPrefix("invoke_action_");
229     if (key.startsWith(invokeActionPrefix)) {
230         const int index = key.midRef(invokeActionPrefix.size()).toInt();
231         executeAction(index);
232     }
233 
234     return {};
235 }
236 
executeAction(int index)237 void ProblemNavigationContext::executeAction(int index)
238 {
239     if (index < 0 || index >= m_assistantsActions.size())
240         return;
241 
242     auto action = m_assistantsActions.at(index);
243     Q_ASSERT(action);
244 
245     if (action) {
246         action->execute();
247         if (topContext())
248             DUChain::self()->updateContextForUrl(topContext()->url(), TopDUContext::ForceUpdate);
249     } else {
250         qCWarning(LANGUAGE()) << "No such action";
251         return;
252     }
253 }
254