9.7 KiB
9.7 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-xand 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 onlysetActions(). 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:
- exact
- substring
- word-boundary initials (
rs-> Rename Symbol) - in-order subsequence
- bounded typo via a single adjacent transposition (
gti->git; deliberately narrow to avoid false positives likeren-> "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, includinggti -> gitandren sym -> Rename Symbol.
M2 — Palette widget (replaces KCommandBar) — DONE
PaletteModel(src/palette/palettemodel.{h,cpp}):QAbstractListModel, re-ranks viaFuzzyRankeronsetQuery, 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. EmitsactivatedId(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 loadableKTextEditor::Plugin(olliepalette.so) built withkcoreaddons_add_plugin.OllieView(one per MainWindow) registers the M-x action,Alt+X, a single modifier with the keys adjacent on a compact layout;ApplicationShortcutcontext so it is always live.- On trigger it aggregates every enabled, visible
QActionfrom the window'sguiFactory()->clients()action collections, keyed by stableobjectName, and feeds them toPaletteWidget. The action's current shortcut is shown in the group column. Accepting a row triggers the underlyingQAction. - Installs to
kf6/ktexteditor; metadata verified valid viaKPluginMetaData. - 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)RadialNodetree, 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 translucentQWidget; 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 followingContextMenuare 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 specialollie_command_paletteslice 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
triggerand a mousebuttonper 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.
- 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.
M5 — Command vocabulary + project model + switchers — NEXT
:-verb pack viaKTextEditor::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
.kateprojectceremony. - File / symbol switchers scoped to the opened folder (e.g.
git ls-files/fdfor 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.