Commit Graph

83 Commits

Author SHA1 Message Date
Levi Neely 38f5468ce6 docs: remove PERF.md and its references
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.
2026-10-08 20:46:31 +02:00
Levi Neely 33790043d8 docs: bring all docs up to date after the split/rename
- 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.
2026-10-08 20:44:50 +02:00
Levi Neely 812ab782f8 docs: drop the roadmap, add AGENTS.md
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).
2026-10-08 20:41:14 +02:00
Levi Neely d9829c9327 docs(README): add plugin screenshots
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.
2026-10-08 20:36:53 +02:00
Levi Neely 4325513c47 build: auto-wire QT_PLUGIN_PATH for a user-local install
A home-prefix install puts the plugins outside Qt's default search path, so
Kate would not list them until QT_PLUGIN_PATH includes the install dir — the
step a bare 'cmake --install' silently left undone. Solve it the way the ollie
project does (a Plasma login env snippet), and automate the common case:

- Add 99-deft.sh.in, a Plasma env snippet that prepends the install's qt6
  plugin dir to QT_PLUGIN_PATH. configure_file() fills in the absolute path.
- On a user-local install (prefix inside $HOME), install(CODE) drops the
  configured 99-deft.sh into ~/.config/plasma-workspace/env/ so the lookup
  path is wired up with no manual step; Plasma sources it at next login. The
  installer also prints a one-line export to use the plugins immediately.
- On a system-prefix install it does not touch the user's config; it just
  prints where the plugins went and leaves the ready snippet in the build dir.

README: add a 'Making Kate find a user-local install' section explaining that
QT_PLUGIN_PATH must include ~/.local, that the installer handles it, and how to
activate it in the current session (or a non-Plasma shell).

Verified: cmake --install with the default prefix installs the six plugins and
writes ~/.config/plasma-workspace/env/99-deft.sh with the correct path.
2026-10-08 20:19:00 +02:00
Levi Neely 29136ac898 build: install to ~/.local and the Qt plugin dir by default
cmake --install previously defaulted to CMAKE_INSTALL_PREFIX=/usr/local (needs
root) and, via KDE_INSTALL_PLUGINDIR, to lib/<arch>/plugins/ — a directory Kate
does NOT scan (it scans lib/<arch>/qt6/plugins/, per QT_PLUGIN_PATH). So a plain
install both failed on permissions and would have put the plugins where they
can't be found.

Follow the sibling ollie project's approach instead of hand-rolling path logic:

- Default CMAKE_INSTALL_PREFIX to $HOME/.local when the user did not pass one.
  ECM's KDECMakeSettings resets an unspecified prefix back to /usr/local, so we
  capture CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT *before* including it and
  re-apply the user-local default *after*, where it is not clobbered.
- Point KDE_INSTALL_PLUGINDIR at QT6_INSTALL_PLUGINS (prefix-relative
  lib/<arch>/qt6/plugins) so kcoreaddons_add_plugin(INSTALL_NAMESPACE
  kf6/ktexteditor) lands where Kate looks. Set the *normal* variable (the macro
  reads it directly; a cache-only FORCE would be shadowed) as well as the cache.

Result: `cmake --install build` with no flags installs all six plugins to
~/.local/lib/<arch>/qt6/plugins/kf6/ktexteditor/ with no root.

README: replace the manual install -D loop with `cmake --install build`, state
clearly that it installs to the home dir by default, show the resulting path,
and document --prefix /usr for a system-wide install.
2026-10-08 20:16:09 +02:00
Levi Neely 2c41a430b6 docs(README): warn that the plugins are opinionated
Several plugins reshape or suppress Kate's defaults (radial takes the right
mouse button, acme rebinds Ctrl+A/E/H/U/W + double-click + mouse chords and
kills X11 primary paste, palette/switch/plumb claim Alt+X/P/G and F2). Add a
callout at the top of 'The plugins' so users enable them deliberately rather
than being surprised.
2026-10-08 20:06:54 +02:00
Levi Neely e9d86b35f8 docs(README): add per-plugin descriptions and usage guides
Add a 'The plugins' section with a short description and a defaults-based usage
guide for each of the six plugins: palette (M-x + the :-verb table + frecency),
switch (Go to File/Symbol, ctags, project scoping, cache), radial (open/choose
gestures + radials.json config), acme (line keys, pair select, mouse chords),
sam (tool view, Run, X/Y multi-file), and plumb (F2 send, edit port, fallback).
Trim the now-redundant usage prose from the install section and point it at the
new section.
2026-10-08 20:05:02 +02:00
Levi Neely 8d06c206bb rename: drop the last Ollie names; command pack -> DeftCommands
The transient core classes were already deleted in the split, but the shared
command library and a scattering of comments/ids still carried the old project
name. Finish de-Ollie-ing the tree so nothing but 'Deft' remains:

