palette + switch: finish the split; delete the core deft plugin

Stage 3c, the final split. The remaining core OllieView carried two distinct
features plus the ":"-verb pack; each becomes its own KTextEditor plugin and
the monolithic core 'deft' plugin is removed entirely.

deft_palette.so ([deft:util] palette): new src/palette/paletteplugin.{h,cpp}
(PalettePlugin/PaletteView) + paletteplugin.json. Owns the command palette
(Alt+X / M-x): collectActions over the window's GUI clients, the ":"-verb
pack (OllieCommands, constructed once in PalettePlugin), positionPalette,
frecency ranking, and the runAction dispatcher. Its "Go to File/Symbol"
entries no longer call switcher code directly — they trigger the switch
plugin's window actions deft_goto_file / deft_goto_symbol by objectName via
findActionByName, so no cross-plugin link is required. Links palette +
olliecommands_lib.

deft_switch.so ([deft:util] switch): new src/switch/switchplugin.{h,cpp}
(SwitchPlugin/SwitchView) + switchplugin.json. Owns the project file (Alt+P)
and symbol (Alt+G) switchers: ProjectIndexer, KateProjectBridge (follows
kateprojectplugin, invalidates the index on project change), async
cache-populated palettes, ctags 1-based->0-based symbol jumps. Registers
deft_goto_file / deft_goto_symbol as window actions so palette and radial can
reach them. Links palette + project_lib.

findActionByName now also searches window()->actions(), since sibling doors are
registered via QWidget::addAction (no XMLGUI client); this is what makes the
cross-plugin door resolution actually work.

Each plugin keeps its own FrecencyStore in a separate file under deft/
(palette.frecency.json, switch.frecency.json) so the two independent rankings
can no longer clobber one another through a shared file; the old config dir
olliepalette/ and the ollie: id prefixes are retired in favour of deft/ and
deft:cmd:/deft:file:/deft:sym:.

Deletes src/plugin/ (ollieplugin.{h,cpp,json}, CMakeLists) and the core 'deft'
CMake target; src/CMakeLists.txt now builds acme, commands, switch under the
KF6TextEditor guard and no longer adds the plugin subdir. The suite is now six
one-feature plugins: palette, switch, radial, acme, sam, plumb.
This commit is contained in:
Levi Neely 2026-10-08 19:55:13 +02:00
parent ec2802b11d
commit d8f0225178
11 changed files with 709 additions and 425 deletions

View File

@ -7,5 +7,5 @@ add_subdirectory(sam)
if(KF6TextEditor_FOUND)
add_subdirectory(acme)
add_subdirectory(commands)
add_subdirectory(switch)
endif()
add_subdirectory(plugin)

View File

