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"