Commit Graph

99 Commits

Author SHA1 Message Date
Levi Neely 7495d82019 Radial: cleaner split labels + warp cursor into the created pane
Replace the ▯▯/⊟ glyphs with 'Split |' (left/right) and 'Split —'
(top/bottom), which render cleanly in any font. Extend isPaneFocusAction
to view_split_vert/horiz so the cursor warps onto the newly created pane
(Kate activates it), and defer the warp ~a frame (16ms) so a freshly laid-
out split pane has geometry before QCursor/mapTo reads its centre.
2026-10-08 12:27:07 +02:00
Levi Neely eeee28a94a Radial: Wayland pointer warp via zwp_pointer_constraints_v1
QCursor::setPos is a no-op on Wayland, so cursor-follows-focus did
nothing there. Add WaylandCursorWarp (src/plugin/waylandcursorwarp.{h,
cpp}): on Wayland, lock the pointer to the top-level window's wl_surface,
set_cursor_position_hint to the target pane centre (surface-local), commit,
then release — the compositor warps to the hint on unlock (the mechanism
plan9port/acme use). Panes are children of one window = one surface, so
it is a warp within that surface. Handles come from QWaylandApplication +
QPlatformNativeInterface 'surface'. X11 keeps QCursor::setPos.

Build: pointer-constraints client glue generated via ECM and compiled as
a C static lib (kcoreaddons_add_plugin silently drops .c sources); enable
C language in project(); link Qt6::GuiPrivate + wayland-client. All
guarded + a stub when the Wayland bits are absent. 13 ctest suites pass.
2026-10-08 12:18:01 +02:00
Levi Neely ea7d84868d Radial: warp the mouse to the pane after a focus-change split action
When a radial slice fires a go_*_split_view action (Focus Left/Right/Up/
Down/Next/Prev), warp the OS pointer onto the center of the newly active
view so the mouse follows the keyboard focus. Deferred via a 0ms timer so
Kate finishes activating the target view first. Scoped to the six pane-
focus actions only — other radial activations and ordinary view changes
(clicks, file opens) do not move the pointer.
2026-10-08 11:58:02 +02:00
Levi Neely 145d597742 Radial: shorten built-in slice labels to fit the wedges
The wedge label rect is fixed-size, so long labels crowded/overflowed.
Edit radial: Comment->Cmnt, Uncomment->Uncmnt, Uppercase->UPPER,
Lowercase->lower, Join Lines->Join, Multi-Cursor->Cursors, Go to File…->
File…, Go to Symbol…->Sym…, Command Palette…->M-x. Window radial:
Split Vertical/Horizontal->Split ▯▯ / Split ⊟, Close Split->Close,
Close Others->Only, Toggle Orientation->Rotate, focus dirs->← → ↑ ↓,
Previous->Prev. Labels only; action ids unchanged. Example config synced.
2026-10-08 11:55:50 +02:00
Levi Neely afedc88ac9 Radial: add window/split radial on the middle mouse button
Second built-in radial 'window' bound to MiddleButton: Split Vertical/
Horizontal, Close Split, Close Others, Toggle Orientation, and a Focus
sub-branch (go_left/right/up/down/next/prev_split_view). Leaf ids are
Kate view-manager action objectNames, resolved via findActionByName.

Route a plain middle press (no left held) to the radial dispatch instead
of unconditionally consuming it: the left+middle Cut chord still fires
(left held), and the X11 middle-click primary paste is still suppressed
on release in both paths. Clear m_swallowMiddleRelease when a gesture
consumes the release.