@ -9,6 +9,24 @@ add_library(palette STATIC
target_link_libraries(palette PUBLIC fuzzyranker Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Concurrent)
target_include_directories(palette PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# The command-palette plugin ([deft:util] palette). Needs KF6::TextEditor; the
# ":"-verb pack comes from olliecommands_lib, the widget/frecency from palette.
if(KF6TextEditor_FOUND)
kcoreaddons_add_plugin(deft_palette
SOURCES paletteplugin.cpp paletteplugin.h
INSTALL_NAMESPACE "kf6/ktexteditor"
)
target_link_libraries(deft_palette PRIVATE
palette
olliecommands_lib
KF6::TextEditor
KF6::XmlGui
KF6::CoreAddons
KF6::I18n
Qt6::Widgets
)
endif()
if(Qt6Test_FOUND)
add_executable(test_palettemodel test_palettemodel.cpp)
target_link_libraries(test_palettemodel PRIVATE palette Qt6::Test Qt6::Widgets)

View File

@ -0,0 +1,443 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "paletteplugin.h"
#include "palettewidget.h"
#include "frecencystore.h"
#include "olliecommands.h"
#include <KTextEditor/Document>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
#include <KActionCollection>
#include <KXMLGUIClient>
#include <KXMLGUIFactory>
#include <KLocalizedString>
#include <KPluginFactory>
#include <QAction>
#include <QApplication>
#include <QDateTime>
#include <QDir>
#include <QEvent>
#include <QHash>
#include <QKeyEvent>
#include <QKeySequence>
#include <QPoint>
#include <QSet>
#include <QStandardPaths>
#include <QWidget>
namespace deft
{
namespace
{
// Prefix marking a palette id that is a ":" command string to execute rather
// than a QAction to trigger.
const QLatin1String kCmdPrefix("deft:cmd:");
// The complete (argument-free) command verbs offered in the palette. Verbs that
// need free-text arguments (pipe) stay on the command line; `case` is expanded
// into one entry per style so each is directly runnable.
struct VerbItem {
QString command; // the full command string to execute
QString label; // palette label
};
QList<VerbItem> paletteVerbs()
{
return {
{QStringLiteral("osort"), QStringLiteral("Sort Lines")},
{QStringLiteral("osort u"), QStringLiteral("Sort Lines (unique)")},
{QStringLiteral("rsort"), QStringLiteral("Reverse Lines")},
{QStringLiteral("case upper"), QStringLiteral("Case: UPPER")},
{QStringLiteral("case lower"), QStringLiteral("Case: lower")},
{QStringLiteral("case title"), QStringLiteral("Case: Title")},
{QStringLiteral("case snake"), QStringLiteral("Case: snake_case")},
{QStringLiteral("case camel"), QStringLiteral("Case: camelCase")},
{QStringLiteral("case kebab"), QStringLiteral("Case: kebab-case")},
{QStringLiteral("b64enc"), QStringLiteral("Base64 Encode")},
{QStringLiteral("b64dec"), QStringLiteral("Base64 Decode")},
{QStringLiteral("rot13"), QStringLiteral("ROT13")},
{QStringLiteral("uuid"), QStringLiteral("Insert UUID")},
};
}
} // namespace
// ---------------------------------------------------------------------------
// PalettePlugin
// ---------------------------------------------------------------------------
PalettePlugin::PalettePlugin(QObject *parent, const QVariantList &args)
: KTextEditor::Plugin(parent)
{
Q_UNUSED(args);
// The ":"-verb pack registers itself with the global editor on construction.
m_commands = new OllieCommands(this);
}
PalettePlugin::~PalettePlugin() = default;
QObject *PalettePlugin::createView(KTextEditor::MainWindow *mainWindow)
{
return new PaletteView(this, mainWindow);
}
// ---------------------------------------------------------------------------
// PaletteView
// ---------------------------------------------------------------------------
PaletteView::PaletteView(PalettePlugin *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("deft/palette.frecency.json")));
m_frecency.load();
// The command-palette launcher "door". Its shortcut is NOT registered as a
// QAction/ApplicationShortcut: Kate (and other plugins) already claim many
// Alt+<letter> combos, and a registered duplicate makes Qt refuse to fire
// either, reporting an "ambiguous shortcut". We therefore register NO
// shortcut here and instead intercept the key in eventFilter(), which
// cannot be ambiguous because nothing is registered. The QAction is kept
// only for its objectName/label (radial escape-hatch, discoverability).
auto *openAction = new QAction(i18n("Command Palette (M-x) [Alt+X]"), this);
openAction->setObjectName(QStringLiteral("deft_command_palette"));
connect(openAction, &QAction::triggered, this, &PaletteView::showCommandPalette);
// Keep it discoverable as a window action (no shortcut attached).
if (QWidget *w = m_mainWindow->window()) {
w->addAction(openAction);
}
// Application-wide filter for the launcher door (Alt+X), catching the key
// no matter which internal child of the view actually receives it.
qApp->installEventFilter(this);
}
PaletteView::~PaletteView()
{
delete m_palette;
}
QList<PaletteView::ActionEntry> PaletteView::collectActions() const
{
QList<ActionEntry> entries;
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
if (!factory) {
return entries;
}
// Dedup by pointer identity: the same QAction is frequently reachable
// through several GUI clients. Those are true duplicates and must appear
// only once.
QSet<QAction *> seen;
const QList<KXMLGUIClient *> clients = factory->clients();
for (KXMLGUIClient *client : clients) {
KActionCollection *coll = client->actionCollection();
if (!coll) {
continue;
}
const QString component = coll->componentDisplayName();
const QList<QAction *> actions = coll->actions();
for (QAction *action : actions) {
if (!action || action->isSeparator()) {
continue;
}
if (seen.contains(action)) {
continue;
}
// Only offer things the user could actually invoke now.
if (!action->isEnabled() || !action->isVisible()) {
continue;
}
const QString label = action->text().remove(QLatin1Char('&')).trimmed();
if (label.isEmpty()) {
continue;
}
seen.insert(action);
ActionEntry e;
e.action = action;
e.label = label;
e.component = component;
e.shortcut = action->shortcut().toString(QKeySequence::NativeText);
entries.push_back(e);
}
}
// Distinct actions can share a visible label (e.g. different plugins each
// offering "Reload"). These are NOT duplicates — never drop them. Instead
// guarantee every visible row is distinguishable by enriching colliding
// labels through a fallback chain of discriminators, and give each entry a
// unique id so activation always triggers the intended action.
QHash<QString, int> labelCounts;
for (const ActionEntry &e : entries) {
++labelCounts[e.label];
}
// First pass: for any shared label, append the strongest discriminator
// available (component, else objectName, else shortcut).
for (ActionEntry &e : entries) {
if (labelCounts.value(e.label) <= 1) {
continue;
}
const QString objName = e.action ? e.action->objectName() : QString();
QString disc;
if (!e.component.isEmpty()) {
disc = e.component;
} else if (!objName.isEmpty()) {
disc = objName;
} else if (!e.shortcut.isEmpty()) {
disc = e.shortcut;
}
if (!disc.isEmpty()) {
e.label = e.label + QStringLiteral(" — ") + disc;
}
}
// Second pass: if any labels are STILL identical (same label and same
// discriminator), append objectName/shortcut, then a numeric suffix, so no
// two rows ever look the same.
QHash<QString, int> stillColliding;
for (const ActionEntry &e : entries) {
++stillColliding[e.label];
}
QHash<QString, int> dupSeq;
for (ActionEntry &e : entries) {
if (stillColliding.value(e.label) <= 1) {
continue;
}
const QString objName = e.action ? e.action->objectName() : QString();
if (!objName.isEmpty() && !e.label.contains(objName)) {
e.label += QStringLiteral(" [") + objName + QLatin1Char(']');
} else if (!e.shortcut.isEmpty() && !e.label.contains(e.shortcut)) {
e.label += QStringLiteral(" (") + e.shortcut + QLatin1Char(')');
} else {
// Last resort: number the indistinguishable rows 1..n.
const int n = ++dupSeq[e.label];
e.label += QStringLiteral(" (") + QString::number(n) + QLatin1Char(')');
}
}
// Assign unique, reasonably stable ids (independent of the display label).
QHash<QString, int> idSeq;
for (ActionEntry &e : entries) {
const QString objName = e.action ? e.action->objectName() : QString();
QString base = e.component;
if (!base.isEmpty()) {
base += QLatin1Char('/');
}
base += objName.isEmpty() ? e.label : objName;
// Guarantee uniqueness even if base still collides.
const int n = idSeq[base]++;
e.id = (n == 0) ? base : base + QStringLiteral("#") + QString::number(n);
}
return entries;
}
QAction *PaletteView::findActionByName(const QString &objectName) const
{
if (objectName.isEmpty()) {
return nullptr;
}
// First: the window's GUI factory clients (plugins exposing XMLGUI actions).
if (KXMLGUIFactory *factory = m_mainWindow->guiFactory()) {
const QList<KXMLGUIClient *> clients = factory->clients();
for (KXMLGUIClient *client : clients) {
if (KActionCollection *coll = client->actionCollection()) {
if (QAction *a = coll->action(objectName)) {
return a;
}
}
}
}
// Second: window-level actions. Sibling deft plugins register their launcher
// doors with QWidget::addAction (no XMLGUI client), so this is where the
// switch plugin's deft_goto_file / deft_goto_symbol actually live.
if (QWidget *w = m_mainWindow->window()) {
const QList<QAction *> actions = w->actions();
for (QAction *a : actions) {
if (a && a->objectName() == objectName) {
return a;
}
}
}
return nullptr;
}
void PaletteView::positionPalette(PaletteWidget *palette)
{
QWidget *w = m_mainWindow->window();
if (!w) {
return;
}
// Three quarters of the window width, horizontally centered, anchored near
// the top — roomy enough for long paths / symbol labels without looking
// edge-to-edge.
const int width = (w->width() * 3) / 4;
palette->resize(width, 320);
palette->move(w->mapToGlobal(QPoint((w->width() - width) / 2, w->height() / 6)));
}
void PaletteView::showCommandPalette()
{
if (!m_palette) {
m_palette = new PaletteWidget(m_mainWindow->window());
connect(m_palette, &PaletteWidget::activatedId, this, &PaletteView::runAction);
}
const QList<ActionEntry> entries = collectActions();
m_actionsById.clear();
QList<PaletteItem> items;
items.reserve(entries.size() + 16);
for (const ActionEntry &e : entries) {
m_actionsById.insert(e.id, e.action);
PaletteItem item;
item.id = e.id;
item.label = e.label;
item.group = e.shortcut; // shortcut shown as the trailing hint
items.push_back(item);
}
// Also offer the ":"-verb pack as directly-runnable palette entries.
for (const VerbItem &v : paletteVerbs()) {
PaletteItem item;
item.id = kCmdPrefix + v.command;
item.label = v.label;
item.group = QStringLiteral(":") + v.command;
items.push_back(item);
}
// Offer the project file switcher as a palette entry too (also on Alt+P).
// Triggered by the sibling switch plugin's window action, by objectName.
{
PaletteItem item;
item.id = QStringLiteral("deft:switch:file");
item.label = i18n("Go to File (project)");
item.group = QStringLiteral("Alt+P");
items.push_back(item);
}
// Offer the project symbol switcher as a palette entry too (also on Alt+G).
{
PaletteItem item;
item.id = QStringLiteral("deft:switch:symbol");
item.label = i18n("Go to Symbol (project)");
item.group = QStringLiteral("Alt+G");
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);
// Span the full window width, anchored near the top.
positionPalette(m_palette);
m_palette->activate();
}
void PaletteView::recordUsage(const QString &id)
{
if (id.isEmpty()) {
return;
}
m_frecency.bump(id, QDateTime::currentSecsSinceEpoch());
m_frecency.save();
}
void PaletteView::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.
if (actionId.startsWith(kCmdPrefix)) {
const QString command = actionId.mid(QString(kCmdPrefix).size());
if (KTextEditor::View *view = m_mainWindow->activeView()) {
QString msg;
OllieCommands::runVerb(view, command, msg);
}
return;
}
// The project file switcher, provided by the sibling switch plugin. Trigger
// its window action by objectName; a no-op when switch is not loaded.
if (actionId == QLatin1String("deft:switch:file")) {
if (QAction *a = findActionByName(QStringLiteral("deft_goto_file"))) {
a->trigger();
}
return;
}
// The project symbol switcher, as above.
if (actionId == QLatin1String("deft:switch:symbol")) {
if (QAction *a = findActionByName(QStringLiteral("deft_goto_symbol"))) {
a->trigger();
}
return;
}
QPointer<QAction> action = m_actionsById.value(actionId);
if (action) {
action->trigger();
}
}
bool PaletteView::eventFilter(QObject *obj, QEvent *event)
{
// Launcher door: Alt+X (M-x). Handled here rather than via a QAction
// shortcut so it can never clash with Kate's own Alt+<letter> bindings
// (which produced "ambiguous shortcut" and swallowed the key). Scope to
// this plugin's window so we do not hijack the combo in other Kate windows.
if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
const bool altOnly =
(ke->modifiers() & Qt::KeyboardModifierMask) == Qt::AltModifier;
if (altOnly && ke->key() == Qt::Key_X) {
auto *w = qobject_cast<QWidget *>(obj);
QWidget *ourWindow = m_mainWindow->window();
const bool ours = w && ourWindow && ourWindow->isAncestorOf(w);
if (ours) {
if (event->type() == QEvent::ShortcutOverride) {
event->accept(); // suppress any registered (ambiguous) shortcut
return true;
}
showCommandPalette();
return true; // consume the keypress
}
}
}
return QObject::eventFilter(obj, event);
}
} // namespace deft
// K_PLUGIN_FACTORY generates the factory and the moc-level registration. The
// matching .moc include is required in this translation unit.
K_PLUGIN_FACTORY_WITH_JSON(PalettePluginFactory, "paletteplugin.json",
registerPlugin<deft::PalettePlugin>();)
#include "paletteplugin.moc"

