Frecency: usage-aware ranking across all palettes
FrecencyStore (palette lib, Qt::Core, 11 tests): per-id {count,last}
persisted as JSON at $XDG_CONFIG_HOME/olliepalette/frecency.json via
QSaveFile. bonus() = recency bucket x capped visit count, in [0,100] —
below the substring tier (400) so it nudges/tie-breaks without overriding
a stronger textual match, and floats habitual choices up on an empty
query.
Plugin: OllieView loads the store, records every activation (M-x
runAction + file/symbol switcher lambdas) and sets PaletteItem::frecency
before showing each palette. File/symbol frecency keys are
project-qualified so usage does not bleed between repos.
This commit is contained in:
parent
cf0cf7e950
commit
7195a6c11b
21
docs/PLAN.md
21
docs/PLAN.md
|
|
@ -86,8 +86,9 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
and feeds them to `PaletteWidget`. The action's current shortcut is shown in
|
||||
the group column. Accepting a row triggers the underlying `QAction`.
|
||||
- Installs to `kf6/ktexteditor`; metadata verified valid via `KPluginMetaData`.
|
||||
- Not yet wired: frecency (the model supports a bonus, but the plugin currently
|
||||
passes 0 for every action — usage tracking is deferred).
|
||||
- Frecency: wired. `OllieView` holds a persisted `FrecencyStore`; activations
|
||||
are recorded and `PaletteItem::frecency` is populated for every palette (see
|
||||
the Frecency entry under M5).
|
||||
- Duplicate handling: actions are deduplicated only by pointer identity (the
|
||||
exact same action object reachable through several GUI clients is listed
|
||||
once — functionally lossless). Distinct actions are never dropped. When two
|
||||
|
|
@ -225,6 +226,22 @@ Therefore the palette is replaced, not extended, and the replacement is built on
|
|||
1-based → cursor 0-based). Also in the M-x palette (`ollie:switch:symbol`)
|
||||
and reachable from a radial by objectName `ollie_goto_symbol`.
|
||||
(Alt+R is reserved for the radial key trigger, so this uses Alt+G.)
|
||||
- Frecency (usage-aware ranking) — DONE.
|
||||
`src/palette/frecencystore.{h,cpp}` (in `palette`, Qt::Core only, 11 unit
|
||||
tests):
|
||||
- Per-id `{count, last-used}` persisted as flat JSON (via `QSaveFile`) at
|
||||
`$XDG_CONFIG_HOME/olliepalette/frecency.json`. `bonus(id, now)` combines a
|
||||
recency bucket (<1d=100, <1w=70, <1mo=50, <3mo=30, else 10) with a visit
|
||||
count capped at 10 → an integer in [0,100]. The cap keeps the bonus well
|
||||
below a textual-match tier (substring=400) so frecency nudges ordering and
|
||||
breaks ties without overriding a clearly better match; on an empty query it
|
||||
floats habitual choices to the top.
|
||||
- Wired into the plugin: `OllieView` loads the store in its ctor, every
|
||||
activation (`runAction` for the M-x palette; the file/symbol switcher
|
||||
lambdas) calls `recordUsage` (bump + save), and each palette sets
|
||||
`PaletteItem::frecency` from `bonus()` before showing. File/symbol keys are
|
||||
**project-qualified** (prefixed by the resolved root) so usage never bleeds
|
||||
between repos that share relative paths.
|
||||
|
||||
## Build and test
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ add_library(palette STATIC
|
|||
palettemodel.h
|
||||
palettewidget.cpp
|
||||
palettewidget.h
|
||||
frecencystore.cpp
|
||||
frecencystore.h
|
||||
)
|
||||
target_link_libraries(palette PUBLIC fuzzyranker Qt6::Core Qt6::Gui Qt6::Widgets)
|
||||
target_include_directories(palette PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
|
@ -11,4 +13,8 @@ if(Qt6Test_FOUND)
|
|||
add_executable(test_palettemodel test_palettemodel.cpp)
|
||||
target_link_libraries(test_palettemodel PRIVATE palette Qt6::Test Qt6::Widgets)
|
||||
add_test(NAME palettemodel COMMAND test_palettemodel)
|
||||
|
||||
add_executable(test_frecencystore test_frecencystore.cpp)
|
||||
target_link_libraries(test_frecencystore PRIVATE palette Qt6::Test)
|
||||
add_test(NAME frecencystore COMMAND test_frecencystore)
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
#include "frecencystore.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QSaveFile>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qint64 kDay = 24 * 60 * 60;
|
||||
} // namespace
|
||||
|
||||
FrecencyStore::FrecencyStore(const QString &filePath)
|
||||
: m_filePath(filePath)
|
||||
{
|
||||
}
|
||||
|
||||
bool FrecencyStore::load()
|
||||
{
|
||||
m_entries.clear();
|
||||
if (m_filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
QFile f(m_filePath);
|
||||
if (!f.open(QIODevice::ReadOnly)) {
|
||||
return false;
|
||||
}
|
||||
const QByteArray data = f.readAll();
|
||||
f.close();
|
||||
|
||||
QJsonParseError err{};
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(data, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
const QJsonObject root = doc.object();
|
||||
for (auto it = root.constBegin(); it != root.constEnd(); ++it) {
|
||||
if (!it.value().isObject()) {
|
||||
continue;
|
||||
}
|
||||
const QJsonObject o = it.value().toObject();
|
||||
Entry e;
|
||||
e.count = o.value(QStringLiteral("count")).toInt();
|
||||
e.last = static_cast<qint64>(o.value(QStringLiteral("last")).toDouble());
|
||||
if (e.count > 0) {
|
||||
m_entries.insert(it.key(), e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FrecencyStore::save() const
|
||||
{
|
||||
if (m_filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QFileInfo info(m_filePath);
|
||||
if (!QDir().mkpath(info.absolutePath())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject root;
|
||||
for (auto it = m_entries.constBegin(); it != m_entries.constEnd(); ++it) {
|
||||
QJsonObject o;
|
||||
o.insert(QStringLiteral("count"), it.value().count);
|
||||
o.insert(QStringLiteral("last"), static_cast<double>(it.value().last));
|
||||
root.insert(it.key(), o);
|
||||
}
|
||||
|
||||
QSaveFile f(m_filePath);
|
||||
if (!f.open(QIODevice::WriteOnly)) {
|
||||
return false;
|
||||
}
|
||||
f.write(QJsonDocument(root).toJson(QJsonDocument::Compact));
|
||||
return f.commit();
|
||||
}
|
||||
|
||||
void FrecencyStore::bump(const QString &id, qint64 nowEpoch)
|
||||
{
|
||||
if (id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Entry &e = m_entries[id];
|
||||
++e.count;
|
||||
e.last = nowEpoch;
|
||||
}
|
||||
|
||||
int FrecencyStore::computeBonus(const Entry &e, qint64 nowEpoch)
|
||||
{
|
||||
if (e.count <= 0) {
|
||||
return 0;
|
||||
}
|
||||
// Recency bucket: how long since last use. Newer buckets weigh more. A
|
||||
// negative age (clock skew / future timestamp) is treated as "just now".
|
||||
const qint64 age = nowEpoch - e.last;
|
||||
int recency;
|
||||
if (age < kDay) {
|
||||
recency = 100;
|
||||
} else if (age < 7 * kDay) {
|
||||
recency = 70;
|
||||
} else if (age < 30 * kDay) {
|
||||
recency = 50;
|
||||
} else if (age < 90 * kDay) {
|
||||
recency = 30;
|
||||
} else {
|
||||
recency = 10;
|
||||
}
|
||||
|
||||
// Frequency factor: 1..10 visits scale the recency weight linearly, so a
|
||||
// single recent use scores 10 and ten-plus recent uses saturate at the full
|
||||
// recency weight (100). Capped so the bonus never dwarfs a textual-match
|
||||
// tier (substring = 400) — frecency nudges, it does not override.
|
||||
const int visits = qBound(1, e.count, 10);
|
||||
return (recency * visits) / 10;
|
||||
}
|
||||
|
||||
int FrecencyStore::bonus(const QString &id, qint64 nowEpoch) const
|
||||
{
|
||||
auto it = m_entries.constFind(id);
|
||||
if (it == m_entries.constEnd()) {
|
||||
return 0;
|
||||
}
|
||||
return computeBonus(it.value(), nowEpoch);
|
||||
}
|
||||
|
||||
} // namespace katecustom
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* FrecencyStore — persisted per-id usage ("frecency": frequency + recency)
|
||||
* feeding the palette's ranking bonus.
|
||||
*
|
||||
* Each entry records how many times an id was activated and when it was last
|
||||
* used. bonus() combines a recency bucket with a capped visit count into a
|
||||
* small integer (0..100) that PaletteModel adds to the match score — enough to
|
||||
* float habitual choices to the top on an empty query and to break ties among
|
||||
* comparable matches, but capped well below a strong textual-match tier so it
|
||||
* never overrides a clearly better match.
|
||||
*
|
||||
* Persistence is a flat JSON object at an explicit file path (so tests use a
|
||||
* temp file); the plugin picks the real path from QStandardPaths. The class is
|
||||
* Qt::Core only and has no display or event-loop dependency.
|
||||
*/
|
||||
#ifndef KATECUSTOM_FRECENCYSTORE_H
|
||||
#define KATECUSTOM_FRECENCYSTORE_H
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
|
||||
namespace katecustom
|
||||
{
|
||||
|
||||
class FrecencyStore
|
||||
{
|
||||
public:
|
||||
struct Entry {
|
||||
int count = 0; // number of recorded activations
|
||||
qint64 last = 0; // last activation, seconds since epoch (UTC)
|
||||
};
|
||||
|
||||
/*! Construct backed by \a filePath. The file is not read until load(). */
|
||||
explicit FrecencyStore(const QString &filePath = QString());
|
||||
|
||||
/*! Replace the backing file path (does not reload). */
|
||||
void setFilePath(const QString &filePath) { m_filePath = filePath; }
|
||||
QString filePath() const { return m_filePath; }
|
||||
|
||||
/*! Read entries from the backing file. Missing/invalid file clears to empty
|
||||
* and is not an error. Returns true if a file was read. */
|
||||
bool load();
|
||||
|
||||
/*! Write entries to the backing file, creating parent dirs. Returns true on
|
||||
* success. */
|
||||
bool save() const;
|
||||
|
||||
/*! Record one activation of \a id at \a nowEpoch (UTC seconds): increments
|
||||
* the count and sets the last-used time. */
|
||||
void bump(const QString &id, qint64 nowEpoch);
|
||||
|
||||
/*! Ranking bonus for \a id at \a nowEpoch, in [0, 100]. Unknown ids yield 0.
|
||||
* Combines a recency bucket (newer = higher) with a capped visit count. */
|
||||
int bonus(const QString &id, qint64 nowEpoch) const;
|
||||
|
||||
/*! Direct entry access (empty Entry if unknown). */
|
||||
Entry entry(const QString &id) const { return m_entries.value(id); }
|
||||
int size() const { return m_entries.size(); }
|
||||
|
||||
/*! Pure scoring used by bonus(); exposed for testing. */
|
||||
static int computeBonus(const Entry &e, qint64 nowEpoch);
|
||||
|
||||
private:
|
||||
QString m_filePath;
|
||||
QHash<QString, Entry> m_entries;
|
||||
};
|
||||
|
||||
} // namespace katecustom
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*
|
||||
* Unit tests for FrecencyStore: bump/bonus scoring (recency buckets, frequency
|
||||
* scaling, caps) and JSON round-trip persistence to a temp file.
|
||||
*/
|
||||
#include "frecencystore.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
using namespace katecustom;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr qint64 kDay = 24 * 60 * 60;
|
||||
constexpr qint64 kNow = 1'700'000'000; // fixed reference epoch
|
||||
} // namespace
|
||||
|
||||
class TestFrecencyStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void unknownIdYieldsZero()
|
||||
{
|
||||
FrecencyStore s;
|
||||
QCOMPARE(s.bonus(QStringLiteral("nope"), kNow), 0);
|
||||
}
|
||||
|
||||
void singleRecentUseScoresTen()
|
||||
{
|
||||
FrecencyStore s;
|
||||
s.bump(QStringLiteral("a"), kNow);
|
||||
// recency 100 (<1 day), 1 visit -> 100*1/10 = 10.
|
||||
QCOMPARE(s.bonus(QStringLiteral("a"), kNow), 10);
|
||||
}
|
||||
|
||||
void frequentRecentUseSaturatesAtHundred()
|
||||
{
|
||||
FrecencyStore s;
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
s.bump(QStringLiteral("a"), kNow);
|
||||
}
|
||||
// visits capped at 10 -> 100*10/10 = 100.
|
||||
QCOMPARE(s.bonus(QStringLiteral("a"), kNow), 100);
|
||||
}
|
||||
|
||||
void recencyDecaysWithAge()
|
||||
{
|
||||
FrecencyStore s;
|
||||
s.bump(QStringLiteral("a"), kNow - 10 * kDay); // 10 days ago
|
||||
// 10 days -> <30 day bucket (50), 1 visit -> 5.
|
||||
QCOMPARE(s.bonus(QStringLiteral("a"), kNow), 5);
|
||||
|
||||
FrecencyStore s2;
|
||||
s2.bump(QStringLiteral("a"), kNow - 200 * kDay); // very old
|
||||
// >90 days -> bucket 10, 1 visit -> 1.
|
||||
QCOMPARE(s2.bonus(QStringLiteral("a"), kNow), 1);
|
||||
}
|
||||
|
||||
void recentBeatsOldAtEqualCount()
|
||||
{
|
||||
FrecencyStore s;
|
||||
s.bump(QStringLiteral("recent"), kNow);
|
||||
s.bump(QStringLiteral("old"), kNow - 60 * kDay);
|
||||
QVERIFY(s.bonus(QStringLiteral("recent"), kNow)
|
||||
> s.bonus(QStringLiteral("old"), kNow));
|
||||
}
|
||||
|
||||
void futureTimestampTreatedAsNow()
|
||||
{
|
||||
FrecencyStore s;
|
||||
s.bump(QStringLiteral("a"), kNow + 5 * kDay); // clock skew
|
||||
QCOMPARE(s.bonus(QStringLiteral("a"), kNow), 10); // age<0 -> recency 100
|
||||
}
|
||||
|
||||
void roundTripsThroughFile()
|
||||
{
|
||||
QTemporaryDir tmp;
|
||||
QVERIFY(tmp.isValid());
|
||||
const QString path = QDir(tmp.path()).filePath(QStringLiteral("sub/frecency.json"));
|
||||
|
||||
FrecencyStore a(path);
|
||||
a.bump(QStringLiteral("x"), kNow);
|
||||
a.bump(QStringLiteral("x"), kNow);
|
||||
a.bump(QStringLiteral("y"), kNow - 40 * kDay);
|
||||
QVERIFY(a.save()); // creates the "sub" dir
|
||||
|
||||
FrecencyStore b(path);
|
||||
QVERIFY(b.load());
|
||||
QCOMPARE(b.size(), 2);
|
||||
QCOMPARE(b.entry(QStringLiteral("x")).count, 2);
|
||||
QCOMPARE(b.entry(QStringLiteral("x")).last, kNow);
|
||||
QCOMPARE(b.bonus(QStringLiteral("x"), kNow), 20); // 100*2/10
|
||||
}
|
||||
|
||||
void loadMissingFileIsEmptyNotError()
|
||||
{
|
||||
FrecencyStore s(QStringLiteral("/no/such/dir/frecency.json"));
|
||||
QVERIFY(!s.load()); // nothing read
|
||||
QCOMPARE(s.size(), 0);
|
||||
}
|
||||
|
||||
void emptyPathSaveAndLoadFail()
|
||||
{
|
||||
FrecencyStore s;
|
||||
QVERIFY(!s.save());
|
||||
QVERIFY(!s.load());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestFrecencyStore)
|
||||
#include "test_frecencystore.moc"
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
#include "ollieplugin.h"
|
||||
|
||||
#include "palettewidget.h"
|
||||
#include "frecencystore.h"
|
||||
#include "radialmenu.h"
|
||||
#include "radialconfig.h"
|
||||
#include "olliecommands.h"
|
||||
|
|
@ -24,6 +25,7 @@
|
|||
|
||||
#include <QAction>
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QEvent>
|
||||
#include <QFileInfo>
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
#include <QKeySequence>
|
||||
#include <QMouseEvent>
|
||||
#include <QSet>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -215,6 +218,14 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
|
|||
: QObject(plugin)
|
||||
, m_mainWindow(mainWindow)
|
||||
{
|
||||
// Load persisted usage so frecency ranking is available from the first
|
||||
// palette invocation. The store lives under the user's generic config dir.
|
||||
const QString cfgDir =
|
||||
QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
|
||||
m_frecency.setFilePath(QDir(cfgDir).filePath(
|
||||
QStringLiteral("olliepalette/frecency.json")));
|
||||
m_frecency.load();
|
||||
|
||||
// The one keyboard door: Alt+X (M-x). A single modifier, no chord, and the
|
||||
// keys sit together on a compact layout.
|
||||
auto *openAction = new QAction(i18n("Command Palette (M-x)"), this);
|
||||
|
|
@ -440,6 +451,12 @@ void OllieView::showCommandPalette()
|
|||
items.push_back(item);
|
||||
}
|
||||
|
||||
// Apply the persisted frecency bonus so habitual commands rank higher.
|
||||
const qint64 now = QDateTime::currentSecsSinceEpoch();
|
||||
for (PaletteItem &item : items) {
|
||||
item.frecency = m_frecency.bonus(item.id, now);
|
||||
}
|
||||
|
||||
m_palette->setItems(items);
|
||||
|
||||
// Center the palette near the top of the active window.
|
||||
|
|
@ -453,8 +470,21 @@ void OllieView::showCommandPalette()
|
|||
m_palette->activate();
|
||||
}
|
||||
|
||||
void OllieView::recordUsage(const QString &id)
|
||||
{
|
||||
if (id.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
m_frecency.bump(id, QDateTime::currentSecsSinceEpoch());
|
||||
m_frecency.save();
|
||||
}
|
||||
|
||||
void OllieView::runAction(const QString &actionId)
|
||||
{
|
||||
// Count every command-palette activation toward frecency (verbs, switcher
|
||||
// entries, and harvested actions all flow through here).
|
||||
recordUsage(actionId);
|
||||
|
||||
// A ":"-verb entry: execute the command directly on the active view.
|
||||
// We call OllieCommands::runVerb rather than Editor::queryCommand because
|
||||
// our Command's auto-registration is not reliably visible to queryCommand.
|
||||
|
|
@ -492,13 +522,19 @@ void OllieView::showFileSwitcher()
|
|||
connect(m_filePalette, &PaletteWidget::activatedId, this,
|
||||
[this](const QString &id) {
|
||||
if (id.startsWith(kFilePrefix)) {
|
||||
openProjectFile(id.mid(QString(kFilePrefix).size()));
|
||||
const QString rel = id.mid(QString(kFilePrefix).size());
|
||||
// Record under a project-qualified key so frecency does
|
||||
// not bleed between repos that share relative paths.
|
||||
recordUsage(kFilePrefix + m_fileSwitcherRoot
|
||||
+ QLatin1Char('\t') + rel);
|
||||
openProjectFile(rel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
m_fileSwitcherRoot = projectRootFor(m_mainWindow);
|
||||
const QStringList files = ProjectIndex::listFiles(m_fileSwitcherRoot);
|
||||
const qint64 now = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
QList<PaletteItem> items;
|
||||
items.reserve(files.size());
|
||||
|
|
@ -511,6 +547,8 @@ void OllieView::showFileSwitcher()
|
|||
// the whole path. The base name still ranks well because an exact or
|
||||
// substring hit on it outscores a scattered subsequence.
|
||||
item.label = rel;
|
||||
item.frecency = m_frecency.bonus(
|
||||
kFilePrefix + m_fileSwitcherRoot + QLatin1Char('\t') + rel, now);
|
||||
items.push_back(item);
|
||||
}
|
||||
m_filePalette->setItems(items);
|
||||
|
|
@ -544,7 +582,11 @@ void OllieView::showSymbolSwitcher()
|
|||
connect(m_symbolPalette, &PaletteWidget::activatedId, this,
|
||||
[this](const QString &id) {
|
||||
if (id.startsWith(kSymbolPrefix)) {
|
||||
goToSymbol(id.mid(QString(kSymbolPrefix).size()));
|
||||
const QString loc = id.mid(QString(kSymbolPrefix).size());
|
||||
// Project-qualified key, as for the file switcher.
|
||||
recordUsage(kSymbolPrefix + m_symbolSwitcherRoot
|
||||
+ QLatin1Char('\t') + loc);
|
||||
goToSymbol(loc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -552,6 +594,7 @@ void OllieView::showSymbolSwitcher()
|
|||
m_symbolSwitcherRoot = projectRootFor(m_mainWindow);
|
||||
const QStringList files = ProjectIndex::listFiles(m_symbolSwitcherRoot);
|
||||
const QList<Symbol> symbols = SymbolIndex::listSymbols(m_symbolSwitcherRoot, files);
|
||||
const qint64 now = QDateTime::currentSecsSinceEpoch();
|
||||
|
||||
QList<PaletteItem> items;
|
||||
items.reserve(symbols.size());
|
||||
|
|
@ -559,7 +602,8 @@ void OllieView::showSymbolSwitcher()
|
|||
PaletteItem item;
|
||||
// The id encodes the jump target as "relpath\tline"; the location is
|
||||
// resolved against m_symbolSwitcherRoot on activation.
|
||||
item.id = kSymbolPrefix + s.file + QLatin1Char('\t') + QString::number(s.line);
|
||||
const QString loc = s.file + QLatin1Char('\t') + QString::number(s.line);
|
||||
item.id = kSymbolPrefix + loc;
|
||||
|
||||
// Label: scope-qualified name, then kind + file as visible provenance.
|
||||
// The delegate renders only the label and FuzzyRanker scores against it,
|
||||
|
|
@ -578,6 +622,8 @@ void OllieView::showSymbolSwitcher()
|
|||
label += QStringLiteral(" — ") + suffix;
|
||||
}
|
||||
item.label = label;
|
||||
item.frecency = m_frecency.bonus(
|
||||
kSymbolPrefix + m_symbolSwitcherRoot + QLatin1Char('\t') + loc, now);
|
||||
|
||||
items.push_back(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
#include <KTextEditor/Plugin>
|
||||
|
||||
#include "frecencystore.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
#include <QHash>
|
||||
|
|
@ -66,6 +68,9 @@ private Q_SLOTS:
|
|||
void goToSymbol(const QString &locationId);
|
||||
|
||||
private:
|
||||
// Record one activation of \a id and persist. Feeds the frecency bonus so
|
||||
// habitual choices float up in every palette.
|
||||
void recordUsage(const QString &id);
|
||||
// One harvested, runnable action with the provenance needed to build a
|
||||
// unique id and to disambiguate labels shared by several commands.
|
||||
struct ActionEntry {
|
||||
|
|
@ -121,6 +126,7 @@ private:
|
|||
bool m_swallowMiddleRelease = false; // swallow the release paired with a consumed middle press
|
||||
bool m_swallowRightRelease = false; // swallow the release paired with a left+right paste chord
|
||||
QHash<QString, QPointer<QAction>> m_actionsById;
|
||||
FrecencyStore m_frecency; // persisted usage feeding the palette ranking
|
||||
};
|
||||
|
||||
} // namespace katecustom
|
||||
|
|
|
|||
Loading…
Reference in New Issue