Commit Graph

99 Commits

Author SHA1 Message Date
Levi Neely 0c379feb4e sloppyfocus: revert to original setFocus() activation
The port regressed because I replaced the original's pane->setFocus() with
MainWindow::activateView() on a theory that setFocus() didn't move Kate's focus.
The original logic was correct and working; activateView() broke it (and can't
target the pointed pane for a split of the same document). Restore the exact
original activation. The port is now logic-identical to the standalone plugin;
confirmed working in a live split.
2026-10-09 12:00:56 +02:00
Levi Neely fd84f59f14 sloppyfocus: fold kate-sloppyfocus into the suite as deft_sloppyfocus
Bring the standalone kate-sloppyfocus plugin (focus-follows-mouse) into Deft as
the seventh plugin, [deft:util] sloppyfocus, under src/sloppyfocus/.

Ported to the suite's conventions:
- Wrapped SloppyFocusPlugin/View/ConfigPage in namespace deft.
- KF6-only CMake (dropped the KF5 fallback), target deft_sloppyfocus, installed
  to kf6/ktexteditor and guarded by KF6TextEditor_FOUND; links KF6::TextEditor/
  CoreAddons/I18n/ConfigCore + Qt6::Widgets.
- metadata.json -> sloppyfocusplugin.json with the [deft:util] sloppyfocus
  display name and author Levi Neely; factory JSON and .moc renamed to match.
- Config group renamed to deft_sloppyfocus.

Two port fixes required by the suite's stricter build:
- forward-declare QCheckBox/QSpinBox at global scope (the original's inline
  'class QSpinBox *' members became deft::QSpinBox once wrapped in the namespace).
- the KConfigGroup group name must be a QString (QT_NO_CAST_FROM_ASCII is on
  here), so the group is a QStringLiteral rather than a const char *.

README (plugin table, a new sloppyfocus section, layout tree) and AGENTS.md
updated. The plugin already removes its qApp event filter in its destructor, so
it is not subject to the reload crash fixed in 629a2da. 18/18 ctest green.
2026-10-09 11:31:30 +02:00
Levi Neely 7de4f8b549 radial: defer Ollie verb invocation to fix intermittent no-op
The Ollie slices only worked intermittently. onRadialActivated runs from
RadialMenu::finishActivated(), which emits activatedId() while the radial popup
(Qt::Popup) is still tearing down its grab/focus. The Ollie action methods read
MainWindow::activeView() and the editor selection live; during that teardown
window activeView() is transiently null (and selection unreadable), so a
synchronous invoke silently no-ops — intermittently, depending on timing. The
Kate-action path is unaffected because QAction::trigger() does not depend on
activeView() being valid at that instant.

Defer the Ollie invoke to the next event-loop turn via QTimer::singleShot(0),
after the popup has fully closed and the editor is active again. Guard the
plugin view with QPointer in case it is unloaded before the deferred call runs.
2026-10-09 11:10:18 +02:00
Levi Neely f05ad9b654 radial: fix Ollie plugin lookup key (ollie_kate, not ollie-kate)
pluginView() is keyed by the plugin's catalog name (the .so / target name),
not its KPlugin metadata Id. Ollie's own code confirms the convention —
pluginView("kateprojectplugin"), pluginView("gitplusplus"). The Ollie Kate
plugin's target is ollie_kate, so the key is "ollie_kate" (underscore), not the
metadata Id "ollie-kate" (hyphen). With the wrong key pluginView returned null
and the Ollie radial branch never appeared.
2026-10-09 10:58:12 +02:00
Levi Neely 629a2da68d fix: remove app event filters on view destruction (crash on plugin reload)
The acme, palette, radial, and switch views each call
qApp->installEventFilter(this) but never removed it. QObject only auto-removes
event filters installed *on itself*, not filters it installed on *another*
object (here qApp). So when a plugin .so is unloaded/reloaded — e.g. reinstalling
the plugin — the view is destroyed while qApp still holds a dangling pointer to
it in its filter list, and the next dispatched event calls into freed memory:

  QCoreApplicationPrivate::sendThroughApplicationEventFilters -> SIGSEGV

