File indexing completed on 2024-05-12 04:38:08

0001 /*
0002     SPDX-FileCopyrightText: 2007 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #ifndef KDEVPLATFORM_QUICKOPEN_FILTER_H
0008 #define KDEVPLATFORM_QUICKOPEN_FILTER_H
0009 
0010 #include <QStringList>
0011 
0012 #include "abbreviations.h"
0013 
0014 #include <util/path.h>
0015 
0016 namespace KDevelop {
0017 /**
0018  * This is a simple filter-implementation that helps you implementing own quickopen data-providers.
0019  * You should use it when possible, because that way additional features(like regexp filtering) can
0020  * be implemented in a central place.
0021  *
0022  * This implementation does incremental filtering while
0023  * typing text, so it quite efficient for the most common case.
0024  *
0025  * The simplest way of using this is by reimplementing your data-provider
0026  * based on QuickOpenDataProviderBase and KDevelop::Filter\<Item\>.
0027  *
0028  * What you need to do to use it:
0029  *
0030  * Reimplement itemText(..) to provide the text filtering
0031  * should be performed on (This must be efficient).
0032  *
0033  * Call setItems(..) when starting a new quickopen session, or when the content
0034  * changes, to initialize the filter with your data.
0035  *
0036  * Call setFilter(..) with the text that should be filtered for on user-input.
0037  *
0038  * Use filteredItems() to provide data to quickopen.
0039  *
0040  * @tparam Item should be the type that holds all the information you need.
0041  * The filter will hold the data, and you can access it through "items()".
0042  */
0043 template <class Item>
0044 class Filter
0045 {
0046 public:
0047     virtual ~Filter()
0048     {
0049     }
0050     ///Clears the filter, but not the data.
0051     void clearFilter()
0052     {
0053         m_filtered = m_items;
0054         m_oldFilterText.clear();
0055     }
0056 
0057     ///Clears the filter and sets new data. The filter-text will be lost.
0058     void setItems(const QVector<Item>& data)
0059     {
0060         m_items = data;
0061         clearFilter();
0062     }
0063 
0064     const QVector<Item>& items() const
0065     {
0066         return m_items;
0067     }
0068 
0069     ///Returns the data that is left after the filtering
0070     const QVector<Item>& filteredItems() const
0071     {
0072         return m_filtered;
0073     }
0074 
0075     ///Changes the filter-text and refilters the data
0076     void setFilter(const QString& text)
0077     {
0078         if (m_oldFilterText == text) {
0079             return;
0080         }
0081         if (text.isEmpty()) {
0082             clearFilter();
0083             return;
0084         }
0085 
0086         const QVector<Item> filterBase = text.startsWith(m_oldFilterText) ?
0087                                          m_filtered :
0088                                          m_items; //Start filtering based on the whole data
0089 
0090         m_filtered.clear();
0091 
0092         QStringList typedFragments = text.split(QStringLiteral("::"), Qt::SkipEmptyParts);
0093         if (typedFragments.isEmpty()) {
0094             clearFilter();
0095             return;
0096         }
0097         if (typedFragments.last().endsWith(QLatin1Char(':'))) {
0098             // remove the trailing colon if there's only one; otherwise,
0099             // this breaks incremental filtering
0100             typedFragments.last().chop(1);
0101         }
0102         if (typedFragments.size() == 1 && typedFragments.last().isEmpty()) {
0103             clearFilter();
0104             return;
0105         }
0106         for (const Item& data : filterBase) {
0107             const QString& itemData = itemText(data);
0108             if (itemData.contains(text, Qt::CaseInsensitive) || matchesAbbreviationMulti(itemData, typedFragments)) {
0109                 m_filtered << data;
0110             }
0111         }
0112 
0113         m_oldFilterText = text;
0114     }
0115 
0116 protected:
0117     ///Should return the text an item should be filtered by.
0118     virtual QString itemText(const Item& data) const = 0;
0119 
0120 private:
0121     QString m_oldFilterText;
0122     QVector<Item> m_filtered;
0123     QVector<Item> m_items;
0124 };
0125 
0126 template <class Item, class Parent>
0127 class PathFilter
0128 {
0129 public:
0130     /**
0131      * Clears the filter and sets new data. The filter-text will be lost.
0132      *
0133      * The complexity of the callback is necessary to avoid redundant possibly
0134      * huge memory allocations, redundant construction and destruction of QVector
0135      * elements, which can dominate the time spent in this function.
0136      *
0137      * @param callback a function that actually sets new data. The callback should
0138      * take a single QVector<Item>& argument, modify it without discarding its
0139      * capacity, and without redundant element destruction and construction. The
0140      * callback should not assign some other QVector to its argument because of
0141      * the immediate costly destruction of the existing elements of m_items, then
0142      * a very likely allocation and element construction when the other QVector is
0143      * modified or during a future call to updateItems(). An exception from the
0144      * above performance guideline: the callback may assign a temporary detached
0145      * QVector, whose data is about to be destroyed, to its argument.
0146      */
0147     template<typename UpdateCallback>
0148     void updateItems(UpdateCallback callback)
0149     {
0150         // "Detach" m_filtered from m_items to avoid an allocation and element
0151         // construction inside the callback; element destruction and deallocation
0152         // in clearFilter() where m_items is assigned to m_filtered.
0153         m_filtered = {};
0154         callback(m_items);
0155         clearFilter();
0156     }
0157 
0158     const QVector<Item>& items() const
0159     {
0160         return m_items;
0161     }
0162 
0163     ///Returns the data that is left after the filtering
0164     const QVector<Item>& filteredItems() const
0165     {
0166         return m_filtered;
0167     }
0168 
0169     ///Changes the filter-text and refilters the data
0170     void setFilter(const QStringList& text)
0171     {
0172         if (m_oldFilterText == text) {
0173             return;
0174         }
0175         if (text.isEmpty()) {
0176             clearFilter();
0177             return;
0178         }
0179 
0180         QVector<Item> filterBase = m_filtered;
0181 
0182         if (m_oldFilterText.isEmpty()) {
0183             filterBase = m_items;
0184             // m_filtered either hasn't been modified after construction or is shared with m_items after a call
0185             // to clearFilter(). Assign a default-initialized value to m_filtered in order to avoid detaching
0186             // it and copying its items to a new buffer when m_filtered is resized below. This copying would be
0187             // a waste of CPU time, because new values are immediately assigned to all elements of m_filtered.
0188             m_filtered = {};
0189         } else if (m_oldFilterText.mid(0, m_oldFilterText.count() - 1) == text.mid(0, text.count() - 1)
0190                    && text.last().startsWith(m_oldFilterText.last())) {
0191             //Good, the prefix is the same, and the last item has been extended
0192         } else if (m_oldFilterText.size() == text.size() - 1 && m_oldFilterText == text.mid(0, text.size() - 1)) {
0193             //Good, an item has been added
0194         } else {
0195             //Start filtering based on the whole data, there was a big change to the filter
0196             filterBase = m_items;
0197         }
0198 
0199         QVector<QPair<int, int>> matches;
0200         for (int i = 0, c = filterBase.size(); i < c; ++i) {
0201             const auto& data = filterBase.at(i);
0202             const auto matchQuality = matchPathFilter(static_cast<Parent*>(this)->itemPath(data), text,
0203                                                       static_cast<Parent*>(this)->itemPrefixPath(data));
0204             if (matchQuality == -1) {
0205                 continue;
0206             }
0207             matches.push_back({matchQuality, i});
0208         }
0209 
0210         std::stable_sort(matches.begin(), matches.end(),
0211                          [](const QPair<int, int>& lhs, const QPair<int, int>& rhs)
0212             {
0213                 return lhs.first < rhs.first;
0214             });
0215         m_filtered.resize(matches.size());
0216         std::transform(matches.begin(), matches.end(), m_filtered.begin(),
0217                        [&filterBase](const QPair<int, int>& match) {
0218                 return filterBase.at(match.second);
0219             });
0220         m_oldFilterText = text;
0221     }
0222 
0223 private:
0224     ///Clears the filter, but not the data.
0225     void clearFilter()
0226     {
0227         m_filtered = m_items;
0228         m_oldFilterText.clear();
0229     }
0230 
0231     QStringList m_oldFilterText;
0232     QVector<Item> m_filtered;
0233     QVector<Item> m_items;
0234 };
0235 }
0236 
0237 #endif