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:
parent
ec2802b11d
commit
d8f0225178
|
|
@ -7,5 +7,5 @@ add_subdirectory(sam)
|
|||
if(KF6TextEditor_FOUND)
|
||||
add_subdirectory(acme)
|
||||
add_subdirectory(commands)
|
||||
add_subdirectory(switch)
|
||||
endif()
|
||||
add_subdirectory(plugin)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue