Commands: register verbs on the ':' command line (fix name collision)

Root cause: the KTextEditor command registry aborts registering a
Command object entirely if ANY of its names collides with an existing
command. Our list led with 'sort', a Kate built-in, so the collision
silently dropped ALL our verbs from queryCommand/commandList — which is
why ':' never saw them (the palette worked only because it calls runVerb
directly).

Fix: register the sort verb as 'osort'. runVerb/help accept both 'sort'
and 'osort', so the palette is unchanged; the ':' line now resolves every
verb (osort/rsort/case/b64enc/b64dec/rot13/uuid/pipe) to our object.

Add test_ollieregistration asserting all verbs register at the global
editor and guarding against reclaiming 'sort'. Palette verb hints updated
to 'osort'.
This commit is contained in:
Levi Neely 2026-10-08 09:42:13 +02:00
parent 219ad8e56c
commit 5b31bf6e40
6 changed files with 91 additions and 14 deletions

View File

@ -178,21 +178,29 @@ 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 (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
- `:`-verb pack — DONE (palette **and** `:` command line). `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).
- They are **also reachable from Kate's `:` command line**. `OllieCommands`
is a `KTextEditor::Command`; its base constructor auto-registers its names
at the global `Editor::instance()`, and `exec()` forwards to `runVerb`.
- ROOT-CAUSE FIX of the earlier "not reachable from `:`" problem: the
KTextEditor registry **aborts registering a Command object entirely if any
one of its names collides** with an already-registered command. Our list
led with `sort`, which Kate ships as a built-in, so the collision silently
dropped **all** our verbs. The sort verb is now registered as **`osort`**
(runVerb still accepts the bare `sort`, so palette entries are unchanged).
`test_ollieregistration` asserts every verb resolves to our object via
`Editor::queryCommand`/`commandList` and guards against reclaiming `sort`.
- 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"): DONE (core + switcher).
`src/project/projectindex.{h,cpp}` (`project_lib`, Qt::Core only, 12 unit
tests):

View File

@ -20,4 +20,9 @@ if(Qt6Test_FOUND)
target_link_libraries(test_olliecommands_live PRIVATE
olliecommands_lib Qt6::Test KF6::TextEditor)
add_test(NAME olliecommands_live COMMAND test_olliecommands_live)
add_executable(test_ollieregistration test_ollieregistration.cpp)
target_link_libraries(test_ollieregistration PRIVATE
olliecommands_lib Qt6::Test Qt6::Widgets KF6::TextEditor)
add_test(NAME ollieregistration COMMAND test_ollieregistration)
endif()

View File

@ -21,7 +21,13 @@ using namespace texttransforms;
QStringList OllieCommands::supportedCmds()
{
return {
QStringLiteral("sort"),
// NOTE: "sort" is a built-in Kate command. The KTextEditor command
// registry aborts registering a Command object entirely if ANY of its
// names collides with an existing one, so claiming "sort" would silently
// drop ALL of our verbs from the ":" command line. We therefore register
// the sort verb as "osort" (runVerb still accepts the bare "sort" so the
// palette entries are unaffected).
QStringLiteral("osort"),
QStringLiteral("rsort"),
QStringLiteral("case"),
QStringLiteral("b64enc"),
@ -88,7 +94,7 @@ bool OllieCommands::runVerb(KTextEditor::View *view, const QString &cmd, QString
const Target t = currentTarget(view);
if (verb == QLatin1String("sort")) {
if (verb == QLatin1String("sort") || verb == QLatin1String("osort")) {
const bool unique = args.contains(QLatin1String("u"));
const bool ci = args.contains(QLatin1String("i"));
applyResult(view, t, sortLines(t.text, unique, ci));
@ -173,8 +179,8 @@ bool OllieCommands::runVerb(KTextEditor::View *view, const QString &cmd, QString
bool OllieCommands::help(KTextEditor::View *, const QString &cmd, QString &msg)
{
const QString verb = cmd.section(QLatin1Char(' '), 0, 0);
if (verb == QLatin1String("sort")) {
msg = i18n("sort [u] [i] — sort selected lines; u=unique, i=case-insensitive");
if (verb == QLatin1String("sort") || verb == QLatin1String("osort")) {
msg = i18n("osort [u] [i] — sort selected lines; u=unique, i=case-insensitive");
} else if (verb == QLatin1String("rsort")) {
msg = i18n("rsort — reverse the order of selected lines");
} else if (verb == QLatin1String("case")) {

View File

@ -5,7 +5,8 @@
* the editor. Each verb transforms the current selection in place (or the whole
* document when nothing is selected). Verbs:
*
* sort [u] [i] sort selected lines (u = unique, i = case-insensitive)
* osort [u] [i] sort selected lines (u = unique, i = case-insensitive)
* (registered as "osort" because Kate already ships "sort")
* rsort reverse the order of selected lines
* case <style> upper|lower|title|snake|camel|kebab
* b64enc / b64dec base64 encode / decode

View File

@ -0,0 +1,57 @@
/*
* SPDX-License-Identifier: LGPL-2.0-or-later
*
* Verifies that OllieCommands registers its verbs at the global
* KTextEditor::Editor instance so they are reachable from Kate's ":" command
* line.
*
* Background: the KTextEditor command registry aborts registering a Command
* object entirely if ANY of its names collides with an already-registered one.
* Kate ships a built-in "sort", so an earlier version that claimed "sort"
* silently dropped ALL of our verbs. We now register the sort verb as "osort";
* this test guards against reintroducing a colliding name.
*/
#include "olliecommands.h"
#include <KTextEditor/Command>
#include <KTextEditor/Editor>
#include <QTest>
using namespace katecustom;
class TestOllieRegistration : public QObject
{
Q_OBJECT
private Q_SLOTS:
void verbsRegisterAtGlobalEditor()
{
auto *cmds = new OllieCommands();
auto *ed = KTextEditor::Editor::instance();
QVERIFY(ed);
// Every verb we register must resolve back to OUR command object and be
// present in the editor's command list.
const QStringList ours = {
QStringLiteral("osort"), QStringLiteral("rsort"),
QStringLiteral("case"), QStringLiteral("b64enc"),
QStringLiteral("b64dec"), QStringLiteral("rot13"),
QStringLiteral("uuid"), QStringLiteral("pipe"),
};
const QStringList list = ed->commandList();
for (const QString &v : ours) {
QVERIFY2(list.contains(v), qPrintable(QStringLiteral("missing from commandList: %1").arg(v)));
QCOMPARE(ed->queryCommand(v), static_cast<KTextEditor::Command *>(cmds));
}
// We must NOT claim "sort": it collides with Kate's built-in, which
// would abort registration of the whole object. queryCommand("sort")
// therefore resolves to Kate's command (or null), never ours.
QVERIFY(ed->queryCommand(QStringLiteral("sort")) != static_cast<KTextEditor::Command *>(cmds));
delete cmds;
}
};
QTEST_MAIN(TestOllieRegistration)
#include "test_ollieregistration.moc"

View File

@ -154,8 +154,8 @@ struct VerbItem {
QList<VerbItem> paletteVerbs()
{
return {
{QStringLiteral("sort"), QStringLiteral("Sort Lines")},
{QStringLiteral("sort u"), QStringLiteral("Sort Lines (unique)")},
{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")},