File indexing completed on 2024-05-12 05:52:04

0001 /*
0002     SPDX-FileCopyrightText: 2019 Mark Nauwelaerts <mark.nauwelaerts@gmail.com>
0003     SPDX-FileCopyrightText: 2022 Waqar Ahmed <waqar.17a@gmail.com>
0004     SPDX-License-Identifier: MIT
0005 */
0006 #include "diagnosticview.h"
0007 
0008 #include "diagnosticitem.h"
0009 #include "drawing_utils.h"
0010 #include "kateapp.h"
0011 #include "kateviewmanager.h"
0012 #include "session_diagnostic_suppression.h"
0013 #include "texthint/KateTextHintManager.h"
0014 
0015 #include <KActionCollection>
0016 #include <KActionMenu>
0017 #include <KTextEditor/Application>
0018 #include <KTextEditor/Editor>
0019 #include <KTextEditor/MainWindow>
0020 
0021 #include <KColorScheme>
0022 #include <KMessageWidget>
0023 #include <KXMLGUIFactory>
0024 #include <QClipboard>
0025 #include <QComboBox>
0026 #include <QDebug>
0027 #include <QFileInfo>
0028 #include <QGuiApplication>
0029 #include <QLineEdit>
0030 #include <QMenu>
0031 #include <QPainter>
0032 #include <QSortFilterProxyModel>
0033 #include <QStyledItemDelegate>
0034 #include <QTextLayout>
0035 #include <QTimer>
0036 #include <QToolButton>
0037 #include <QTreeView>
0038 #include <QVBoxLayout>
0039 
0040 class ProviderListModel final : public QAbstractListModel
0041 {
0042 public:
0043     explicit ProviderListModel(DiagnosticsView *parent)
0044         : QAbstractListModel(parent)
0045         , m_diagView(parent)
0046     {
0047     }
0048 
0049     int rowCount(const QModelIndex &) const override
0050     {
0051         return m_providers.size();
0052     }
0053 
0054     QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
0055     {
0056         if (size_t(index.row()) >= m_providers.size()) {
0057             return {};
0058         }
0059         if (role == Qt::DisplayRole) {
0060             if (index.row() == 0) {
0061                 return i18n("All");
0062             }
0063             return m_providers.at(index.row())->name;
0064         }
0065         if (role == Qt::UserRole) {
0066             return QVariant::fromValue(m_providers.at(index.row()));
0067         }
0068         return {};
0069     }
0070 
0071     void update(const std::vector<DiagnosticsProvider *> &providerList)
0072     {
0073         beginResetModel();
0074         m_providers.clear();
0075         m_providers.push_back(nullptr);
0076         m_providers.insert(m_providers.end(), providerList.begin(), providerList.end());
0077         endResetModel();
0078     }
0079 
0080     DiagnosticsView *m_diagView;
0081     std::vector<DiagnosticsProvider *> m_providers;
0082 };
0083 
0084 class DiagnosticsProxyModel final : public QSortFilterProxyModel
0085 {
0086 public:
0087     explicit DiagnosticsProxyModel(QObject *parent)
0088         : QSortFilterProxyModel(parent)
0089     {
0090     }
0091 
0092     void setActiveProvider(DiagnosticsProvider *provider)
0093     {
0094         activeProvider = provider;
0095         invalidateFilter();
0096     }
0097 
0098     void setActiveSeverity(DiagnosticSeverity s)
0099     {
0100         severity = s;
0101         invalidateFilter();
0102     }
0103 
0104     bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
0105     {
0106         bool ret = true;
0107         auto model = static_cast<QStandardItemModel *>(sourceModel());
0108         if (activeProvider) {
0109             auto index = sourceModel()->index(sourceRow, 0, sourceParent);
0110             if (index.isValid()) {
0111                 const auto item = model->itemFromIndex(index);
0112                 if (item->type() == DiagnosticItem_File) {
0113                     ret = static_cast<DocumentDiagnosticItem *>(item)->providers().contains(activeProvider);
0114                 } else {
0115                     ret = index.data(DiagnosticModelRole::ProviderRole).value<DiagnosticsProvider *>() == activeProvider;
0116                 }
0117             }
0118         }
0119 
0120         if (ret && severity != DiagnosticSeverity::Unknown) {
0121             auto index = sourceModel()->index(sourceRow, 0, sourceParent);
0122             const auto item = model->itemFromIndex(index);
0123             if (item && item->type() == DiagnosticItem_Diag) {
0124                 auto castedItem = static_cast<DiagnosticItem *>(item);
0125                 ret = castedItem->m_diagnostic.severity == severity;
0126             } else if (item && item->type() == DiagnosticItem_File) {
0127                 // Hide parent if all childs hidden
0128                 int rc = item->rowCount();
0129                 int count = 0;
0130                 for (int i = 0; i < rc; ++i) {
0131                     auto child = item->child(i);
0132                     if (child && child->type() == DiagnosticItem_Diag) {
0133                         count += static_cast<DiagnosticItem *>(child)->m_diagnostic.severity == severity;
0134                     }
0135                 }
0136                 ret = count > 0;
0137             } else if (item && item->type() == DiagnosticItem_Fix) {
0138                 if (item->parent() && item->parent()->type() == DiagnosticItem_Diag) {
0139                     auto castedItem = static_cast<DiagnosticItem *>(item);
0140                     ret = castedItem->m_diagnostic.severity == severity;
0141                 }
0142             }
0143         }
0144 
0145         if (ret) {
0146             ret = QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent);
0147         }
0148         return ret;
0149     }
0150 
0151 private:
0152     DiagnosticsProvider *activeProvider = nullptr;
0153     DiagnosticSeverity severity = DiagnosticSeverity::Unknown;
0154 };
0155 
0156 DiagnosticsProvider::DiagnosticsProvider(KTextEditor::MainWindow *mainWindow, QObject *parent)
0157     : QObject(parent)
0158 {
0159     Q_ASSERT(mainWindow);
0160     DiagnosticsView::instance(mainWindow)->registerDiagnosticsProvider(this);
0161 }
0162 
0163 void DiagnosticsProvider::showDiagnosticsView(DiagnosticsProvider *filterTo)
0164 {
0165     if (diagnosticView && !diagnosticView->isVisible()) {
0166         diagnosticView->showToolview(filterTo);
0167     }
0168 }
0169 
0170 void DiagnosticsProvider::filterDiagnosticsViewTo(DiagnosticsProvider *filterTo)
0171 {
0172     if (diagnosticView) {
0173         diagnosticView->filterViewTo(filterTo);
0174     }
0175 }
0176 
0177 static constexpr KTextEditor::Document::MarkTypes markTypeDiagError = KTextEditor::Document::Error;
0178 static constexpr KTextEditor::Document::MarkTypes markTypeDiagWarning = KTextEditor::Document::Warning;
0179 static constexpr KTextEditor::Document::MarkTypes markTypeDiagOther = KTextEditor::Document::markType30;
0180 static constexpr KTextEditor::Document::MarkTypes markTypeDiagAll =
0181     KTextEditor::Document::MarkTypes(markTypeDiagError | markTypeDiagWarning | markTypeDiagOther);
0182 
0183 struct DiagModelIndex {
0184     int row;
0185     int parentRow;
0186     bool autoApply;
0187 };
0188 Q_DECLARE_METATYPE(DiagModelIndex)
0189 
0190 class DiagnosticsLocationTreeDelegate : public QStyledItemDelegate
0191 {
0192 public:
0193     DiagnosticsLocationTreeDelegate(QObject *parent)
0194         : QStyledItemDelegate(parent)
0195         , m_monoFont(Utils::editorFont())
0196     {
0197     }
0198 
0199     void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
0200     {
0201         const bool docItem = !index.parent().isValid();
0202         if (!docItem) {
0203             QStyledItemDelegate::paint(painter, option, index);
0204             return;
0205         }
0206 
0207         auto options = option;
0208         initStyleOption(&options, index);
0209 
0210         painter->save();
0211 
0212         QString text = index.data().toString();
0213         options.text = QString(); // clear old text
0214         options.widget->style()->drawControl(QStyle::CE_ItemViewItem, &options, painter, options.widget);
0215 
0216         QList<QTextLayout::FormatRange> formats;
0217         int lastSlash = text.lastIndexOf(QLatin1Char('/'));
0218         if (lastSlash != -1) {
0219             QTextCharFormat fmt;
0220             fmt.setFontWeight(QFont::Bold);
0221             formats.append({lastSlash + 1, int(text.length() - (lastSlash + 1)), fmt});
0222         }
0223 
0224         /** There might be an icon? Make sure to not draw over it **/
0225         auto textRect = options.widget->style()->subElementRect(QStyle::SE_ItemViewItemText, &options, options.widget);
0226         auto width = textRect.x() - options.rect.x();
0227         painter->translate(width, 0);
0228         Utils::paintItemViewText(painter, text, options, formats);
0229 
0230         painter->restore();
0231     }
0232 
0233 private:
0234     QFont m_monoFont;
0235 };
0236 
0237 static QIcon diagnosticsIcon(DiagnosticSeverity severity)
0238 {
0239     switch (severity) {
0240     case DiagnosticSeverity::Error: {
0241         static QIcon icon(QIcon::fromTheme(QStringLiteral("data-error"), QIcon::fromTheme(QStringLiteral("dialog-error"))));
0242         return icon;
0243     }
0244     case DiagnosticSeverity::Warning: {
0245         static QIcon icon(QIcon::fromTheme(QStringLiteral("data-warning"), QIcon::fromTheme(QStringLiteral("dialog-warning"))));
0246         return icon;
0247     }
0248     case DiagnosticSeverity::Information:
0249     case DiagnosticSeverity::Hint: {
0250         static QIcon icon(QIcon::fromTheme(QStringLiteral("data-information"), QIcon::fromTheme(QStringLiteral("dialog-information"))));
0251         return icon;
0252     }
0253     default:
0254         break;
0255     }
0256     return QIcon();
0257 }
0258 
0259 DiagnosticsView::DiagnosticsView(QWidget *parent, KTextEditor::MainWindow *mainWindow)
0260     : QWidget(parent)
0261     , KXMLGUIClient()
0262     , m_mainWindow(mainWindow)
0263     , m_diagnosticsTree(new QTreeView(this))
0264     , m_clearButton(new QToolButton(this))
0265     , m_filterLineEdit(new QLineEdit(this))
0266     , m_providerCombo(new QComboBox(this))
0267     , m_errFilterBtn(new QToolButton(this))
0268     , m_warnFilterBtn(new QToolButton(this))
0269     , m_diagLimitReachedWarning(new KMessageWidget(this))
0270     , m_proxy(new DiagnosticsProxyModel(this))
0271     , m_sessionDiagnosticSuppressions(std::make_unique<SessionDiagnosticSuppressions>())
0272     , m_posChangedTimer(new QTimer(this))
0273     , m_filterChangedTimer(new QTimer(this))
0274     , m_urlChangedTimer(new QTimer(this))
0275     , m_textHintProvider(new KateTextHintProvider(mainWindow, this))
0276 {
0277     setComponentName(QStringLiteral("kate_diagnosticsview"), i18n("Diagnostics View"));
0278     setXMLFile(QStringLiteral("ui.rc"));
0279 
0280     auto l = new QVBoxLayout(this);
0281     l->setContentsMargins({});
0282     setupDiagnosticViewToolbar(l);
0283 
0284     m_diagLimitReachedWarning->setText(i18n("Diagnostics limit reached, ignoring further diagnostics. The limit can be configured in settings."));
0285     m_diagLimitReachedWarning->setWordWrap(true);
0286     m_diagLimitReachedWarning->setCloseButtonVisible(false);
0287     m_diagLimitReachedWarning->setMessageType(KMessageWidget::Warning);
0288     l->addWidget(m_diagLimitReachedWarning);
0289     m_diagLimitReachedWarning->hide(); // hidden by default
0290 
0291     l->addWidget(m_diagnosticsTree);
0292 
0293     m_filterChangedTimer->setInterval(400);
0294     m_filterChangedTimer->callOnTimeout(this, [this] {
0295         m_proxy->setFilterRegularExpression(m_filterLineEdit->text());
0296     });
0297 
0298     m_model.setColumnCount(1);
0299 
0300     m_diagnosticsTree->setHeaderHidden(true);
0301     m_diagnosticsTree->setFocusPolicy(Qt::NoFocus);
0302     m_diagnosticsTree->setLayoutDirection(Qt::LeftToRight);
0303     m_diagnosticsTree->setSortingEnabled(false);
0304     m_diagnosticsTree->setEditTriggers(QAbstractItemView::NoEditTriggers);
0305     m_diagnosticsTree->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge}));
0306     m_diagnosticsTree->setUniformRowHeights(true);
0307     m_diagnosticsTree->setContextMenuPolicy(Qt::CustomContextMenu);
0308     m_diagnosticsTree->setItemDelegate(new DiagnosticsLocationTreeDelegate(this));
0309 
0310     m_proxy->setSourceModel(&m_model);
0311     m_proxy->setFilterKeyColumn(0);
0312     m_proxy->setRecursiveFilteringEnabled(true);
0313     m_proxy->setFilterRole(Qt::DisplayRole);
0314 
0315     m_diagnosticsTree->setModel(m_proxy);
0316 
0317     connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &DiagnosticsView::onViewChanged);
0318 
0319     connect(m_diagnosticsTree, &QTreeView::customContextMenuRequested, this, &DiagnosticsView::onContextMenuRequested);
0320     connect(m_diagnosticsTree, &QTreeView::clicked, this, &DiagnosticsView::goToItemLocation);
0321     connect(m_diagnosticsTree, &QTreeView::doubleClicked, this, [this](const QModelIndex &index) {
0322         onDoubleClicked(m_proxy->mapToSource(index));
0323     });
0324 
0325     auto *ac = actionCollection();
0326 
0327     auto *diagMenu = new KActionMenu(i18nc("@action:inmenu Diagnostics View actions menu", "Diagnostics View"), this);
0328     ac->addAction(QStringLiteral("tools_diagnosticsview"), diagMenu);
0329 
0330     auto *a = ac->addAction(QStringLiteral("kate_quick_fix"), this, &DiagnosticsView::quickFix);
0331     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0332     a->setText(i18n("Quick Fix"));
0333     a->setIcon(QIcon::fromTheme(QStringLiteral("quickopen")));
0334     ac->setDefaultShortcut(a, QKeySequence((Qt::CTRL | Qt::Key_Period)));
0335 
0336     a = ac->addAction(QStringLiteral("goto_next_diagnostic"), this, &DiagnosticsView::nextItem);
0337     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0338     a->setText(i18n("Next Item"));
0339     a->setIcon(QIcon::fromTheme(QStringLiteral("go-next")));
0340     ac->setDefaultShortcut(a, Qt::SHIFT | Qt::ALT | Qt::Key_Right);
0341 
0342     a = ac->addAction(QStringLiteral("goto_prev_diagnostic"), this, &DiagnosticsView::previousItem);
0343     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0344     a->setText(i18n("Previous Item"));
0345     a->setIcon(QIcon::fromTheme(QStringLiteral("go-previous")));
0346     ac->setDefaultShortcut(a, Qt::SHIFT | Qt::ALT | Qt::Key_Left);
0347 
0348     a = ac->addAction(QStringLiteral("diagnostics_clear_filter"), this, [this]() {
0349         DiagnosticsView::filterViewTo(nullptr);
0350     });
0351     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0352     a->setText(i18n("Clear Diagnostics Filter"));
0353     a->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-all")));
0354     ac->setDefaultShortcut(a, Qt::SHIFT | Qt::ALT | Qt::Key_C);
0355 
0356     m_posChangedTimer->setInterval(500);
0357     m_posChangedTimer->setSingleShot(true);
0358     m_posChangedTimer->callOnTimeout(this, [this] {
0359         auto v = m_mainWindow->activeView();
0360         if (v) {
0361             if (auto doc = v->document()) {
0362                 syncDiagnostics(doc, v->cursorPosition().line(), true, false);
0363             }
0364         }
0365     });
0366 
0367     // collapse url changed events
0368     m_urlChangedTimer->setInterval(100);
0369     m_urlChangedTimer->setSingleShot(true);
0370     m_urlChangedTimer->callOnTimeout(this, &DiagnosticsView::onDocumentUrlChanged);
0371 
0372     // handle tab button creation
0373     connect(mainWindow->window(), SIGNAL(tabForToolViewAdded(QWidget *, QWidget *)), this, SLOT(tabForToolViewAdded(QWidget *, QWidget *)));
0374 
0375     connect(m_textHintProvider.get(), &KateTextHintProvider::textHintRequested, this, &DiagnosticsView::onTextHint);
0376     connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &DiagnosticsView::handleEsc);
0377 
0378     mainWindow->guiFactory()->addClient(this);
0379 
0380     // We don't want our shortcuts available in the whole mainwindow, as it can conflict
0381     // with other KParts' shortcus (e.g. Konsole). The KateViewManager will be added to
0382     // the asscioted widgets in KateMainWindow::setupDiagnosticsView(), after the view
0383     // manager has been initialized.
0384     ac->clearAssociatedWidgets();
0385     ac->addAssociatedWidget(this);
0386 
0387     auto readConfig = [this] {
0388         KSharedConfig::Ptr config = KSharedConfig::openConfig();
0389         KConfigGroup cgGeneral = KConfigGroup(config, QStringLiteral("General"));
0390         m_diagnosticLimit = cgGeneral.readEntry("Diagnostics Limit", 12000);
0391 
0392         const bool diagnosticsAreLimited = m_diagnosticLimit > -1;
0393         if (!diagnosticsAreLimited && m_diagLimitReachedWarning->isVisible()) {
0394             m_diagLimitReachedWarning->hide();
0395         }
0396     };
0397     connect(KateApp::self(), &KateApp::configurationChanged, this, readConfig);
0398     readConfig();
0399 }
0400 
0401 DiagnosticsView *DiagnosticsView::instance(KTextEditor::MainWindow *mainWindow)
0402 {
0403     Q_ASSERT(mainWindow);
0404     auto dv = static_cast<DiagnosticsView *>(mainWindow->property("diagnosticsView").value<QObject *>());
0405     if (!dv) {
0406         auto tv = mainWindow->createToolView(nullptr /* toolview has no plugin it belongs to */,
0407                                              QStringLiteral("diagnostics"),
0408                                              KTextEditor::MainWindow::Bottom,
0409                                              QIcon::fromTheme(QStringLiteral("dialog-warning-symbolic")),
0410                                              i18n("Diagnostics"));
0411         dv = new DiagnosticsView(tv, mainWindow);
0412         mainWindow->setProperty("diagnosticsView", QVariant::fromValue(dv));
0413     }
0414     return dv;
0415 }
0416 
0417 DiagnosticsView::~DiagnosticsView()
0418 {
0419     // clear the model first so that destruction is faster
0420     m_model.clear();
0421     onViewChanged(nullptr);
0422     const auto providers = m_providers;
0423     for (auto *p : providers) {
0424         unregisterDiagnosticsProvider(p);
0425     }
0426     Q_ASSERT(m_providers.empty());
0427 
0428     m_mainWindow->guiFactory()->removeClient(this);
0429 }
0430 
0431 void DiagnosticsView::setupDiagnosticViewToolbar(QVBoxLayout *mainLayout)
0432 {
0433     mainLayout->setSpacing(0);
0434     auto l = new QHBoxLayout();
0435     l->setContentsMargins(style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0436                           style()->pixelMetric(QStyle::PM_LayoutTopMargin),
0437                           style()->pixelMetric(QStyle::PM_LayoutRightMargin),
0438                           style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
0439     l->setSpacing(style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));
0440     mainLayout->addLayout(l);
0441 
0442     l->addWidget(m_providerCombo);
0443     m_providerModel = new ProviderListModel(this);
0444     m_providerCombo->setModel(m_providerModel);
0445     m_providerCombo->setCurrentIndex(0);
0446     connect(m_providerCombo, &QComboBox::currentIndexChanged, this, [this] {
0447         auto proxy = static_cast<DiagnosticsProxyModel *>(m_proxy);
0448         proxy->setActiveProvider(m_providerCombo->currentData().value<DiagnosticsProvider *>());
0449         m_diagnosticsTree->expandAll();
0450     });
0451 
0452     m_errFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("data-error")));
0453     m_errFilterBtn->setText(i18n("Errors"));
0454     m_errFilterBtn->setCheckable(true);
0455     m_errFilterBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0456     l->addWidget(m_errFilterBtn);
0457     connect(m_errFilterBtn, &QToolButton::clicked, this, [this](bool c) {
0458         if (m_warnFilterBtn->isChecked()) {
0459             const QSignalBlocker b(m_warnFilterBtn);
0460             m_warnFilterBtn->setChecked(false);
0461         }
0462         auto proxy = static_cast<DiagnosticsProxyModel *>(m_proxy);
0463         proxy->setActiveSeverity(c ? DiagnosticSeverity::Error : DiagnosticSeverity::Unknown);
0464         QTimer::singleShot(200, m_diagnosticsTree, &QTreeView::expandAll);
0465     });
0466 
0467     m_warnFilterBtn->setIcon(QIcon::fromTheme(QStringLiteral("data-warning")));
0468     m_warnFilterBtn->setCheckable(true);
0469     m_warnFilterBtn->setText(i18n("Warnings"));
0470     m_warnFilterBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
0471     l->addWidget(m_warnFilterBtn);
0472     connect(m_warnFilterBtn, &QToolButton::clicked, this, [this](bool c) {
0473         if (m_errFilterBtn->isChecked()) {
0474             const QSignalBlocker b(m_errFilterBtn);
0475             m_errFilterBtn->setChecked(false);
0476         }
0477         auto proxy = static_cast<DiagnosticsProxyModel *>(m_proxy);
0478         proxy->setActiveSeverity(c ? DiagnosticSeverity::Warning : DiagnosticSeverity::Unknown);
0479         QTimer::singleShot(200, m_diagnosticsTree, &QTreeView::expandAll);
0480     });
0481 
0482     l->addWidget(m_filterLineEdit);
0483     m_filterLineEdit->setPlaceholderText(i18n("Filter..."));
0484     m_filterLineEdit->setClearButtonEnabled(true);
0485     connect(m_filterLineEdit, &QLineEdit::textChanged, m_filterChangedTimer, [this] {
0486         m_filterChangedTimer->start();
0487     });
0488 
0489     m_clearButton->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear-all")));
0490     connect(m_clearButton, &QToolButton::clicked, this, [this] {
0491         std::vector<KTextEditor::Document *> docs(m_diagnosticsMarks.begin(), m_diagnosticsMarks.end());
0492         for (auto d : docs) {
0493             clearAllMarks(d);
0494         }
0495         m_model.clear();
0496         m_diagnosticsCount = 0;
0497         m_diagLimitReachedWarning->hide();
0498     });
0499     l->addWidget(m_clearButton);
0500 }
0501 
0502 void DiagnosticsView::onViewChanged(KTextEditor::View *v)
0503 {
0504     disconnect(posChangedConnection);
0505     m_posChangedTimer->stop();
0506     if (v) {
0507         posChangedConnection = connect(v, &KTextEditor::View::cursorPositionChanged, this, [this] {
0508             m_posChangedTimer->start();
0509         });
0510         m_posChangedTimer->start();
0511     }
0512 
0513     if (v && v->document()) {
0514         connect(v->document(), &KTextEditor::Document::documentUrlChanged, m_urlChangedTimer, QOverload<>::of(&QTimer::start), Qt::UniqueConnection);
0515     }
0516 }
0517 
0518 void DiagnosticsView::registerDiagnosticsProvider(DiagnosticsProvider *provider)
0519 {
0520     if (std::find(m_providers.begin(), m_providers.end(), provider) != m_providers.end()) {
0521         qWarning() << provider << " already registred, ignoring!";
0522         return;
0523     }
0524 
0525     provider->diagnosticView = this;
0526     connect(provider, &DiagnosticsProvider::diagnosticsAdded, this, &DiagnosticsView::onDiagnosticsAdded);
0527     connect(provider, &DiagnosticsProvider::fixesAvailable, this, &DiagnosticsView::onFixesAvailable);
0528     connect(provider, &DiagnosticsProvider::requestClearDiagnostics, this, &DiagnosticsView::clearDiagnosticsFromProvider);
0529     connect(provider, &DiagnosticsProvider::requestClearSuppressions, this, &DiagnosticsView::clearSuppressionsFromProvider);
0530     connect(provider, &QObject::destroyed, this, [this](QObject *o) {
0531         unregisterDiagnosticsProvider(static_cast<DiagnosticsProvider *>(o));
0532     });
0533     m_providers.push_back(provider);
0534 
0535     m_providerModel->update(m_providers);
0536 }
0537 
0538 void DiagnosticsView::unregisterDiagnosticsProvider(DiagnosticsProvider *provider)
0539 {
0540     auto it = std::find(m_providers.begin(), m_providers.end(), provider);
0541     if (it == m_providers.end()) {
0542         return;
0543     }
0544     disconnect(provider, &DiagnosticsProvider::diagnosticsAdded, this, &DiagnosticsView::onDiagnosticsAdded);
0545     disconnect(provider, &DiagnosticsProvider::fixesAvailable, this, &DiagnosticsView::onFixesAvailable);
0546     disconnect(provider, &DiagnosticsProvider::requestClearDiagnostics, this, &DiagnosticsView::clearDiagnosticsFromProvider);
0547     disconnect(provider, &DiagnosticsProvider::requestClearSuppressions, this, &DiagnosticsView::clearSuppressionsFromProvider);
0548     m_providers.erase(it);
0549 
0550     m_providerModel->update(m_providers);
0551 
0552     // clear diagnostics
0553     clearDiagnosticsFromProvider(provider);
0554 }
0555 
0556 void DiagnosticsView::readSessionConfig(const KConfigGroup &config)
0557 {
0558     m_sessionDiagnosticSuppressions->readSessionConfig(config);
0559 }
0560 
0561 void DiagnosticsView::writeSessionConfig(KConfigGroup &config)
0562 {
0563     m_sessionDiagnosticSuppressions->writeSessionConfig(config);
0564 }
0565 
0566 void DiagnosticsView::showEvent(QShowEvent *e)
0567 {
0568     if (m_tabButtonOverlay) {
0569         m_tabButtonOverlay->setActive(false);
0570     }
0571     QWidget::showEvent(e);
0572 }
0573 
0574 void DiagnosticsView::handleEsc(QEvent *event)
0575 {
0576     if (event->type() != QEvent::ShortcutOverride)
0577         return;
0578 
0579     auto keyEvent = static_cast<QKeyEvent *>(event);
0580     if (keyEvent && keyEvent->key() == Qt::Key_Escape && keyEvent->modifiers() == Qt::NoModifier) {
0581         if (auto p = qobject_cast<QWidget *>(parent()); p && p->isVisible()) {
0582             m_mainWindow->hideToolView(p);
0583             event->accept();
0584         }
0585     }
0586 }
0587 
0588 void DiagnosticsView::tabForToolViewAdded(QWidget *toolView, QWidget *tab)
0589 {
0590     if (parent() == toolView) {
0591         m_tabButtonOverlay = new DiagTabOverlay(tab);
0592         m_tabButtonOverlay->setActive(!isVisible() && m_model.rowCount() > 0);
0593     }
0594 }
0595 
0596 static DocumentDiagnosticItem *getItem(const QStandardItemModel &model, const QUrl &url)
0597 {
0598     // local file in custom role, Qt::DisplayRole might have additional elements
0599     auto l = model.match(model.index(0, 0, QModelIndex()), Qt::UserRole, url.toString(QUrl::PreferLocalFile | QUrl::RemovePassword), 1, Qt::MatchExactly);
0600     if (l.length()) {
0601         return static_cast<DocumentDiagnosticItem *>(model.itemFromIndex(l.at(0)));
0602     }
0603     return nullptr;
0604 }
0605 
0606 static QStandardItem *getItem(const QStandardItem *topItem, KTextEditor::Cursor pos, bool onlyLine)
0607 {
0608     QStandardItem *targetItem = nullptr;
0609     if (topItem) {
0610         int count = topItem->rowCount();
0611         int first = 0, last = count;
0612         // let's not run wild on a linear search in a flood of diagnostics
0613         if (count > 50) {
0614             // instead, let's *assume* sorted and use binary search to get closer
0615             // it probably is sorted, so it should work out
0616             // if not, at least we tried (without spending/wasting more on sorting)
0617             auto getLine = [topItem, count](int index) {
0618                 Q_ASSERT(index >= 0);
0619                 Q_ASSERT(index < count);
0620                 auto range = topItem->child(index)->data(DiagnosticModelRole::RangeRole).value<KTextEditor::Range>();
0621                 return range.start().line();
0622             };
0623             int first = 0, last = count - 1;
0624             int target = pos.line();
0625             while (first + 1 < last) {
0626                 int middle = first + (last - first) / 2;
0627                 Q_ASSERT(first != middle);
0628                 Q_ASSERT(middle != last);
0629                 if (getLine(middle) < target) {
0630                     first = middle;
0631                 } else {
0632                     last = middle;
0633                 }
0634             }
0635         }
0636         for (int i = first; i < last; ++i) {
0637             auto item = topItem->child(i);
0638             if (!(item->flags() & Qt::ItemIsEnabled)) {
0639                 continue;
0640             }
0641             auto range = item->data(DiagnosticModelRole::RangeRole).value<KTextEditor::Range>();
0642             if ((onlyLine && pos.line() == range.start().line()) || (range.contains(pos))) {
0643                 targetItem = item;
0644                 break;
0645             }
0646         }
0647     }
0648     return targetItem;
0649 }
0650 
0651 static void fillItemRoles(QStandardItem *item, const QUrl &url, const KTextEditor::Range _range, DiagnosticSeverity kind)
0652 {
0653     // auto range = snapshot ? transformRange(url, *snapshot, _range) : _range;
0654     item->setData(QVariant(url), DiagnosticModelRole::FileUrlRole);
0655     QVariant vrange;
0656     vrange.setValue(_range);
0657     item->setData(vrange, DiagnosticModelRole::RangeRole);
0658     item->setData(QVariant::fromValue(kind), DiagnosticModelRole::KindRole);
0659 }
0660 
0661 void DiagnosticsView::onFixesAvailable(const QList<DiagnosticFix> &fixes, const QVariant &data)
0662 {
0663     if (fixes.empty() || data.isNull()) {
0664         return;
0665     }
0666     const auto diagModelIdx = data.value<DiagModelIndex>();
0667     if (diagModelIdx.parentRow == -1) {
0668         qWarning() << "Unexpected -1 parentRow";
0669         return;
0670     }
0671     const auto parentIdx = m_model.index(diagModelIdx.parentRow, 0);
0672     const auto idx = m_model.index(diagModelIdx.row, 0, parentIdx);
0673     if (!idx.isValid()) {
0674         qWarning() << Q_FUNC_INFO << "Unexpected invalid idx";
0675         return;
0676     }
0677     const auto item = m_model.itemFromIndex(idx);
0678     if (!item) {
0679         return;
0680     }
0681     bool autoApply = diagModelIdx.autoApply;
0682     if (autoApply) {
0683         if (fixes.size() == 1) {
0684             fixes.constFirst().fixCallback();
0685         } else {
0686             showFixesInMenu(fixes);
0687         }
0688         return;
0689     }
0690     for (const auto &fix : fixes) {
0691         if (!fix.fixCallback) {
0692             continue;
0693         }
0694         auto f = new DiagnosticFixItem;
0695         f->fix = fix;
0696         f->setText(fix.fixTitle);
0697         item->appendRow(f);
0698     }
0699 }
0700 
0701 void DiagnosticsView::showFixesInMenu(const QList<DiagnosticFix> &fixes)
0702 {
0703     auto av = m_mainWindow->activeView();
0704     if (av) {
0705         auto pos = av->cursorPositionCoordinates();
0706         QMenu menu(this);
0707         for (const auto &fix : fixes) {
0708             menu.addAction(fix.fixTitle, fix.fixCallback);
0709         }
0710         menu.exec(av->mapToGlobal(pos));
0711     }
0712 }
0713 
0714 void DiagnosticsView::quickFix()
0715 {
0716     KTextEditor::View *activeView = m_mainWindow->activeView();
0717     if (!activeView) {
0718         return;
0719     }
0720     KTextEditor::Document *document = activeView->document();
0721 
0722     if (!document) {
0723         return;
0724     }
0725 
0726     QStandardItem *topItem = getItem(m_model, document->url());
0727 
0728     // try to find current diagnostic based on cursor position
0729     auto pos = activeView->cursorPosition();
0730     QStandardItem *targetItem = getItem(topItem, pos, false);
0731     if (!targetItem) {
0732         // match based on line position only
0733         targetItem = getItem(topItem, pos, true);
0734     }
0735 
0736     if (targetItem && targetItem->type() == DiagnosticItem_Diag) {
0737         if (static_cast<DiagnosticItem *>(targetItem)->hasFixes()) {
0738             QList<DiagnosticFix> fixes;
0739             for (int i = 0; i < targetItem->rowCount(); ++i) {
0740                 auto item = targetItem->child(i);
0741                 if (item && item->type() == DiagnosticItem_Fix) {
0742                     fixes << static_cast<DiagnosticFixItem *>(item)->fix;
0743                 }
0744             }
0745             if (fixes.size() > 1) {
0746                 showFixesInMenu(fixes);
0747             } else if (fixes.size() == 1 && fixes[0].fixCallback) {
0748                 fixes[0].fixCallback();
0749             }
0750         } else {
0751             onDoubleClicked(m_model.indexFromItem(targetItem), true);
0752         }
0753     }
0754 }
0755 
0756 void DiagnosticsView::onDoubleClicked(const QModelIndex &index, bool quickFix)
0757 {
0758     auto itemFromIndex = m_model.itemFromIndex(index);
0759     if (!itemFromIndex) {
0760         qWarning() << "invalid item clicked";
0761         return;
0762     }
0763 
0764     if (itemFromIndex->type() == DiagnosticItem_Diag) {
0765         if (static_cast<DiagnosticItem *>(itemFromIndex)->hasFixes()) {
0766             return;
0767         }
0768         auto provider = index.data(DiagnosticModelRole::ProviderRole).value<DiagnosticsProvider *>();
0769         if (!provider) {
0770             return;
0771         }
0772         auto item = static_cast<DiagnosticItem *>(itemFromIndex);
0773         DiagModelIndex idx;
0774         idx.row = item->row();
0775         idx.parentRow = item->parent() ? item->parent()->row() : -1;
0776         idx.autoApply = quickFix;
0777         QVariant data = QVariant::fromValue(idx);
0778         Q_EMIT provider->requestFixes(item->data(DiagnosticModelRole::FileUrlRole).toUrl(), item->m_diagnostic, data);
0779     }
0780 
0781     if (itemFromIndex->type() == DiagnosticItem_Fix) {
0782         static_cast<DiagnosticFixItem *>(itemFromIndex)->fix.fixCallback();
0783     }
0784 }
0785 
0786 void DiagnosticsView::onDiagnosticsAdded(const FileDiagnostics &diagnostics)
0787 {
0788     auto view = m_mainWindow->activeView();
0789     auto doc = view ? view->document() : nullptr;
0790     // We allow diagnostics for the active document always because it might be that diagnostic limit is reached
0791     // and the user doesn't know about it and thus won't get any further diagnostics at all while typing
0792     const bool diagnosticsAreForActiveDoc = doc ? diagnostics.uri == doc->url() : false;
0793 
0794     const bool diagnosticsAreLimited = m_diagnosticLimit > -1;
0795     if (diagnosticsAreLimited && m_diagnosticsCount > m_diagnosticLimit && !diagnosticsAreForActiveDoc) {
0796         return;
0797     }
0798 
0799     auto diagWarnShowGuard = qScopeGuard([this] {
0800         const bool diagnosticsAreLimited = m_diagnosticLimit > -1;
0801         if (!diagnosticsAreLimited) {
0802             return;
0803         }
0804 
0805         if (m_diagnosticsCount > m_diagnosticLimit && !m_diagLimitReachedWarning->isVisible()) {
0806             m_diagLimitReachedWarning->show();
0807         } else if (m_diagnosticsCount < m_diagnosticLimit && m_diagLimitReachedWarning->isVisible()) {
0808             m_diagLimitReachedWarning->hide();
0809         }
0810     });
0811 
0812     auto *provider = qobject_cast<DiagnosticsProvider *>(sender());
0813     Q_ASSERT(provider);
0814 
0815     QStandardItemModel *model = &m_model;
0816     DocumentDiagnosticItem *topItem = getItem(m_model, diagnostics.uri);
0817 
0818     auto toProxyIndex = [this](const QModelIndex &index) {
0819         return m_proxy->mapFromSource(index);
0820     };
0821 
0822     // current diagnostics row, if one of incoming diagnostics' document
0823     int row = -1;
0824     if (!topItem) {
0825         // no need to create an empty one
0826         if (diagnostics.diagnostics.empty()) {
0827             return;
0828         }
0829         topItem = new DocumentDiagnosticItem();
0830         model->appendRow(topItem);
0831         QString prettyUri = diagnostics.uri.toString(QUrl::PreferLocalFile | QUrl::RemovePassword);
0832         topItem->setText(prettyUri);
0833         topItem->setData(prettyUri, Qt::UserRole);
0834     } else {
0835         // try to retain current position
0836         auto currentIndex = m_proxy->mapToSource(m_diagnosticsTree->currentIndex());
0837         if (currentIndex.parent() == topItem->index()) {
0838             row = currentIndex.row();
0839         }
0840 
0841         // Remove old diagnostics of this provider
0842         if (!provider->m_persistentDiagnostics) {
0843             int removedCount = topItem->removeItemsForProvider(provider);
0844             m_diagnosticsCount -= removedCount;
0845         }
0846     }
0847     topItem->addProvider(provider);
0848 
0849     QList<QStandardItem *> diagItems;
0850     diagItems.reserve(diagnostics.diagnostics.size());
0851     for (const auto &diag : diagnostics.diagnostics) {
0852         auto item = new DiagnosticItem(diag);
0853         diagItems.push_back(item);
0854         QString source;
0855         if (diag.source.length()) {
0856             source = QStringLiteral("[%1] ").arg(diag.source);
0857         }
0858         if (diag.code.length()) {
0859             source += QStringLiteral("(%1) ").arg(diag.code);
0860         }
0861         item->setData(diagnosticsIcon(diag.severity), Qt::DecorationRole);
0862         // rendering of lines with embedded newlines does not work so well
0863         // so ... split message by lines
0864         auto lines = diag.message.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
0865         item->setText(source + (lines.size() > 0 ? lines[0] : QString()));
0866         fillItemRoles(item, diagnostics.uri, diag.range, diag.severity);
0867         item->setData(QVariant::fromValue(provider), DiagnosticModelRole::ProviderRole);
0868         // add subsequent lines to subitems
0869         // no metadata is added to these,
0870         // as it can be taken from the parent (for marks and ranges)
0871         for (int l = 1; l < lines.size(); ++l) {
0872             auto subitem = new QStandardItem();
0873             subitem->setText(lines[l]);
0874             item->appendRow(subitem);
0875         }
0876         const auto &relatedInfo = diag.relatedInformation;
0877         for (const auto &related : relatedInfo) {
0878             if (related.location.uri.isEmpty()) {
0879                 continue;
0880             }
0881             auto relatedItemMessage = new QStandardItem();
0882 
0883             fillItemRoles(relatedItemMessage, related.location.uri, related.location.range, DiagnosticSeverity::Information);
0884 
0885             auto basename = QFileInfo(related.location.uri.toLocalFile()).fileName();
0886             // display line number is 1-based (as opposed to internal 0-based)
0887             auto location = QStringLiteral("%1:%2").arg(basename).arg(related.location.range.start().line() + 1);
0888             relatedItemMessage->setText(QStringLiteral("[%1] %2").arg(location).arg(related.message));
0889             relatedItemMessage->setData(diagnosticsIcon(DiagnosticSeverity::Information), Qt::DecorationRole);
0890             item->appendRow(relatedItemMessage);
0891         }
0892     }
0893     m_diagnosticsCount += diagItems.size();
0894     topItem->appendRows(diagItems);
0895 
0896     // TODO perhaps add some custom delegate that only shows 1 line
0897     // and only the whole text when item selected ??
0898     m_diagnosticsTree->expandRecursively(toProxyIndex(topItem->index()), 1);
0899 
0900     // topItem might be removed after this
0901     updateDiagnosticsState(topItem);
0902     // also sync updated diagnostic to current position
0903     auto currentView = m_mainWindow->activeView();
0904     if (topItem && currentView && currentView->document()) {
0905         if (!syncDiagnostics(currentView->document(), currentView->cursorPosition().line(), false, false)) {
0906             // avoid jitter; only restore previous if applicable
0907             if (row >= 0 && row < topItem->rowCount()) {
0908                 m_diagnosticsTree->scrollTo(toProxyIndex(topItem->child(row)->index()));
0909             }
0910         }
0911     }
0912     if (m_tabButtonOverlay) {
0913         m_tabButtonOverlay->setActive(!isVisible() && m_model.rowCount() > 0);
0914     }
0915 }
0916 
0917 static auto getProvider(QStandardItem *item)
0918 {
0919     return item->data(DiagnosticModelRole::ProviderRole).value<DiagnosticsProvider *>();
0920 }
0921 
0922 void DiagnosticsView::clearDiagnosticsForStaleDocs(const QList<QString> &filesToKeep, DiagnosticsProvider *provider)
0923 {
0924     auto diagWarnShowGuard = qScopeGuard([this] {
0925         const bool diagnosticsAreLimited = m_diagnosticLimit > -1;
0926         if (!diagnosticsAreLimited) {
0927             return;
0928         }
0929 
0930         if (m_diagnosticsCount < m_diagnosticLimit && m_diagLimitReachedWarning->isVisible()) {
0931             m_diagLimitReachedWarning->hide();
0932         }
0933     });
0934 
0935     auto all_diags_from_provider = [provider](DocumentDiagnosticItem *file) {
0936         if (file->rowCount() == 0) {
0937             return true;
0938         }
0939         if (provider && file->providers().size() == 1 && file->providers().contains(provider)) {
0940             return true;
0941         }
0942         if (!provider) {
0943             const QList<DiagnosticsProvider *> &providers = file->providers();
0944             return std::all_of(providers.begin(), providers.end(), [](DiagnosticsProvider *p) {
0945                 return !p->persistentDiagnostics();
0946             });
0947         }
0948         return false;
0949     };
0950 
0951     auto bulk_remove = [this](QStandardItemModel *model, int &start, int &count, int &i) {
0952         if (start > -1 && count != 0) {
0953             for (int r = start; r < (start + count); r++) {
0954                 m_diagnosticsCount -= model->item(r)->rowCount();
0955             }
0956             model->removeRows(start, count);
0957             i = start - 1; // reset i
0958         }
0959         start = -1;
0960         count = 0;
0961     };
0962 
0963     int start = -1, count = 0;
0964 
0965     for (int i = 0; i < m_model.rowCount(); ++i) {
0966         auto fileItem = static_cast<DocumentDiagnosticItem *>(m_model.item(i));
0967         if (!filesToKeep.contains(fileItem->data(Qt::UserRole).toString())) {
0968             if (!all_diags_from_provider(fileItem)) {
0969                 // If the diagnostics of this file item are from multiple providers
0970                 // delete the ones from @p provider
0971                 bulk_remove(&m_model, start, count, i);
0972                 int removedCount = fileItem->removeItemsForProvider(provider);
0973                 m_diagnosticsCount -= removedCount;
0974             } else {
0975                 if (start == -1) {
0976                     start = i;
0977                 }
0978                 count += 1;
0979             }
0980         } else {
0981             bulk_remove(&m_model, start, count, i);
0982         }
0983     }
0984 
0985     if (start != -1 && count != 0) {
0986         for (int r = start; r < (start + count); r++) {
0987             m_diagnosticsCount -= m_model.item(r)->rowCount();
0988         }
0989         m_model.removeRows(start, count);
0990     }
0991 
0992     updateMarks();
0993 
0994     if (m_tabButtonOverlay) {
0995         m_tabButtonOverlay->setActive(!isVisible() && m_model.rowCount() > 0);
0996     }
0997 }
0998 
0999 void DiagnosticsView::clearSuppressionsFromProvider(DiagnosticsProvider *provider)
1000 {
1001     // need to clear suppressions
1002     // will be filled again at suitable time by re-requesting provider
1003     for (int i = 0; i < m_model.rowCount(); ++i) {
1004         auto item = m_model.item(i);
1005         if (item->type() != DiagnosticItem_File) {
1006             continue;
1007         }
1008         auto fileItem = static_cast<DocumentDiagnosticItem *>(item);
1009         if (getProvider(fileItem) == provider) {
1010             fileItem->diagnosticSuppression.reset();
1011         }
1012     }
1013 }
1014 
1015 void DiagnosticsView::addMarks(KTextEditor::Document *doc, QStandardItem *item)
1016 {
1017     Q_ASSERT(item);
1018     using Style = KSyntaxHighlighting::Theme::TextStyle;
1019 
1020     // only consider enabled items
1021     if (!(item->flags() & Qt::ItemIsEnabled)) {
1022         return;
1023     }
1024 
1025     auto url = item->data(DiagnosticModelRole::FileUrlRole).toUrl();
1026     // document url could end up empty while in intermediate reload state
1027     // (and then it might match a parent item with no RangeData at all)
1028     if (url != doc->url() || url.isEmpty()) {
1029         return;
1030     }
1031 
1032     KTextEditor::Range range = item->data(DiagnosticModelRole::RangeRole).value<KTextEditor::Range>();
1033     if (!range.isValid()) {
1034         return;
1035     }
1036 
1037     KTextEditor::Attribute::Ptr attr;
1038     KTextEditor::Document::MarkTypes markType = markTypeDiagWarning;
1039 
1040     const auto kind = item->data(DiagnosticModelRole::KindRole).value<DiagnosticSeverity>();
1041     switch (kind) {
1042     // use underlining for diagnostics to avoid lots of fancy flickering
1043     case DiagnosticSeverity::Error: {
1044         static KTextEditor::Attribute::Ptr errorAttr;
1045         if (!errorAttr) {
1046             const auto theme = KTextEditor::Editor::instance()->theme();
1047             errorAttr = new KTextEditor::Attribute();
1048             errorAttr->setUnderlineColor(theme.textColor(Style::Error));
1049             errorAttr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
1050         }
1051         attr = errorAttr;
1052         markType = markTypeDiagError;
1053         break;
1054     }
1055     case DiagnosticSeverity::Warning: {
1056         static KTextEditor::Attribute::Ptr warnAttr;
1057         if (!warnAttr) {
1058             const auto theme = KTextEditor::Editor::instance()->theme();
1059             warnAttr = new KTextEditor::Attribute();
1060             warnAttr->setUnderlineColor(theme.textColor(Style::Warning));
1061             warnAttr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
1062         }
1063         attr = warnAttr;
1064         markType = markTypeDiagWarning;
1065         break;
1066     }
1067     case DiagnosticSeverity::Information:
1068     case DiagnosticSeverity::Hint: {
1069         static KTextEditor::Attribute::Ptr infoAttr;
1070         if (!infoAttr) {
1071             const auto theme = KTextEditor::Editor::instance()->theme();
1072             infoAttr = new KTextEditor::Attribute();
1073             infoAttr->setUnderlineColor(theme.textColor(Style::Information));
1074             infoAttr->setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
1075         }
1076         attr = infoAttr;
1077         markType = markTypeDiagOther;
1078         break;
1079     }
1080     case DiagnosticSeverity::Unknown:
1081         qWarning() << "Unknown diagnostic severity";
1082         return;
1083     }
1084 
1085     if (!attr) {
1086         qWarning() << "Unexpected null attr";
1087     }
1088 
1089     // highlight the range
1090     if (attr && !range.isEmpty()) {
1091         KTextEditor::MovingRange *mr = doc->newMovingRange(range);
1092         mr->setZDepth(-90000.0); // Set the z-depth to slightly worse than the selection
1093         mr->setAttribute(attr);
1094         mr->setAttributeOnlyForViews(true);
1095         m_diagnosticsRanges[doc].push_back(mr);
1096     }
1097 
1098     auto iface = doc;
1099     // add match mark for range
1100     switch (markType) {
1101     case markTypeDiagError:
1102         iface->setMarkDescription(markType, i18n("Error"));
1103         iface->setMarkIcon(markType, diagnosticsIcon(DiagnosticSeverity::Error));
1104         break;
1105     case markTypeDiagWarning:
1106         iface->setMarkDescription(markType, i18n("Warning"));
1107         iface->setMarkIcon(markType, diagnosticsIcon(DiagnosticSeverity::Warning));
1108         break;
1109     case markTypeDiagOther:
1110         iface->setMarkDescription(markType, i18n("Information"));
1111         iface->setMarkIcon(markType, diagnosticsIcon(DiagnosticSeverity::Information));
1112         break;
1113     default:
1114         Q_ASSERT(false);
1115         break;
1116     }
1117 
1118     const auto line = range.start().line();
1119     iface->addMark(line, markType);
1120     m_diagnosticsMarks.insert(doc);
1121 
1122     // ensure runtime match
1123     using Doc = KTextEditor::Document;
1124     connect(doc, &Doc::aboutToInvalidateMovingInterfaceContent, this, &DiagnosticsView::clearAllMarks, Qt::UniqueConnection);
1125     connect(doc, &Doc::aboutToDeleteMovingInterfaceContent, this, &DiagnosticsView::clearAllMarks, Qt::UniqueConnection);
1126     // reload might save/restore marks before/after above signals, so let's clear before that
1127     connect(doc, &Doc::aboutToReload, this, &DiagnosticsView::clearAllMarks, Qt::UniqueConnection);
1128     connect(doc, &Doc::markClicked, this, &DiagnosticsView::onMarkClicked, Qt::UniqueConnection);
1129 }
1130 
1131 void DiagnosticsView::addMarksRec(KTextEditor::Document *doc, QStandardItem *item)
1132 {
1133     Q_ASSERT(item);
1134     // We only care about @p doc items
1135     if (item->type() == DiagnosticItem_File && QUrl::fromLocalFile(item->data(Qt::UserRole).toString()) != doc->url()) {
1136         return;
1137     }
1138     addMarks(doc, item);
1139     for (int i = 0; i < item->rowCount(); ++i) {
1140         addMarksRec(doc, item->child(i));
1141     }
1142 }
1143 
1144 void DiagnosticsView::addMarks(KTextEditor::Document *doc)
1145 {
1146     // check if already added
1147     if (m_diagnosticsRanges.contains(doc) && m_diagnosticsMarks.contains(doc)) {
1148         return;
1149     }
1150     addMarksRec(doc, m_model.invisibleRootItem());
1151 }
1152 
1153 void DiagnosticsView::clearAllMarks(KTextEditor::Document *doc)
1154 {
1155     if (m_diagnosticsMarks.contains(doc)) {
1156         const QHash<int, KTextEditor::Mark *> marks = doc->marks();
1157         for (auto mark : marks) {
1158             if (mark->type & markTypeDiagAll) {
1159                 doc->removeMark(mark->line, markTypeDiagAll);
1160             }
1161         }
1162         m_diagnosticsMarks.remove(doc);
1163     }
1164 
1165     auto it = m_diagnosticsRanges.find(doc);
1166     if (it != m_diagnosticsRanges.end()) {
1167         for (auto range : it.value()) {
1168             delete range;
1169         }
1170         it = m_diagnosticsRanges.erase(it);
1171     }
1172 }
1173 
1174 void DiagnosticsView::updateMarks(const std::vector<QUrl> &urls)
1175 {
1176     std::vector<KTextEditor::Document *> docs;
1177     if (!urls.empty()) {
1178         auto app = KTextEditor::Editor::instance()->application();
1179         for (const auto &url : urls) {
1180             if (auto doc = app->findUrl(url)) {
1181                 docs.push_back(doc);
1182             }
1183         }
1184     } else {
1185         KTextEditor::View *activeView = m_mainWindow->activeView();
1186         if (activeView && activeView->document()) {
1187             docs.push_back(activeView->document());
1188         }
1189     }
1190 
1191     for (auto doc : docs) {
1192         clearAllMarks(doc);
1193         addMarks(doc);
1194     }
1195 }
1196 
1197 void DiagnosticsView::updateDiagnosticsState(DocumentDiagnosticItem *&topItem)
1198 {
1199     if (!topItem) {
1200         return;
1201     }
1202 
1203     auto diagTopItem = static_cast<DocumentDiagnosticItem *>(topItem);
1204     auto enabled = diagTopItem->enabled;
1205     auto suppressions = enabled ? diagTopItem->diagnosticSuppression.get() : nullptr;
1206 
1207     const int totalCount = topItem->rowCount();
1208     int count = 0;
1209     for (int i = 0; i < totalCount; ++i) {
1210         auto item = topItem->child(i);
1211         auto hide = suppressions && item && suppressions->match(*item);
1212         // mark accordingly as flag and (un)hide
1213         auto flags = item->flags();
1214         const auto ENABLED = Qt::ItemFlag::ItemIsEnabled;
1215         if ((flags & ENABLED) != !hide) {
1216             flags = hide ? (flags & ~ENABLED) : (flags | ENABLED);
1217             if (item->flags() != flags) {
1218                 item->setFlags(flags);
1219             }
1220             const auto proxyIdx = m_proxy->mapFromSource(item->index());
1221             if (m_diagnosticsTree->isRowHidden(proxyIdx.row(), proxyIdx.parent()) != hide) {
1222                 m_diagnosticsTree->setRowHidden(proxyIdx.row(), proxyIdx.parent(), hide);
1223             }
1224         }
1225         count += hide ? 0 : 1;
1226     }
1227     // adjust file item level text
1228     auto suppressed = totalCount - count;
1229     const QString path = topItem->data(Qt::UserRole).toString();
1230     topItem->setText(suppressed ? i18nc("@info", "%1 [suppressed: %2]", path, suppressed) : path);
1231     // only hide if really nothing below
1232     const auto proxyIdx = m_proxy->mapFromSource(topItem->index());
1233     m_diagnosticsTree->setRowHidden(proxyIdx.row(), proxyIdx.parent(), totalCount == 0);
1234     if (topItem->rowCount() == 0) {
1235         m_model.removeRow(topItem->row());
1236         topItem = nullptr;
1237     }
1238 
1239     updateMarks({QUrl::fromLocalFile(path)});
1240 }
1241 
1242 void DiagnosticsView::goToItemLocation(QModelIndex index)
1243 {
1244     auto getPrimaryModelIndex = [](QModelIndex index) {
1245         // in case of a multiline diagnostics item, a split secondary line has no data set
1246         // so we need to go up to the primary parent item
1247         if (!index.data(DiagnosticModelRole::RangeRole).isValid() && index.parent().data(DiagnosticModelRole::RangeRole).isValid()) {
1248             return index.parent();
1249         }
1250         return index;
1251     };
1252 
1253     index = getPrimaryModelIndex(index);
1254     auto url = index.data(DiagnosticModelRole::FileUrlRole).toUrl();
1255     auto start = index.data(DiagnosticModelRole::RangeRole).value<KTextEditor::Range>();
1256     if (url.isEmpty()) {
1257         return;
1258     }
1259 
1260     int line = start.start().line();
1261     int column = start.start().column();
1262     KTextEditor::View *activeView = m_mainWindow->activeView();
1263     if (line < 0 || column < 0) {
1264         return;
1265     }
1266 
1267     KTextEditor::Document *document = activeView ? activeView->document() : nullptr;
1268     KTextEditor::Cursor cdef(line, column);
1269 
1270     KTextEditor::View *targetView = nullptr;
1271     if (document && url == document->url()) {
1272         targetView = activeView;
1273     } else {
1274         targetView = m_mainWindow->openUrl(url);
1275     }
1276     if (targetView) {
1277         // save current position for location history
1278         if (activeView) {
1279             Utils::addPositionToHistory(activeView->document()->url(), activeView->cursorPosition(), m_mainWindow);
1280         }
1281         // save the position to which we are jumping in location history
1282         Utils::addPositionToHistory(targetView->document()->url(), cdef, m_mainWindow);
1283         targetView->setCursorPosition(cdef);
1284     }
1285 }
1286 
1287 void DiagnosticsView::onMarkClicked(KTextEditor::Document *document, KTextEditor::Mark mark, bool &handled)
1288 {
1289     // no action if no mark was sprinkled here
1290     if (m_diagnosticsMarks.contains(document) && syncDiagnostics(document, mark.line, false, true)) {
1291         handled = true;
1292     }
1293 }
1294 
1295 void DiagnosticsView::showToolview(DiagnosticsProvider *filterTo)
1296 {
1297     m_mainWindow->showToolView(qobject_cast<QWidget *>(parent()));
1298     filterViewTo(filterTo);
1299 }
1300 
1301 void DiagnosticsView::filterViewTo(DiagnosticsProvider *provider)
1302 {
1303     if (provider) {
1304         auto index = m_providerCombo->findData(QVariant::fromValue(provider));
1305         if (index != -1) {
1306             m_providerCombo->setCurrentIndex(index);
1307         }
1308     } else {
1309         m_providerCombo->setCurrentIndex(0);
1310     }
1311 }
1312 
1313 bool DiagnosticsView::syncDiagnostics(KTextEditor::Document *document, int line, bool allowTop, bool doShow)
1314 {
1315     auto hint = QAbstractItemView::PositionAtTop;
1316     DocumentDiagnosticItem *topItem = getItem(m_model, document->url());
1317     updateDiagnosticsSuppression(topItem, document);
1318     QStandardItem *targetItem = getItem(topItem, {line, 0}, true);
1319     if (targetItem) {
1320         hint = QAbstractItemView::PositionAtCenter;
1321     }
1322     if (!targetItem && allowTop) {
1323         targetItem = topItem;
1324     }
1325     if (targetItem) {
1326         m_diagnosticsTree->blockSignals(true);
1327         const auto idx = m_proxy->mapFromSource(targetItem->index());
1328         m_diagnosticsTree->scrollTo(idx, hint);
1329         m_diagnosticsTree->setCurrentIndex(idx);
1330         m_diagnosticsTree->blockSignals(false);
1331         if (doShow) {
1332             m_mainWindow->showToolView(qobject_cast<QWidget *>(parent()));
1333         }
1334     }
1335     return targetItem != nullptr;
1336 }
1337 
1338 void DiagnosticsView::updateDiagnosticsSuppression(DocumentDiagnosticItem *diagTopItem, KTextEditor::Document *doc, bool force)
1339 {
1340     if (!diagTopItem || !doc) {
1341         return;
1342     }
1343 
1344     auto &suppressions = diagTopItem->diagnosticSuppression;
1345     if (!suppressions || force) {
1346         std::vector<QJsonObject> providerSupressions;
1347         const QList<DiagnosticsProvider *> &providers = diagTopItem->providers();
1348         for (auto p : providers) {
1349             auto suppressions = p->suppressions(doc);
1350             if (!suppressions.isEmpty()) {
1351                 providerSupressions.push_back(suppressions);
1352             }
1353         }
1354         const auto sessionSuppressions = m_sessionDiagnosticSuppressions->getSuppressions(doc->url().toLocalFile());
1355         auto supp = new DiagnosticSuppression(doc, providerSupressions, sessionSuppressions);
1356         const bool hadSuppression = suppressions != nullptr;
1357         suppressions.reset(supp);
1358         if (!providerSupressions.empty() || !sessionSuppressions.empty() || hadSuppression) {
1359             updateDiagnosticsState(diagTopItem);
1360         }
1361     }
1362 }
1363 
1364 void DiagnosticsView::onContextMenuRequested(const QPoint &pos)
1365 {
1366     Q_UNUSED(pos);
1367 
1368     auto menu = new QMenu(m_diagnosticsTree);
1369     menu->addAction(i18n("Expand All"), m_diagnosticsTree, &QTreeView::expandAll);
1370     menu->addAction(i18n("Collapse All"), m_diagnosticsTree, &QTreeView::collapseAll);
1371     menu->addSeparator();
1372 
1373     QModelIndex index = m_proxy->mapToSource(m_diagnosticsTree->currentIndex());
1374     if (QStandardItem *item = m_model.itemFromIndex(index)) {
1375         auto diagText = index.data().toString();
1376         menu->addAction(QIcon::fromTheme(QLatin1String("edit-copy")), i18n("Copy to Clipboard"), [diagText]() {
1377             QClipboard *clipboard = QGuiApplication::clipboard();
1378             clipboard->setText(diagText);
1379         });
1380 
1381         if (item->type() == DiagnosticItem_Diag) {
1382             menu->addSeparator();
1383             auto parent = index.parent();
1384             auto docDiagItem = static_cast<DocumentDiagnosticItem *>(m_model.itemFromIndex(parent));
1385             // track validity of raw pointer
1386             QPersistentModelIndex pindex(parent);
1387             auto h = [this, pindex, diagText, docDiagItem](bool add, const QString &file, const QString &diagnostic) {
1388                 if (!pindex.isValid()) {
1389                     return;
1390                 }
1391                 if (add) {
1392                     m_sessionDiagnosticSuppressions->add(file, diagnostic);
1393                 } else {
1394                     m_sessionDiagnosticSuppressions->remove(file, diagnostic);
1395                 }
1396                 updateDiagnosticsSuppression(docDiagItem, docDiagItem->diagnosticSuppression->document(), true);
1397             };
1398             using namespace std::placeholders;
1399             const auto empty = QString();
1400             if (m_sessionDiagnosticSuppressions->hasSuppression(empty, diagText)) {
1401                 menu->addAction(i18n("Remove Global Suppression"), this, std::bind(h, false, empty, diagText));
1402             } else {
1403                 menu->addAction(i18n("Add Global Suppression"), this, std::bind(h, true, empty, diagText));
1404             }
1405             auto file = parent.data(Qt::UserRole).toString();
1406             if (m_sessionDiagnosticSuppressions->hasSuppression(file, diagText)) {
1407                 menu->addAction(i18n("Remove Local Suppression"), this, std::bind(h, false, file, diagText));
1408             } else {
1409                 menu->addAction(i18n("Add Local Suppression"), this, std::bind(h, true, file, diagText));
1410             }
1411         } else if (item->type() == DiagnosticItem_File) {
1412             // track validity of raw pointer
1413             QPersistentModelIndex pindex(index);
1414             auto docDiagItem = static_cast<DocumentDiagnosticItem *>(item);
1415             auto h = [this, item, pindex](bool enabled) {
1416                 if (!pindex.isValid()) {
1417                     return;
1418                 }
1419                 auto docDiagItem = static_cast<DocumentDiagnosticItem *>(item);
1420                 docDiagItem->enabled = enabled;
1421                 updateDiagnosticsState(docDiagItem);
1422             };
1423             if (docDiagItem->enabled) {
1424                 menu->addAction(i18n("Disable Suppression"), this, std::bind(h, false));
1425             } else {
1426                 menu->addAction(i18n("Enable Suppression"), this, std::bind(h, true));
1427             }
1428         }
1429     }
1430     menu->popup(m_diagnosticsTree->viewport()->mapToGlobal(pos));
1431 }
1432 
1433 void DiagnosticsView::onTextHint(KTextEditor::View *view, const KTextEditor::Cursor &position) const
1434 {
1435     QString result;
1436     auto document = view->document();
1437 
1438     QStandardItem *topItem = getItem(m_model, document->url());
1439     QStandardItem *targetItem = getItem(topItem, position, false);
1440     if (targetItem) {
1441         result = targetItem->text();
1442         // also include related info
1443         int count = targetItem->rowCount();
1444         for (int i = 0; i < count; ++i) {
1445             auto item = targetItem->child(i);
1446             result += QStringLiteral("\n");
1447             result += item->text();
1448         }
1449         // but let's not get carried away too far
1450         constexpr int maxsize = 1000;
1451         if (result.size() > maxsize) {
1452             result.resize(maxsize);
1453             result.append(QStringLiteral("..."));
1454         }
1455     }
1456 
1457     m_textHintProvider->textHintAvailable(result, TextHintMarkupKind::PlainText, position);
1458 }
1459 
1460 void DiagnosticsView::onDocumentUrlChanged()
1461 {
1462     // remove lingering diagnostics
1463     // collect active urls
1464     QSet<QString> fpaths;
1465     const auto views = m_mainWindow->views();
1466     for (const auto view : views) {
1467         if (auto doc = view->document()) {
1468             fpaths.insert(doc->url().toLocalFile());
1469         }
1470     }
1471     clearDiagnosticsForStaleDocs({fpaths.begin(), fpaths.end()}, nullptr);
1472 }
1473 
1474 void DiagnosticsView::moveDiagnosticsSelection(bool forward)
1475 {
1476     // If there's no items we just return
1477     if (m_diagnosticsTree->model()->rowCount() == 0) {
1478         return;
1479     }
1480 
1481     auto model = m_diagnosticsTree->model();
1482     auto index = m_diagnosticsTree->currentIndex();
1483 
1484     // Nothing is selected, select first visible item
1485     if (!index.isValid()) {
1486         index = model->index(0, 0);
1487     }
1488 
1489     auto isDiagItem = [](const QModelIndex &index) {
1490         // If parent is valid and parent of parent is invalid,
1491         // we are a level 2 item which means we are diagnostic item
1492         return index.parent().isValid() && !index.parent().parent().isValid();
1493     };
1494 
1495     auto getRow = [forward](const QModelIndex &index) {
1496         return forward ? 0 : index.model()->rowCount(index) - 1;
1497     };
1498 
1499     if (isDiagItem(index)) {
1500         auto next = forward ? m_diagnosticsTree->indexBelow(index) : m_diagnosticsTree->indexAbove(index);
1501         if (next.isValid() && isDiagItem(next)) {
1502             goToItemLocation(next);
1503         } else {
1504             // Next is not a diagnostic, are we at the end of current file's diagnostics?
1505             // If so, then jump to the next file
1506             auto parent = index.parent();
1507             auto nextFile = parent.siblingAtRow(parent.row() + (forward ? 1 : -1));
1508             if (nextFile.isValid()) {
1509                 // Iterate and find valid first child and jump to that
1510                 goToItemLocation(model->index(getRow(nextFile), 0, nextFile));
1511             }
1512             // If file is not valid, we are in the end of the list
1513         }
1514     } else {
1515         // Current is not a diagnostic item
1516         if (!index.parent().isValid()) {
1517             // Current is a file item, select it's first child
1518             goToItemLocation(model->index(0, 0, index));
1519         } else {
1520             // We are likely in third subitem, so we need to go back up
1521             goToItemLocation(model->index(getRow(index.parent()), 0, index));
1522         }
1523     }
1524 }
1525 
1526 void DiagnosticsView::nextItem()
1527 {
1528     moveDiagnosticsSelection(true);
1529 }
1530 
1531 void DiagnosticsView::previousItem()
1532 {
1533     moveDiagnosticsSelection(false);
1534 }
1535 
1536 #include "moc_diagnosticview.cpp"