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"