Add qApp->removeEventFilter(this) to each view destructor (giving AcmeView a real
dtor body instead of =default). 18/18 ctest green.
2026-10-09 10:56:52 +02:00
Levi Neely 67e8d53827 radial: add a conditional Ollie submenu to the right-button radial
When the sibling Ollie Kate plugin (ollie-kate) is loaded in the window, the
right-button radial grows an 'Ollie' branch: Ask, Explain, Fix, Refactor, Tests,
Doc, Verbatim, Start.

Ollie's context actions are not window/collection actions (they live on a
transient context menu, which the radial suppresses anyway), so findActionByName
cannot resolve them. Instead each slice carries an 'ollie:<verb>' id; triggerOllieVerb()
resolves the plugin view via pluginView("ollie-kate") and invokes its
Q_INVOKABLE action method by name (submitSelectionPrompt for fix/refactor/test/doc,
the zero-arg methods for the rest). The branch is built at show time and omitted
when the plugin is not loaded, mirroring the LSP branch. 18/18 ctest green.
2026-10-09 10:54:47 +02:00
Levi Neely 36e9890ec7 radial: add a conditional LSP submenu to the right-button radial
When Kate's LSP Client plugin is active, the right-button (editing) radial now
grows an 'LSP' branch: Go to Definition/Declaration/Type, Find References/
Implementations, Rename, Format, Code Action, Symbol Info, Hover.

The branch is built in showRadialAt() at open time, not in setupRadials(), so
it tracks the plugin being enabled/disabled without a restart. Each entry is
included only if its action currently resolves via findActionByName — the LSP
actions are referenced by their visible menu labels (the objectNames are not a
stable contract, the labels are) and the text-fallback resolver triggers them
like any other slice. When none resolve (LSP not loaded) the branch is empty and
the caller omits it, so nothing shows for non-LSP buffers.

Only the def with button == RightButton gets the branch (the editing radial),
not the middle-button window radial. 18/18 ctest green; deft_radial reinstalled.
2026-10-09 09:49:18 +02:00
Levi Neely efa1fe74c2 docs: credit eaburns's devdraw/acme as the source of the Wayland warp technique
Reword from 'following'/'taken from' to 'sourced from' — the pointer-constraints
warp technique was drawn from eaburns's Wayland devdraw/acme, not tracked or
lifted wholesale.
2026-10-08 21:04:01 +02:00
Levi Neely 4a785a8d46 docs: correct Wayland warp attribution to eaburns's devdraw/acme
The pointer-constraints warp technique follows eaburns's Wayland implementation
of devdraw/acme (github.com/eaburns/plan9port, wayland branch), not plan9port
proper. Fix the attribution in both the README radial section and the
waylandcursorwarp.h header comment, and link the source.
2026-10-08 21:02:50 +02:00
Levi Neely 6b8f66aabc docs(README): document radial's pointer-follows-focus warping
The radial menu warps the pointer to the centre of the newly active pane after
a split/close/focus-move slice (X11 QCursor::setPos; Wayland pointer-constraints,
as plan9port does), so the mouse follows keyboard/pane focus — acme-style.
2026-10-08 20:59:21 +02:00
Levi Neely e2a0338f40 docs(README): plug the plumb plugin 2026-10-08 20:58:15 +02:00
Levi Neely 5830340f1d docs(README): note plan9port as a runtime dep of the plumb plugin 2026-10-08 20:57:25 +02:00
Levi Neely 59e1178958 docs(README): remove 'Why not just use Kate's command bar?' section 2026-10-08 20:56:47 +02:00
Levi Neely d799603587 docs(README): link Kate's website on first mention 2026-10-08 20:54:53 +02:00
Levi Neely 94d609f3ab docs(README): credit the video-game radial lineage; foreground one-handed typing
- Name video games as the source for the radial/pie menu (weapon/emote wheel)
  alongside acme/sam/plumber, Emacs, and Sublime.
- Stop understating the one-handed-typing shift: state plainly that it is the
  driving ergonomic constraint — the reason the mouse carries the reaching and
  chords are off the table — in the intro, the design-constraints list, and the
  radial section.
2026-10-08 20:53:42 +02:00
Levi Neely e31fee664e docs(README): rewrite the intro to the honest motivation
Replace 'for people who despise menus' with the real story: an attempt to turn
Kate into a daily driver by pulling in ideas from the author's toolkit (acme,
sam, plumber; Emacs M-x; a Sublime-style palette), rather than claiming Kate is
already the author's editor.
2026-10-08 20:50:50 +02:00
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