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"