109
src/palette/paletteplugin.h Normal file
View File

@ -0,0 +1,109 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* The Deft command-palette plugin for Kate / KTextEditor ([deft:util] palette).
*
* PalettePlugin is the KTextEditor::Plugin entry point and also owns the single
* global ":"-verb command pack. For every MainWindow the host creates a
* PaletteView, which registers the "Command Palette (M-x)" door (Alt+X) and,
* when triggered, gathers every QAction currently exposed by the window's GUI
* clients into a PaletteWidget for orderless fuzzy selection. The palette also
* offers the ":"-verbs and — when the sibling switch plugin is loaded — the
* "Go to File / Symbol" switchers, which it triggers by action objectName so no
* cross-plugin link is required.
*/
#ifndef DEFT_PALETTEPLUGIN_H
#define DEFT_PALETTEPLUGIN_H
#include <KTextEditor/Plugin>
#include "frecencystore.h"
#include "palettemodel.h" // PaletteItem
#include <QObject>
#include <QPointer>
#include <QHash>
#include <QList>
class QAction;
class QEvent;
namespace KTextEditor
{
class MainWindow;
class View;
}
namespace deft
{
class PaletteWidget;
class OllieCommands;
class PalettePlugin : public KTextEditor::Plugin
{
Q_OBJECT
public:
explicit PalettePlugin(QObject *parent, const QVariantList &args = {});
~PalettePlugin() override;
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
private:
OllieCommands *m_commands = nullptr; // ":"-verb pack, registered once globally
};
class PaletteView : public QObject
{
Q_OBJECT
public:
PaletteView(PalettePlugin *plugin, KTextEditor::MainWindow *mainWindow);
~PaletteView() override;
private Q_SLOTS:
void showCommandPalette();
void runAction(const QString &actionId);
private:
// Record one activation of \a id and persist. Feeds the frecency bonus so
// habitual choices float up in the palette.
void recordUsage(const QString &id);
// Size and place a palette: it spans the active window's width (minus a
// small margin), anchored near the top.
void positionPalette(PaletteWidget *palette);
// Resolve a sibling-plugin action by objectName, searching the window's GUI
// factory clients first and the window's own action list second. Returns
// nullptr when no plugin currently provides it.
QAction *findActionByName(const QString &objectName) const;
// One harvested, runnable action with the provenance needed to build a
// unique id and to disambiguate labels shared by several commands.
struct ActionEntry {
QPointer<QAction> action;
QString label; // visible text, ampersands stripped
QString component; // owning collection's display name (provenance)
QString shortcut; // current shortcut, native text
QString id; // unique, stable key used on activation
};
// Collect every unique, enabled, visible QAction reachable from the
// window's GUI factory clients, deduplicated by pointer identity.
QList<ActionEntry> collectActions() const;
// The application event filter: intercepts the launcher-door key (Alt+X) so
// it can never clash with Kate's own Alt bindings.
bool eventFilter(QObject *obj, QEvent *event) override;
KTextEditor::MainWindow *m_mainWindow;
PaletteWidget *m_palette = nullptr;
QHash<QString, QPointer<QAction>> m_actionsById;
FrecencyStore m_frecency; // persisted usage feeding the palette ranking
};
} // namespace deft
#endif