- src/commands: OllieCommands -> DeftCommands; files olliecommands.{h,cpp} ->
  deftcommands.{h,cpp}; lib target olliecommands_lib -> deftcommands_lib; tests
  test_olliecommands_live/test_ollieregistration -> test_deftcommands_live/
  test_deftregistration (ctest names deftcommands_live/deftregistration).
  palette plugin updated to match (include, fwd-decl, member type, ctor call,
  runVerb call, CMake link).
- Config paths: radial's user config moves olliepalette/radials.json ->
  deft/radials.json, matching the switch plugin's earlier olliepalette/ -> deft/
  move; comments updated.
- Stale ollie: id prefixes in the project benchmark (ollie:file:/ollie:sym: ->
  deft:file:/deft:sym:) and sample paths in test_palettemodel referencing the
  deleted src/plugin/ollieplugin.cpp.
- Comments still naming OllieView/OlliePlugin corrected to the real post-split
  classes (SamView, PlumbView) or to deft paths.
- README layout refreshed (deftcommands.{h,cpp}).

PLAN.md keeps the Ollie names inside its pre-split historical prose, as flagged
by the architecture note at its top.

Full clean rebuild + 18/18 ctest green; palette and radial reinstalled.
2026-10-08 20:03:17 +02:00
Levi Neely ed3bd4249b docs: describe the six-plugin layout after the split
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.
2026-10-08 19:56:11 +02:00
Levi Neely d8f0225178 palette + switch: finish the split; delete the core deft plugin
Stage 3c, the final split. The remaining core OllieView carried two distinct
features plus the ":"-verb pack; each becomes its own KTextEditor plugin and
the monolithic core 'deft' plugin is removed entirely.

deft_palette.so ([deft:util] palette): new src/palette/paletteplugin.{h,cpp}
(PalettePlugin/PaletteView) + paletteplugin.json. Owns the command palette
(Alt+X / M-x): collectActions over the window's GUI clients, the ":"-verb
pack (OllieCommands, constructed once in PalettePlugin), positionPalette,
frecency ranking, and the runAction dispatcher. Its "Go to File/Symbol"
entries no longer call switcher code directly — they trigger the switch
plugin's window actions deft_goto_file / deft_goto_symbol by objectName via
findActionByName, so no cross-plugin link is required. Links palette +
olliecommands_lib.

deft_switch.so ([deft:util] switch): new src/switch/switchplugin.{h,cpp}
(SwitchPlugin/SwitchView) + switchplugin.json. Owns the project file (Alt+P)
and symbol (Alt+G) switchers: ProjectIndexer, KateProjectBridge (follows
kateprojectplugin, invalidates the index on project change), async
cache-populated palettes, ctags 1-based->0-based symbol jumps. Registers
deft_goto_file / deft_goto_symbol as window actions so palette and radial can
reach them. Links palette + project_lib.

findActionByName now also searches window()->actions(), since sibling doors are
registered via QWidget::addAction (no XMLGUI client); this is what makes the
cross-plugin door resolution actually work.

Each plugin keeps its own FrecencyStore in a separate file under deft/
(palette.frecency.json, switch.frecency.json) so the two independent rankings
can no longer clobber one another through a shared file; the old config dir
olliepalette/ and the ollie: id prefixes are retired in favour of deft/ and
deft:cmd:/deft:file:/deft:sym:.

