The performance notes documented one-off benchmarking of the switcher re-rank
and the fixes already applied; it is no longer useful as living documentation.
Drop docs/PERF.md and the references to it in README (layout tree), AGENTS.md
(reference-docs list and the build-type note), and the CMakeLists comment.
- docs/radials.example.json: the config comments and the launcher-door action
names were still Ollie-era — 'Copy to .../olliepalette/radials.json' ->
'deft/radials.json', and ollie_goto_file/ollie_goto_symbol/ollie_command_palette
-> deft_goto_file/deft_goto_symbol/deft_command_palette (the objectNames the
radial plugin actually resolves; verified against radialconfig.cpp).
- docs/PLUMBING.md: replaced the dead 'See PLAN.md' pointer with the real
implementation location (src/plumb/, plumb_lib + plumbplugin).
- README: link docs/radials.example.json from the radial Configure note.
- AGENTS.md: drop the environment-specific LSP-bridge remark (not universal
contributor guidance); keep the plain 'read before you edit' rule.
PERF.md and SAM.md were already accurate and are unchanged.
Remove docs/PLAN.md (goal/constraints/milestone roadmap — stale planning prose)
and the README's Status/milestone section; the README already documents what
the plugins are and do. Refresh the layout tree's docs/ listing accordingly.
Add AGENTS.md: a working guide for contributors/agents — the one-.so-per-feature
architecture and shared libs, build/test/install commands, naming and style
conventions, the two load-bearing design decisions (eventFilter'd launcher keys
vs ambiguous shortcuts; cross-plugin calls by action objectName with no link
dependency), and the gotchas (osort collision, per-plugin frecency files,
live-Kate behaviour being unverifiable in CI).
Add the docs/img/ screenshots and embed one in each plugin's subsection:
palette (deft-mx), radial (deft-radial), acme (deft-acme-select), sam
(deft-sam), and plumb (deft-ollie-plumb). The switch plugin has no screenshot
yet.
README: replace the single-deft.so install instructions with the per-feature
plugin table (palette, switch, radial, acme, sam, plumb), a glob install loop,
and a note that the plugins cooperate by resolving each other's actions by
object name; refresh the layout tree (plugin/ gone, switch/ added, palette/ and
radial/ now hold their plugins) and mark milestones 5 and 6 DONE.
PLAN.md: add an architecture note at the top flagging the split and that the
design prose below refers to the former monolith as historical.
Replace Document::wordAt() in plumbAtCursor() with fileTokenAt(), an
acme-style button-3 file expansion: grow over isFileChar() (alnum, '_',
and '. - + / : @', matching plan9port acme's isfilec), with ':' ending
the file name and only a digit-led ':line[:col]' suffix kept attached.
This grabs whole paths like /a/b/dpg_identity.php:584 under the caret.
Also reverts the earlier ^W change so acmeEraseWord again matches acme's
C isalnum (which excludes '_').
Document the user-side plumber rules that make files and directories open in
Kate: the edit-port routing model, setting editor = /usr/bin/kate so files
(with their addr line/col) open in Kate instead of acme, the isdir rule that
opens a directory as a folder (plumb start kate $dir), reloading a running
plumber (9p write plumb/rules), verification, and troubleshooting. Cross-link
from PLAN.md.
Note: ~/lib/plumbing itself lives in the user's home, not the repo; this
documents how to configure it.
When a plumbed path resolves to an existing directory, onPlumbEdit now opens
it as a folder in Kate (acme-style directory open) instead of trying to open
it as a document. Kate accepts a directory on its command line and is
single-instance, so relaunching the hosting kate binary (QCoreApplication::
applicationFilePath, QStandardPaths fallback) on the dir opens it in the
running window. No in-process public API exists to open a folder as a project
(our project bridge is read-only), so this uses the documented command-line
path rather than a guessed slot. 18/18 ctest.
Scope X/Y to the set of documents Kate currently has open
(Application::documents()) — acme's open-window set — instead of the project
index on disk. The regex matches each buffer's local file path (or display
name for an unsaved scratch buffer). Nothing is opened or read from disk; the
live Documents are edited in place, each its own undo step.
Smaller blast radius than a project-wide sweep and closer to acme's X/Y, which
iterate open windows. Drops the ProjectIndexer dependency from the X/Y path.
Docs (SAM.md, PLAN.md) updated. 18/18 ctest green.
Replace QRegularExpression with SamRegex (src/sam/samregex.{h,cpp}), a
faithful port of plan9port src/cmd/sam/regexp.c: a Thompson/Pike NFA.
- Leftmost-LONGEST (POSIX): matches real sam exactly, including overlapping
alternation (a|ab on 'ab' -> 'ab'), eliminating the one remaining
greedy-vs-longest divergence from PCRE.
- Linear time, no backtracking: immune to catastrophic blowup ((a*)*b over
10k 'a' returns instantly).
- sam dialect only: . * + ? | ( ) [ ] ^ $ and \ escaping (\n = newline);
^/$ per-line and ./negated-classes exclude newline, all intrinsic. No
PCRE extras (\d \w \b, lookaround, non-greedy) — sam never had them.
Port notes: shunting-yard compiler + Pike VM with per-thread capture sets and
leftmost-longest newmatch(). Fixed two porting bugs vs the C original: the
l-union aliasing of OR's left/continuation with .next, and reading the popped
subid during popator for correct capture-group ids.
SamEngine now compiles/matches via SamRegex (search, s, x/y/g/v, replacement
captures). Tests: new test_samregex (18) + updated test_samengine (31, incl.
leftmostLongestSubstitution, sam-dialect capture groups). 18/18 ctest suites.
Docs: rewrite docs/SAM.md (no more PCRE deviation; dialect + semantics are
sam's), update PLAN.md and the panel hint.
Verified against plan9port: regexp(7) defines ^ as 'beginning of a line'
and $ as 'end of a line', and sam's regexp.c BOL (p==0 || prev=='\\n') /
EOL (next char '\\n') confirm per-line anchoring. The engine was matching
with buffer-wide anchors, so ,s/^/> /g only touched the first line and the
sam idiom ,x/.+/ s/^/> / failed to prefix each line.
Fix: compile every pattern with QRegularExpression::MultilineOption. '.'
still does not cross newlines (sam-faithful; PCRE default). Add 3 regression
tests (caretIsPerLine, dollarIsPerLine, samAnchorIdiom); 30/30 engine tests.
Docs: correct docs/SAM.md — ^/$ per-line is faithful, not a deviation;
reframe PCRE extras as an escape hatch (prefer structural composition), not
'free upgrades'. Fix the panel hint and PLAN.md accordingly.
Add docs/SAM.md — a user-facing reference for the regex deviation, with
examples verified against the engine:
- leftmost-greedy (PCRE) vs leftmost-longest (sam): diverges only on
overlapping alternation (a|ab); order alternatives longest-first.
- ^/$ anchor buffer-wide, not per-line, by default. Workarounds:
structural ,x/.+/ … (preferred) or the (?m) inline flag.
- identical behaviour table (. and newlines, empty-match advance, &/\N).
- PCRE bonuses sam lacks: \d \w \b, lookahead, non-greedy, (?i)(?m)(?s).
Cross-link from PLAN.md and surface the two biggest gotchas (buffer-wide
anchors, alternation order) directly in the Sam panel hint label.
A dockable 'Sam' tool view runs the plan9 sam command language against the
active document, or project-wide via X/Y. Each Run is one undo step.
Engine (src/sam/, pure, 27 unit tests, no Kate dep):
- SamEngine::run(program,text,dotStart,dotEnd) -> edits (original-offset,
non-overlapping, sorted), dot, output, applied text. Addresses computed
against the original snapshot per sam semantics.
- Recursive-descent parser + evaluator mirroring plan9port src/cmd/sam
(cmd.c table, address.c lineaddr/charaddr, xec.c s_cmd/looper).
- Language: #n n 0 $ . ' /re/ ?re? and compound + - , ; ; commands
a c i d, s (sN, g, &, \1..\9), p =, m t, k, loops x y g v (nestable),
{ } groups, shell < > | ! via sh -c.
- peelFileLoop(): recognises a leading X/Y file loop (sam allows one per
command) for the Kate layer to drive.
Kate layer:
- SamPanel (src/plugin/sampanel): program editor + Run + output log.
- OllieView creates the tool view (MainWindow::createToolView) and owns
runSamProgram/applySamToDocument/runSamFileLoop. Single-doc edits apply
inside one Document::EditingTransaction (atomic undo). X/Y enumerates the
project index (ProjectIndexer::cachedFiles), opens each matching file, and
applies the inner program; undo is per-file (no global multi-file undo).
Regexes use QRegularExpression (PCRE), not plan9 regexp(7); documented. Out of
scope: b B n D menu, e r w f file I/O, "re" file-addressing, sam u (Kate undo).
17/17 ctest green.
Native C++ 9P2000 client over QLocalSocket (no libplumb/lib9pclient
link) talking to a running plan9port plumber:
- ninep: Tversion/Tattach/Twalk/Topen/Tread/Twrite/Tclunk, LE wire,
msize negotiated from 8192; cancellable split read (beginRead +
recvReadReply) so the edit-port reader thread honours a stop flag
without a second outstanding Tread or any cross-thread socket touch.
- plumbmsg: PlumbMsg pack/unpack with libplumb attribute quoting;
unit-tested against golden bytes captured from a live plumber.
- plumbresolve: plan9 addr (N / N:C / N.C, 1-based) -> 0-based cursor.
- plumber: send() via the send port; PlumbReader QThread on the edit
port emits edit(file,addr,wdir) via a queued signal. Namespace socket
is $NAMESPACE/plumb or /tmp/ns.$USER.$DISPLAY/plumb (plan9 getns).
Plugin: F2 (ollie_plumb) plumbs selection/wordAt(cursor) through the
send port, falling back to internal URL/file resolution; incoming edit
messages open via openUrl + activateView + setCursorPosition.
Tests: test_plumbmsg + test_plumbresolve headless; test_plumb_live does
a real send->rules->edit round-trip against a running plumber and
skips+passes when none is reachable. 16/16 ctest green; live 4/4.
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.
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.
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.
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.
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.
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.
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.
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).
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).
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.
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.
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.
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.
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.
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'.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.