View File

@ -0,0 +1,14 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Levi Neely"
}
],
"Description": "Command palette (M-x, Alt+X): orderless fuzzy launcher over every window action, the \":\"-verb text-transform pack, and the project file/symbol switchers, ranked by frecency.",
"Icon": "search",
"License": "LGPL-2.0-or-later",
"Name": "[deft:util] palette",
"Version": "0.1.0"
}
}

View File

@ -1,32 +0,0 @@
find_package(KF6 ${KF_MIN_VERSION} COMPONENTS
TextEditor
XmlGui
CoreAddons
I18n
)
if(NOT KF6TextEditor_FOUND)
message(STATUS "KF6::TextEditor not found — skipping the Kate plugin target")
return()
endif()
# The core deft plugin: command palette (M-x), project file/symbol switchers,
# and the Acme line-editing keys + bracket/quote selection. (radial, sam, and
# plumb are now their own plugins; the util features here will split further.)
kcoreaddons_add_plugin(deft
SOURCES ollieplugin.cpp ollieplugin.h
INSTALL_NAMESPACE "kf6/ktexteditor"
)
target_include_directories(deft PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
target_link_libraries(deft PRIVATE
palette
olliecommands_lib
project_lib
KF6::TextEditor
KF6::XmlGui
KF6::CoreAddons
KF6::I18n
Qt6::Widgets
)

View File

@ -1,14 +0,0 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Levi Neely"
}
],
"Description": "Deft: menu-free, mouse-first editing — orderless fuzzy palette (M-x), radial caret menu, switchers, Plan 9 plumbing, and sam.",
"Icon": "code-context",
"License": "LGPL-2.0-or-later",
"Name": "Deft",
"Version": "0.1.0"
}
}

21
src/switch/CMakeLists.txt Normal file
View File

@ -0,0 +1,21 @@
# switch — project file/symbol switchers ([deft:util] switch).
#
# No separate library: the plugin is self-contained. The widget/frecency come
# from the palette lib, the index/bridge from project_lib. Needs KF6::TextEditor.
if(NOT KF6TextEditor_FOUND)
return()
endif()
kcoreaddons_add_plugin(deft_switch
SOURCES switchplugin.cpp switchplugin.h
INSTALL_NAMESPACE "kf6/ktexteditor"
)
target_link_libraries(deft_switch PRIVATE
palette
project_lib
KF6::TextEditor
KF6::XmlGui
KF6::CoreAddons
KF6::I18n
Qt6::Widgets
)

View File