Deletes src/plugin/ (ollieplugin.{h,cpp,json}, CMakeLists) and the core 'deft'
CMake target; src/CMakeLists.txt now builds acme, commands, switch under the
KF6TextEditor guard and no longer adds the plugin subdir. The suite is now six
one-feature plugins: palette, switch, radial, acme, sam, plumb.
2026-10-08 19:55:13 +02:00
Levi Neely ec2802b11d acme: extract into standalone [deft:nineify] acme plugin (deft_acme.so)
Stage 3b of the split. acme becomes its own KTextEditor plugin: new
src/acme/acmeplugin.{h,cpp} (AcmePlugin/AcmeView) + acmeplugin.json
("[deft:nineify] acme"), built as deft_acme.so with its own event filter:
the ^A/^E/^H/^U/^W line keys, double-click bracket/quote pair selection, and
the left-button mouse chords (left+middle=Cut, left+right=Paste) plus X11
primary-paste suppression. The bracket/quote/step helpers move here too.

The core deft plugin drops all acme code; its event filter now only
intercepts the Alt+X/P/G launcher-door keys for palette + switch.
2026-10-08 19:46:59 +02:00
Levi Neely 3bf6f9425f radial: extract into standalone [deft:util] radial plugin (deft_radial.so)
Stage 3a of the util split. radial becomes its own KTextEditor plugin:
new src/radial/radialplugin.{h,cpp} (RadialPlugin/RadialView) + radialplugin.json
("[deft:util] radial"), built as deft_radial.so with its own event filter
(mouse-button gesture + context-menu suppression + pane-focus pointer warp).

The optional Wayland pointer-warp glue (waylandcursorwarp.* + the ECM
wayland-scanner protocol lib) moves from src/plugin into src/radial, since the
warp is only used to follow focus after a radial pane action.

Door object names renamed ollie_* -> deft_* (deft_command_palette /
deft_goto_file / deft_goto_symbol) so the radial resolves the sibling plugins'
launcher actions via the GUI factory; built-in radial config + test updated.

The core deft plugin drops radial code, members, the radial branches of its
event filter, and the Wayland glue; it keeps palette + switch + acme keys.
2026-10-08 19:41:09 +02:00
Levi Neely cd72aa043b plumb: extract into standalone [deft:nineify] plumb plugin (deft_plumb.so)
Stage 2b of the multi-.so split. plumb becomes its own KTextEditor plugin:
new src/plumb/plumbplugin.{h,cpp} (PlumbPlugin/PlumbView) + plumbplugin.json
("[deft:nineify] plumb"), built as deft_plumb.so. The F2 plumb action, the
edit-port reader wiring, plumbAtCursor/onPlumbEdit, and the acme isFileChar/
fileTokenAt file-token helpers move out of OllieView into PlumbView.

The core deft plugin no longer links plumb_lib, owns the Plumber, or carries
the plumb/file-token code. activeDocumentDirFor stays in the core (the
switchers still use it) and is duplicated into the plumb plugin.
2026-10-08 19:24:34 +02:00
Levi Neely debe1226f7 sam: extract into standalone [deft:nineify] sam plugin (deft_sam.so)
Stage 2a of the multi-.so split. sam becomes its own KTextEditor plugin:
new src/sam/samplugin.{h,cpp} (SamPlugin/SamView) + samplugin.json
("[deft:nineify] sam"), built as deft_sam.so. The sam tool-view wiring and
the runSamProgram/applySamToDocument/runSamFileLoop glue move out of
OllieView into SamView; sampanel.{h,cpp} move from src/plugin to src/sam.

The core deft plugin no longer links sam_lib or owns the sam panel.
2026-10-08 19:20:35 +02:00
Levi Neely d96cb63baf rename suite to Deft (stage 1: namespace + artifact)
Rename the C++ namespace katecustom -> deft and include guards
KATECUSTOM_* -> DEFT_* across all sources. Rename the plugin artifact
olliepalette.so -> deft.so (CMake target 'deft') and set the plugin
display name to 'Deft'. Update README title/install paths.

