M5: run :-verbs directly via OllieCommands::runVerb, fix palette no-op

queryCommand/commandList do not see our auto-registered KTextEditor::Command
on this build, so the palette now calls OllieCommands::runVerb directly.
Rewrite the live test to exercise runVerb against a real doc/view (case,
rot13, sort, uuid, base64 round-trip). Document the command-line limitation.
This commit is contained in:
Levi Neely 2026-10-07 18:35:40 +02:00
parent e1bf0c9432
commit a69e471e08
6 changed files with 140 additions and 21 deletions

View File

@ -171,18 +171,21 @@ Therefore the palette is replaced, not extended, and the replacement is built on
consuming it — no registered shortcut, so no ambiguity, no per-view sweeping.
### M5 — Command vocabulary + project model + switchers — IN PROGRESS
- `:`-verb pack — DONE. `src/commands/`: `texttransforms.{h,cpp}` (pure,
unit-tested: sort/rsort, case upper|lower|title|snake|camel|kebab, base64
enc/dec, rot13, uuid) + `olliecommands.{h,cpp}` (a `KTextEditor::Command`
registering `sort`, `rsort`, `case`, `b64enc`, `b64dec`, `rot13`, `uuid`,
`pipe`). Verbs transform the selection in place (or the whole document when
nothing is selected); `pipe <shell>` runs `sh -c <shell>` with the selection
on stdin and replaces it with stdout. 15 transform tests. The command object
is created once by `OlliePlugin` (auto-registers globally).
- Also surfaced in the M-x palette as directly-runnable entries (`case` is
expanded into one entry per style). The palette id is prefixed `ollie:cmd:`
and executed via `Editor::queryCommand(verb)->exec(view, cmd, msg)`. The
free-text `pipe` verb stays on the command line only.
- `:`-verb pack — DONE (palette path). `src/commands/`: `texttransforms.{h,cpp}`
(pure, unit-tested: sort/rsort, case upper|lower|title|snake|camel|kebab,
base64 enc/dec, rot13, uuid) + `olliecommands.{h,cpp}`. The verb logic lives in
`OllieCommands::runVerb(view, cmd, msg)` and transforms the selection in place
(or the whole document when nothing is selected); `pipe <shell>` runs
`sh -c <shell>` with the selection on stdin.
- Verbs are invoked from the **M-x palette** (entries prefixed `ollie:cmd:`,
executed by calling `OllieCommands::runVerb` directly). `case` is expanded
into one entry per style; `pipe` is palette-omitted (needs free text).
- A live test (`test_olliecommands_live`, headless KTextEditor doc/view)
proves the transforms mutate a real buffer.
- KNOWN LIMITATION: `KTextEditor::Command` auto-registration via the base
constructor is NOT visible to `Editor::queryCommand()`/`commandList()` on
this build, so the verbs are not (yet) reachable from Kate's `:` command
line — only via the palette. Proper command-line registration is a follow-up.
- Project = directory ("open folder == open project"): TODO.
- Fuzzy file / symbol switchers scoped to the folder: TODO.

View File

@ -15,4 +15,9 @@ if(Qt6Test_FOUND)
add_executable(test_texttransforms test_texttransforms.cpp texttransforms.cpp texttransforms.h)
target_link_libraries(test_texttransforms PRIVATE Qt6::Core Qt6::Test)
add_test(NAME texttransforms COMMAND test_texttransforms)
add_executable(test_olliecommands_live test_olliecommands_live.cpp)
target_link_libraries(test_olliecommands_live PRIVATE
olliecommands_lib Qt6::Test KF6::TextEditor)
add_test(NAME olliecommands_live COMMAND test_olliecommands_live)
endif()

View File

@ -63,6 +63,11 @@ void applyResult(KTextEditor::View *view, const Target &t, const QString &result
bool OllieCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg,
const KTextEditor::Range &)
{
return runVerb(view, cmd, msg);
}
bool OllieCommands::runVerb(KTextEditor::View *view, const QString &cmd, QString &msg)
{
if (!view) {
return false;

View File

@ -33,6 +33,14 @@ public:
const KTextEditor::Range &range = KTextEditor::Range::invalid()) override;
bool help(KTextEditor::View *view, const QString &cmd, QString &msg) override;
/*!
* Execute a verb command string (e.g. "case upper", "sort u") directly on
* \a view, without going through the editor's command registry. Used by the
* command palette, whose entries cannot rely on Command auto-registration
* being visible to Editor::queryCommand(). Returns true on success.
*/
static bool runVerb(KTextEditor::View *view, const QString &cmd, QString &msg);
private:
static QStringList supportedCmds();
};

