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

0001 /*
0002     SPDX-FileCopyrightText: 2007 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "projectfilequickopen.h"
0008 
0009 #include <QIcon>
0010 #include <QTextBrowser>
0011 
0012 #include <KLocalizedString>
0013 
0014 #include <interfaces/iprojectcontroller.h>
0015 #include <interfaces/idocumentcontroller.h>
0016 #include <interfaces/iproject.h>
0017 #include <interfaces/icore.h>
0018 
0019 #include <language/duchain/topducontext.h>
0020 #include <language/duchain/duchain.h>
0021 #include <language/duchain/duchainlock.h>
0022 #include <serialization/indexedstring.h>
0023 #include <language/duchain/parsingenvironment.h>
0024 #include <util/algorithm.h>
0025 #include <util/texteditorhelpers.h>
0026 
0027 #include <project/projectmodel.h>
0028 #include <project/projectutils.h>
0029 
0030 #include "../openwith/iopenwith.h"
0031 
0032 #include <timsort/timsort.hpp>
0033 
0034 #include <algorithm>
0035 #include <iterator>
0036 #include <utility>
0037 #include <vector>
0038 
0039 using namespace KDevelop;
0040 
0041 namespace {
0042 QSet<IndexedString> openFiles()
0043 {
0044     QSet<IndexedString> openFiles;
0045     const QList<IDocument*>& docs = ICore::self()->documentController()->openDocuments();
0046     openFiles.reserve(docs.size());
0047     for (IDocument* doc : docs) {
0048         openFiles << IndexedString(doc->url());
0049     }
0050 
0051     return openFiles;
0052 }
0053 
0054 QString iconNameForUrl(const IndexedString& url)
0055 {
0056     if (url.isEmpty()) {
0057         return QStringLiteral("tab-duplicate");
0058     }
0059     ProjectBaseItem* item = ICore::self()->projectController()->projectModel()->itemForPath(url);
0060     if (item) {
0061         return item->iconName();
0062     }
0063     return QStringLiteral("unknown");
0064 }
0065 }
0066 
0067 ProjectFile::ProjectFile(const ProjectFileItem* fileItem)
0068     : path{fileItem->path()}
0069     , projectPath{fileItem->project()->path()}
0070     , indexedPath{fileItem->indexedPath()}
0071     , outsideOfProject{!projectPath.isParentOf(path)}
0072 {
0073 }
0074 
0075 ProjectFileData::ProjectFileData(const ProjectFile& file)
0076     : m_file(file)
0077 {
0078 }
0079 
0080 QString ProjectFileData::text() const
0081 {
0082     return m_file.projectPath.relativePath(m_file.path);
0083 }
0084 
0085 QString ProjectFileData::htmlDescription() const
0086 {
0087     return
0088         QLatin1String("<small><small>") +
0089         i18nc("%1: project name", "Project %1", project()) +
0090         QLatin1String("</small></small>");
0091 }
0092 
0093 bool ProjectFileData::execute(QString& filterText)
0094 {
0095     const QUrl url = m_file.path.toUrl();
0096     IOpenWith::openFiles(QList<QUrl>() << url);
0097 
0098     auto cursor = KTextEditorHelpers::extractCursor(filterText);
0099     if (cursor.isValid()) {
0100         IDocument* doc = ICore::self()->documentController()->documentForUrl(url);
0101         if (doc) {
0102             doc->setCursorPosition(cursor);
0103         }
0104     }
0105     return true;
0106 }
0107 
0108 bool ProjectFileData::isExpandable() const
0109 {
0110     return true;
0111 }
0112 
0113 QList<QVariant> ProjectFileData::highlighting() const
0114 {
0115     QTextCharFormat boldFormat;
0116     boldFormat.setFontWeight(QFont::Bold);
0117     QTextCharFormat normalFormat;
0118 
0119     QString txt = text();
0120 
0121     int fileNameLength = m_file.path.lastPathSegment().length();
0122 
0123     const QList<QVariant> ret{
0124         0,
0125         txt.length() - fileNameLength,
0126         QVariant(normalFormat),
0127         txt.length() - fileNameLength,
0128         fileNameLength,
0129         QVariant(boldFormat),
0130     };
0131     return ret;
0132 }
0133 
0134 QWidget* ProjectFileData::expandingWidget() const
0135 {
0136     const QUrl url = m_file.path.toUrl();
0137     DUChainReadLocker lock;
0138 
0139     ///Find a du-chain for the document
0140     const QList<TopDUContext*> contexts = DUChain::self()->chainsForDocument(url);
0141 
0142     ///Pick a non-proxy context
0143     TopDUContext* chosen = nullptr;
0144     for (TopDUContext* ctx : contexts) {
0145         if (!(ctx->parsingEnvironmentFile() && ctx->parsingEnvironmentFile()->isProxyContext())) {
0146             chosen = ctx;
0147         }
0148     }
0149 
0150     if (chosen) {
0151         // TODO: show project name, by introducing a generic wrapper widget that supports QuickOpenEmbeddedWidgetInterface
0152         return chosen->createNavigationWidget();
0153     } else {
0154         auto* ret = new QTextBrowser();
0155         ret->resize(400, 100);
0156         ret->setText(
0157             QLatin1String("<small><small>")
0158             + i18nc("%1: project name", "Project %1", project())
0159             + QLatin1String("<br>") + i18n("Not parsed yet")
0160             + QLatin1String("</small></small>"));
0161         return ret;
0162     }
0163 
0164     return nullptr;
0165 }
0166 
0167 QIcon ProjectFileData::icon() const
0168 {
0169     return QIcon::fromTheme(iconNameForUrl(m_file.indexedPath));
0170 }
0171 
0172 QString ProjectFileData::project() const
0173 {
0174     const IProject* project = ICore::self()->projectController()->findProjectForUrl(m_file.path.toUrl());
0175     if (project) {
0176         return project->name();
0177     } else {
0178         return i18nc("@item no project", "none");
0179     }
0180 }
0181 
0182 Path ProjectFileData::projectPath() const
0183 {
0184     return m_file.projectPath;
0185 }
0186 
0187 BaseFileDataProvider::BaseFileDataProvider()
0188 {
0189 }
0190 
0191 void BaseFileDataProvider::setFilterText(const QString& text)
0192 {
0193     int pathLength;
0194     KTextEditorHelpers::extractCursor(text, &pathLength);
0195     QString path(text.mid(0, pathLength));
0196     if (path.startsWith(QLatin1String("./")) || path.startsWith(QLatin1String("../"))) {
0197         // assume we want to filter relative to active document's url
0198         IDocument* doc = ICore::self()->documentController()->activeDocument();
0199         if (doc) {
0200             path = Path(Path(doc->url()).parent(), path).pathOrUrl();
0201         }
0202     }
0203     setFilter(path.split(QLatin1Char('/'), Qt::SkipEmptyParts));
0204 }
0205 
0206 uint BaseFileDataProvider::itemCount() const
0207 {
0208     return filteredItems().count();
0209 }
0210 
0211 uint BaseFileDataProvider::unfilteredItemCount() const
0212 {
0213     return items().count();
0214 }
0215 
0216 QuickOpenDataPointer BaseFileDataProvider::data(uint row) const
0217 {
0218     return QuickOpenDataPointer(new ProjectFileData(filteredItems().at(row)));
0219 }
0220 
0221 ProjectFileDataProvider::ProjectFileDataProvider()
0222 {
0223     auto projectController = ICore::self()->projectController();
0224     connect(projectController, &IProjectController::projectClosing,
0225             this, &ProjectFileDataProvider::projectClosing);
0226     connect(projectController, &IProjectController::projectOpened,
0227             this, &ProjectFileDataProvider::projectOpened);
0228     const auto projects = projectController->projects();
0229     for (auto* project : projects) {
0230         projectOpened(project);
0231     }
0232 }
0233 
0234 void ProjectFileDataProvider::projectClosing(IProject* project)
0235 {
0236     // Once we remove all project's files from set, there is no need to listen
0237     // to &IProject::fileRemovedFromSet signal from this project and waste time
0238     // searching in m_projectFiles. No need to listen to &IProject::fileAddedToSet
0239     // signal from this project either - we are not interested in hypothetical
0240     // file additions to the project that is about to be closed and destroyed.
0241     disconnect(project, nullptr, this, nullptr);
0242 
0243     if (ICore::self()->projectController()->projectCount() == 0) {
0244         // No open projects left => just remove all files. This is a little faster
0245         // than the algorithm below. Releasing the memory here would slow down the
0246         // next call to projectOpened() => keep the capacity of m_projectFiles.
0247         m_projectFiles.clear();
0248         return;
0249     }
0250 
0251     const Path projectPath = project->path();
0252     const auto logicalEnd = std::remove_if(m_projectFiles.begin(), m_projectFiles.end(),
0253                                            [&projectPath](const ProjectFile& f) {
0254                                                return f.projectPath == projectPath;
0255                                            });
0256     m_projectFiles.erase(logicalEnd, m_projectFiles.end());
0257 }
0258 
0259 void ProjectFileDataProvider::projectOpened(IProject* project)
0260 {
0261     connect(project, &IProject::fileAddedToSet,
0262             this, &ProjectFileDataProvider::fileAddedToSet);
0263     connect(project, &IProject::fileRemovedFromSet,
0264             this, &ProjectFileDataProvider::fileRemovedFromSet);
0265 
0266     // Collect the opened project's files.
0267     const auto oldSize = m_projectFiles.size();
0268     KDevelop::forEachFile(project->projectItem(), [this](ProjectFileItem* fileItem) {
0269         m_projectFiles.push_back(ProjectFile{fileItem});
0270     });
0271     const auto justAddedBegin = m_projectFiles.begin() + oldSize;
0272 
0273     // Sort the opened project's files.
0274     // Sorting stability is not useful here, but timsort vastly outperforms all
0275     // std and boost sorting algorithms (boost::sort::flat_stable_sort is the
0276     // second best) on the files of large real-life projects, because
0277     // KDevelop::forEachFile() collects files in an almost sorted order.
0278     gfx::timsort(justAddedBegin, m_projectFiles.end());
0279 
0280     // Merge the sorted ranges of files belonging to previously opened projects
0281     // and to the just opened project.
0282     // Since the file sets from different projects usually don't overlap or overlap
0283     // very little, timmerge is the perfect merge algorithm. Furthermore, the
0284     // comparison of ProjectFile objects is expensive and cache-unfriendly. This
0285     // aspect lets timmerge outperform std::inplace_merge even more here. This same
0286     // aspect also helps timsort outperform other sorting algorithms.
0287     gfx::timmerge(m_projectFiles.begin(), justAddedBegin, m_projectFiles.end());
0288 
0289     // Remove duplicates across all open projects. Usually different projects have no
0290     // common files. But since a file can belong to multiple targets within one project,
0291     // a single call to KDevelop::forEachFile() often produces many duplicates.
0292     const auto equalFiles = [](const ProjectFile& a, const ProjectFile& b) {
0293         return a.indexedPath == b.indexedPath;
0294     };
0295     m_projectFiles.erase(std::unique(m_projectFiles.begin(), m_projectFiles.end(), equalFiles),
0296                          m_projectFiles.end());
0297 }
0298 
0299 void ProjectFileDataProvider::fileAddedToSet(ProjectFileItem* fileItem)
0300 {
0301     ProjectFile f(fileItem);
0302     auto it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), f);
0303     if (it == m_projectFiles.end() || it->indexedPath != f.indexedPath) {
0304         m_projectFiles.insert(it, std::move(f));
0305     }
0306 }
0307 
0308 void ProjectFileDataProvider::fileRemovedFromSet(ProjectFileItem* file)
0309 {
0310     ProjectFile item;
0311     item.path = file->path();
0312     item.indexedPath = file->indexedPath();
0313 
0314     // fast-path for non-generated files
0315     // NOTE: figuring out whether something is generated is expensive... and since
0316     // generated files are rare we apply this two-step algorithm here
0317     auto it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), item);
0318     if (it != m_projectFiles.end() && it->indexedPath == item.indexedPath) {
0319         m_projectFiles.erase(it);
0320         return;
0321     }
0322 
0323     // last try: maybe it was generated
0324     item.outsideOfProject = true;
0325     it = std::lower_bound(m_projectFiles.begin(), m_projectFiles.end(), item);
0326     if (it != m_projectFiles.end() && it->indexedPath == item.indexedPath) {
0327         m_projectFiles.erase(it);
0328         return;
0329     }
0330 }
0331 
0332 void ProjectFileDataProvider::reset()
0333 {
0334     updateItems([this](QVector<ProjectFile>& closedFiles) {
0335         const auto open = openFiles();
0336         // Don't "optimize" by assigning m_projectFiles to closedFiles and
0337         // returning early if there are no open files. Such an optimization may
0338         // speed up this call to reset() sometimes - if the destruction of the
0339         // previous data of closedFiles doesn't take long for some reason. But
0340         // this "optimization" will discard the current elements and capacity of
0341         // closedFiles, eventually will trigger an extra allocation and
0342         // construction of many ProjectFile objects: when a project is opened or
0343         // closed, when a file is added to/removed from a project, or when a
0344         // file is opened and reset() is called again. See also the
0345         // documentation of PathFilter::updateItems().
0346 
0347         closedFiles.resize(m_projectFiles.size());
0348         const auto logicalEnd = std::remove_copy_if(
0349                 m_projectFiles.cbegin(), m_projectFiles.cend(),
0350                 closedFiles.begin(), [&open](const ProjectFile& f) {
0351                                          return open.contains(f.indexedPath);
0352                                      });
0353         closedFiles.erase(logicalEnd, closedFiles.end());
0354     });
0355 }
0356 
0357 QSet<IndexedString> ProjectFileDataProvider::files() const
0358 {
0359     const auto projects = ICore::self()->projectController()->projects();
0360     if (projects.empty()) {
0361         return {}; // don't call openFiles() needlessly
0362     }
0363 
0364     std::vector<QSet<IndexedString>> sets;
0365     sets.reserve(projects.size());
0366     std::transform(projects.cbegin(), projects.cend(), std::back_inserter(sets),
0367                    [](const IProject* project) {
0368                        return project->fileSet();
0369                    });
0370 
0371     auto result = Algorithm::unite(std::move(sets));
0372     result.subtract(openFiles());
0373     return result;
0374 }
0375 
0376 void OpenFilesDataProvider::reset()
0377 {
0378     updateItems([](QVector<ProjectFile>& currentFiles) {
0379         const auto* const projCtrl = ICore::self()->projectController();
0380         const auto docs = ICore::self()->documentController()->openDocuments();
0381 
0382         currentFiles.resize(docs.size());
0383         std::transform(docs.cbegin(), docs.cend(), currentFiles.begin(),
0384                        [projCtrl](const IDocument* doc) {
0385                            ProjectFile f;
0386                            const QUrl docUrl = doc->url();
0387                            f.path = Path(docUrl);
0388                            if (const IProject* project = projCtrl->findProjectForUrl(docUrl)) {
0389                                f.projectPath = project->path();
0390                            }
0391                            return f;
0392                        });
0393         std::sort(currentFiles.begin(), currentFiles.end());
0394     });
0395 }
0396 
0397 QSet<IndexedString> OpenFilesDataProvider::files() const
0398 {
0399     return openFiles();
0400 }
0401 
0402 #include "moc_projectfilequickopen.cpp"