Example config + builtinHasWindowRadialOnMiddleButton test updated.
2026-10-08 11:53:19 +02:00
Levi Neely 5597dae15d Palette: 75% width, centered (was full-width edge-to-edge) 2026-10-08 11:46:40 +02:00
Levi Neely d9df62e7ed Palette: make the M-x command palette full-width too
All three palettes now span the window width. positionPalette drops its
fullWidth parameter (the centered path had no remaining callers) and
always full-widths.
2026-10-08 11:45:20 +02:00
Levi Neely e791e403d8 Palette: make the file and symbol switchers full-width
Add OllieView::positionPalette(palette, fullWidth) and route all three
palettes through it. The Go-to-File and Go-to-Symbol switchers now span
the window width (minus a 12px margin) — long project-relative paths and
scope-qualified symbol labels fit without truncation. The M-x command
palette keeps its compact centered width. Replaces three duplicated
center-positioning blocks.
2026-10-08 11:43:45 +02:00
Levi Neely 00f81f7f56 Perf: parallelize the full-N palette scan across the thread pool
PaletteModel::rebuild splits the bulk score-only scan into per-thread
chunks via QtConcurrent::blockingMapped when item count exceeds
kParallelThreshold (10000). The scan is a pure map over read-only data
(FuzzyRanker::score is a stateless static; m_items not mutated); chunks
are concatenated in order and finalizeVisible imposes a deterministic
sort, so the result is bit-identical to the serial path regardless of
thread timing. Incremental (type-forward) path stays serial — it only
touches <=1000 items.

First-scan at 397k symbols (mobydick, 16 cores): ~373ms -> ~61ms.
Link Qt6::Concurrent into the palette lib. New test
parallelScanIsCorrectAndDeterministic (40k items) guards correctness.
2026-10-08 11:38:32 +02:00
Levi Neely 1caa8d2221 Perf: prefer Universal Ctags, top-K cap + score-only + min query len
Universal Ctags: SymbolIndex::ctagsBinary() prefers ctags-universal over
the alternatives-managed ctags (Exuberant 5.9 has no Kotlin/TS parser).
Kotlin/TypeScript now indexed — symbol count on mobydick: 17k -> 397k.

At 397k, per-keystroke ranking was 0.3-0.7s. Three structural fixes:

1. FuzzyRanker: replace std::set with vector+sort+unique; add
   withRanges=false fast path that skips highlight computation during
   the bulk scan (ranges materialised only for displayed rows).
2. PaletteModel: cap visible results to top-1000 (partial_sort),
   score-only bulk, ranges for the top-K only. Nobody scrolls 400k.
3. Min query length (2): 1-char queries match everything and produce a
   useless, expensive full scan; below the threshold the palette shows
   the capped full list unranked. First real ranking at 2 chars; from
   there incremental narrowing makes every keystroke sub-2ms.

Measured (mobydick 397k symbols, RelWithDebInfo, type-forward):
  s: 337ms -> ~0ms | se: 507ms -> 373ms (1x first scan) |
  ser: 586ms -> 1.9ms | server: 198ms -> 0.8ms.

3 new tests (incrementalMatchesFullScan, visibleResultsAreCapped,
shortQueryDoesNotRank). docs/PERF.md updated.
2026-10-08 11:31:36 +02:00
Levi Neely a5b8d628b3 Perf: debounce palette filter + default to optimized build
Two fixes for the Alt+G typing lag:

1. Default CMAKE_BUILD_TYPE to RelWithDebInfo when unspecified — the
   shipped plugin was -O0. On mobydick (17k symbols) the worst
   type-forward keystroke drops ~340ms -> ~29ms (~12x).

2. Debounce the PaletteWidget filter with an 80ms single-shot timer so a
   fast typist's burst triggers one re-rank, not one per character.
   applyQueryNow() flushes the pending query on Enter so activation uses
   the latest text; activate() resets the debounce state.

The broad early-query keystrokes (1-2 chars matching most of N) are the
only ones still ~O(N); debounce ensures they are paid at most once per
pause. docs/PERF.md updated with measured numbers.
2026-10-08 11:13:37 +02:00
Levi Neely 2bf8d4a3c2 Perf: async + cached project indexing (fixes Alt+G open stall)
Add ProjectIndexer (project_lib, QtConcurrent + QFutureWatcher, 6 tests):
runs listFiles/listSymbols on the thread pool, delivers results on the UI
thread via filesReady/symbolsReady, caches per project root, and drops
stale results via a per-root generation counter.

