File indexing completed on 2024-04-28 04:37:28

0001 /*
0002     SPDX-FileCopyrightText: 2010 Dmitry Risenberg <dmitry.risenberg@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "watcheddocumentset.h"
0008 
0009 #include <interfaces/icore.h>
0010 #include <interfaces/idocumentcontroller.h>
0011 #include <interfaces/idocument.h>
0012 #include <interfaces/iprojectcontroller.h>
0013 #include <interfaces/iproject.h>
0014 #include <language/duchain/duchain.h>
0015 #include <language/duchain/duchainlock.h>
0016 #include <language/duchain/topducontext.h>
0017 #include <project/interfaces/iprojectfilemanager.h>
0018 #include <project/projectmodel.h>
0019 
0020 #include <QFileInfo>
0021 
0022 namespace KDevelop
0023 {
0024 
0025 enum ActionFlag {
0026     DoUpdate = 1,
0027     DoEmit = 2
0028 };
0029 Q_DECLARE_FLAGS(ActionFlags, ActionFlag)
0030 Q_DECLARE_OPERATORS_FOR_FLAGS(ActionFlags)
0031 
0032 class WatchedDocumentSetPrivate : public QObject
0033 {
0034     Q_OBJECT
0035 
0036 public:
0037     using DocumentSet = WatchedDocumentSet::DocumentSet;
0038 
0039     explicit WatchedDocumentSetPrivate(WatchedDocumentSet* documentSet)
0040         : m_documentSet(documentSet)
0041         , m_showImports(false)
0042     {
0043         connect(DUChain::self(), &DUChain::updateReady, this, &WatchedDocumentSetPrivate::updateReady);
0044     }
0045 
0046     inline bool showImports() const
0047     {
0048         return m_showImports;
0049     }
0050 
0051     void setShowImports(bool showImports)
0052     {
0053         if (m_showImports == showImports)
0054             return;
0055 
0056         DocumentSet oldImports = m_imports;
0057 
0058         m_showImports = showImports;
0059         updateImports();
0060 
0061         if (m_imports != oldImports)
0062             emit m_documentSet->changed();
0063     }
0064 
0065     inline const DocumentSet& documents() const
0066     {
0067         return m_documents;
0068     }
0069 
0070     inline const DocumentSet& imports() const
0071     {
0072         return m_imports;
0073     }
0074 
0075     inline void doUpdate(ActionFlags flags)
0076     {
0077         if (flags.testFlag(DoUpdate))
0078             updateImports();
0079 
0080         if (flags.testFlag(DoEmit))
0081             emit m_documentSet->changed();
0082     }
0083 
0084     void setDocuments(const DocumentSet& docs, ActionFlags flags = {})
0085     {
0086         m_documents = docs;
0087         doUpdate(flags);
0088     }
0089 
0090     bool addDocument(const IndexedString& doc, ActionFlags flags = {})
0091     {
0092         if (m_documents.contains(doc))
0093             return false;
0094 
0095         m_documents.insert(doc);
0096         doUpdate(flags);
0097         return true;
0098     }
0099 
0100     bool delDocument(const IndexedString& doc, ActionFlags flags = {})
0101     {
0102         const auto documentIt = m_documents.find(doc);
0103         if (documentIt == m_documents.end())
0104             return false;
0105 
0106         m_documents.erase(documentIt);
0107         doUpdate(flags);
0108         return true;
0109     }
0110 
0111     void renameDocument(const IndexedString& previousUrl, const IndexedString& currentUrl, ActionFlags flags = {})
0112     {
0113         const bool deleted = delDocument(previousUrl);
0114         const bool added = addDocument(currentUrl);
0115         if (deleted || added) {
0116             doUpdate(flags);
0117         }
0118     }
0119 
0120     void updateImports()
0121     {
0122         if (!m_showImports) {
0123             if (!m_imports.isEmpty()) {
0124                 m_imports.clear();
0125                 return;
0126             }
0127             return;
0128         }
0129 
0130         getImportsFromDUChain();
0131     }
0132 
0133     void clear()
0134     {
0135         m_documents.clear();
0136         m_imports.clear();
0137         emit m_documentSet->changed();
0138     }
0139 
0140 private:
0141     void getImportsFromDU(TopDUContext* context, QSet<TopDUContext*>& visitedContexts)
0142     {
0143         if (!context || visitedContexts.contains(context))
0144             return;
0145 
0146         visitedContexts.insert(context);
0147         const auto importedParentContexts = context->importedParentContexts();
0148         for (const DUContext::Import& ctx : importedParentContexts) {
0149             auto* topCtx = dynamic_cast<TopDUContext*>(ctx.context(nullptr));
0150 
0151             if (topCtx)
0152                 getImportsFromDU(topCtx, visitedContexts);
0153         }
0154     }
0155 
0156     void getImportsFromDUChain()
0157     {
0158         KDevelop::DUChainReadLocker lock;
0159         QSet<TopDUContext*> visitedContexts;
0160 
0161         m_imports.clear();
0162         for (const IndexedString& doc : qAsConst(m_documents)) {
0163             TopDUContext* ctx = DUChain::self()->chainForDocument(doc);
0164             getImportsFromDU(ctx, visitedContexts);
0165             visitedContexts.remove(ctx);
0166         }
0167 
0168         for (TopDUContext* ctx : qAsConst(visitedContexts)) {
0169             m_imports.insert(ctx->url());
0170         }
0171     }
0172 
0173     void updateReady(const IndexedString& doc, const ReferencedTopDUContext&)
0174     {
0175         if (!m_showImports || !m_documents.contains(doc))
0176             return;
0177 
0178         DocumentSet oldImports = m_imports;
0179 
0180         updateImports();
0181         if (m_imports != oldImports)
0182             emit m_documentSet->changed();
0183     }
0184 
0185     WatchedDocumentSet* m_documentSet;
0186 
0187     DocumentSet m_documents;
0188     DocumentSet m_imports;
0189 
0190     bool m_showImports;
0191 };
0192 
0193 WatchedDocumentSet::WatchedDocumentSet(QObject* parent)
0194     : QObject(parent)
0195     , d_ptr(new WatchedDocumentSetPrivate(this))
0196 {
0197 }
0198 
0199 WatchedDocumentSet::~WatchedDocumentSet()
0200 {
0201 }
0202 
0203 bool WatchedDocumentSet::showImports() const
0204 {
0205     Q_D(const WatchedDocumentSet);
0206 
0207     return d->showImports();
0208 }
0209 
0210 void WatchedDocumentSet::setShowImports(bool showImports)
0211 {
0212     Q_D(WatchedDocumentSet);
0213 
0214     d->setShowImports(showImports);
0215 }
0216 
0217 void WatchedDocumentSet::setCurrentDocument(const IndexedString&)
0218 {
0219 }
0220 
0221 WatchedDocumentSet::DocumentSet WatchedDocumentSet::get() const
0222 {
0223     Q_D(const WatchedDocumentSet);
0224 
0225     return d->documents();
0226 }
0227 
0228 WatchedDocumentSet::DocumentSet WatchedDocumentSet::imports() const
0229 {
0230     Q_D(const WatchedDocumentSet);
0231 
0232     return d->imports();
0233 }
0234 
0235 CurrentDocumentSet::CurrentDocumentSet(const IndexedString& document, QObject* parent)
0236     : WatchedDocumentSet(parent)
0237 {
0238     Q_D(WatchedDocumentSet);
0239 
0240     d->setDocuments({document}, DoUpdate);
0241 }
0242 
0243 void CurrentDocumentSet::setCurrentDocument(const IndexedString& url)
0244 {
0245     Q_D(WatchedDocumentSet);
0246 
0247     d->setDocuments({url}, DoUpdate | DoEmit);
0248 }
0249 
0250 ProblemScope CurrentDocumentSet::scope() const
0251 {
0252     return CurrentDocument;
0253 }
0254 
0255 OpenDocumentSet::OpenDocumentSet(QObject* parent)
0256     : WatchedDocumentSet(parent)
0257 {
0258     Q_D(WatchedDocumentSet);
0259 
0260     const auto documents = ICore::self()->documentController()->openDocuments();
0261     for (IDocument* doc : documents) {
0262         d->addDocument(IndexedString(doc->url()));
0263     }
0264     d->updateImports();
0265 
0266     connect(ICore::self()->documentController(), &IDocumentController::documentClosed, this, &OpenDocumentSet::documentClosed);
0267     connect(ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this, &OpenDocumentSet::documentCreated);
0268     connect(ICore::self()->documentController(), &IDocumentController::documentUrlChanged, this,
0269             &OpenDocumentSet::documentUrlChanged);
0270 }
0271 
0272 void OpenDocumentSet::documentClosed(IDocument* doc)
0273 {
0274     Q_D(WatchedDocumentSet);
0275 
0276     d->delDocument(IndexedString(doc->url()), DoUpdate | DoEmit);
0277 }
0278 
0279 void OpenDocumentSet::documentCreated(IDocument* doc)
0280 {
0281     Q_D(WatchedDocumentSet);
0282 
0283     d->addDocument(IndexedString(doc->url()), DoUpdate | DoEmit);
0284 }
0285 
0286 void OpenDocumentSet::documentUrlChanged(IDocument* doc, const QUrl& previousUrl)
0287 {
0288     Q_D(WatchedDocumentSet);
0289 
0290     if (doc->textDocument()) {
0291         d->renameDocument(IndexedString{previousUrl}, IndexedString{doc->url()}, DoUpdate | DoEmit);
0292     }
0293 }
0294 
0295 ProblemScope OpenDocumentSet::scope() const
0296 {
0297     return OpenDocuments;
0298 }
0299 
0300 ProjectSet::ProjectSet(QObject* parent)
0301     : WatchedDocumentSet(parent)
0302 {
0303     // Multiple projects can share a project file manager, e.g. CMakeManager. When a project P that
0304     // is being opened uses an already tracked project file manager, fileAdded() is invoked for each file
0305     // that belongs to P. If P contains thousands of files with many problems, KDevelop UI freezes,
0306     // because the ProblemReporterModel model is reset on each document set change.
0307     // Suspend adding files while a project is being opened. Derived classes connect to the signal
0308     // IProjectController::projectOpened and add all of the newly opened project's files at once, if needed.
0309     const auto* const projectController = ICore::self()->projectController();
0310     connect(projectController, &IProjectController::projectAboutToBeOpened, this, [this] {
0311         m_pauseAddingFiles = true;
0312     });
0313     const auto projectOpeningOver = [this] {
0314         m_pauseAddingFiles = false;
0315     };
0316     connect(projectController, &IProjectController::projectOpened, this, projectOpeningOver);
0317     connect(projectController, &IProjectController::projectOpeningAborted, this, projectOpeningOver);
0318 }
0319 
0320 void ProjectSet::fileAdded(ProjectFileItem* file)
0321 {
0322     Q_D(WatchedDocumentSet);
0323 
0324     if (m_pauseAddingFiles) {
0325         return;
0326     }
0327 
0328     const auto path = IndexedString(file->indexedPath());
0329     if (include(path)) {
0330         d->addDocument(path, DoUpdate | DoEmit);
0331     }
0332 }
0333 
0334 void ProjectSet::fileRemoved(ProjectFileItem* file)
0335 {
0336     Q_D(WatchedDocumentSet);
0337 
0338     d->delDocument(IndexedString(file->indexedPath()), DoUpdate | DoEmit);
0339 }
0340 
0341 void ProjectSet::fileRenamed(const Path& oldFile, ProjectFileItem* newFile)
0342 {
0343     Q_D(WatchedDocumentSet);
0344 
0345     d->delDocument(IndexedString(oldFile.pathOrUrl()));
0346     fileAdded(newFile);
0347 }
0348 
0349 static const QObject* qobjectProjectFileManager(const IProjectFileManager* projectFileManager)
0350 {
0351     // The implementation should derive from QObject somehow
0352     return dynamic_cast<const QObject*>(projectFileManager);
0353 }
0354 
0355 void ProjectSet::trackProjectFiles(const IProjectFileManager* projectFileManager)
0356 {
0357     auto* const fileManager = qobjectProjectFileManager(projectFileManager);
0358     if (!fileManager) {
0359         return;
0360     }
0361     // can't use new signal/slot syntax here, IProjectFileManager is no a QObject
0362     connect(fileManager, SIGNAL(fileAdded(KDevelop::ProjectFileItem*)), this,
0363             SLOT(fileAdded(KDevelop::ProjectFileItem*)));
0364     connect(fileManager, SIGNAL(fileRemoved(KDevelop::ProjectFileItem*)), this,
0365             SLOT(fileRemoved(KDevelop::ProjectFileItem*)));
0366     connect(fileManager, SIGNAL(fileRenamed(KDevelop::Path, KDevelop::ProjectFileItem*)), this,
0367             SLOT(fileRenamed(KDevelop::Path, KDevelop::ProjectFileItem*)));
0368 }
0369 
0370 void ProjectSet::stopTrackingProjectFiles(const IProjectFileManager* projectFileManager)
0371 {
0372     auto* const fileManager = qobjectProjectFileManager(projectFileManager);
0373     if (!fileManager) {
0374         return;
0375     }
0376     fileManager->disconnect(this);
0377 }
0378 
0379 CurrentProjectSet::CurrentProjectSet(const IndexedString& document, QObject* parent)
0380     : ProjectSet(parent)
0381     , m_currentDocumentUrl(document.toUrl())
0382 {
0383     handleCurrentDocumentChange();
0384 
0385     // Opening or closing a project can change m_currentDocument's associated project.
0386     const auto* const projectController = ICore::self()->projectController();
0387     connect(projectController, &IProjectController::projectOpened, this,
0388             &CurrentProjectSet::handleCurrentDocumentChange);
0389     connect(projectController, &IProjectController::projectClosed, this,
0390             &CurrentProjectSet::handleCurrentDocumentChange);
0391 }
0392 
0393 void CurrentProjectSet::setCurrentDocument(const IndexedString& url)
0394 {
0395     m_currentDocumentUrl = url.toUrl();
0396     handleCurrentDocumentChange();
0397 }
0398 
0399 void CurrentProjectSet::handleCurrentDocumentChange()
0400 {
0401     Q_D(WatchedDocumentSet);
0402 
0403     const auto* const projectForUrl = ICore::self()->projectController()->findProjectForUrl(m_currentDocumentUrl);
0404     if (projectForUrl == m_currentProject) {
0405         return;
0406     }
0407 
0408     if (m_currentProject) {
0409         stopTrackingProjectFiles(m_currentProject->projectFileManager());
0410     }
0411     m_currentProject = projectForUrl;
0412 
0413     if (m_currentProject) {
0414         d->setDocuments(m_currentProject->fileSet(), DoUpdate | DoEmit);
0415         trackProjectFiles(m_currentProject->projectFileManager());
0416     } else {
0417         d->clear();
0418     }
0419 }
0420 
0421 bool CurrentProjectSet::include(const IndexedString& url) const
0422 {
0423     return m_currentProject && m_currentProject->fileSet().contains(url);
0424 }
0425 
0426 ProblemScope CurrentProjectSet::scope() const
0427 {
0428     return CurrentProject;
0429 }
0430 
0431 AllProjectSet::AllProjectSet(QObject* parent)
0432     : AllProjectSet(InitFlag::LoadOnInit, parent)
0433 {
0434 }
0435 
0436 AllProjectSet::AllProjectSet(InitFlag initFlag, QObject* parent)
0437     : ProjectSet(parent)
0438 {
0439     switch (initFlag) {
0440     case InitFlag::LoadOnInit:
0441         reload();
0442         break;
0443     case InitFlag::SkipLoadOnInit:
0444         break;
0445     }
0446 
0447     const auto* const projectController = ICore::self()->projectController();
0448     connect(projectController, &IProjectController::projectOpened, this, &AllProjectSet::projectOpened);
0449     // When a project is closed, simply reloading can be more efficient than removing the closed project's
0450     // files, especially if this was the only open project. Furthermore, a single file can belong to
0451     // multiple open projects. In this corner case removing the closed project's files is incorrect.
0452     connect(projectController, &IProjectController::projectClosed, this, &AllProjectSet::reload);
0453 }
0454 
0455 void AllProjectSet::reload()
0456 {
0457     Q_D(WatchedDocumentSet);
0458 
0459     d->clear();
0460 
0461     QSet<const IProjectFileManager*> openProjectFileManagers;
0462 
0463     const auto projects = ICore::self()->projectController()->projects();
0464     for (const IProject* project : projects) {
0465         addProjectFiles(*project);
0466         openProjectFileManagers.insert(project->projectFileManager());
0467     }
0468 
0469     if (m_trackedProjectFileManagers != openProjectFileManagers) {
0470         const auto removedFileManagers = m_trackedProjectFileManagers - openProjectFileManagers;
0471         for (auto* f : removedFileManagers) {
0472             stopTrackingProjectFiles(f);
0473         }
0474 
0475         const auto addedFileManagers = openProjectFileManagers - m_trackedProjectFileManagers;
0476         for (auto* f : addedFileManagers) {
0477             trackProjectFiles(f);
0478         }
0479 
0480         m_trackedProjectFileManagers = openProjectFileManagers;
0481     }
0482 
0483     d->updateImports();
0484     emit changed();
0485 }
0486 
0487 void AllProjectSet::projectOpened(const IProject* project)
0488 {
0489     Q_D(WatchedDocumentSet);
0490 
0491     addProjectFiles(*project);
0492 
0493     const auto* const projectFileManager = project->projectFileManager();
0494     if (!m_trackedProjectFileManagers.contains(projectFileManager)) {
0495         trackProjectFiles(projectFileManager);
0496         m_trackedProjectFileManagers.insert(projectFileManager);
0497     }
0498 
0499     d->updateImports();
0500     emit changed();
0501 }
0502 
0503 void AllProjectSet::addProjectFiles(const IProject& project)
0504 {
0505     Q_D(WatchedDocumentSet);
0506 
0507     const auto fileSet = project.fileSet();
0508     for (const IndexedString& document : fileSet) {
0509         if (include(document)) {
0510             d->addDocument(document);
0511         }
0512     }
0513 }
0514 
0515 ProblemScope AllProjectSet::scope() const
0516 {
0517     return AllProjects;
0518 }
0519 
0520 DocumentsInPathSet::DocumentsInPathSet(const QString& path, QObject* parent)
0521     : AllProjectSet(InitFlag::SkipLoadOnInit, parent)
0522     , m_path(path)
0523 {
0524     reload();
0525 }
0526 
0527 ProblemScope DocumentsInPathSet::scope() const
0528 {
0529     return DocumentsInPath;
0530 }
0531 
0532 void DocumentsInPathSet::setPath(const QString& path)
0533 {
0534     if (m_path == path) {
0535         return;
0536     }
0537 
0538     m_path = path;
0539     reload();
0540 }
0541 
0542 bool DocumentsInPathSet::include(const IndexedString& path) const
0543 {
0544     if (m_path.isEmpty()) {
0545         return true;
0546     }
0547 
0548     return path.str().contains(m_path, Qt::CaseInsensitive);
0549 }
0550 
0551 DocumentsInCurrentPathSet::DocumentsInCurrentPathSet(const IndexedString& document, QObject* parent)
0552     : DocumentsInPathSet(QFileInfo(document.str()).path(), parent)
0553 {
0554 }
0555 
0556 ProblemScope DocumentsInCurrentPathSet::scope() const
0557 {
0558     return DocumentsInCurrentPath;
0559 }
0560 
0561 void DocumentsInCurrentPathSet::setCurrentDocument(const IndexedString& document)
0562 {
0563     setPath(QFileInfo(document.str()).path());
0564 }
0565 
0566 BypassSet::BypassSet(QObject* parent)
0567     : WatchedDocumentSet(parent)
0568 {
0569 }
0570 
0571 ProblemScope BypassSet::scope() const
0572 {
0573     return BypassScopeFilter;
0574 }
0575 
0576 }
0577 
0578 #include "watcheddocumentset.moc"
0579 #include "moc_watcheddocumentset.cpp"