File indexing completed on 2024-05-05 04:40:14

0001 /*
0002     SPDX-FileCopyrightText: 2006 Adam Treat <treat@kde.org>
0003     SPDX-FileCopyrightText: 2006-2007 Hamish Rodda <rodda@kde.org>
0004     SPDX-FileCopyrightText: 2007-2008 David Nolden <david.nolden.kdevelop@art-master.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "problemreporterplugin.h"
0010 
0011 #include <QMenu>
0012 
0013 #include <KLocalizedString>
0014 #include <KPluginFactory>
0015 
0016 #include <KTextEditor/Document>
0017 
0018 #include <interfaces/icore.h>
0019 #include <interfaces/iuicontroller.h>
0020 #include <interfaces/idocumentcontroller.h>
0021 
0022 #include <interfaces/ilanguagecontroller.h>
0023 #include <language/duchain/duchainlock.h>
0024 #include <language/duchain/duchain.h>
0025 #include <util/kdevstringhandler.h>
0026 
0027 #include "problemhighlighter.h"
0028 #include "probleminlinenoteprovider.h"
0029 #include "problemreportermodel.h"
0030 #include "language/assistant/staticassistantsmanager.h"
0031 #include <interfaces/context.h>
0032 #include <language/interfaces/editorcontext.h>
0033 #include <language/duchain/duchainutils.h>
0034 #include <interfaces/contextmenuextension.h>
0035 #include <interfaces/iassistant.h>
0036 #include <QAction>
0037 
0038 #include "shell/problemmodelset.h"
0039 #include "problemsview.h"
0040 #include <debug.h>
0041 
0042 #include <shell/problem.h>
0043 
0044 K_PLUGIN_FACTORY_WITH_JSON(KDevProblemReporterFactory, "kdevproblemreporter.json",
0045                            registerPlugin<ProblemReporterPlugin>();)
0046 
0047 using namespace KDevelop;
0048 
0049 class ProblemReporterFactory : public KDevelop::IToolViewFactory
0050 {
0051 public:
0052     QWidget* create(QWidget* parent = nullptr) override
0053     {
0054         Q_UNUSED(parent);
0055 
0056         auto* v = new ProblemsView();
0057         v->load();
0058         return v;
0059     }
0060 
0061     Qt::DockWidgetArea defaultPosition() const override
0062     {
0063         return Qt::BottomDockWidgetArea;
0064     }
0065 
0066     QString id() const override { return QStringLiteral("org.kdevelop.ProblemReporterView"); }
0067 };
0068 
0069 class ProblemReporterPlugin::ProblemVisualizer
0070 {
0071 public:
0072     explicit ProblemVisualizer(KTextEditor::Document* document)
0073         : m_highlighter(document)
0074         , m_inlineNoteProvider(document)
0075     {}
0076 
0077     KTextEditor::Document* document() const
0078     {
0079         return m_highlighter.document();
0080     }
0081 
0082     void setProblems(const QVector<IProblem::Ptr>& problems)
0083     {
0084         m_highlighter.setProblems(problems);
0085         m_inlineNoteProvider.setProblems(problems);
0086     }
0087 
0088 private:
0089     ProblemHighlighter m_highlighter;
0090     ProblemInlineNoteProvider m_inlineNoteProvider;
0091 };
0092 
0093 ProblemReporterPlugin::ProblemReporterPlugin(QObject* parent, const QVariantList&)
0094     : KDevelop::IPlugin(QStringLiteral("kdevproblemreporter"), parent)
0095     , m_factory(new ProblemReporterFactory)
0096     , m_model(new ProblemReporterModel(this))
0097 {
0098     KDevelop::ProblemModelSet* pms = core()->languageController()->problemModelSet();
0099     pms->addModel(QStringLiteral("Parser"), i18n("Parser"), m_model);
0100     core()->uiController()->addToolView(i18nc("@title:window", "Problems"), m_factory);
0101     setXMLFile(QStringLiteral("kdevproblemreporter.rc"));
0102 
0103     connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this,
0104             &ProblemReporterPlugin::documentClosed);
0105     connect(ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this,
0106             &ProblemReporterPlugin::textDocumentCreated);
0107     connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this,
0108             &ProblemReporterPlugin::documentUrlChanged);
0109     connect(ICore::self()->documentController(), &IDocumentController::documentActivated, this,
0110             &ProblemReporterPlugin::documentActivated);
0111     connect(DUChain::self(), &DUChain::updateReady,
0112             this, &ProblemReporterPlugin::updateReady);
0113     connect(ICore::self()->languageController()->staticAssistantsManager(), &StaticAssistantsManager::problemsChanged,
0114             this, &ProblemReporterPlugin::updateHighlight);
0115     connect(pms, &ProblemModelSet::showRequested, this, &ProblemReporterPlugin::showModel);
0116     connect(pms, &ProblemModelSet::problemsChanged, this, &ProblemReporterPlugin::updateOpenedDocumentsHighlight);
0117 }
0118 
0119 ProblemReporterPlugin::~ProblemReporterPlugin()
0120 {
0121     qDeleteAll(m_visualizers);
0122 }
0123 
0124 ProblemReporterModel* ProblemReporterPlugin::model() const
0125 {
0126     return m_model;
0127 }
0128 
0129 void ProblemReporterPlugin::unload()
0130 {
0131     KDevelop::ProblemModelSet* pms = KDevelop::ICore::self()->languageController()->problemModelSet();
0132     pms->removeModel(QStringLiteral("Parser"));
0133 
0134     core()->uiController()->removeToolView(m_factory);
0135 }
0136 
0137 void ProblemReporterPlugin::documentClosed(IDocument* doc)
0138 {
0139     if (!doc->textDocument())
0140         return;
0141 
0142     const IndexedString url(doc->url());
0143 
0144     const auto it = m_visualizers.constFind(url);
0145     if (it == m_visualizers.cend()) {
0146         qCDebug(PLUGIN_PROBLEMREPORTER) << "closed an unregistered text document:" << doc << doc->url().toString();
0147         return;
0148     }
0149 
0150     if (it.value()->document() != doc->textDocument()) {
0151         // doc is being renamed, DocumentControllerPrivate::changeDocumentUrl() is closing it
0152         // because of a conflict with another open modified document at doc's new URL.
0153         // documentUrlChanged(doc, ...) will be invoked soon and will remove doc's visualizer. Nothing to do here.
0154         qCDebug(PLUGIN_PROBLEMREPORTER) << "closed a text document that shares another text document's URL:" << doc
0155                                         << doc->url().toString();
0156         return;
0157     }
0158 
0159     delete it.value();
0160     m_visualizers.erase(it);
0161     m_reHighlightNeeded.remove(url);
0162 }
0163 
0164 void ProblemReporterPlugin::textDocumentCreated(KDevelop::IDocument* document)
0165 {
0166     Q_ASSERT(document->textDocument());
0167     const IndexedString documentUrl(document->url());
0168 
0169     Q_ASSERT(!m_visualizers.contains(documentUrl));
0170     m_visualizers.insert(documentUrl, new ProblemVisualizer{document->textDocument()});
0171 
0172     DUChain::self()->updateContextForUrl(documentUrl,
0173                                          KDevelop::TopDUContext::AllDeclarationsContextsAndUses, this);
0174 }
0175 
0176 void ProblemReporterPlugin::documentUrlChanged(IDocument* document, const QUrl& previousUrl)
0177 {
0178     if (!document->textDocument())
0179         return;
0180 
0181     qCDebug(PLUGIN_PROBLEMREPORTER) << "document URL changed from" << previousUrl.toString() << "to"
0182                                     << document->url().toString();
0183 
0184     const IndexedString previousUrlIndexed(previousUrl);
0185     const auto it = m_visualizers.constFind(previousUrlIndexed);
0186     if (it == m_visualizers.cend()) {
0187         qCWarning(PLUGIN_PROBLEMREPORTER)
0188             << "a visualizer for renamed document is missing:" << document->textDocument();
0189         return;
0190     }
0191     Q_ASSERT(it.value()->document() == document->textDocument());
0192 
0193     m_reHighlightNeeded.remove(previousUrlIndexed);
0194 
0195     auto* const visualizer = it.value();
0196     m_visualizers.erase(it);
0197 
0198     const IndexedString currentUrl{document->url()};
0199     if (m_visualizers.contains(currentUrl)) {
0200         // The renamed document must have been closed already in DocumentControllerPrivate::changeDocumentUrl()
0201         // because of a conflict with another open modified document at its new URL. See a similar comment in
0202         // ProblemReporterPlugin::documentClosed(). Just destroy document's obsolete visualizer here.
0203         delete visualizer;
0204         qCDebug(PLUGIN_PROBLEMREPORTER) << "the renamed document's URL equals another document's URL:" << document;
0205         return;
0206     }
0207     m_visualizers.insert(currentUrl, visualizer);
0208 }
0209 
0210 void ProblemReporterPlugin::documentActivated(KDevelop::IDocument* document)
0211 {
0212   IndexedString documentUrl(document->url());
0213 
0214   const auto neededIt = m_reHighlightNeeded.find(documentUrl);
0215   if (neededIt != m_reHighlightNeeded.end()) {
0216     m_reHighlightNeeded.erase(neededIt);
0217     updateHighlight(documentUrl);
0218   }
0219 }
0220 
0221 void ProblemReporterPlugin::updateReady(const IndexedString& url, const KDevelop::ReferencedTopDUContext&)
0222 {
0223     m_model->problemsUpdated(url);
0224     updateHighlight(url);
0225 }
0226 
0227 void ProblemReporterPlugin::updateHighlight(const KDevelop::IndexedString& url)
0228 {
0229     auto* const visualizer = m_visualizers.value(url);
0230     if (!visualizer)
0231         return;
0232 
0233     KDevelop::ProblemModelSet* pms(core()->languageController()->problemModelSet());
0234     QVector<IProblem::Ptr> documentProblems;
0235 
0236     const auto models = pms->models();
0237     for (const ModelData& modelData : models) {
0238         documentProblems += modelData.model->problems({url});
0239     }
0240 
0241     visualizer->setProblems(documentProblems);
0242 }
0243 
0244 void ProblemReporterPlugin::showModel(const QString& id)
0245 {
0246     auto w = qobject_cast<ProblemsView*>(core()->uiController()->findToolView(i18nc("@title:window", "Problems"), m_factory));
0247     if (w)
0248       w->showModel(id);
0249 }
0250 
0251 KDevelop::ContextMenuExtension ProblemReporterPlugin::contextMenuExtension(KDevelop::Context* context, QWidget* parent)
0252 {
0253     KDevelop::ContextMenuExtension extension;
0254 
0255     auto* editorContext = dynamic_cast<KDevelop::EditorContext*>(context);
0256     if (editorContext) {
0257         DUChainReadLocker lock(DUChain::lock(), 1000);
0258         if (!lock.locked()) {
0259             qCDebug(PLUGIN_PROBLEMREPORTER) << "failed to lock duchain in time";
0260             return extension;
0261         }
0262 
0263         QString title;
0264         QList<QAction*> actions;
0265 
0266         TopDUContext* top = DUChainUtils::standardContextForUrl(editorContext->url());
0267         if (top) {
0268             const auto problems = top->problems();
0269             for (auto& problem : problems) {
0270                 if (problem->range().contains(
0271                         top->transformToLocalRevision(KTextEditor::Cursor(editorContext->position())))) {
0272                     KDevelop::IAssistant::Ptr solution = problem->solutionAssistant();
0273                     if (solution) {
0274                         title = solution->title();
0275                         const auto solutionActions = solution->actions();
0276                         for (auto& action : solutionActions) {
0277                             actions << action->toQAction(parent);
0278                         }
0279                     }
0280                 }
0281             }
0282         }
0283 
0284         if (!actions.isEmpty()) {
0285             QString text;
0286             if (title.isEmpty())
0287                 text = i18nc("@action:inmenu", "Solve Problem");
0288             else {
0289                 text = i18nc("@action:inmenu", "Solve: %1", KDevelop::htmlToPlainText(title));
0290             }
0291 
0292             auto* menu = new QMenu(text, parent);
0293             for (QAction* action : qAsConst(actions)) {
0294                 menu->addAction(action);
0295             }
0296 
0297             extension.addAction(ContextMenuExtension::ExtensionGroup, menu->menuAction());
0298         }
0299     }
0300     return extension;
0301 }
0302 
0303 void ProblemReporterPlugin::updateOpenedDocumentsHighlight()
0304 {
0305     const auto openDocuments = core()->documentController()->openDocuments();
0306     for (auto* document : openDocuments) {
0307         // Skip non-text documents.
0308         // This also fixes crash caused by calling updateOpenedDocumentsHighlight() method without
0309         // any opened documents. In this case documentController()->openDocuments() returns single
0310         // (non-text) document with url like file:///tmp/kdevelop_QW2530.patch which has fatal bug:
0311         // if we call isActive() method from this document the crash will happens.
0312         if (!document->textDocument())
0313             continue;
0314 
0315         IndexedString documentUrl(document->url());
0316 
0317         if (document->isActive())
0318             updateHighlight(documentUrl);
0319         else
0320             m_reHighlightNeeded.insert(documentUrl);
0321     }
0322 }
0323 
0324 #include "problemreporterplugin.moc"
0325 #include "moc_problemreporterplugin.cpp"