Switchers now open the palette immediately — cache hit is instant, miss
shows a transient 'Indexing…' placeholder — and repopulate when the
background job finishes. The ~0.5-1s ctags stall on Alt+G no longer
blocks the UI thread. Cache invalidated on KateProjectBridge::
projectChanged. Register QList<Symbol> metatype; link Qt6::Concurrent.
Item building factored into buildFileItems/buildSymbolItems (shared by
the cache and async paths).
2026-10-08 11:02:52 +02:00
Levi Neely 8dd2995e36 Perf: measure switcher bottlenecks, add incremental re-rank
Add a benchmark harness (src/project/bench_switcher.cpp, manual target)
that times the Go-to-File/Symbol pipeline against a real project and
writes a metrics file. Measured on mobydick (12.2k files, 17.2k symbols);
findings in docs/PERF.md.

Bottleneck: PaletteModel::setQuery re-scored all N every keystroke
(Debug: 270-600ms/keystroke; cost grew with query length). Fix: when the
query is a pure append, re-score only the currently-visible subset — the
match set is monotonic under appending, so this is exact. Later keystrokes
drop from O(N) to O(matches) (symbols 'server' 30ms -> 9ms in Release).
Guarded by test_palettemodel::incrementalMatchesFullScan.

Also documented: shipped plugin is a Debug build (~5-8x slower than
Release), and ctags/listFiles run ~0.1-1s synchronously on open (move
off-thread = open follow-up).
2026-10-08 10:54:04 +02:00
Levi Neely caa1e911b8 Radial: add Go-to-File and Go-to-Symbol slices
Add ollie_goto_file and ollie_goto_symbol to the built-in radial's 'More'
branch, next to the command-palette escape hatch (panel-opening switchers
belong there, not the in-place top ring). onRadialActivated already
resolves both ids. Example config + a builtinDefaultHasLauncherSlices
test updated.
2026-10-08 10:43:56 +02:00
Levi Neely 0e1c6943ac Launchers: handle Alt+X/P/G in eventFilter, not QAction shortcuts
Alt+P (and the other launcher keys) collided with Kate's own
Alt+<letter> bindings; registered as ApplicationShortcut QActions this
makes Qt report an ambiguous shortcut and fire neither. Go nuclear: drop
the QAction shortcuts entirely and intercept Alt+X/P/G in eventFilter
(accept ShortcutOverride, act on KeyPress), scoped to our window — the
same ambiguity-proof technique used for the Acme keys. QActions kept for
objectName/label; onRadialActivated now resolves all three launcher ids
directly so the radial escape-hatch still reaches them.
2026-10-08 10:42:28 +02:00
Levi Neely 07af27e82c Project: drop redundant explicit folder-as-project machinery
Kate's project plugin already opens folders and shows a file tree, and we
now build on it via KateProjectBridge, so the parallel Ollie-side model
was redundant. Remove the Alt+O 'Open Folder as Project' action, the
sticky m_projectRoot + its $XDG_CONFIG_HOME/olliepalette/project
persistence (load/save), 'Close Project', and the ollie:project:* palette
entries/handlers. currentProjectRoot() is now two-tier: bridge baseDir
-> VCS discovery. Frees Alt+O. 12 ctest suites still pass.
2026-10-08 10:19:31 +02:00
Levi Neely f5b532c77d Project: build on Kate's project plugin via KateProjectBridge
Read kateprojectplugin's view through the meta-object system (no link
dependency): projectBaseDir/projectName/projectFiles properties plus
projectMapChanged/projectFileNameChanged/pluginProjectAdded|Removed
signals relayed to one projectChanged(). OllieView attaches on
construction and follows MainWindow::pluginViewCreated/Deleted.

