// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include "CompletionProxyModel.h" #include #include #include "CompletionModelRoles.h" #include "Logging.h" #include "Utils.h" CompletionProxyModel::CompletionProxyModel(QAbstractItemModel *model, int max_mistakes, size_t max_completions, QObject *parent) : QAbstractProxyModel(parent) , maxMistakes_(max_mistakes) , max_completions_(max_completions) { setSourceModel(model); auto insertParts = [this](const QString &str, int id) { QTextBoundaryFinder finder(QTextBoundaryFinder::BoundaryType::Word, str); finder.toStart(); do { auto start = finder.position(); finder.toNextBoundary(); auto end = finder.position(); auto ref = str.midRef(start, end - start).trimmed(); if (!ref.isEmpty()) trie_.insert(ref.toUcs4(), id); } while (finder.position() < str.size()); }; const auto start_at = std::chrono::steady_clock::now(); // insert full texts and partial matches for (int i = 0; i < sourceModel()->rowCount(); i++) { // full texts are ranked first and partial matches second // that way when searching full texts will be first in result list auto string1 = sourceModel() ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole) .toString() .toCaseFolded(); if (!string1.isEmpty()) { trie_.insert(string1.toUcs4(), i); insertParts(string1, i); } auto string2 = sourceModel() ->data(sourceModel()->index(i, 0), CompletionModel::SearchRole2) .toString() .toCaseFolded(); if (!string2.isEmpty()) { trie_.insert(string2.toUcs4(), i); insertParts(string2, i); } } const auto end_at = std::chrono::steady_clock::now(); const auto build_time = std::chrono::duration(end_at - start_at); nhlog::ui()->debug("CompletionProxyModel: build trie: {} ms", build_time.count()); // initialize default mapping mapping.resize(std::min(max_completions_, static_cast(model->rowCount()))); std::iota(mapping.begin(), mapping.end(), 0); connect( this, &CompletionProxyModel::newSearchString, this, [this](const QString &s) { searchString_ = s.toCaseFolded(); invalidate(); }, Qt::QueuedConnection); } void CompletionProxyModel::invalidate() { auto key = searchString_.toUcs4(); beginResetModel(); if (!key.empty()) // return default model data, if no search string mapping = trie_.search(key, max_completions_, maxMistakes_); endResetModel(); } QHash CompletionProxyModel::roleNames() const { return this->sourceModel()->roleNames(); } int CompletionProxyModel::rowCount(const QModelIndex &) const { if (searchString_.isEmpty()) return std::min( static_cast(std::min(max_completions_, std::numeric_limits::max())), sourceModel()->rowCount()); else return (int)mapping.size(); } QModelIndex CompletionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const { // return default model data, if no search string if (searchString_.isEmpty()) { return index(sourceIndex.row(), 0); } for (int i = 0; i < (int)mapping.size(); i++) { if (mapping[i] == sourceIndex.row()) { return index(i, 0); } } return QModelIndex(); } QModelIndex CompletionProxyModel::mapToSource(const QModelIndex &proxyIndex) const { auto row = proxyIndex.row(); // return default model data, if no search string if (searchString_.isEmpty()) { return index(row, 0); } if (row < 0 || row >= (int)mapping.size()) return QModelIndex(); return sourceModel()->index(mapping[row], 0); } QModelIndex CompletionProxyModel::index(int row, int column, const QModelIndex &) const { return createIndex(row, column); } QModelIndex CompletionProxyModel::parent(const QModelIndex &) const { return QModelIndex{}; } int CompletionProxyModel::columnCount(const QModelIndex &) const { return sourceModel()->columnCount(); } QVariant CompletionProxyModel::completionAt(int i) const { if (i >= 0 && i < rowCount()) return data(index(i, 0), CompletionModel::CompletionRole); else return {}; } void CompletionProxyModel::setSearchString(const QString &s) { emit newSearchString(s); }