@ -1,24 +1,17 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "ollieplugin.h"
#include "switchplugin.h"
#include "palettewidget.h"
#include "frecencystore.h"
#include "olliecommands.h"
#include "projectindex.h"
#include "symbolindex.h"
#include <KTextEditor/Application>
#include <KTextEditor/Command>
#include <KTextEditor/Document>
#include <KTextEditor/Editor>
#include <KTextEditor/MainWindow>
#include <KTextEditor/View>
#include <KActionCollection>
#include <KXMLGUIClient>
#include <KXMLGUIFactory>
#include <KLocalizedString>
#include <KPluginFactory>
@ -28,10 +21,9 @@
#include <QDir>
#include <QEvent>
#include <QFileInfo>
#include <QHash>
#include <QKeyEvent>
#include <QKeySequence>
#include <QSet>
#include <QPoint>
#include <QString>
#include <QStandardPaths>
#include <QUrl>
#include <QWidget>
@ -41,43 +33,13 @@ namespace deft
namespace
{
// Prefix marking a palette id that is a ":" command string to execute rather
// than a QAction to trigger.
const QLatin1String kCmdPrefix("ollie:cmd:");
// Prefix marking a palette id that is a project-relative file path to open in
// the file switcher.
const QLatin1String kFilePrefix("ollie:file:");
const QLatin1String kFilePrefix("deft:file:");
// Prefix marking a palette id that is a symbol location ("relpath\tline") to
// jump to in the symbol switcher.
const QLatin1String kSymbolPrefix("ollie:sym:");
// The complete (argument-free) command verbs offered in the palette. Verbs that
// need free-text arguments (pipe) stay on the command line; `case` is expanded
// into one entry per style so each is directly runnable.
struct VerbItem {
QString command; // the full command string to execute
QString label; // palette label
};
QList<VerbItem> paletteVerbs()
{
return {
{QStringLiteral("osort"), QStringLiteral("Sort Lines")},
{QStringLiteral("osort u"), QStringLiteral("Sort Lines (unique)")},
{QStringLiteral("rsort"), QStringLiteral("Reverse Lines")},
{QStringLiteral("case upper"), QStringLiteral("Case: UPPER")},
{QStringLiteral("case lower"), QStringLiteral("Case: lower")},
{QStringLiteral("case title"), QStringLiteral("Case: Title")},
{QStringLiteral("case snake"), QStringLiteral("Case: snake_case")},
{QStringLiteral("case camel"), QStringLiteral("Case: camelCase")},
{QStringLiteral("case kebab"), QStringLiteral("Case: kebab-case")},
{QStringLiteral("b64enc"), QStringLiteral("Base64 Encode")},
{QStringLiteral("b64dec"), QStringLiteral("Base64 Decode")},
{QStringLiteral("rot13"), QStringLiteral("ROT13")},
{QStringLiteral("uuid"), QStringLiteral("Insert UUID")},
};
}
const QLatin1String kSymbolPrefix("deft:sym:");
// Directory of the active document in \a mainWindow, or empty when there is no
// view, no document, or the document is untitled / not a local file.
@ -96,63 +58,56 @@ QString activeDocumentDirFor(KTextEditor::MainWindow *mainWindow)
} // namespace
// ---------------------------------------------------------------------------
// OlliePlugin
// SwitchPlugin
// ---------------------------------------------------------------------------
OlliePlugin::OlliePlugin(QObject *parent, const QVariantList &args)
SwitchPlugin::SwitchPlugin(QObject *parent, const QVariantList &args)
: KTextEditor::Plugin(parent)
{
Q_UNUSED(args);
// The ":"-verb pack registers itself with the global editor on construction.
m_commands = new OllieCommands(this);
}
OlliePlugin::~OlliePlugin() = default;
SwitchPlugin::~SwitchPlugin() = default;
QObject *OlliePlugin::createView(KTextEditor::MainWindow *mainWindow)
QObject *SwitchPlugin::createView(KTextEditor::MainWindow *mainWindow)
{
return new OllieView(this, mainWindow);
return new SwitchView(this, mainWindow);
}
// ---------------------------------------------------------------------------
// OllieView
// SwitchView
// ---------------------------------------------------------------------------
OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
SwitchView::SwitchView(SwitchPlugin *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.
// switcher 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")));
QStringLiteral("deft/switch.frecency.json")));
m_frecency.load();
// The three launcher "doors". Their shortcuts are NOT registered as
// The two launcher "doors". Their shortcuts are NOT registered as
// QAction/ApplicationShortcut: Kate (and other plugins) already claim many
// Alt+<letter> combos, and a registered duplicate makes Qt refuse to fire
// either, reporting an "ambiguous shortcut". We therefore register NO
// shortcut here and instead intercept the key in eventFilter() (the same
// technique used for the Acme line-editing keys), which cannot be
// ambiguous because nothing is registered. The QActions are kept only for
// their objectName/label (command-palette harvest, radial escape-hatch).
auto *openAction = new QAction(i18n("Command Palette (M-x) [Alt+X]"), this);
openAction->setObjectName(QStringLiteral("deft_command_palette"));
connect(openAction, &QAction::triggered, this, &OllieView::showCommandPalette);
// shortcut here and instead intercept the key in eventFilter(), which
// cannot be ambiguous because nothing is registered. The QActions are kept
// for their objectName/label so sibling plugins (palette, radial) can
// trigger them by objectName and so they stay discoverable.
auto *gotoFileAction = new QAction(i18n("Go to File (project) [Alt+P]"), this);
gotoFileAction->setObjectName(QStringLiteral("deft_goto_file"));
connect(gotoFileAction, &QAction::triggered, this, &OllieView::showFileSwitcher);
connect(gotoFileAction, &QAction::triggered, this, &SwitchView::showFileSwitcher);
auto *gotoSymbolAction = new QAction(i18n("Go to Symbol (project) [Alt+G]"), this);
gotoSymbolAction->setObjectName(QStringLiteral("deft_goto_symbol"));
connect(gotoSymbolAction, &QAction::triggered, this, &OllieView::showSymbolSwitcher);
connect(gotoSymbolAction, &QAction::triggered, this, &SwitchView::showSymbolSwitcher);
// Keep them discoverable as window actions (no shortcut attached).
if (QWidget *w = m_mainWindow->window()) {
w->addAction(openAction);
w->addAction(gotoFileAction);
w->addAction(gotoSymbolAction);
}
@ -181,140 +136,44 @@ OllieView::OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow)
[this]() { m_indexer.invalidateAll(); });
}
// Application-wide filter for the Acme line-editing keys, the launcher
// doors (Alt+X/P/G), and the Acme mouse chords, catching events no matter
// which internal child of the view actually receives them.
// Application-wide filter for the launcher doors (Alt+P/Alt+G), catching
// the key no matter which internal child of the view actually receives it.
qApp->installEventFilter(this);
}
OllieView::~OllieView()
SwitchView::~SwitchView()
{
delete m_palette;
delete m_filePalette;
delete m_symbolPalette;
}
QList<OllieView::ActionEntry> OllieView::collectActions() const
void SwitchView::recordUsage(const QString &id)
{
QList<ActionEntry> entries;
KXMLGUIFactory *factory = m_mainWindow->guiFactory();
if (!factory) {
return entries;
if (id.isEmpty()) {
return;
}
// Dedup by pointer identity: the same QAction is frequently reachable
// through several GUI clients. Those are true duplicates and must appear
// only once.
QSet<QAction *> seen;
const QList<KXMLGUIClient *> clients = factory->clients();
for (KXMLGUIClient *client : clients) {
KActionCollection *coll = client->actionCollection();
if (!coll) {
continue;
}
const QString component = coll->componentDisplayName();
const QList<QAction *> actions = coll->actions();
for (QAction *action : actions) {
if (!action || action->isSeparator()) {
continue;
}
if (seen.contains(action)) {
continue;
}
// Only offer things the user could actually invoke now.
if (!action->isEnabled() || !action->isVisible()) {
continue;
}
const QString label = action->text().remove(QLatin1Char('&')).trimmed();
if (label.isEmpty()) {
continue;
}
seen.insert(action);
ActionEntry e;
e.action = action;
e.label = label;
e.component = component;
e.shortcut = action->shortcut().toString(QKeySequence::NativeText);
entries.push_back(e);
}
}
// Distinct actions can share a visible label (e.g. different plugins each
// offering "Reload"). These are NOT duplicates — never drop them. Instead
// guarantee every visible row is distinguishable by enriching colliding
// labels through a fallback chain of discriminators, and give each entry a
// unique id so activation always triggers the intended action.
QHash<QString, int> labelCounts;
for (const ActionEntry &e : entries) {
++labelCounts[e.label];
}
// First pass: for any shared label, append the strongest discriminator
// available (component, else objectName, else shortcut).
for (ActionEntry &e : entries) {
if (labelCounts.value(e.label) <= 1) {
continue;
}
const QString objName = e.action ? e.action->objectName() : QString();
QString disc;
if (!e.component.isEmpty()) {
disc = e.component;
} else if (!objName.isEmpty()) {
disc = objName;
} else if (!e.shortcut.isEmpty()) {
disc = e.shortcut;
}
if (!disc.isEmpty()) {
e.label = e.label + QStringLiteral(" — ") + disc;
}
}
// Second pass: if any labels are STILL identical (same label and same
// discriminator), append objectName/shortcut, then a numeric suffix, so no
// two rows ever look the same.
QHash<QString, int> stillColliding;
for (const ActionEntry &e : entries) {
++stillColliding[e.label];
}
QHash<QString, int> dupSeq;
for (ActionEntry &e : entries) {
if (stillColliding.value(e.label) <= 1) {
continue;
}
const QString objName = e.action ? e.action->objectName() : QString();
if (!objName.isEmpty() && !e.label.contains(objName)) {
e.label += QStringLiteral(" [") + objName + QLatin1Char(']');
} else if (!e.shortcut.isEmpty() && !e.label.contains(e.shortcut)) {
e.label += QStringLiteral(" (") + e.shortcut + QLatin1Char(')');
} else {
// Last resort: number the indistinguishable rows 1..n.
const int n = ++dupSeq[e.label];
e.label += QStringLiteral(" (") + QString::number(n) + QLatin1Char(')');
}
}
// Assign unique, reasonably stable ids (independent of the display label).
QHash<QString, int> idSeq;
for (ActionEntry &e : entries) {
const QString objName = e.action ? e.action->objectName() : QString();
QString base = e.component;
if (!base.isEmpty()) {
base += QLatin1Char('/');
}
base += objName.isEmpty() ? e.label : objName;
// Guarantee uniqueness even if base still collides.
const int n = idSeq[base]++;
e.id = (n == 0) ? base : base + QStringLiteral("#") + QString::number(n);
}
return entries;
m_frecency.bump(id, QDateTime::currentSecsSinceEpoch());
m_frecency.save();
}
void OllieView::positionPalette(PaletteWidget *palette)
QString SwitchView::activeDocumentDir() const
{
return activeDocumentDirFor(m_mainWindow);
}
QString SwitchView::currentProjectRoot() const
{
// Precedence: Kate's project plugin (if a project is loaded) wins; else
// discovery from the active document, with cwd as the final fallback
// inside resolveRoot().
const QString bridgeDir = m_projectBridge.baseDir();
if (!bridgeDir.isEmpty() && QDir(bridgeDir).exists()) {
return QDir(bridgeDir).absolutePath();
}
return ProjectIndex::resolveRoot(QString(), activeDocumentDir());
}
void SwitchView::positionPalette(PaletteWidget *palette)
{
QWidget *w = m_mainWindow->window();
if (!w) {
@ -328,133 +187,8 @@ void OllieView::positionPalette(PaletteWidget *palette)
palette->move(w->mapToGlobal(QPoint((w->width() - width) / 2, w->height() / 6)));
}
void OllieView::showCommandPalette()
{
if (!m_palette) {
m_palette = new PaletteWidget(m_mainWindow->window());
connect(m_palette, &PaletteWidget::activatedId, this, &OllieView::runAction);
}
const QList<ActionEntry> entries = collectActions();
m_actionsById.clear();
QList<PaletteItem> items;
items.reserve(entries.size() + 16);
for (const ActionEntry &e : entries) {
m_actionsById.insert(e.id, e.action);
PaletteItem item;
item.id = e.id;
item.label = e.label;
item.group = e.shortcut; // shortcut shown as the trailing hint
items.push_back(item);
}
// Also offer the ":"-verb pack as directly-runnable palette entries.
for (const VerbItem &v : paletteVerbs()) {
PaletteItem item;
item.id = kCmdPrefix + v.command;
item.label = v.label;
item.group = QStringLiteral(":") + v.command;
items.push_back(item);
}
// Offer the project file switcher as a palette entry too (also on Alt+P).
{
PaletteItem item;
item.id = QStringLiteral("ollie:switch:file");
item.label = i18n("Go to File (project)");
item.group = QStringLiteral("Alt+P");
items.push_back(item);
}
// Offer the project symbol switcher as a palette entry too (also on Alt+G).
{
PaletteItem item;
item.id = QStringLiteral("ollie:switch:symbol");
item.label = i18n("Go to Symbol (project)");
item.group = QStringLiteral("Alt+G");
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);
// Span the full window width, anchored near the top.
positionPalette(m_palette);
m_palette->activate();
}
void OllieView::recordUsage(const QString &id)
{
if (id.isEmpty()) {
return;
}
m_frecency.bump(id, QDateTime::currentSecsSinceEpoch());
m_frecency.save();
}
QString OllieView::activeDocumentDir() const
{
return activeDocumentDirFor(m_mainWindow);
}
QString OllieView::currentProjectRoot() const
{
// Precedence: Kate's project plugin (if a project is loaded) wins; else
// discovery from the active document, with cwd as the final fallback
// inside resolveRoot().
const QString bridgeDir = m_projectBridge.baseDir();
if (!bridgeDir.isEmpty() && QDir(bridgeDir).exists()) {
return QDir(bridgeDir).absolutePath();
}
return ProjectIndex::resolveRoot(QString(), activeDocumentDir());
}
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.
if (actionId.startsWith(kCmdPrefix)) {
const QString command = actionId.mid(QString(kCmdPrefix).size());
if (KTextEditor::View *view = m_mainWindow->activeView()) {
QString msg;
OllieCommands::runVerb(view, command, msg);
}
return;
}
// The project file switcher, surfaced as a palette entry.
if (actionId == QLatin1String("ollie:switch:file")) {
showFileSwitcher();
return;
}
// The project symbol switcher, surfaced as a palette entry.
if (actionId == QLatin1String("ollie:switch:symbol")) {
showSymbolSwitcher();
return;
}
QPointer<QAction> action = m_actionsById.value(actionId);
if (action) {
action->trigger();
}
}
QList<PaletteItem> OllieView::buildFileItems(const QString &root,
const QStringList &files) const
QList<PaletteItem> SwitchView::buildFileItems(const QString &root,
const QStringList &files) const
{
const qint64 now = QDateTime::currentSecsSinceEpoch();
QList<PaletteItem> items;
@ -475,7 +209,7 @@ QList<PaletteItem> OllieView::buildFileItems(const QString &root,
return items;
}
void OllieView::showFileSwitcher()
void SwitchView::showFileSwitcher()
{
if (!m_filePalette) {
m_filePalette = new PaletteWidget(m_mainWindow->window());
@ -510,7 +244,7 @@ void OllieView::showFileSwitcher()
buildFileItems(m_fileSwitcherRoot, m_indexer.cachedFiles(m_fileSwitcherRoot)));
} else {
PaletteItem loading;
loading.id = QStringLiteral("ollie:loading");
loading.id = QStringLiteral("deft:loading");
loading.label = i18n("Indexing project…");
m_filePalette->setItems({loading});
}
@ -522,7 +256,7 @@ void OllieView::showFileSwitcher()
m_indexer.requestFiles(m_fileSwitcherRoot);
}
void OllieView::openProjectFile(const QString &relativePath)
void SwitchView::openProjectFile(const QString &relativePath)
{
if (relativePath.isEmpty() || m_fileSwitcherRoot.isEmpty()) {
return;
@ -534,8 +268,8 @@ void OllieView::openProjectFile(const QString &relativePath)
}
}
QList<PaletteItem> OllieView::buildSymbolItems(const QString &root,
const QList<Symbol> &symbols) const
QList<PaletteItem> SwitchView::buildSymbolItems(const QString &root,
const QList<Symbol> &symbols) const
{
const qint64 now = QDateTime::currentSecsSinceEpoch();
QList<PaletteItem> items;
@ -572,7 +306,7 @@ QList<PaletteItem> OllieView::buildSymbolItems(const QString &root,
return items;
}
void OllieView::showSymbolSwitcher()
void SwitchView::showSymbolSwitcher()
{
if (!m_symbolPalette) {
m_symbolPalette = new PaletteWidget(m_mainWindow->window());
@ -605,7 +339,7 @@ void OllieView::showSymbolSwitcher()
m_indexer.cachedSymbols(m_symbolSwitcherRoot)));
} else {
PaletteItem loading;
loading.id = QStringLiteral("ollie:loading");
loading.id = QStringLiteral("deft:loading");
loading.label = i18n("Indexing symbols…");
m_symbolPalette->setItems({loading});
}
@ -616,7 +350,7 @@ void OllieView::showSymbolSwitcher()
m_indexer.requestSymbols(m_symbolSwitcherRoot);
}
void OllieView::goToSymbol(const QString &locationId)
void SwitchView::goToSymbol(const QString &locationId)
{
if (m_symbolSwitcherRoot.isEmpty()) {
return;
@ -644,19 +378,18 @@ void OllieView::goToSymbol(const QString &locationId)
}
}
bool OllieView::eventFilter(QObject *obj, QEvent *event)
bool SwitchView::eventFilter(QObject *obj, QEvent *event)
{
// Launcher doors: Alt+X (M-x), Alt+P (Go to File), Alt+G (Go to Symbol).
// Handled here rather than via QAction shortcuts so they can never clash
// with Kate's own Alt+<letter> bindings (which produced "ambiguous
// shortcut" and swallowed the key). Scope to this plugin's window so we do
// not hijack the combo in other Kate main windows.
// Launcher doors: Alt+P (Go to File), Alt+G (Go to Symbol). Handled here
// rather than via QAction shortcuts so they can never clash with Kate's own
// Alt+<letter> bindings (which produced "ambiguous shortcut" and swallowed
// the key). Scope to this plugin's window so we do not hijack the combo in
// other Kate main windows.
if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
const bool altOnly =
(ke->modifiers() & Qt::KeyboardModifierMask) == Qt::AltModifier;
if (altOnly
&& (ke->key() == Qt::Key_X || ke->key() == Qt::Key_P || ke->key() == Qt::Key_G)) {
if (altOnly && (ke->key() == Qt::Key_P || ke->key() == Qt::Key_G)) {
auto *w = qobject_cast<QWidget *>(obj);
QWidget *ourWindow = m_mainWindow->window();
const bool ours = w && ourWindow && ourWindow->isAncestorOf(w);
@ -666,7 +399,6 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
return true;
}
switch (ke->key()) {
case Qt::Key_X: showCommandPalette(); break;
case Qt::Key_P: showFileSwitcher(); break;
case Qt::Key_G: showSymbolSwitcher(); break;
default: break;
@ -683,7 +415,7 @@ bool OllieView::eventFilter(QObject *obj, QEvent *event)
// K_PLUGIN_FACTORY generates the factory and the moc-level registration. The
// matching .moc include is required in this translation unit.
K_PLUGIN_FACTORY_WITH_JSON(OlliePluginFactory, "ollieplugin.json",
registerPlugin<deft::OlliePlugin>();)
K_PLUGIN_FACTORY_WITH_JSON(SwitchPluginFactory, "switchplugin.json",
registerPlugin<deft::SwitchPlugin>();)
#include "ollieplugin.moc"
#include "switchplugin.moc"

View File

@ -1,15 +1,19 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* The Ollie command-palette plugin for Kate / KTextEditor.
* The Deft project switcher plugin for Kate / KTextEditor ([deft:util] switch).
*
* OlliePlugin is the KTextEditor::Plugin entry point. For every MainWindow the
* host creates an OllieView, which registers the "Command Palette (M-x)" action
* (Alt+X) and, when triggered, gathers every QAction currently exposed by the
* window's GUI clients into a PaletteWidget for orderless fuzzy selection.
* SwitchPlugin is the KTextEditor::Plugin entry point. For every MainWindow the
* host creates a SwitchView, which registers two launcher "doors":
* - Go to File (Alt+P, objectName "deft_goto_file")
* - Go to Symbol (Alt+G, objectName "deft_goto_symbol")
* Each opens a PaletteWidget populated from an asynchronous, cached project
* index (files via VCS/discovery, symbols via ctags), scoped to Kate's loaded
* project when present. The doors are exposed as window actions so sibling deft
* plugins (palette, radial) can trigger them by objectName.
*/
#ifndef DEFT_OLLIEPLUGIN_H
#define DEFT_OLLIEPLUGIN_H
#ifndef DEFT_SWITCHPLUGIN_H
#define DEFT_SWITCHPLUGIN_H
#include <KTextEditor/Plugin>
@ -19,51 +23,42 @@
#include "palettemodel.h" // PaletteItem
#include <QObject>
#include <QPointer>
#include <QHash>
#include <QList>
#include <QString>
class QAction;
class QKeyEvent;
class QEvent;
namespace KTextEditor
{
class MainWindow;
class View;
class Document;
}
namespace deft
{
class PaletteWidget;
class OllieCommands;
class OlliePlugin : public KTextEditor::Plugin
class SwitchPlugin : public KTextEditor::Plugin
{
Q_OBJECT
public:
explicit OlliePlugin(QObject *parent, const QVariantList &args = {});
~OlliePlugin() override;
explicit SwitchPlugin(QObject *parent, const QVariantList &args = {});
~SwitchPlugin() override;
QObject *createView(KTextEditor::MainWindow *mainWindow) override;
private:
OllieCommands *m_commands = nullptr; // ":"-verb pack, registered once globally
};
class OllieView : public QObject
class SwitchView : public QObject
{
Q_OBJECT
public:
OllieView(OlliePlugin *plugin, KTextEditor::MainWindow *mainWindow);
~OllieView() override;
SwitchView(SwitchPlugin *plugin, KTextEditor::MainWindow *mainWindow);
~SwitchView() override;
private Q_SLOTS:
void showCommandPalette();
void runAction(const QString &actionId);
void showFileSwitcher();
void openProjectFile(const QString &relativePath);
void showSymbolSwitcher();
@ -71,7 +66,7 @@ private Q_SLOTS:
private:
// Record one activation of \a id and persist. Feeds the frecency bonus so
// habitual choices float up in every palette.
// habitual choices float up in the switchers.
void recordUsage(const QString &id);
// The effective project root: Kate's loaded project (via the bridge) when
@ -89,32 +84,16 @@ private:
// small margin), anchored near the top.
void positionPalette(PaletteWidget *palette);
// One harvested, runnable action with the provenance needed to build a
// unique id and to disambiguate labels shared by several commands.
struct ActionEntry {
QPointer<QAction> action;
QString label; // visible text, ampersands stripped
QString component; // owning collection's display name (provenance)
QString shortcut; // current shortcut, native text
QString id; // unique, stable key used on activation
};
// Collect every unique, enabled, visible QAction reachable from the
// window's GUI factory clients, deduplicated by pointer identity.
QList<ActionEntry> collectActions() const;
// The application event filter: intercepts the launcher-door keys
// (Alt+X/P/G) so they can never clash with Kate's own Alt bindings.
// (Alt+P/Alt+G) so they can never clash with Kate's own Alt bindings.
bool eventFilter(QObject *obj, QEvent *event) override;
KTextEditor::MainWindow *m_mainWindow;
PaletteWidget *m_palette = nullptr;
PaletteWidget *m_filePalette = nullptr; // project "Go to File" switcher
QString m_fileSwitcherRoot; // project root for the open switcher
PaletteWidget *m_symbolPalette = nullptr; // project "Go to Symbol" switcher
QString m_symbolSwitcherRoot; // project root for the symbol switcher
QHash<QString, QPointer<QAction>> m_actionsById;
FrecencyStore m_frecency; // persisted usage feeding the palette ranking
FrecencyStore m_frecency; // persisted usage feeding the switcher ranking
KateProjectBridge m_projectBridge; // read-only view of Kate's project plugin
ProjectIndexer m_indexer; // async, cached file/symbol indexing
};

View File

@ -0,0 +1,14 @@
{
"KPlugin": {
"Authors": [
{
"Name": "Levi Neely"
}
],
"Description": "Project switchers: Go to File (Alt+P) and Go to Symbol (Alt+G), fuzzy-matched over an async, cached index of the active Kate project (VCS/discovery for files, ctags for symbols), ranked by frecency.",
"Icon": "go-jump",
"License": "LGPL-2.0-or-later",
"Name": "[deft:util] switch",
"Version": "0.1.0"
}
}