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 "quickopenmodel.h"
0008 #include "debug.h"
0009 
0010 #include <QTreeView>
0011 #include <QTimer>
0012 
0013 #include <KTextEditor/CodeCompletionModel>
0014 #include <typeinfo>
0015 #include <utility>
0016 #include <vector>
0017 
0018 #include <util/algorithm.h>
0019 
0020 #include "expandingtree/expandingtree.h"
0021 #include "projectfilequickopen.h"
0022 #include "duchainitemquickopen.h"
0023 
0024 #define QUICKOPEN_USE_ITEM_CACHING
0025 
0026 using namespace KDevelop;
0027 
0028 QuickOpenModel::QuickOpenModel(QWidget* parent) : ExpandingWidgetModel(parent)
0029     , m_treeView(nullptr)
0030     , m_expandingWidgetHeightIncrease(0)
0031     , m_resetBehindRow(0)
0032 {
0033     m_resetTimer = new QTimer(this);
0034     m_resetTimer->setSingleShot(true);
0035     m_resetTimer->setInterval(0);
0036     connect(m_resetTimer, &QTimer::timeout, this, &QuickOpenModel::resetTimer);
0037 }
0038 
0039 void QuickOpenModel::setExpandingWidgetHeightIncrease(int pixels)
0040 {
0041     m_expandingWidgetHeightIncrease = pixels;
0042 }
0043 
0044 QStringList QuickOpenModel::allScopes() const
0045 {
0046     QStringList scopes;
0047     for (const ProviderEntry& provider : m_providers) {
0048         for (const QString& scope : provider.scopes) {
0049             if (!scopes.contains(scope)) {
0050                 scopes << scope;
0051             }
0052         }
0053     }
0054 
0055     return scopes;
0056 }
0057 
0058 QStringList QuickOpenModel::allTypes() const
0059 {
0060     QSet<QString> types;
0061     for (const ProviderEntry& provider : m_providers) {
0062         types += provider.types;
0063     }
0064 
0065     return types.values();
0066 }
0067 
0068 void QuickOpenModel::registerProvider(const QStringList& scopes, const QStringList& types, KDevelop::QuickOpenDataProviderBase* provider)
0069 {
0070     ProviderEntry e;
0071     e.scopes = QSet<QString>(scopes.begin(), scopes.end());
0072     e.types = QSet<QString>(types.begin(), types.end());
0073     e.provider = provider;
0074 
0075     m_providers << e; //.insert( types, e );
0076 
0077     connect(provider, &QuickOpenDataProviderBase::destroyed, this, &QuickOpenModel::destroyed);
0078 
0079     restart(true);
0080 }
0081 
0082 bool QuickOpenModel::removeProvider(KDevelop::QuickOpenDataProviderBase* provider)
0083 {
0084     bool ret = false;
0085     for (ProviderList::iterator it = m_providers.begin(); it != m_providers.end(); ++it) {
0086         if ((*it).provider == provider) {
0087             m_providers.erase(it);
0088             disconnect(provider, &QuickOpenDataProviderBase::destroyed, this, &QuickOpenModel::destroyed);
0089             ret = true;
0090             break;
0091         }
0092     }
0093 
0094     restart(true);
0095 
0096     return ret;
0097 }
0098 
0099 void QuickOpenModel::enableProviders(const QStringList& _items, const QStringList& _scopes)
0100 {
0101     const QSet<QString> items(_items.begin(), _items.end());
0102     const QSet<QString> scopes(_scopes.begin(), _scopes.end());
0103     if (m_enabledItems == items && m_enabledScopes == scopes && !items.isEmpty() && !scopes.isEmpty()) {
0104         return;
0105     }
0106     m_enabledItems = items;
0107     m_enabledScopes  = scopes;
0108     qCDebug(PLUGIN_QUICKOPEN) << "params " << items << " " << scopes;
0109 
0110     //We use 2 iterations here: In the first iteration, all providers that implement QuickOpenFileSetInterface are initialized, then the other ones.
0111     //The reason is that the second group can refer to the first one.
0112     for (auto& provider : m_providers) {
0113         if (!qobject_cast<QuickOpenFileSetInterface*>(provider.provider)) {
0114             continue;
0115         }
0116         qCDebug(PLUGIN_QUICKOPEN) << "comparing" << provider.scopes << provider.types;
0117         if ((scopes.isEmpty() || !(scopes & provider.scopes).isEmpty()) && (!(items & provider.types).isEmpty() || items.isEmpty())) {
0118             qCDebug(PLUGIN_QUICKOPEN) << "enabling " << provider.types << " " << provider.scopes;
0119             provider.enabled = true;
0120             provider.provider->enableData(_items, _scopes);
0121         } else {
0122             qCDebug(PLUGIN_QUICKOPEN) << "disabling " << provider.types << " " << provider.scopes;
0123             provider.enabled = false;
0124             if ((scopes.isEmpty() || !(scopes & provider.scopes).isEmpty())) {
0125                 provider.provider->enableData(_items, _scopes); //The provider may still provide files
0126             }
0127         }
0128     }
0129 
0130     for (auto & provider : m_providers) {
0131         if (qobject_cast<QuickOpenFileSetInterface*>(provider.provider)) {
0132             continue;
0133         }
0134         qCDebug(PLUGIN_QUICKOPEN) << "comparing" << provider.scopes << provider.types;
0135         if ((scopes.isEmpty() || !(scopes & provider.scopes).isEmpty()) && (!(items & provider.types).isEmpty() || items.isEmpty())) {
0136             qCDebug(PLUGIN_QUICKOPEN) << "enabling " << provider.types << " " << provider.scopes;
0137             provider.enabled = true;
0138             provider.provider->enableData(_items, _scopes);
0139         } else {
0140             qCDebug(PLUGIN_QUICKOPEN) << "disabling " << provider.types << " " << provider.scopes;
0141             provider.enabled = false;
0142         }
0143     }
0144 
0145     restart(true);
0146 }
0147 
0148 void QuickOpenModel::textChanged(const QString& str)
0149 {
0150     if (m_filterText == str) {
0151         return;
0152     }
0153 
0154     beginResetModel();
0155 
0156     m_filterText = str;
0157     for (const ProviderEntry& provider : qAsConst(m_providers)) {
0158         if (provider.enabled) {
0159             provider.provider->setFilterText(str);
0160         }
0161     }
0162 
0163     m_cachedData.clear();
0164     clearExpanding();
0165 
0166     //Get the 50 first items, so the data-providers notice changes without ui-glitches due to resetting
0167     for (int a = 0; a < 50 && a < rowCount(QModelIndex()); ++a) {
0168         getItem(a, true);
0169     }
0170 
0171     endResetModel();
0172 }
0173 
0174 void QuickOpenModel::restart(bool keepFilterText)
0175 {
0176     // make sure we do not restart recursively which could lead to
0177     // recursive loading of provider plugins e.g. (happened for the cpp plugin)
0178     QMetaObject::invokeMethod(this, "restart_internal", Qt::QueuedConnection,
0179                               Q_ARG(bool, keepFilterText));
0180 }
0181 
0182 void QuickOpenModel::restart_internal(bool keepFilterText)
0183 {
0184     if (!keepFilterText) {
0185         m_filterText.clear();
0186     }
0187 
0188     bool anyEnabled = std::any_of(m_providers.constBegin(), m_providers.constEnd(), [](const ProviderEntry& e) {
0189         return e.enabled;
0190     });
0191 
0192     if (!anyEnabled) {
0193         return;
0194     }
0195 
0196     for (const ProviderEntry& provider : qAsConst(m_providers)) {
0197         if (!qobject_cast<QuickOpenFileSetInterface*>(provider.provider)) {
0198             continue;
0199         }
0200 
0201         ///Always reset providers that implement QuickOpenFileSetInterface and have some matching scopes, because they may be needed by other providers.
0202         if (m_enabledScopes.isEmpty() || !(m_enabledScopes & provider.scopes).isEmpty()) {
0203             provider.provider->reset();
0204         }
0205     }
0206 
0207     for (const ProviderEntry& provider : qAsConst(m_providers)) {
0208         if (qobject_cast<QuickOpenFileSetInterface*>(provider.provider)) {
0209             continue;
0210         }
0211 
0212         if (provider.enabled && provider.provider) {
0213             provider.provider->reset();
0214         }
0215     }
0216 
0217     if (keepFilterText) {
0218         textChanged(m_filterText);
0219     } else {
0220         beginResetModel();
0221         m_cachedData.clear();
0222         clearExpanding();
0223         endResetModel();
0224     }
0225 }
0226 
0227 void QuickOpenModel::destroyed(QObject* obj)
0228 {
0229     removeProvider(static_cast<KDevelop::QuickOpenDataProviderBase*>(obj));
0230 }
0231 
0232 QModelIndex QuickOpenModel::index(int row, int column, const QModelIndex& /*parent*/) const
0233 {
0234     if (column >= columnCount() || row >= rowCount(QModelIndex())) {
0235         return QModelIndex();
0236     }
0237     if (row < 0 || column < 0) {
0238         return QModelIndex();
0239     }
0240     return createIndex(row, column);
0241 }
0242 
0243 QModelIndex QuickOpenModel::parent(const QModelIndex&) const
0244 {
0245     return QModelIndex();
0246 }
0247 
0248 int QuickOpenModel::rowCount(const QModelIndex& i) const
0249 {
0250     if (i.isValid()) {
0251         return 0;
0252     }
0253 
0254     int count = 0;
0255     for (const ProviderEntry& provider : m_providers) {
0256         if (provider.enabled) {
0257             count += provider.provider->itemCount();
0258         }
0259     }
0260 
0261     return count;
0262 }
0263 
0264 int QuickOpenModel::unfilteredRowCount() const
0265 {
0266     int count = 0;
0267     for (const ProviderEntry& provider : m_providers) {
0268         if (provider.enabled) {
0269             count += provider.provider->unfilteredItemCount();
0270         }
0271     }
0272 
0273     return count;
0274 }
0275 
0276 int QuickOpenModel::columnCount() const
0277 {
0278     return 2;
0279 }
0280 
0281 int QuickOpenModel::columnCount(const QModelIndex& index) const
0282 {
0283     if (index.parent().isValid()) {
0284         return 0;
0285     } else {
0286         return columnCount();
0287     }
0288 }
0289 
0290 QVariant QuickOpenModel::data(const QModelIndex& index, int role) const
0291 {
0292     QuickOpenDataPointer d = getItem(index.row());
0293 
0294     if (!d) {
0295         return QVariant();
0296     }
0297 
0298     switch (role) {
0299     case KTextEditor::CodeCompletionModel::ItemSelected: {
0300         QString desc = d->htmlDescription();
0301         if (desc.isEmpty()) {
0302             return QVariant();
0303         } else {
0304             return desc;
0305         }
0306     }
0307 
0308     case KTextEditor::CodeCompletionModel::IsExpandable:
0309         return d->isExpandable();
0310     case KTextEditor::CodeCompletionModel::ExpandingWidget: {
0311         QVariant v;
0312         QWidget* w =  d->expandingWidget();
0313         if (w && m_expandingWidgetHeightIncrease) {
0314             w->resize(w->width(), w->height() + m_expandingWidgetHeightIncrease);
0315         }
0316 
0317         v.setValue<QWidget*>(w);
0318         return v;
0319     }
0320     case ExpandingTree::ProjectPathRole:
0321         // TODO: put this into the QuickOpenDataBase API
0322         //       we cannot do this in 5.0, cannot change ABI
0323         if (auto projectFile = dynamic_cast<const ProjectFileData*>(d.constData())) {
0324             return QVariant::fromValue(projectFile->projectPath());
0325         } else if (auto duchainItem = dynamic_cast<const DUChainItemData*>(d.constData())) {
0326             return QVariant::fromValue(duchainItem->projectPath());
0327         }
0328     }
0329 
0330     if (index.column() == 1) { //This column contains the actual content
0331         switch (role) {
0332         case Qt::DecorationRole:
0333             return d->icon();
0334 
0335         case Qt::DisplayRole:
0336             return d->text();
0337         case KTextEditor::CodeCompletionModel::HighlightingMethod:
0338             return KTextEditor::CodeCompletionModel::CustomHighlighting;
0339         case KTextEditor::CodeCompletionModel::CustomHighlight:
0340             return d->highlighting();
0341         }
0342     } else if (index.column() == 0) { //This column only contains the expanded/not expanded icon
0343         switch (role) {
0344         case Qt::DecorationRole:
0345         {
0346             if (isExpandable(index)) {
0347                 //Show the expanded/unexpanded handles
0348                 if (isExpanded(index)) {
0349                     return QIcon::fromTheme(QStringLiteral("arrow-down"));
0350                 } else {
0351                     return QIcon::fromTheme(QStringLiteral("arrow-right"));
0352                 }
0353             }
0354         }
0355         }
0356     }
0357 
0358     return ExpandingWidgetModel::data(index, role);
0359 }
0360 
0361 void QuickOpenModel::resetTimer()
0362 {
0363     int currentRow = treeView() ? mapToSource(treeView()->currentIndex()).row() : -1;
0364 
0365     beginResetModel();
0366     //Remove all cached data behind row m_resetBehindRow
0367     for (DataList::iterator it = m_cachedData.begin(); it != m_cachedData.end(); ) {
0368         if (it.key() > m_resetBehindRow) {
0369             it = m_cachedData.erase(it);
0370         } else {
0371             ++it;
0372         }
0373     }
0374 
0375     endResetModel();
0376 
0377     if (currentRow != -1) {
0378         treeView()->setCurrentIndex(mapFromSource(index(currentRow, 0, QModelIndex()))); //Preserve the current index
0379     }
0380     m_resetBehindRow = 0;
0381 }
0382 
0383 QuickOpenDataPointer QuickOpenModel::getItem(int row, bool noReset) const
0384 {
0385     ///@todo mix all the models alphabetically here. For now, they are simply ordered.
0386     ///@todo Deal with unexpected item-counts, like for example in the case of overloaded function-declarations
0387 
0388 #ifdef QUICKOPEN_USE_ITEM_CACHING
0389     const auto dataIt = m_cachedData.constFind(row);
0390     if (dataIt != m_cachedData.constEnd()) {
0391         return *dataIt;
0392     }
0393 #endif
0394     int rowOffset = 0;
0395 
0396     Q_ASSERT(row < rowCount(QModelIndex()));
0397 
0398     for (const ProviderEntry& provider : m_providers) {
0399         if (!provider.enabled) {
0400             continue;
0401         }
0402         uint itemCount = provider.provider->itemCount();
0403         if (( uint )row < itemCount) {
0404             QuickOpenDataPointer item = provider.provider->data(row);
0405 
0406             if (!noReset && provider.provider->itemCount() != itemCount) {
0407                 qCDebug(PLUGIN_QUICKOPEN) << "item-count in provider has changed, resetting model";
0408                 m_resetTimer->start();
0409                 m_resetBehindRow = rowOffset + row; //Don't reset everything, only everything behind this position
0410             }
0411 
0412 #ifdef QUICKOPEN_USE_ITEM_CACHING
0413             m_cachedData[row + rowOffset] = item;
0414 #endif
0415             return item;
0416         } else {
0417             row -= provider.provider->itemCount();
0418             rowOffset += provider.provider->itemCount();
0419         }
0420     }
0421 
0422 //   qWarning() << "No item for row " <<  row;
0423 
0424     return QuickOpenDataPointer();
0425 }
0426 
0427 QSet<IndexedString> QuickOpenModel::fileSet() const
0428 {
0429     std::vector<QSet<IndexedString>> sets;
0430     for (const ProviderEntry& provider : m_providers) {
0431         if (m_enabledScopes.isEmpty() || !(m_enabledScopes & provider.scopes).isEmpty()) {
0432             if (auto* iface = qobject_cast<QuickOpenFileSetInterface*>(provider.provider)) {
0433                 sets.push_back(iface->files());
0434                 //qCDebug(PLUGIN_QUICKOPEN) << "got file-list with" << ifiles.count() << "entries from data-provider" << typeid(*iface).name();
0435             }
0436         }
0437     }
0438     return Algorithm::unite(std::move(sets));
0439 }
0440 
0441 QTreeView* QuickOpenModel::treeView() const
0442 {
0443     return m_treeView;
0444 }
0445 
0446 bool QuickOpenModel::indexIsItem(const QModelIndex& index) const
0447 {
0448     Q_ASSERT(index.model() == this);
0449     Q_UNUSED(index);
0450     return true;
0451 }
0452 
0453 void QuickOpenModel::setTreeView(QTreeView* view)
0454 {
0455     m_treeView = view;
0456 }
0457 
0458 int QuickOpenModel::contextMatchQuality(const QModelIndex& /*index*/) const
0459 {
0460     return -1;
0461 }
0462 
0463 bool QuickOpenModel::execute(const QModelIndex& index, QString& filterText)
0464 {
0465     qCDebug(PLUGIN_QUICKOPEN) << "executing model";
0466     if (!index.isValid()) {
0467         qCWarning(PLUGIN_QUICKOPEN) << "Invalid index executed";
0468         return false;
0469     }
0470 
0471     QuickOpenDataPointer item = getItem(index.row());
0472 
0473     if (item) {
0474         return item->execute(filterText);
0475     } else {
0476         qCWarning(PLUGIN_QUICKOPEN) << "Got no item for row " << index.row() << " ";
0477     }
0478 
0479     return false;
0480 }
0481 
0482 #include "moc_quickopenmodel.cpp"