currentProjectRoot() becomes three-tier: explicit sticky folder ->
bridge baseDir (Kate's loaded project) -> VCS discovery from the active
doc. 7 headless tests via a stand-in QObject mirroring the plugin's
property/signal surface.
2026-10-08 10:03:15 +02:00
Levi Neely a327a1a12e Project: explicit 'Open Folder as Project' (sticky root, Alt+O)
Replaces reliance on Kate's project plugin with our own folder-as-project
model. ProjectIndex::resolveRoot(explicit, start) honours an existing
explicit pick EXACTLY (no walk-up, VSCode/Sublime semantics) and
otherwise derives via findRoot with cwd fallback (3 new tests, 15 total).

Plugin: Alt+O 'Open Folder as Project...' pops a dir chooser, sets a
sticky m_projectRoot persisted to $XDG_CONFIG_HOME/olliepalette/project
and restored on load (if still present). 'Close Project' reverts to
auto-derive. Both in the M-x palette. Switchers now scope to
currentProjectRoot() instead of recomputing from the active document.
2026-10-08 09:48:16 +02:00
Levi Neely 5b31bf6e40 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'.
2026-10-08 09:42:13 +02:00
Levi Neely 219ad8e56c Radial: load user config from XDG, fall back to built-in
RadialConfig::load() reads $XDG_CONFIG_HOME/olliepalette/radials.json
when present and non-empty, else builtinDefault(); a broken/empty file
also falls back so the user is never left without radials. Add
userConfigPath() and loadFile(path,&error). setupRadials() now calls
load(). Example at docs/radials.example.json. 4 new tests.
2026-10-08 09:35:27 +02:00
Levi Neely 7195a6c11b Frecency: usage-aware ranking across all palettes
FrecencyStore (palette lib, Qt::Core, 11 tests): per-id {count,last}
persisted as JSON at $XDG_CONFIG_HOME/olliepalette/frecency.json via
QSaveFile. bonus() = recency bucket x capped visit count, in [0,100] —
below the substring tier (400) so it nudges/tie-breaks without overriding
a stronger textual match, and floats habitual choices up on an empty
query.

Plugin: OllieView loads the store, records every activation (M-x
runAction + file/symbol switcher lambdas) and sets PaletteItem::frecency
before showing each palette. File/symbol frecency keys are
project-qualified so usage does not bleed between repos.
2026-10-07 21:00:00 +02:00
Levi Neely cf0cf7e950 Fix Alt+R collision: move symbol switcher to Alt+G
The default radial key trigger is Alt+R (radialconfig), and the new
symbol switcher had also claimed Alt+R with ApplicationShortcut on the
same window — an ambiguous-shortcut conflict. Move Go-to-Symbol to Alt+G
(mnemonic: Go to), leaving Alt+R to the radial. Updated the M-x hint and
plan docs.
2026-10-07 20:54:41 +02:00
Levi Neely 1e9ee0e665 Go-to-Symbol: fold kind+file into the visible label
The delegate renders only the label, so the symbol switcher's group
(kind+file) was invisible. Fold it into the label as
'Scope::name  —  kind file': the name leads so a name hit still ranks
first, while the kind and file become visible and filterable in the same
fuzzy query.
2026-10-07 20:53:29 +02:00
Levi Neely 707e7c7250 Go-to-File: match and show the full project-relative path
The palette model matches only against PaletteItem::label (and the
delegate renders only the label; group was never shown). The file
switcher put the base name in label and the dir in group, so the
directory was neither matched nor displayed. Use the full relative path
as the label: FuzzyRanker already treats '/' as a word boundary and
length-penalises longer paths, so base-name hits still rank first while
orderless path-spanning queries work. 2 path-ranking tests added.
2026-10-07 20:10:31 +02:00
Levi Neely 42363157d3 M5: project symbol switcher via ctags (Alt+R)
SymbolIndex (project_lib): parseTags reads Exuberant Ctags 5.9 extended
tab format (name/file/line/kind/scope); listSymbols runs
ctags -f - -L - --fields=+nK --excmd=number --sort=no with the file list
on stdin, paths relative to root. 7 unit tests incl. a live ctags run.

Plugin: Alt+R 'Go to Symbol (project)' opens a PaletteWidget of project
symbols (label Scope::name, hint 'kind file'), opens the pick via
MainWindow::openUrl and jumps to its line. Also in the M-x palette and
reachable from a radial by objectName ollie_goto_symbol.
2026-10-07 19:38:24 +02:00
Levi Neely f4b891efa7 M5: project = directory + Go-to-File switcher (Alt+P)
ProjectIndex (project_lib): findRoot walks up for a VCS marker and falls
back to the start dir; listFiles prefers git ls-files (--cached --others
--exclude-standard) and falls back to a bounded recursive walk skipping
noise dirs/symlinks. 12 unit tests.

Plugin: Alt+P 'Go to File (project)' opens a second PaletteWidget of the
project files, scoped to the active document's root (fallback cwd), and
opens the pick via MainWindow::openUrl+activateView. Also surfaced in the
M-x palette and reachable from a radial by objectName ollie_goto_file.
2026-10-07 19:14:03 +02:00
Levi Neely a69e471e08 M5: run :-verbs directly via OllieCommands::runVerb, fix palette no-op
queryCommand/commandList do not see our auto-registered KTextEditor::Command
on this build, so the palette now calls OllieCommands::runVerb directly.
Rewrite the live test to exercise runVerb against a real doc/view (case,
rot13, sort, uuid, base64 round-trip). Document the command-line limitation.
2026-10-07 18:35:40 +02:00
Levi Neely e1bf0c9432 Palette: surface :-verbs as runnable M-x entries
The command palette now lists the :-verb pack alongside QActions: Sort Lines,
Sort Lines (unique), Reverse Lines, Case: UPPER/lower/Title/snake/camel/kebab,
Base64 Encode/Decode, ROT13, Insert UUID. Entries carry an ollie:cmd: id; on
activation runAction executes the command via
Editor::queryCommand(verb)->exec(view, cmd, msg). The free-text pipe verb
stays on the command line. All 6 ctest suites green.
2026-10-07 18:21:55 +02:00
Levi Neely 217d042a1b M5: :-verb command pack (sort/case/base64/rot13/uuid/pipe)
Add src/commands/: texttransforms (pure, unit-tested string transforms) and
olliecommands (a KTextEditor::Command registering sort, rsort, case, b64enc,
b64dec, rot13, uuid, pipe). Verbs act on the selection in place, or the whole
document when nothing is selected. 'pipe <shell>' runs sh -c <shell> with the
selection on stdin and replaces it with stdout (the user types the command,
so shell execution is intended). The command object is created once by
OlliePlugin and auto-registers with the global editor.

15 transform unit tests; 6 ctest suites green overall. Verbs are reachable
from Kate's command line; surfacing them in the M-x palette is a follow-up
(they are Commands, not QActions).
2026-10-07 17:54:20 +02:00
Levi Neely bfbb32a95a Acme keys: handle via ShortcutOverride/KeyPress, not QActions
Registering QActions for Ctrl+A/E/H/U/W kept colliding with Kate's own
actions (Select All, Close, …) — Qt reported ambiguous shortcuts and the
per-view shortcut-stripping sweep was fragile and incomplete.

Drop the QActions and the clearConflictingShortcuts machinery entirely.
Handle the keys in the application event filter: accept QEvent::ShortcutOverride
for our combos (so Kate's registered shortcut never fires) and act on the
following QEvent::KeyPress, consuming it. No shortcut is registered, so there
is nothing to be ambiguous with, and new views need no special handling.
All 5 ctest suites green.
2026-10-07 17:46:51 +02:00
Levi Neely 1b56cca1a2 Acme keys: resolve shortcut ambiguity by stripping conflicting bindings
Adding our Ctrl+A/E/H/U/W actions alongside Kate's own (Select All, Close,
lowercase, …) left both claiming the same sequence, so Qt reported ambiguous
shortcuts. clearConflictingShortcuts() now walks every GUI-client action
collection and removes our sequences from any foreign action, also clearing
their KActionCollection default shortcuts so they are not restored. The sweep
runs at setup, once more deferred to the event loop (after the GUI factory
merges all clients), and on every viewCreated so new documents don't
re-introduce the conflict.

Also implements the five Acme motions against the View/Document API with
semantics ported from plan9port acme text.c. All 5 ctest suites green.
2026-10-07 17:43:39 +02:00
Levi Neely 9bcbece09a Radial: drop Cut/Copy/Paste now that chords cover them
With left+middle = Cut and left+right = Paste (standard KDE clipboard) and
X11 primary selection for copy, those actions are redundant in the radial
and duplicate a more in-flow gesture. Top-level radial is now Undo, Redo,
Cursors to Line Ends, and More. All 5 ctest suites green.
2026-10-07 17:35:42 +02:00
Levi Neely 5439d43ac4 Chord: arm anchor on left double-click so dblclick -> chord flows
A double-click's second press arrives as MouseButtonDblClick, not a plain
press, so m_leftDownInView was never set and the still-held button could not
anchor a chord. Set the chord anchor on a left double-click inside the view
(bracket- or word-select), letting the user proceed directly from a
double-click into a left+middle (Cut) or left+right (Paste) chord without
releasing. All 5 ctest suites green.
2026-10-07 17:28:14 +02:00
Levi Neely a804dbe521 Add bracket-pair selection on double-click (multi-line, nesting-aware)
Double-clicking next to a bracket ()[]{} now selects the text strictly
between the matching pair, across lines and honouring nesting. The event
filter catches MouseButtonDblClick inside the view, maps the click to a
document cursor via coordinatesToCursor, inspects the characters on both
sides, and scans with depth counting (stepForward/stepBackward wrap across
line ends) to find the match. Openers select forward, closers backward;
with no bracket adjacent it falls through to Kate's default word-select.
All 5 ctest suites green.
2026-10-07 17:26:24 +02:00
Levi Neely 3129662f31 Chord: add Acme left+right = Paste
The paste chord (Acme 1-3) now works: with the left button held, a right tap
pastes over the selection via edit_paste (standard KDE clipboard). This is
conflict-free because the radial gesture is already suppressed while the left
button is held — right alone opens the radial, right while left-held pastes.
The paired right release is swallowed so it doesn't leak a context menu or
other action. All 5 ctest suites green.
2026-10-07 17:19:57 +02:00
Levi Neely 87f5587bd4 Chord: suppress middle-click primary paste on both press and release
The X11 primary-selection paste was leaking through on the middle-button
release because only the press was consumed. Now the middle button is
consumed inside the editor on BOTH press and release (tracked via
m_swallowMiddleRelease), fully suppressing primary paste there. The chord
Cut uses edit_cut, which writes to the standard KDE clipboard (Klipper), so
paste is the normal Ctrl+V / palette paste. All 5 ctest suites green.
2026-10-07 17:18:01 +02:00
Levi Neely 75367f85ef Radial: suppress gesture while left button is held
An RMB press during a left-drag (mid-selection, or while setting up a
left+middle chord) must not pop the radial. Guard the gesture start on both
the tracked left-held-in-view flag and the live QMouseEvent::buttons() state
so a right press with left already down falls through instead of opening the
radial. All 5 ctest suites green.
2026-10-07 17:14:26 +02:00
Levi Neely d117aebb7f Add Acme-style left+middle mouse chord = Cut
Left-button chords only (right button stays the radial). With the left
button held while selecting, a middle-button tap cuts the current selection
and is consumed, so it does not also trigger a primary-selection paste;
middle-click without the left held still pastes as usual.

Implemented in the shared application event filter: track the left button
held inside the active view, and on a middle press while held trigger
edit_cut. The filter is now installed unconditionally (it serves both the
radial gesture and the chord). All 5 ctest suites green.
2026-10-07 17:11:29 +02:00
Levi Neely dca2514023 Radial: add top-level 'Cursors to Line Ends' multi-cursor action
Multi-cursor on every selected line is a quintessential in-place action
(select lines, gesture, keep typing) — a natural fit for a top-level radial
slice. Wired to Kate's createMultiCursorsFromSelection via its visible text
'Add Cursors to Line Ends'.

findActionByName now falls back to a case-insensitive visible-text match
when the objectName lookup misses, so slices resolve across Kate versions
even when action ids differ. Dropped 'Select All' from the radial (reachable
via Ctrl+A and the palette). All 5 ctest suites green.
2026-10-07 17:06:56 +02:00
Levi Neely fc483cfc9c Radial: in-place actions only; move Find/Replace out
Guiding principle: natural actions drive editing. The radial is for actions
that complete at the cursor without pulling attention elsewhere. Find and
Replace open a panel that demands immediate focus, so they break flow in a
cursor gesture — they belong in the command palette or a keybind instead.

Replace Find/Replace in the 'More' submenu with in-place transforms:
Uppercase, Lowercase, Join Lines (verified Kate action names: uppercase,
lowercase, joinLines; edit_* are KStandardAction ids). The Command Palette…
slice remains as the escape hatch to panel-opening actions. Documented the
principle in docs/PLAN.md. All 5 ctest suites green.
2026-10-07 17:04:58 +02:00
Levi Neely 2e006b34d9 Radial: fix held-drag hover and wrong-slice selection
The opening RMB press leaves an implicit mouse grab on the editor view, so
move/release events during the hold were delivered there, never to the
popup's grabMouse(). Hover froze and the release resolved at a stale
position (always landing on 'More').

Drive the whole gesture from the plugin's application event filter instead,
which reliably sees those events: on press show the radial and set
m_gestureActive; forward each MouseMove to driveHoverGlobal() and the
MouseButtonRelease to driveReleaseGlobal(), both in global coordinates
mapped locally by the widget. The widget's own mouse handlers are gated off
during this external drive (m_externalDrive) to avoid double-handling, and
the manual grabMouse()/releaseMouse() is removed. Key-triggered radials keep
using the widget's local click handling. All 5 ctest suites green.
2026-10-07 17:00:15 +02:00
Levi Neely 9ecfd379c2 Radial: dual drag/click interaction, open at cursor without moving caret
The radial now supports both intended interactions from one widget:
- Hold-and-drag: opened with a button held, drag toward a slice and release
  to trigger the slice under the cursor (far flicks allowed).
- Release-and-click: a quick press/release that never leaves the centre dead
  zone switches to click mode and keeps the menu open; then hover and click a
  slice to trigger. A click outside the ring cancels instead of snapping.

Armed state is detected from QApplication::mouseButtons() at popup time, so
key-triggered radials start directly in click mode. mousePressEvent is
accepted to stop a Qt::Popup from self-dismissing on an in-widget press. The
menu opens centred on the given point; the RMB press is consumed upstream so
the caret does not move. All 5 ctest suites green.
2026-10-07 16:52:15 +02:00
Levi Neely 4be3ead30d Radial: add right-mouse-button gesture trigger
RMB is the natural radial trigger. The plugin now installs an application
event filter (gated to the active view) that, on an RMB press inside the
editor, pops the radial at the pointer and consumes the press plus the
following ContextMenu event, suppressing Kate's context menu (an
intentional, user-approved trade-off). The radial grabs the mouse on popup
so an in-progress RMB drag is tracked and the release selects the slice.