View File

@ -0,0 +1,101 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* Live test: run the ":"-verbs directly (OllieCommands::runVerb) on a real
* KTextEditor Document/View and confirm the buffer is mutated. This is the path
* the command palette uses, independent of the editor's command registry.
* Requires a (headless) QApplication; run with QT_QPA_PLATFORM=offscreen.
*/
#include "olliecommands.h"
#include <KTextEditor/Document>
#include <KTextEditor/Editor>
#include <KTextEditor/View>
#include <QTest>
using namespace katecustom;
class TestOllieCommandsLive : public QObject
{
Q_OBJECT
KTextEditor::Editor *m_editor = nullptr;
KTextEditor::View *makeView(const QString &text, KTextEditor::Document *&docOut)
{
docOut = m_editor->createDocument(this);
docOut->setText(text);
return docOut->createView(nullptr);
}
private Q_SLOTS:
void initTestCase()
{
m_editor = KTextEditor::Editor::instance();
QVERIFY(m_editor);
}
void caseUpperWholeDocument()
{
KTextEditor::Document *doc = nullptr;
KTextEditor::View *v = makeView(QStringLiteral("helloWorld"), doc);
QString msg;
QVERIFY2(OllieCommands::runVerb(v, QStringLiteral("case upper"), msg), qPrintable(msg));
QCOMPARE(doc->text(), QStringLiteral("HELLOWORLD"));
delete v;
delete doc;
}
void rot13OnSelection()
{
KTextEditor::Document *doc = nullptr;
KTextEditor::View *v = makeView(QStringLiteral("abc XYZ"), doc);
v->setSelection(doc->documentRange());
QString msg;
QVERIFY(OllieCommands::runVerb(v, QStringLiteral("rot13"), msg));
QCOMPARE(doc->text(), QStringLiteral("nop KLM"));
delete v;
delete doc;
}
void sortLinesWholeDocument()
{
KTextEditor::Document *doc = nullptr;
KTextEditor::View *v = makeView(QStringLiteral("banana\napple\ncherry"), doc);
QString msg;
QVERIFY(OllieCommands::runVerb(v, QStringLiteral("sort"), msg));
QCOMPARE(doc->text(), QStringLiteral("apple\nbanana\ncherry"));
delete v;
delete doc;
}
void uuidInsertsAtCursor()
{
KTextEditor::Document *doc = nullptr;
KTextEditor::View *v = makeView(QString(), doc);
QString msg;
QVERIFY(OllieCommands::runVerb(v, QStringLiteral("uuid"), msg));
QCOMPARE(doc->text().size(), 36); // 8-4-4-4-12 + 4 hyphens
delete v;
delete doc;
}
void base64RoundTripOnSelection()
{
KTextEditor::Document *doc = nullptr;
KTextEditor::View *v = makeView(QStringLiteral("hello"), doc);
v->setSelection(doc->documentRange());
QString msg;
QVERIFY(OllieCommands::runVerb(v, QStringLiteral("b64enc"), msg));
QCOMPARE(doc->text(), QStringLiteral("aGVsbG8="));
v->setSelection(doc->documentRange());
QVERIFY(OllieCommands::runVerb(v, QStringLiteral("b64dec"), msg));
QCOMPARE(doc->text(), QStringLiteral("hello"));
delete v;
delete doc;
}
};
QTEST_MAIN(TestOllieCommandsLive)
#include "test_olliecommands_live.moc"

View File

@ -375,17 +375,14 @@ void OllieView::showCommandPalette()
void OllieView::runAction(const QString &actionId)
{
// A ":"-verb entry: execute the command on the active view.
// 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());
KTextEditor::View *view = m_mainWindow->activeView();
KTextEditor::Editor *editor = KTextEditor::Editor::instance();
if (view && editor) {
const QString verb = command.section(QLatin1Char(' '), 0, 0);
if (KTextEditor::Command *c = editor->queryCommand(verb)) {
if (KTextEditor::View *view = m_mainWindow->activeView()) {
QString msg;
c->exec(view, command, msg);
}
OllieCommands::runVerb(view, command, msg);
}
return;
}