diff --git a/docs/PLAN.md b/docs/PLAN.md index 9b8d4e2..47e60cb 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -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 ` runs `sh -c ` 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 ` runs + `sh -c ` 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. diff --git a/src/commands/CMakeLists.txt b/src/commands/CMakeLists.txt index a7ef504..5352c59 100644 --- a/src/commands/CMakeLists.txt +++ b/src/commands/CMakeLists.txt @@ -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() diff --git a/src/commands/olliecommands.cpp b/src/commands/olliecommands.cpp index 834d421..db2505b 100644 --- a/src/commands/olliecommands.cpp +++ b/src/commands/olliecommands.cpp @@ -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; diff --git a/src/commands/olliecommands.h b/src/commands/olliecommands.h index 43c4bcb..8445d43 100644 --- a/src/commands/olliecommands.h +++ b/src/commands/olliecommands.h @@ -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(); }; diff --git a/src/commands/test_olliecommands_live.cpp b/src/commands/test_olliecommands_live.cpp new file mode 100644 index 0000000..b46f923 --- /dev/null +++ b/src/commands/test_olliecommands_live.cpp @@ -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 +#include +#include + +#include + +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" diff --git a/src/plugin/ollieplugin.cpp b/src/plugin/ollieplugin.cpp index 0a88bb0..bba3943 100644 --- a/src/plugin/ollieplugin.cpp +++ b/src/plugin/ollieplugin.cpp @@ -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)) { - QString msg; - c->exec(view, command, msg); - } + if (KTextEditor::View *view = m_mainWindow->activeView()) { + QString msg; + OllieCommands::runVerb(view, command, msg); } return; }