Config gains a per-radial "button" field (RightButton/MiddleButton)
alongside the key "trigger"; the built-in editing radial binds both Alt+R
and RightButton. All 5 ctest suites green.
2026-10-07 16:43:52 +02:00
Levi Neely a98db2913c M4: multi-level radial menu (mouse door), config-driven triggers
- RadialModel: pure tree + geometry (angle hit-test, dead zone, far-flick
  snap, descend/ascend). 14 tests.
- RadialMenu: frameless translucent widget painting the current ring with
  wedges, labels, branch markers and a central back/cancel hub; mouse hover
  highlights, release/click activates a leaf, descends a branch, or
  ascends/cancels from the hub; Esc ascends/cancels.
- RadialConfig: JSON -> radial tree parser with a built-in default, so
  multiple radials can bind to different triggers. 8 tests.
- Plugin wiring: one trigger action per configured radial (default Alt+R ->
  editing radial), popped at the caret via cursorPositionCoordinates().
  Slices reference Kate actions by objectName (resolved via findActionByName);
  the special ollie_command_palette slice opens the palette.

Honours the agreed model (keys/weapon-wheel triggers now; config carries a
per-radial trigger for mouse-button/gesture triggers later). All 5 ctest
suites green (36 tests).
2026-10-07 16:38:54 +02:00
Levi Neely d227d6161b Palette: guarantee distinguishable labels, never drop distinct actions
Dedup is now strictly by QAction pointer identity (functionally lossless:
the same action object listed once). Distinct actions are never collapsed.
When two distinct actions would show the same label, enrich it through a
fallback chain until every visible row is unique: component name ->
objectName -> shortcut -> numeric suffix. Ids remain unique so activation
always hits the intended action.
2026-10-07 16:26:03 +02:00
Levi Neely 485d395d4d M3: dedup palette actions by identity, disambiguate shared labels
The same QAction is often reachable through multiple GUI clients; those
are true duplicates and are now collapsed by pointer identity. Distinct
actions that merely share a visible label (common across plugins) are kept
but disambiguated by appending the owning component's display name, and
every entry is assigned a unique, stable id so activating a row always
triggers the intended action rather than whichever one won a hash key.
2026-10-07 16:05:22 +02:00
Levi Neely 91a0e2d260 M3: loadable KTextEditor plugin with M-x command palette
- OlliePlugin (KTextEditor::Plugin) + OllieView, built as olliepalette.so
  via kcoreaddons_add_plugin, installed under kf6/ktexteditor.