File names, class names (Ollie*), and the src/ directory grouping are
left for the follow-up stages that split features into per-plugin .so's
([deft:nineify] / [deft:util]).
2026-10-08 18:49:35 +02:00
Levi Neely 019fed6db4 plugin: plumb file token with acme isfilec expansion
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 '_').
2026-10-08 18:11:23 +02:00
Levi Neely 08a75c06af plumb: bound 9P message size in recvMsg to fix SIGBUS on desync
A corrupt or desynced stream could deliver a size[4] far larger than the
negotiated msize. recvMsg trusted it, so bodyLen=int(size)-7 became huge
(or negative when size>INT_MAX), and the following b.resize()/readExactly
corrupted the heap -> SIGBUS in the PlumbReader edit-port thread. Reject
any message whose size exceeds the negotiated msize.
2026-10-08 18:06:13 +02:00
Levi Neely f503c53dfc plugin: treat '_' as a word char in acme ^W erase-word 2026-10-08 18:02:37 +02:00
Levi Neely 03f209bfd7 plugin: extend double-click selection to '' "" `` quote pairs 2026-10-08 17:33:29 +02:00
Levi Neely 376a190b52 docs: add docs/PLUMBING.md for configuring ~/lib/plumbing
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.
2026-10-08 16:14:41 +02:00
Levi Neely 50d037690f plumb: open a plumbed directory as a folder in Kate
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.
2026-10-08 15:45:49 +02:00
Levi Neely 82f91755a5 plumb: fix crash from deleting a running reader thread on shutdown
Kate crashed on a worker thread inside olliepalette.so near
QArrayData::deallocate. Root cause: ~Plumber did stop(); wait(2000);
delete m_reader; — but the reader can block longer than 2s (NineP connect
waitForConnected(5000), or readExactly's unbounded waitForReadyRead(-1) mid
message). When wait() timed out we deleted a still-running QThread, freeing
its QString/QByteArray members under run() → use-after-free / double free on
the reader thread.

Fixes:
  - ~Plumber never deletes a running thread. stop(); wait(3000); delete only
    if it finished, else hand ownership to the thread via finished->deleteLater
    so it frees only after run() returns.
  - NineP::readExactly waits with a bounded 2s timeout instead of forever, so a
    truncated/stalled message fails cleanly and the reader stays promptly
    stoppable (never force-deleted while blocked).

test_plumb_live still 4/4 (reader start/recv/clean shutdown); 18/18 ctest.
2026-10-08 15:20:21 +02:00
Levi Neely b7320a5f23 sam panel: add Clear button for the output log
Run and Clear sit in a horizontal row (Run expands, Clear fixed) so Clear
costs no extra vertical space. Clear calls the existing clearOutput().
2026-10-08 15:16:08 +02:00
Levi Neely df287b3a7f sam X/Y: target open buffers, not the whole project
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.
2026-10-08 15:11:52 +02:00
Levi Neely b9a539d99c docs: mark M5 DONE (all sub-items complete; label was stale) 2026-10-08 15:01:51 +02:00
Levi Neely bace9bbbbf sam panel: drop the in-panel docs hint label
Users know sam or read docs/SAM.md; the hint was clutter. Panel is now just
the program editor, Run button, and output log.
2026-10-08 14:51:28 +02:00
Levi Neely e53dacbf90 sam panel: fix tiny top-left controls (toolview already has a layout)
Kate's createToolView returns a KateMDI::ToolView that already installs its
own layout. The code created a second QVBoxLayout on it, which Qt silently
rejects, leaving SamPanel unmanaged — pinned top-left at its size hint. Add
to the existing layout when present, else create one.

Also harden SamPanel: give the program editor, Run button, and output log
minimum heights and explicit Expanding size policies so they never collapse.
2026-10-08 14:48:08 +02:00
Levi Neely 6341b1ad01 sam: port sam's own regex engine (leftmost-longest, linear-time)
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.
2026-10-08 14:44:05 +02:00
Levi Neely 4cdd762bb6 sam: ^/$ are per-line (MultilineOption), matching plan9 regexp(7)
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.
2026-10-08 14:29:19 +02:00
Levi Neely 997055f0b5 docs(sam): document PCRE deviations + implications for katesam usage
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.
2026-10-08 14:03:55 +02:00
Levi Neely 25a0276e8e Sam: structural-regexp editing panel (sam in Kate)
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.
2026-10-08 13:54:56 +02:00
Levi Neely bd3a58e246 Plumbing: Kate as a full plan9 plumb client (plumb to edit)
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.
2026-10-08 13:35:39 +02:00
Levi Neely eba8312969 Radial: warp cursor to the surviving pane on close
Add view_close_current_space / view_close_others to isPaneFocusAction so
closing a pane warps the pointer onto the neighbour Kate activates. Same
deferred warp as split/focus.
2026-10-08 12:28:46 +02:00
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