kate-deft/docs/PLAN.md

11 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.

Guiding principle: natural actions drive editing

Each door suits a different kind of action, and placing an action in the wrong door breaks flow:

  • Radial is for actions that complete in place — cut, copy, paste, comment, case changes, join lines. The gesture starts and ends at the cursor; attention never leaves the text.
  • Actions that open a panel demanding immediate focus elsewhere (Find, Replace, goto) do not belong in the radial. Being flung to a panel right after a cursor gesture is jarring. These belong in the command palette or a keybind, where the context switch is expected. The radial keeps a "Command Palette…" slice as the escape hatch to them.

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. Two interaction modes from one widget: a drag gesture (opened with a button held — drag out and release to select; a release that never left the dead zone switches to click mode and keeps the menu open) and a discrete click mode (click a slice to select, click outside the ring to cancel). Esc ascends/cancels. Opens centred on the given point without moving the caret.
  • 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. Because the opening RMB press leaves an implicit grab on the editor view, the whole gesture (hover + release) is driven from the plugin's application event filter in global coordinates (driveHoverGlobal/driveReleaseGlobal), not from a popup mouse grab — this fixes stale hover and wrong-slice selection. The press and the following ContextMenu are consumed, so the context menu is suppressed and the caret does not move. Key-triggered radials pop at the caret (View::cursorPositionCoordinates()) and use the widget's own click handling.
  • 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.

Mouse chording (Acme-style) — DONE

  • Left-button chords (button 1 held while selecting is the anchor): left + middle = Cut, left + right = Paste. Both use edit_cut / edit_paste → the standard KDE clipboard (Klipper-backed), not the X11 primary selection.
  • The right-button paste chord is free of conflict because the radial gesture is already suppressed while the left button is held: right alone = radial, right while left-held = paste.
  • Cut/Copy/Paste were removed from the radial once the chords covered them — the radial should not duplicate what a more in-flow gesture already does.
  • Middle-click primary paste is suppressed inside the editor — the middle button is consumed on both press and release (the paste fires on release, so swallowing only the press leaked it). Paste in the editor is the KDE-clipboard paste (Ctrl+V, palette, or the left+right chord).

Bracket-pair double-click selection — DONE

  • Double-clicking next to a bracket (()[]{}) selects the text strictly between the matching pair, multi-line aware and nesting-aware. Adjacency is checked on both sides of the click; openers select forward, closers select backward.
  • Implemented in the event filter via selectBracketPairAt: maps the click to a document cursor (coordinatesToCursor), reads neighbouring characters (characterAt), scans with depth counting across line ends, and sets the selection. When no bracket is adjacent it falls through to Kate's default double-click (word select).
  • A left double-click leaves the button held, so it also arms the chord anchor: the user can go straight from a double-click (bracket- or word-select) into a left+middle / left+right chord without releasing.

Acme line-editing keys — DONE

  • Keys bound (overriding Kate/KDE defaults, ApplicationShortcut): C-a beginning of line, C-e end of line, C-h erase char back, C-u erase to line start, C-w erase word back. Semantics ported from plan9port acme text.c (^A/^U stop at the line's newline; ^W skips non-alphanumerics then eats the alphanumeric run; isalnum excludes _).
  • Implemented directly against the View/Document API (cursorPosition, setCursorPosition, removeText, characterAt, lineLength).
  • Shortcut conflicts (e.g. Ctrl+A = Select All, Ctrl+W = Close) are resolved by stripping our sequences from every other GUI-client action (clearConflictingShortcuts), which also clears their KActionCollection defaults. The sweep runs at setup, deferred once via the event loop (after the GUI factory merges all clients), and again on every viewCreated.

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.