- M-x bound to Alt+X (single modifier, adjacent keys on a compact layout,
  ApplicationShortcut so it is always live).
- On trigger, aggregates every enabled/visible QAction from the window's
  guiFactory()->clients() action collections (keyed by objectName) and
  shows them in PaletteWidget; the chosen row triggers the QAction. The
  action's current shortcut is surfaced in the group column.
- Enable position-independent code for linking the static palette lib into
  the shared plugin module.
- Plugin metadata verified valid via KPluginMetaData.

Frecency wiring deferred (model supports it; plugin passes 0). All three
ctest suites remain green.
2026-10-07 15:59:05 +02:00
Levi Neely 617b526e3f M2: command palette widget + ranked list model
- PaletteModel: QAbstractListModel ranking items via FuzzyRanker, with
  id/label/group/score and highlight-range roles plus a frecency bonus.
  Headless, unit-tested (10 cases).
- PaletteWidget: frameless popup with a filter line and results list.
  Live company-style updates, top hit preselected, custom delegate bolds
  matched characters. Keyboard (Up/Down/PageUp/PageDown/Enter/Esc, focus
  kept in the filter) and mouse (click to activate). No chords.
- Tighten FuzzyRanker typo tolerance: single adjacent transposition instead
  of deletion. Still recovers 'gti'->'git' but no longer false-matches
  'ren' against 'Open File'.

All tests green: fuzzyranker (14) + palettemodel (10).
2026-10-07 15:49:48 +02:00
Levi Neely 95bc9f7ed6 Initial commit: FuzzyRanker keystone + project plan
Kate plugin suite for menu-free, mouse-first, one-handed UX.

- FuzzyRanker: orderless + layered fuzzy matcher (exact/substring/
  initials/subsequence/bounded-typo) with merged highlight ranges.
  Fixes Kate's KFuzzyMatcher gaps (no orderless, 'gti' fails 'git').
- 14 QTest cases, all green.
- README + docs/PLAN.md: goal, design constraints, 5-milestone roadmap.
2026-10-07 15:45:58 +02:00