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 "projectitemquickopen.h" 0008 #include "debug.h" 0009 0010 #include <language/duchain/topducontext.h> 0011 #include <language/duchain/duchain.h> 0012 #include <language/duchain/duchainlock.h> 0013 #include <language/duchain/declaration.h> 0014 #include <language/duchain/abstractfunctiondeclaration.h> 0015 #include <language/duchain/types/structuretype.h> 0016 #include <language/duchain/duchainutils.h> 0017 #include <language/duchain/codemodel.h> 0018 #include <language/interfaces/iquickopen.h> 0019 #include <language/interfaces/abbreviations.h> 0020 0021 #include <interfaces/iproject.h> 0022 #include <interfaces/iprojectcontroller.h> 0023 #include <interfaces/icore.h> 0024 0025 #include <project/projectmodel.h> 0026 0027 #include <KLocalizedString> 0028 0029 using namespace KDevelop; 0030 0031 namespace { 0032 struct SubstringCache 0033 { 0034 explicit SubstringCache(const QString& string = QString()) 0035 : substring(string) 0036 { 0037 } 0038 0039 inline int containedIn(const Identifier& id) const 0040 { 0041 int index = id.index(); 0042 QHash<int, int>::const_iterator it = cache.constFind(index); 0043 if (it != cache.constEnd()) { 0044 return *it; 0045 } 0046 0047 const QString idStr = id.identifier().str(); 0048 0049 int result = idStr.lastIndexOf(substring, -1, Qt::CaseInsensitive); 0050 if (result < 0 && !idStr.isEmpty() && !substring.isEmpty()) { 0051 // no match; try abbreviations 0052 result = matchesAbbreviation(idStr.midRef(0), substring) ? 0 : -1; 0053 } 0054 0055 //here we shift the values if the matched string is bigger than the substring, 0056 //so closer matches will appear first 0057 if (result >= 0) { 0058 result = result + (idStr.size() - substring.size()); 0059 } 0060 0061 cache[index] = result; 0062 0063 return result; 0064 } 0065 0066 QString substring; 0067 mutable QHash<int, int> cache; 0068 }; 0069 0070 struct ClosestMatchToText 0071 { 0072 explicit ClosestMatchToText(const QHash<int, int>& _cache) 0073 : cache(_cache) 0074 { 0075 } 0076 0077 /** @brief Calculates the distance to two pre-filtered match items 0078 * 0079 * @param a The CodeModelView witch represents the first item to be tested 0080 * @param b The CodeModelView witch represents the second item to be tested 0081 * 0082 * @b 0083 */ 0084 inline bool operator()(const CodeModelViewItem& a, const CodeModelViewItem& b) const 0085 { 0086 const int height_a = cache.value(a.m_id.index(), -1); 0087 const int height_b = cache.value(b.m_id.index(), -1); 0088 0089 Q_ASSERT(height_a != -1); 0090 Q_ASSERT(height_b != -1); 0091 0092 if (height_a == height_b) { 0093 // stable sorting for equal items based on index 0094 // TODO: fast string-based sorting in such cases? 0095 return a.m_id.index() < b.m_id.index(); 0096 } 0097 0098 return height_a < height_b; 0099 } 0100 private: 0101 const QHash<int, int>& cache; 0102 }; 0103 0104 Path findProjectForForPath(const IndexedString& path) 0105 { 0106 const auto model = ICore::self()->projectController()->projectModel(); 0107 const auto item = model->itemForPath(path); 0108 return item ? item->project()->path() : Path(); 0109 } 0110 uint addedItems(const AddedItems& items) 0111 { 0112 uint add = 0; 0113 for (auto& item : items) { 0114 add += item.count(); 0115 } 0116 return add; 0117 } 0118 0119 } 0120 0121 ProjectItemDataProvider::ProjectItemDataProvider(KDevelop::IQuickOpen* quickopen) 0122 : m_itemTypes(NoItems) 0123 , m_quickopen(quickopen) 0124 , m_addedItemsCountCache([this]() { return addedItems(m_addedItems); }) 0125 { 0126 } 0127 0128 void ProjectItemDataProvider::setFilterText(const QString& text) 0129 { 0130 m_addedItems.clear(); 0131 m_addedItemsCountCache.markDirty(); 0132 0133 QStringList search(text.split(QStringLiteral("::"), Qt::SkipEmptyParts)); 0134 for (auto& s : search) { 0135 if (s.endsWith(QLatin1Char(':'))) { //Don't get confused while the :: is being typed 0136 s.chop(1); 0137 } 0138 } 0139 0140 if (!search.isEmpty() && search.back().endsWith(QLatin1Char('('))) { 0141 search.back().chop(1); 0142 } 0143 0144 if (text.isEmpty() || search.isEmpty()) { 0145 m_filteredItems = m_currentItems; 0146 return; 0147 } 0148 0149 KDevVarLengthArray<SubstringCache, 5> cache; 0150 for (const QString& searchPart : qAsConst(search)) { 0151 cache.append(SubstringCache(searchPart)); 0152 } 0153 0154 if (!text.startsWith(m_currentFilter)) { 0155 m_filteredItems = m_currentItems; 0156 } 0157 0158 m_currentFilter = text; 0159 0160 const QVector<CodeModelViewItem> oldFiltered = m_filteredItems; 0161 QHash<int, int> heights; 0162 0163 m_filteredItems.clear(); 0164 0165 for (const CodeModelViewItem& item : oldFiltered) { 0166 const QualifiedIdentifier& currentId = item.m_id; 0167 0168 int last_pos = currentId.count() - 1; 0169 int current_height = 0; 0170 int distance = 0; 0171 0172 //iter over each search item from last to first 0173 //this makes easier to calculate the distance based on where we hit the result or nothing 0174 //Iterating from the last item to the first is more efficient, as we want to match the 0175 //class/function name, which is the last item on the search fields and on the identifier. 0176 for (int b = search.count() - 1; b >= 0; --b) { 0177 //iter over each id for the current identifier, from last to first 0178 for (; last_pos >= 0; --last_pos, distance++) { 0179 // the more distant we are from the class definition, the less priority it will have 0180 current_height += distance * 10000; 0181 int result; 0182 //if the current search item is contained on the current identifier 0183 if ((result = cache[b].containedIn(currentId.at(last_pos))) >= 0) { 0184 //when we find a hit, whe add the distance to the searched word. 0185 //so the closest item will be displayed first 0186 current_height += result; 0187 0188 if (b == 0) { 0189 heights[currentId.index()] = current_height; 0190 m_filteredItems << item; 0191 } 0192 break; 0193 } 0194 } 0195 } 0196 } 0197 0198 //then, for the last part, we use the already built cache to sort the items according with their distance 0199 std::sort(m_filteredItems.begin(), m_filteredItems.end(), ClosestMatchToText(heights)); 0200 } 0201 0202 0203 KDevelop::QuickOpenDataPointer ProjectItemDataProvider::data(uint pos) const 0204 { 0205 //Check whether this position falls into an appended item-list, else apply the offset 0206 uint filteredItemOffset = 0; 0207 for (AddedItems::const_iterator it = m_addedItems.constBegin(); it != m_addedItems.constEnd(); ++it) { 0208 int offsetInAppended = pos - (it.key() + 1); 0209 if (offsetInAppended >= 0 && offsetInAppended < it.value().count()) { 0210 return it.value()[offsetInAppended]; 0211 } 0212 if (it.key() >= pos) { 0213 break; 0214 } 0215 filteredItemOffset += it.value().count(); 0216 } 0217 0218 const uint a = pos - filteredItemOffset; 0219 if (a > ( uint )m_filteredItems.size()) { 0220 return KDevelop::QuickOpenDataPointer(); 0221 } 0222 0223 const auto& filteredItem = m_filteredItems[a]; 0224 0225 QList<KDevelop::QuickOpenDataPointer> ret; 0226 KDevelop::DUChainReadLocker lock(DUChain::lock()); 0227 TopDUContext* ctx = DUChainUtils::standardContextForUrl(filteredItem.m_file.toUrl()); 0228 if (ctx) { 0229 QList<Declaration*> decls = ctx->findDeclarations(filteredItem.m_id, CursorInRevision::invalid(), AbstractType::Ptr(), nullptr, DUContext::DirectQualifiedLookup); 0230 0231 //Filter out forward-declarations or duplicate imported declarations 0232 const auto unfilteredDecls = decls; 0233 for (Declaration* decl : unfilteredDecls) { 0234 bool filter = false; 0235 if (decls.size() > 1 && decl->isForwardDeclaration()) { 0236 filter = true; 0237 } else if (decl->url() != filteredItem.m_file && m_files.contains(decl->url())) { 0238 filter = true; 0239 } 0240 if (filter) { 0241 decls.removeOne(decl); 0242 } 0243 } 0244 0245 ret.reserve(ret.size() + decls.size()); 0246 for (Declaration* decl : qAsConst(decls)) { 0247 DUChainItem item; 0248 item.m_item = decl; 0249 item.m_text = decl->qualifiedIdentifier().toString(); 0250 item.m_projectPath = findProjectForForPath(filteredItem.m_file); 0251 ret << QuickOpenDataPointer(new DUChainItemData(item)); 0252 } 0253 0254 if (decls.isEmpty()) { 0255 DUChainItem item; 0256 item.m_text = filteredItem.m_id.toString(); 0257 item.m_projectPath = findProjectForForPath(filteredItem.m_file); 0258 ret << QuickOpenDataPointer(new DUChainItemData(item)); 0259 } 0260 } else { 0261 qCDebug(PLUGIN_QUICKOPEN) << "Could not find standard-context"; 0262 } 0263 0264 if (!ret.isEmpty()) { 0265 QList<KDevelop::QuickOpenDataPointer> append = ret.mid(1); 0266 if (!append.isEmpty()) { 0267 m_addedItemsCountCache.markDirty(); 0268 0269 AddedItems addMap; 0270 for (AddedItems::iterator it = m_addedItems.begin(); it != m_addedItems.end(); ) { 0271 if (it.key() == pos) { //There already is appended data stored, nothing to do 0272 return ret.first(); 0273 } else if (it.key() > pos) { 0274 addMap[it.key() + append.count()] = it.value(); 0275 it = m_addedItems.erase(it); 0276 } else { 0277 ++it; 0278 } 0279 } 0280 0281 m_addedItems.insert(pos, append); 0282 0283 for (AddedItems::const_iterator it = addMap.constBegin(); it != addMap.constEnd(); ++it) { 0284 m_addedItems.insert(it.key(), it.value()); 0285 } 0286 } 0287 return ret.first(); 0288 } else { 0289 return KDevelop::QuickOpenDataPointer(); 0290 } 0291 } 0292 0293 void ProjectItemDataProvider::reset() 0294 { 0295 m_files = m_quickopen->fileSet(); 0296 m_currentItems.clear(); 0297 m_addedItems.clear(); 0298 m_addedItemsCountCache.markDirty(); 0299 0300 KDevelop::DUChainReadLocker lock(DUChain::lock()); 0301 for (const IndexedString& u : qAsConst(m_files)) { 0302 uint count; 0303 const KDevelop::CodeModelItem* items; 0304 CodeModel::self().items(u, count, items); 0305 0306 for (uint a = 0; a < count; ++a) { 0307 if (!items[a].id.isValid() || items[a].kind & CodeModelItem::ForwardDeclaration) { 0308 continue; 0309 } 0310 if (((m_itemTypes & Classes) && (items[a].kind & CodeModelItem::Class)) || 0311 ((m_itemTypes & Functions) && (items[a].kind & CodeModelItem::Function))) { 0312 QualifiedIdentifier id = items[a].id.identifier(); 0313 0314 if (id.isEmpty() || id.at(0).identifier().isEmpty()) { 0315 // id.isEmpty() not always hit when .toString() is actually empty... 0316 // anyhow, this makes sure that we don't show duchain items without 0317 // any name that could be searched for. This happens e.g. in the c++ 0318 // plugin for anonymous structs or sometimes for declarations in macro 0319 // expressions 0320 continue; 0321 } 0322 m_currentItems << CodeModelViewItem(u, id); 0323 } 0324 } 0325 } 0326 0327 m_filteredItems = m_currentItems; 0328 m_currentFilter.clear(); 0329 } 0330 0331 0332 uint ProjectItemDataProvider::itemCount() const 0333 { 0334 return m_filteredItems.count() + m_addedItemsCountCache.cachedResult(); 0335 } 0336 0337 uint ProjectItemDataProvider::unfilteredItemCount() const 0338 { 0339 return m_currentItems.count() + m_addedItemsCountCache.cachedResult(); 0340 } 0341 0342 QStringList ProjectItemDataProvider::supportedItemTypes() 0343 { 0344 const QStringList ret{ 0345 i18nc("@item quick open item type", "Classes"), 0346 i18nc("@item quick open item type", "Functions"), 0347 }; 0348 return ret; 0349 } 0350 0351 void ProjectItemDataProvider::enableData(const QStringList& items, const QStringList& scopes) 0352 { 0353 //FIXME: property support different scopes 0354 if (scopes.contains(i18nc("@item quick open scope", "Project"))) { 0355 m_itemTypes = NoItems; 0356 if (items.contains(i18nc("@item quick open item type", "Classes"))) { 0357 m_itemTypes = ( ItemTypes )(m_itemTypes | Classes); 0358 } 0359 if (items.contains(i18nc("@item quick open item type", "Functions"))) { 0360 m_itemTypes = ( ItemTypes )(m_itemTypes | Functions); 0361 } 0362 } else { 0363 m_itemTypes = NoItems; 0364 } 0365 } 0366 0367 #include "moc_projectitemquickopen.cpp"