Commit ee0193ed authored by Waqar Ahmed's avatar Waqar Ahmed
Browse files

Use fuzzy matching in kate-quick-open

This change uses the lib_fts (Sublime text like fuzzy match library)
to do fuzzy matching in quick-open.
parent 01b6d50f
......@@ -15,8 +15,6 @@
#include <ktexteditor/document.h>
#include <ktexteditor/view.h>
#include <tuple>
#include <KAboutData>
#include <KActionCollection>
#include <KLineEdit>
......@@ -35,31 +33,45 @@
#include <QStandardItemModel>
#include <QTreeView>
class QuickOpenFilterProxyModel : public QSortFilterProxyModel {
#include "kfts_fuzzy_match.h"
class QuickOpenFilterProxyModel : public QSortFilterProxyModel
{
public:
QuickOpenFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent)
{}
protected:
bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override
{
int l = source_left.data(KateQuickOpenModel::Score).toInt();
int r = source_right.data(KateQuickOpenModel::Score).toInt();
return l < r;
}
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
{
if (filterStrings.isEmpty())
return true;
const QString fileName = sourceModel()->index(sourceRow, 0, sourceParent).data().toString();
for (const QString& str : filterStrings) {
if (!fileName.contains(str, Qt::CaseInsensitive)) {
return false;
}
}
return true;
int score;
auto res = kfts::fuzzy_match(filterStrings.constData(), fileName.constData(), score);
auto idx = sourceModel()->index(sourceRow, 0, sourceParent);
sourceModel()->setData(idx, score, KateQuickOpenModel::Score);
return res;
}
public Q_SLOTS:
void setFilterText(const QString& text)
{
filterStrings = text.split(QLatin1Char(' '), Qt::SkipEmptyParts);
invalidateFilter();
beginResetModel();
filterStrings = text;
endResetModel();
}
private:
QStringList filterStrings;
QString filterStrings;
};
Q_DECLARE_METATYPE(QPointer<KTextEditor::Document>)
......@@ -87,10 +99,10 @@ KateQuickOpen::KateQuickOpen(QWidget *parent, KateMainWindow *mainWindow)
m_model = new QuickOpenFilterProxyModel(this);
m_model->setFilterRole(Qt::DisplayRole);
m_model->setSortRole(Qt::DisplayRole);
m_model->setSortRole(KateQuickOpenModel::Score);
m_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_model->setSortCaseSensitivity(Qt::CaseInsensitive);
m_model->setFilterKeyColumn(0);
m_model->setFilterKeyColumn(Qt::DisplayRole);
connect(m_inputLine, &KLineEdit::textChanged, m_model, &QuickOpenFilterProxyModel::setFilterText);
connect(m_inputLine, &KLineEdit::returnPressed, this, &KateQuickOpen::slotReturnPressed);
......@@ -100,6 +112,7 @@ KateQuickOpen::KateQuickOpen(QWidget *parent, KateMainWindow *mainWindow)
connect(m_listView, &QTreeView::activated, this, &KateQuickOpen::slotReturnPressed);
m_listView->setModel(m_model);
m_listView->setSortingEnabled(true);
m_model->setSourceModel(m_base_model);
m_inputLine->installEventFilter(this);
......
......@@ -9,6 +9,7 @@
#include "kateapp.h"
#include "kateviewmanager.h"
#include "katemainwindow.h"
#include <ktexteditor/document.h>
#include <ktexteditor/view.h>
......@@ -39,7 +40,7 @@ QVariant KateQuickOpenModel::data(const QModelIndex &idx, int role) const
return {};
}
if (role != Qt::DisplayRole && role != Qt::FontRole && role != Qt::UserRole) {
if (role != Qt::DisplayRole && role != Qt::FontRole && role != Qt::UserRole && role != Role::Score) {
return {};
}
......@@ -59,6 +60,8 @@ QVariant KateQuickOpenModel::data(const QModelIndex &idx, int role) const
}
} else if (role == Qt::UserRole) {
return entry.url;
} else if (role == Role::Score) {
return entry.score;
}
return {};
......@@ -77,18 +80,18 @@ void KateQuickOpenModel::refresh()
size_t sort_id = static_cast<size_t>(-1);
for (auto *view : qAsConst(sortedViews)) {
auto doc = view->document();
allDocuments.push_back({doc->url(), doc->documentName(), doc->url().toDisplayString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), true, sort_id--});
allDocuments.push_back({doc->url(), doc->documentName(), doc->url().toDisplayString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), true, sort_id--, -1});
}
for (auto *doc : qAsConst(openDocs)) {
const auto normalizedUrl = doc->url().toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile);
allDocuments.push_back({doc->url(), doc->documentName(), normalizedUrl, true, 0});
allDocuments.push_back({doc->url(), doc->documentName(), normalizedUrl, true, 0, -1});
}
for (const auto &file : qAsConst(projectDocs)) {
QFileInfo fi(file);
const auto localFile = QUrl::fromLocalFile(fi.absoluteFilePath());
allDocuments.push_back({localFile, fi.fileName(), localFile.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), false, 0});
allDocuments.push_back({localFile, fi.fileName(), localFile.toString(QUrl::NormalizePathSegments | QUrl::PreferLocalFile), false, 0, -1});
}
/** Sort the arrays by filePath. */
......@@ -109,5 +112,6 @@ void KateQuickOpenModel::refresh()
beginResetModel();
m_modelEntries = allDocuments;
currentRowCount = 0;
endResetModel();
}
......@@ -11,9 +11,9 @@
#include <QAbstractTableModel>
#include <QVariant>
#include <QVector>
#include <tuple>
#include <QUrl>
#include "katemainwindow.h"
class KateMainWindow;
struct ModelEntry {
QUrl url; // used for actually opening a selected file (local or remote)
......@@ -21,6 +21,7 @@ struct ModelEntry {
QString filePath; // display string for right column
bool bold; // format line in bold text or not
size_t sort_id;
int score;
};
// needs to be defined outside of class to support forward declaration elsewhere
......@@ -31,6 +32,7 @@ class KateQuickOpenModel : public QAbstractTableModel
Q_OBJECT
public:
enum Columns : int { FileName, FilePath, Bold };
enum Role { Score = Qt::UserRole + 1 };
explicit KateQuickOpenModel(KateMainWindow *mainWindow, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
......@@ -47,6 +49,17 @@ public:
m_listMode = mode;
}
bool setData(const QModelIndex &index, const QVariant &value, int role) override
{
if (!index.isValid())
return false;
if (role == Role::Score) {
auto row = index.row();
m_modelEntries[row].score = value.toInt();
}
return QAbstractTableModel::setData(index, value, role);
}
private:
QVector<ModelEntry> m_modelEntries;
......
/*
SPDX-FileCopyrightText: 2017 Forrest Smith
SPDX-FileCopyrightText: 2020 Waqar Ahmed
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifndef KFTS_FUZZY_MATCH_H
#define KFTS_FUZZY_MATCH_H
#include <QString>
/**
* This is based on https://github.com/forrestthewoods/lib_fts/blob/master/code/fts_fuzzy_match.h
* with modifications for Qt
*/
namespace kfts {
static bool fuzzy_match_simple(const QChar* pattern, const QChar* str);
static bool fuzzy_match(const QChar* pattern, const QChar* str, int & outScore);
static bool fuzzy_match(const QChar* pattern, const QChar* str, int & outScore, uint8_t * matches, int maxMatches);
}
namespace kfts {
// Forward declarations for "private" implementation
namespace fuzzy_internal {
static bool fuzzy_match_recursive(const QChar* pattern, const QChar* str, int & outScore, const QChar* strBegin,
uint8_t const * srcMatches, uint8_t * newMatches, int maxMatches, int nextMatch,
int & recursionCount, int recursionLimit);
}
// Public interface
static bool fuzzy_match_simple(const QChar* pattern, const QChar* str)
{
while (!pattern->isNull() && !str->isNull()) {
if (pattern->toLower() == str->toLower())
++pattern;
++str;
}
return pattern->isNull() ? true : false;
}
static bool fuzzy_match(const QChar* pattern, const QChar* str, int & outScore)
{
uint8_t matches[32];
return fuzzy_match(pattern, str, outScore, matches, sizeof(matches));
}
static bool fuzzy_match(const QChar* pattern, const QChar* str, int & outScore, uint8_t * matches, int maxMatches)
{
int recursionCount = 0;
int recursionLimit = 10;
return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 0, recursionCount, recursionLimit);
}
// Private implementation
static bool fuzzy_internal::fuzzy_match_recursive(const QChar* pattern,
const QChar* str,
int & outScore,
const QChar* strBegin,
uint8_t const * srcMatches,
uint8_t * matches,
int maxMatches,
int nextMatch,
int & recursionCount,
int recursionLimit)
{
// Count recursions
++recursionCount;
if (recursionCount >= recursionLimit)
return false;
// Detect end of strings
if (pattern->isNull() || str->isNull())
return false;
// Recursion params
bool recursiveMatch = false;
uint8_t bestRecursiveMatches[256];
int bestRecursiveScore = 0;
// Loop through pattern and str looking for a match
bool first_match = true;
while (!pattern->isNull() && !str->isNull()) {
// Found match
if (pattern->toLower() == str->toLower()) {
// Supplied matches buffer was too short
if (nextMatch >= maxMatches)
return false;
// "Copy-on-Write" srcMatches into matches
if (first_match && srcMatches) {
memcpy(matches, srcMatches, nextMatch);
first_match = false;
}
// Recursive call that "skips" this match
uint8_t recursiveMatches[256];
int recursiveScore;
if (fuzzy_match_recursive(pattern, str + 1, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) {
// Pick best recursive score
if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
memcpy(bestRecursiveMatches, recursiveMatches, 256);
bestRecursiveScore = recursiveScore;
}
recursiveMatch = true;
}
// Advance
matches[nextMatch++] = (uint8_t)(str - strBegin);
++pattern;
}
++str;
}
// Determine if full pattern was matched
bool matched = pattern->isNull() ? true : false;
// Calculate score
if (matched) {
const int sequential_bonus = 15; // bonus for adjacent matches
const int separator_bonus = 30; // bonus if match occurs after a separator
const int camel_bonus = 30; // bonus if match is uppercase and prev is lower
const int first_letter_bonus = 15; // bonus if the first letter is matched
const int leading_letter_penalty = -5; // penalty applied for every letter in str before the first match
const int max_leading_letter_penalty = -15; // maximum penalty for leading letters
const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter
// Iterate str to end
while (!str->isNull())
++str;
// Initialize score
outScore = 100;
// Apply leading letter penalty
int penalty = leading_letter_penalty * matches[0];
if (penalty < max_leading_letter_penalty)
penalty = max_leading_letter_penalty;
outScore += penalty;
// Apply unmatched penalty
int unmatched = (int)(str - strBegin) - nextMatch;
outScore += unmatched_letter_penalty * unmatched;
// Apply ordering bonuses
for (int i = 0; i < nextMatch; ++i) {
uint8_t currIdx = matches[i];
if (i > 0) {
uint8_t prevIdx = matches[i - 1];
// Sequential
if (currIdx == (prevIdx + 1))
outScore += sequential_bonus;
}
// Check for bonuses based on neighbor character value
if (currIdx > 0) {
// Camel case
QChar neighbor = strBegin[currIdx - 1];
QChar curr = strBegin[currIdx];
if (neighbor.isLower() && curr.isUpper())
outScore += camel_bonus;
// Separator
bool neighborSeparator = neighbor == QLatin1Char('_') || neighbor == QLatin1Char(' ');
if (neighborSeparator)
outScore += separator_bonus;
}
else {
// First letter
outScore += first_letter_bonus;
}
}
}
// Return best result
if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
// Recursive score is better than "this"
memcpy(matches, bestRecursiveMatches, maxMatches);
outScore = bestRecursiveScore;
return true;
}
else if (matched) {
// "this" score is better than recursive
return true;
}
else {
// no match
return false;
}
}
} // namespace fts
#endif // KFTS_FUZZY_MATCH_H
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment