kate-deft/docs/PLAN.md

7.0 KiB

Plan — kate-custom

Goal

Build a suite of Kate plugins that make the editor usable without menus, tuned to one specific operator profile:

  • mouse-heavy, with the mouse used at the caret rather than at the menubar
  • one-handed keyboard typing
  • no complex chords; single modifier at most
  • compact keyboard where F-keys/nav clusters sit behind a layer toggle
  • long Emacs history and some Sublime Text; M-x and type-to-filter palettes are familiar and welcome

The spine of the suite is a single action registry reached through three interchangeable doors: M-x (keyboard), a radial caret menu (mouse), and the : command line (optional). Every feature registers its commands once and becomes reachable from all three. No action depends on an F-key or a chord.

Why a custom matcher is the keystone

Kate's built-in command bar cannot be fixed in place:

  • KCommandBar (KConfigWidgets) exposes only setActions(). There is no hook to replace or configure its matcher.
  • KFuzzyMatcher (KCoreAddons) is single-needle, strictly in-order subsequence, and not typo tolerant. KDE's own docs state "gti" will not match "git". No orderless, no company-style completion.

Therefore the palette is replaced, not extended, and the replacement is built on FuzzyRanker, a matcher with:

  • orderless tokenization (space-separated needles matched in any order)
  • layered scoring, strongest tier first:
    1. exact
    2. substring
    3. word-boundary initials (rs -> Rename Symbol)
    4. in-order subsequence
    5. bounded typo via a single adjacent transposition (gti -> git; deliberately narrow to avoid false positives like ren -> "Open File")
  • merged highlight ranges for the UI
  • AND semantics across needles, case-folded, shorter-candidate preference

Milestones

M1 — FuzzyRanker (keystone) — DONE

  • Repo scaffold: top-level CMake (KF6/Qt6/ECM), src/, src/fuzzy/.
  • src/fuzzy/fuzzyranker.{h,cpp}: tokenize + layered scoring + ranges.
  • src/fuzzy/test_fuzzyranker.cpp: QTest suite, 14 cases, all green, including gti -> git and ren sym -> Rename Symbol.

M2 — Palette widget (replaces KCommandBar) — DONE

  • PaletteModel (src/palette/palettemodel.{h,cpp}): QAbstractListModel, re-ranks via FuzzyRanker on setQuery, exposes id/label/group/score and highlight ranges per row, frecency bonus. No widget dependency.
  • PaletteWidget (src/palette/palettewidget.{h,cpp}): frameless popup, filter line + results list, live company-style updates, top hit preselected, custom delegate bolds matched ranges. Keyboard: Up/Down/PageUp/PageDown, Enter activates, Esc cancels — cursor stays in the filter line, no chords. Mouse: click a row to activate. Emits activatedId(id) / cancelled().
  • src/palette/test_palettemodel.cpp: 10 QTest cases, all green (filtering, orderless, typo, best-first ordering, frecency tie-break, highlight ranges).

M3 — KTextEditor plugin + M-x + action registry — DONE

  • src/plugin/ollieplugin.{h,cpp} + ollieplugin.json: a loadable KTextEditor::Plugin (olliepalette.so) built with kcoreaddons_add_plugin.
  • OllieView (one per MainWindow) registers the M-x action, Alt+X, a single modifier with the keys adjacent on a compact layout; ApplicationShortcut context so it is always live.
  • On trigger it aggregates every enabled, visible QAction from the window's guiFactory()->clients() action collections, keyed by stable objectName, and feeds them to PaletteWidget. The action's current shortcut is shown in the group column. Accepting a row triggers the underlying QAction.
  • Installs to kf6/ktexteditor; metadata verified valid via KPluginMetaData.
  • Not yet wired: frecency (the model supports a bonus, but the plugin currently passes 0 for every action — usage tracking is deferred).
  • Duplicate handling: actions are deduplicated only by pointer identity (the exact same action object reachable through several GUI clients is listed once — functionally lossless). Distinct actions are never dropped. When two distinct actions would render with the same label, the label is enriched through a fallback chain until every visible row is unique: component name → objectName → shortcut → numeric suffix. Each entry also gets a unique id so activation always triggers the intended action.

M4 — Radial caret menu (mouse door) — DONE (first increment)

  • src/radial/radialmodel.{h,cpp}: pure tree + geometry. Multi-level (branch/leaf) RadialNode tree, angle hit-testing (0 = up, clockwise), inner dead zone, far-flick snaps by angle, descend/ascend navigation. 14 tests.
  • src/radial/radialmenu.{h,cpp}: frameless translucent QWidget; paints the current ring (wedges, labels, branch ▸, central "back" hub), highlights the slice under the pointer, release/click activates a leaf, descends a branch, or ascends/cancels from the dead zone; Esc ascends/cancels.
  • src/radial/radialconfig.{h,cpp}: JSON → radial tree parser (+ built-in default) so multiple radials can be bound to different triggers. 8 tests.
  • Wired into the plugin: OllieView::setupRadials() registers a key trigger (default Alt+R) and a mouse-button gesture (default right mouse button) per configured radial. The RMB gesture is caught by an application event filter gated to the active view; it pops the radial at the press point and suppresses the normal context menu (MouseButtonPress + ContextMenu consumed) — an intentional, configured trade-off. The radial grabs the mouse on popup so an in-progress RMB drag is tracked and the release selects. Key-triggered radials pop at the caret (View::cursorPositionCoordinates()).
  • A slice may reference any Kate action by objectName, resolved via findActionByName; the special ollie_command_palette slice opens the palette.
  • Honours the agreed model: radials are triggerable by keys (weapon-wheel style) and by mouse buttons/gestures; the config carries both a key trigger and a mouse button per radial.
  • Not yet done: free-form stroke gestures (only button-press gesture so far); user-file config loading (built-in default only); frecency.

M5 — Command vocabulary + project model + switchers — NEXT

  • :-verb pack via KTextEditor::Command: sort, align, json, b64, uuid, case, pipe <shell>, etc. These populate M-x and the radial.
  • Project = directory. "Open folder == open project" (VSCode/Sublime model); no .kateproject ceremony.
  • File / symbol switchers scoped to the opened folder (e.g. git ls-files / fd for files; LSP or ctags for symbols), all through the M2 palette.

Build and test

cmake -B build -S .
cmake --build build
ctest --test-dir build
# or directly:
QT_QPA_PLATFORM=offscreen ./build/bin/test_fuzzyranker

Environment facts (verified)

  • Kate 25.12.3, KDE Frameworks 6, Qt 6.10, cmake 4.2.3, g++, extra-cmake-modules.
  • KTextEditor + KCoreAddons + KConfigWidgets + KXmlGui + KRunner dev headers present.
  • No Python/Pâté binding installed; C++ is the only first-class plugin path.