Commit Graph

238 Commits

Author SHA1 Message Date
Levi Neuwirth 8c962998f6 audit remediation: workspace clippy gate + stale metadata + cruft (F-001/F-013/F-015)
Acts on the 2026-07-03 repository audit (tracked as
docs/repository-audit-2026-07-03.md).

F-001 — the pmacs-gpu clippy CI blind spot, closed:
- Fix the 3 workspace-clippy lints in pmacs-gpu/src/main.rs (two
  cast_possible_wrap in minimap_jump_to via i64::try_from; a
  format_push_string in compose_status_spans via write!).
- README documents `cargo clippy --workspace --all-targets` and
  `cargo test --workspace`.
- CI's Lint job now also runs `cargo clippy -p pmacs-gpu --all-targets
  -- -D warnings` — the root-package clippy never lints the flavor-
  independent pmacs-gpu crate, which is how these (and recent) warnings
  slipped through.

F-013 — refresh stale metadata: pmacs-gpu/Cargo.toml description and the
docs/pmacs-gpu-design.md status header, from "session 2: hello-world /
pre-implementation" to the real feature set + the per-feature docs.

F-015 — .gitignore editor backups (escaped `#*#`, `.local-bak`) and the
root `/pmacs` dev symlink, so they stop showing as untracked. Untracked
docs are left for triage per the audit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 10:31:17 -04:00
Levi Neuwirth e5d77816b5
Merge pull request #74 from levineuwirth/session-gpu-chord-forwarding
GPU: general command-chord forwarding (no protocol change)
2026-07-03 10:15:02 -04:00
Levi Neuwirth 0de4b3bee0 docs: GPU general chord forwarding framing + as-built (Q#GC)
Records the rule (forward Char/Enter/Tab + Ctrl/Alt), what stays local
(Ctrl-V paste, Escape, Meta/Super → OS), what didn't change
(should_forward_key, motion, the optimistic path), and the bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 10:05:55 -04:00
Levi Neuwirth bc32332a41 pmacs GPU: forward all command chords, not an allowlist (Q#GC1)
The GUI now forwards any command chord (Char/Enter/Tab with Ctrl or Alt)
to the daemon, so the whole Emacs keymap is reachable in pmacs-gpu —
`C-a`/`C-e`, `M-f`/`M-b`, `M-d`, `C-/`, `C-k`, `C-x C-s`, … — not just a
hand-carved allowlist. Pure GPU input routing; the wire and daemon are
untouched (no protocol change).

`is_command_chord` replaces `is_search_entry_chord` / `is_clipboard_chord`
/ `is_minibuffer_open_chord` (all three were just `Char + Ctrl/Alt`,
subsumed), collapsing three near-identical handler blocks into one
(−51 lines). This is the payoff of the minibuffer arc: the reason the GUI
withheld command chords was the un-renderable minibuffer (Q#MB1), now
gone. Forwarded chords still mark the optimistic cursor stale and never
flip `dispatch_idle` locally (the search-entry precedent).

Unchanged: `should_forward_key` still withholds Ctrl/Alt chords (they're
caught before it — its test holds); motion / Backspace / Delete keep
their defer-aware path; `Ctrl-V` stays local OS paste; `Escape` stays
quit/cancel; `Meta`/`Super`-only chords are left to the OS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-07-03 10:05:55 -04:00
Levi Neuwirth aae37d821d
Merge pull request #73 from levineuwirth/session-gpu-minibuffer
GPU minibuffer: prompt line + completion dropdown (protocol v12)
2026-07-03 09:51:44 -04:00
Levi Neuwirth e0f7ff6eca docs: GPU minibuffer framing + as-built (Q#MB)
Framing + as-built record: the render-only architecture, the v12 wire, the
band prompt line + caret, the candidate dropdown, opening-chord forwarding,
the delivered phasing, and an "As-built divergences" section (monospace
caret, a third TextRenderer, best-at-top dropdown ordering) plus the
categorical bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-30 21:03:32 -04:00
Levi Neuwirth b9bd231e64 pmacs GPU minibuffer: wire v12 + band prompt + candidate dropdown (Q#MB1)
The pmacs-gpu frontend can now render the minibuffer, so M-x, C-x-prefixed
commands, and the LSP rename prompt work in the GUI. Render-only — the
minibuffer logic already lives in the core, which is untouched (its fields
are public, so the producer reads them directly).

Protocol v12 (additive; SUPPORTED = [6..12]):
- `InstanceMessage::MinibufferPrompt { prompt, input, cursor, candidates,
  selected, total }` — bufferless (the minibuffer is one global core
  instance), daemon-gated >= 12. The candidate list ships as a windowed
  slice (<= MB_VISIBLE = 10) around the selection, so a 1000-command M-x
  sends ~10 strings per keystroke, not 1000.

Producer / daemon / TUI:
- `semantic_render::minibuffer_prompt_msg` — cached-compare suppressed
  (a single value, not per-buffer), emitted from the active-buffer
  viewport. daemon gates the variant >= 12. The TUI ignores it (it paints
  the minibuffer via its own bottom row).

GPU:
- The bottom band shows `prompt + input` (ahead of search/status) with a
  band caret at the input cursor (monospace advance off the shaped band
  width); the buffer caret hides while a prompt is open.
- A vertical completion dropdown above the band — best match at top,
  selected row highlighted — via a third `TextRenderer` over bg quads
  (the menu popup pattern, reusing its colors). Only shows when there are
  candidates.
- `is_minibuffer_open_chord` forwards M-x and the C-x prefix (otherwise
  withheld) so the GUI can open a prompt / enter a prefix; the daemon then
  flips `dispatch_idle` false and the intercept gate round-trips the rest.
  (Also collapsed two unnested_or_patterns clippy nits in the chord
  helpers.)

Tests: candidate windowing, the producer (open M-x via Lua -> prompt +
windowed candidates -> cached-compare -> cancel clears), a v12 postcard
round-trip, and the version pin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-30 21:03:32 -04:00
Levi Neuwirth a6070e201d
Merge pull request #72 from levineuwirth/session-context-menu
Right-click context menus + OS clipboard (protocol v11)
2026-06-30 16:55:38 -04:00
Levi Neuwirth 844821bd27 docs: right-click context menu framing + as-built (Q#CM)
Consolidates the framing into an as-built record: the architecture,
context model, clipboard, input/routing, wire (v11), the delivered
phasing, and an "As-built divergences" section noting where the build
departed from the framing (no `SetClipboard` message, resolve runs in
Lua, `MenuPrompt` carries rows not "MenuItemWire", Emacs clipboard keys,
etc.) plus the categorical bets that held.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:19:00 -04:00
Levi Neuwirth 640b998d6b pmacs context menu: protocol v11 + dispatch + TUI/GPU surfaces (Q#CM1/Q#CM5)
The wiring that makes the menu and OS clipboard work end-to-end. The
protocol bump touches every exhaustive match on the wire enums, so the
daemon / frontend / GPU consumers all land together.

Protocol v11 (additive; SUPPORTED = [6..11]):
- `PointerKind::Context` (right-click), `FrontendEvent::MenuPointer`
  (GPU->daemon navigation, index-only), `InstanceMessage::MenuPrompt` +
  `MenuPromptRow` (daemon->GPU rows + highlight, daemon-gated >= 11).

Dispatch + producer:
- `EditorState`: menu interception in `dispatch_key`/`dispatch_mouse`,
  `MenuKey`, `dispatch_menu_key`/`_mouse`, `open_context_menu` (TUI) /
  `open_menu_at_byte` + `dispatch_menu_pointer` (GPU), `build_menu_rows`
  (calls the Lua resolver), `dispatch_idle` now false while a menu is
  open. `dispatch_pointer` gains the `Context` arm.
- daemon: routes `Context` -> open, `MenuPointer` -> navigate; gates
  `MenuPrompt` >= 11; drains the clipboard publish as
  `InstanceSignal::Clipboard`; honors the previously-dropped
  `FrontendEvent::Paste` (so paste works for the first time).
- `semantic_render`: `MenuPrompt` producer with cached-compare.

Frontends:
- TUI (`frontend.rs`): OSC 52 clipboard write; ignores `MenuPrompt`
  (the cell overlay renders the menu).
- GPU (`pmacs-gpu`): `arboard` dep; clipboard write/read + Ctrl-V inbound
  paste; right-click -> `Context`; `MenuLocal` + `MenuPrompt` handler;
  the popup (a second `TextRenderer` over bg quads) at the click pixel;
  hover/click -> `MenuPointer`; key intercept while open.

Also folds a pre-existing clippy `unnested_or_patterns` nit in a search
test (`Color::Indexed(11 | 3)`) that newer CI clippy surfaced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:19:00 -04:00
Levi Neuwirth b934723dfd pmacs context menu: clipboard commands/keys + LSP context accessor (Q#CM5/Q#CM6)
The Lua glue that gives the menu real items to surface.

- `edit.copy/cut/paste/select-all` commands (Q#CM6) over the core
  clipboard, with the Emacs kill/yank bindings `M-w`/`C-w`/`C-y` and
  `C-x h` (the CUA trio's keys are already bound: `C-a` line-start,
  `C-v` page-down).
- `pmacs.lsp.active_attachment()` (Q#CM5): a pure, side-effect-free
  attachment lookup for the menu's `symbol`/`diagnostic` visibility
  checks. Unlike `attached_for_active`, it never triggers an attach just
  because the menu opened.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:03:58 -04:00
Levi Neuwirth 8929bf0d25 pmacs context menu: core clipboard + menu methods + Lua surface (Q#CM1/Q#CM3/Q#CM6)
The core-side machinery the menu and clipboard ride on, plus the Lua
resolver. Still no dispatch wiring (that needs the protocol/frontend
commit), so this builds but nothing is reachable yet.

- Clipboard (Q#CM6): an in-core slot + `copy`/`cut`/`paste`/`select-all`
  on `EditorCore`, plus a one-shot `pending_clipboard` the dispatcher
  will drain. `region_bytes` / `word_at_cursor` (the latter feeds the
  `symbol` context).
- Menu core (Q#CM1): `SharedMenu` field + `menu_open/close/step/
  set_active_row/active_command/hit` + `ensure_menu_overlay`.
- `pmacs.menu` install (item/list/remove/clear/_raw) and `ed.*` bindings
  (clipboard_copy/cut/paste, select_all, word_at_cursor); the `install`
  signature gains the menu registry, threaded through `lua.rs`.
- `builtin/menus/default.lua`: `pmacs.menu.build` resolves visible items
  (predicate or context tag), groups/sorts, and emits rows (Q#CM3). The
  default items reference commands by name (resolved at invoke).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 22:03:58 -04:00
Levi Neuwirth 487c12cca9 pmacs context menu: registry + open-menu state types (Q#CM1/Q#CM2)
The data types for the right-click context menu, with no behavior yet
(nothing opens a menu until the dispatch wiring lands).

- `MenuRegistry` / `MenuItem` / `MenuError` (Q#CM2): the Lua-facing item
  registry, mirroring `CommandRegistry`. Items carry id / label / command
  / context tag / predicate / group / order; `context` is validated
  against a known vocabulary (typo -> hard error, R50-style), and a
  matching `id` replaces in place so config reloads and user overrides
  are idempotent.
- `MenuState` / `MenuRow` / `SharedMenu` (Q#CM1): the open-menu runtime
  state, plus the self-suppressing TUI `MenuView` overlay (the
  `SearchView` pattern). `MenuRow` is Item|Separator; navigation and
  hit-testing skip separators.

Pure additions behind `pub mod menu` — the lib still builds with the
module unused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014TXbAwk27agwhrNNrhLi2U
2026-06-27 21:42:28 -04:00
Levi Neuwirth 5f342ed2c6
Merge pull request #71 from levineuwirth/session-regex-search
Regex in-buffer search (multi-line, C-M-s + M-r toggle)
2026-06-27 15:04:01 -04:00
Levi Neuwirth 6b7d3fb95d docs: consolidate in-buffer search framing (substring + regex)
Folds incremental-search-framing.md (#70) and regex-search-framing.md
into a single in-buffer-search-framing.md that records the design as
shipped, and removes the two superseded docs. The incremental doc in
particular described the C-f + minibuffer-hosted plan that never
shipped (C-s/C-r + a dedicated core SearchSession did), which was
actively misleading.

The consolidated doc adds an "As-built divergences" section capturing
the four places implementation departed from the framing passes:
C-f → C-s/C-r (veto resolved), minibuffer-hosted → frontend-agnostic
core mode (the GUI has no minibuffer), regex deferred → shipped, and
single-line substring → multi-line regex wash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 15:03:31 -04:00
Levi Neuwirth 6e47fb4725 regex-search: GUI regex prompt + protocol v10 (Q#RX5/RX6)
Carries regex mode to the GUI status band and lets the GUI start a
regex search.

SearchPrompt gains `regex` + `invalid` (protocol v10; SUPPORTED grows
to [6,7,8,9,10]). The fields changed that variant's encoding, so the
daemon's per-session gate moves from >= 9 to >= 10 — a v9 peer
negotiates v9 and is simply sent no SearchPrompt (the decorations
still highlight) rather than mis-decoding the wider shape. The
producer fills both from the active SearchSession.

GUI: `is_search_entry_chord` also forwards C-M-s / C-M-r (Ctrl+Alt) so
a regex search can start; M-r (the toggle) already round-trips via the
intercept path once a search runs. The status band reads
`Regex I-search:` in regex mode and `[invalid]` when the pattern won't
compile. Multi-line regex matches needed no GUI change —
push_glyph_extent_rects already fans a byte range across lines.

Tests: SearchPrompt postcard round-trip extended to regex/invalid
shapes; protocol version pin 9→10 + ladder grows to v10; GUI entry
chord accepts C-s/C-r and C-M-s/C-M-r. (last_search_prompt's 5-tuple
factored into a SearchPromptFacts alias to satisfy type_complexity.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:46:43 -04:00
Levi Neuwirth 7723f51f12 regex-search: TUI regex mode + multi-line wash (Q#RX3/RX4/RX5)
Wires regex matching into the search session and the terminal
frontend.

SearchSession gains `regex` and `invalid` flags. search_begin takes a
`regex` argument; recompute dispatches find_all_regex (regex) vs
find_all (literal), recording `invalid` when the pattern won't
compile (an invalid pattern clears the matches and shows [invalid]
rather than a stale count). search_toggle_regex flips the mode and
re-runs the current query.

Input: C-M-s / C-M-r start a regex search (search.forward-regex /
search.backward-regex commands → ed.search_start(forward, regex)).
M-r toggles literal <-> regex mid-search — a new SearchKey decoded in
dispatch_search_key, so it works the same in both frontends (the GUI
already round-trips every key while searching). The TUI prompt reads
"Regex I-search:" in regex mode and "[invalid]" when the pattern
won't compile.

Multi-line: SearchView now washes each row a match spans, mirroring
paint_local_selection's per-row clip (newline excluded so a spanning
match doesn't paint a phantom trailing cell). Single-line matches —
every literal match — touch exactly one row, unchanged. The GPU
already fans multi-line ranges per-line, so it needs no change here.

Tests: regex match / smart-case / invalid-flags-and-recovers /
toggle-reinterprets-query (core); C-M-s starts regex + M-r toggles
mid-search (dispatch); multi-line per-row wash (SearchView render).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:46:33 -04:00
Levi Neuwirth 5c34b3c37a regex-search: smart-case multi-line find_all_regex (Q#RX1/RX2)
The regex sibling of find_all, on regex::bytes::Regex over the whole
buffer. Returns Option<Vec<ByteRange>>: Some for a valid pattern
(possibly empty), None when it fails to compile — so the caller can
tell an invalid pattern (show [invalid]) from a valid zero-match
search.

Smart-case mirrors the literal path: case-insensitive via a (?i)
prefix unless the pattern carries an uppercase letter. Multi-line is
free — the regex runs over the whole byte slice, so an explicit \n (or
(?s).) spans lines while `.` keeps its default. Zero-width matches
(a*, ^, $) are filtered. The regex crate (already transitive in the
lockfile) is promoted to a direct dependency; its linear-time engine
makes a pathological pattern slow at worst, never catastrophic.

Tests: pattern match, smart-case both ways, \n-spanning + dotall +
default-no-cross, invalid→None vs valid-zero→Some(empty), zero-width
filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:22:51 -04:00
Levi Neuwirth e03f5620c9 framing: regex in-buffer search (multi-line, C-M-s + M-r toggle)
Extends the merged isearch (#70) with regex matching: both dedicated
entry keys (C-M-s / C-M-r) and a mid-search toggle (M-r), with
multi-line matches. Q#RX1–RX6 stances + bets.

Key survey finding recorded: the GPU's push_glyph_extent_rects already
fans a byte range across visual lines (multi-line selections use it),
so multi-line needs no GPU rendering change — only the TUI SearchView
(which assumed single-line) mirrors paint_local_selection's per-row
wash. SearchPrompt gains regex/invalid → protocol v10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 14:20:47 -04:00
Levi Neuwirth f359d4a634
Merge pull request #70 from levineuwirth/session-incremental-search
Incremental in-buffer search (smart-case isearch), both frontends
2026-06-27 14:16:01 -04:00
Levi Neuwirth 2326fa6e32 search: route GUI keystrokes into the daemon's search (Q#SR5)
Fix for "C-s highlights but typing still edits the buffer" in the GPU
frontend. The GUI withholds Ctrl chords (`should_forward_key`) and
treated Escape as a hard quit, so it could neither *start* a search
(C-s never reached the daemon) nor route the query keys into one —
they fell through to the optimistic-apply path and edited the
document.

The GUI now mirrors the daemon's input-interception state. A new
`daemon_intercepts_keys` (true while a `SearchPrompt` is live or the
daemon reports `DispatchIdle { idle: false }`) gates the key path:

- While intercepting, every key round-trips to the daemon — no
  optimistic apply — so chars extend the query, C-s/C-r step, BS
  shortens, RET accepts, C-g cancels. This reuses the M11.6 gate
  the optimistic path already honored; the new part is round-tripping
  the *command* chords that `should_forward_key` would otherwise drop.
- While idle, C-s / C-r are forwarded as search-entry chords (still
  withheld for every other Ctrl chord) so a search can begin. No
  optimistic local idle-flip: the daemon's `DispatchIdle` /
  `SearchPrompt` flip the gate one round-trip later, so a rebound C-s
  that doesn't start a search can never wedge the gate.
- Escape cancels an active search (round-tripped to the daemon's
  `SearchKey::Cancel`) instead of quitting the window; it stays the
  local quit when nothing is intercepting.

Also aligns the GUI status band's empty-query prompt with the TUI
(`I-search: ` with no `[no match]` until a non-empty query misses).

Tests: `is_search_entry_chord` (C-s/C-r + Ctrl only; and the fact that
`should_forward_key` withholds it, which is what the entry path
exists to override). The end-to-end routing is GUI-window behavior,
validated manually.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:02:53 -04:00
Levi Neuwirth 22773737e9 search: attach the TUI match-wash overlay so isearch is visible
Fix for "seems to only search for the first character." The TUI's
`SearchView` overlay was written (commit 2) but never attached to a
window, so the terminal frontend painted no match highlights — the
only feedback was the cursor jumping to the first match, which made
refining the query past the first character look like a no-op even
though the search was working (verified: the query accumulates
correctly through the full run-loop path).

`search_begin` now attaches a `SearchView` to the active window
(deduped by overlay kind, so repeat searches don't stack it). The
view self-suppresses when the store has no matches or is stale, so a
persistent attach is safe — it paints only while a search has live
matches and stops the moment an edit invalidates them.

`SearchView` now keys on the *rendered* buffer (`Buffer::id`) instead
of a fixed id captured at construction, so one attached instance
keeps highlighting correctly even if the window later switches
buffers (the store is per-buffer; a buffer with no entry paints
nothing).

Tests: a render-level test that paints a real frame mid-search and
asserts both the match wash (bright `Indexed(11)` on the active
match) and the full `I-search: foo` prompt land on the grid — the
coverage that was missing, which would have caught the unattached
overlay. Plus a run-loop-fidelity test (renders interleaved with
keystrokes) pinning that the query accumulates rather than sticking
at the first character.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:47:12 -04:00
Levi Neuwirth 5111ae82e7 search: GPU isearch surface (Q#SR5, protocol v9)
Brings incremental search to pmacs-gpu, which has no minibuffer, by
reusing the shared daemon-side search core from the previous commit.

Key routing needs no new mechanism: `dispatch_idle` now also reports
false while a search is running, so the GPU's existing M11.6
optimistic-apply gate round-trips every keystroke to the daemon —
where `dispatch_search_key` extends the query / steps — instead of
self-inserting it. The match highlights were already wired (commit
2's SearchMatch / SearchMatchActive decoration colors), so they
light up live the moment keys round-trip.

The one thing a semantic frontend can't derive locally is the query
text, so a new additive `InstanceMessage::SearchPrompt { buffer_id,
query, active, total }` carries it (protocol v9, SUPPORTED grows to
[6,7,8,9]). The producer emits it cached-compare-suppressed like
StatusFacts — `query: Some` while searching, `None` to clear on
accept/cancel (matches keep highlighting via decorations), and
stays silent on a fresh buffer that never searched. The daemon's
per-session filter keeps the variant off wires negotiated < 9. The
GPU mirrors it into the status band: while searching, the band's
left side shows `I-search: <query> (n/m)` (or `[no match]`) in
place of the buffer name, returning to the name when the search
ends.

Tests: protocol version pin + SearchPrompt postcard round-trip
(active / failing / cleared shapes); producer emit-on-change +
suppress + clear-on-accept + first-sight silence; dispatch_idle
flips false during search (the GPU round-trip contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:26:25 -04:00
Levi Neuwirth 58b68f6ac0 search: TUI incremental isearch input (Q#SR5)
Wires the live-typing half of in-buffer search for the terminal
frontend, on a frontend-agnostic core so the GPU (next commit) can
share it.

EditorCore gains a `search: Option<SearchSession>` (query + origin
cursor + direction) and the `search_*` methods that drive it:
begin records the origin, input_char/backspace re-run `find_all`
against the origin buffer and refocus the match nearest the origin
(failing searches anchor the cursor back at the origin), step walks
the store's active match (wrapping, also usable post-accept), and
finish either keeps the cursor + matches (accept) or restores the
origin and clears them (cancel). The matches live in the shared
`search_store`, so the decorations producer and the TUI SearchView
light up live as you type.

Input routing is intercepted in `EditorState::dispatch_key`: while
a search runs, every key flows through `dispatch_search_key`
(SearchKey::from_chord) instead of the global keymap — printable
chars extend the query, C-s/C-r (and Down/Up) step, RET accepts,
C-g/Esc cancel, BS shortens. This is the same dispatch path the
daemon's `FrontendEvent::Key` round-trip uses, so the daemon-side
search already works; the GPU just needs to route keys + show the
prompt (commit 4). The TUI paints an `I-search: <query> (n/m)`
prompt on the bottom row while keeping the terminal cursor in the
buffer at the active match.

C-s / C-r start the search (search.forward / search.backward Lua
commands → ed.search_start). Both keys were free in the default
map (save is C-x C-s, redo is C-x r), so isearch lands without
disturbing the CUA / Emacs editing keys — no cursor.right rebind
needed (the framing doc had flagged C-f for veto; C-s is cleaner
and Emacs-faithful).

Any edit now marks the buffer's matches stale in apply_active_edit
(M11.8), closing the headline "stale-after-edit linger" bet:
accepted highlights vanish the moment the text they described
changes, rather than painting at wrong offsets.

Tests: EditorCore-level (begin/type/step/wrap/focus-from-origin/
cancel/accept/backspace/smart-case/stale-on-edit) and dispatch-
level acceptance (C-s drives the whole loop; Esc restores; query
keys never self-insert).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:15:26 -04:00
Levi Neuwirth 7c46288ef0 search: render matches in both frontends (Q#SR3/SR4)
The search store now hangs off EditorCore (reachable by the
producer, the Lua commands, and the TUI view). The decorations
producer emits SearchMatch for every visible match and
SearchMatchActive for the active one, byte-range-direct (no
line/col conversion), viewport-clipped, and stale-skipped on the
M11.8 model. pmacs-gpu wires the two decoration_kind_to_bg_color
arms (translucent yellow / stronger amber). The TUI gets a
SearchView overlay mirroring DiagnosticView (black-on-yellow wash,
brighter for the active match), reusing diag.rs's now-pub(crate)
line/col helpers.

Nothing populates the store yet (commit 3 wires the input), so the
paths are dormant until then — verified by populating the store
directly: producer emits the right kinds + stale-suppresses, the
TUI view washes the cells.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:48:06 -04:00
Levi Neuwirth b167b9a6df search: core SearchStore + smart-case find_all (Q#SR1/SR2)
The per-buffer in-buffer-search store, mirroring DiagnosticStore:
keyed Arc<Mutex>, sorted match ranges + active index, next/prev
stepping with wrap, focus-from-cursor, and stale-on-edit tracking
(M11.8 model — an edit suppresses matches at pre-edit byte
positions until re-search). find_all is smart-case substring
(case-insensitive unless the query has an uppercase char), ASCII
case-folded to keep byte offsets exact, non-overlapping matches.

Pure core, no wiring yet. 8 unit tests (case folding, overlap,
wrap, focus, stale, active-clamp-on-reset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:38:22 -04:00
Levi Neuwirth d45b064ab2 framing: incremental in-buffer search (smart-case isearch)
Decided: incremental isearch (live highlight, same key steps next,
Enter accepts, Esc restores origin) + smart-case substring. Q#SR1
per-buffer SearchStore mirroring DiagnosticStore; Q#SR2 smart-case
find_all over a rope snapshot (regex deferred); Q#SR3 producer +
TUI SearchView mirror the diagnostics path; Q#SR4 yellow/amber
palette; Q#SR5 minibuffer-hosted query + new on_changed hook +
next/prev interception; Q#SR6 navigation mirrors diag.next/prev.
Four bets (stale-after-edit linger the headline). Three-commit arc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:33:40 -04:00
Levi Neuwirth 4d63f21905
Merge pull request #69 from levineuwirth/session-cua-typeover-undo
CUA type-over is a single undo step
2026-06-27 10:20:59 -04:00
Levi Neuwirth db073cc668 CUA type-over is a single undo step (Q#U1)
Typing over a selection composed two edits in Lua —
delete_region() + insert_char() — so it recorded two undo steps:
one undo left the half-replaced text, two restored the original.
Undo granularity is per apply_edit in both modes (v0.1 pushes one
UndoEntry per edit; CRDT commits per edit via export and groups by
commit, with record_checkpoint unused), so the fix is to make
type-over one edit.

New core EditorCore::insert_char_over_region emits a single
EditOp::Replace when a region is active (cursor past the inserted
bytes, selection cleared) and delegates to insert_char otherwise.
The three type-over commands (buffer.newline / tab / self-insert)
call it via a new Lua binding instead of the delete+insert pair.
delete_region and insert_char are unchanged for their other
callers; region-aware backspace/delete already emit one op.

Verified one undo unit in BOTH modes (dual_mode
replace_is_a_single_undo_step covers v01 + crdt — a CRDT Replace is
delete-then-insert internally but one commit) plus an end-to-end
acceptance test through the key-dispatch path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:00:53 -04:00
Levi Neuwirth fde5453721
Merge pull request #68 from levineuwirth/session-gpu-wavy-squiggles
GPU diagnostic squiggles: wavy shader + optimistic clear-on-edit
2026-06-15 19:24:50 -04:00
Levi Neuwirth 77f76f14b0 pmacs-gpu: optimistically clear a diagnostic when an edit touches it
The squiggle was holding a stale wave over corrected text until
rust-analyzer re-analyzed and republished (0.5-2s for a fixed
error) — the daemon's hold-while-stale carries the diagnostic
through the stale window and the GPU translates it through local
edits, so a fixed line kept its red squiggle until republish. (The
2px bar did this too; the bold wave just made it glaring.)

translate_decorations now drops a diagnostic decoration the moment
a local edit touches its range, instead of translating it: editing
or fixing an error clears that squiggle immediately. Scoped to the
touched diagnostic — an error elsewhere still translates and holds,
preserving the no-blink benefit — and to diagnostic kinds only, so
selection / current-line decorations are untouched. If the error
survives the edit, the next republish re-adds it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:00:25 -04:00
Levi Neuwirth 4d71bf7455 pmacs-gpu: wavy diagnostic squiggles (Q#W1)
Replaces the straight 2px diagnostic underline bar (PR #65) with an
actual sine squiggle. A dedicated SquiggleRenderer pipeline whose
fragment shader computes A·sin(x·2π/λ) and alphas pixels by distance
to the curve (fwidth/smoothstep — core WGSL, no MSAA or feature
flag). Geometry reuses push_glyph_extent_rects unchanged; only the
vertex format differs, adding a uv: absolute screen-x (so phase is
continuous across separately emitted glyph-run rects) and signed px
from the band centerline.

The diagnostic underline is split out of the solid wash batch into
its own buffer + draw, slotted under the glyphs where the bar was;
washes (selection / current-line) stay solid quads. The severity
palette (decoration_kind_to_underline_color) and minimap marks are
untouched. DIAG_UNDERLINE_PX (2px) → DIAG_SQUIGGLE_PX (6px) so the
wave fits inside the band.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 18:41:56 -04:00
Levi Neuwirth d1ca124b33
Merge pull request #67 from levineuwirth/session-gpu-status-band
GPU status band: local L:C/scroll + StatusFacts (protocol v8)
2026-06-12 19:33:45 -04:00
Levi Neuwirth 44430e8377 StatusFacts (protocol v8): name, modified, exact diag counts
The wire-authoritative half of the status band (Q#S1): an additive
InstanceMessage::StatusFacts { buffer_id, name, modified,
diag_errors, diag_warnings }, emitted by the semantic producer on
change (cached-compare). Counts freeze at their last value while
the diag store is stale — positions go wrong mid-edit but counts
merely lag, and flickering to zero per keystroke would be worse.
The daemon's write loop keeps the variant off wires negotiated
< 8, the DispatchIdle gate shape; SUPPORTED grows to [6, 7, 8].

GPU side: the band's left shows name + modified dot, the right
gains severity-colored E:n/W:n ahead of the local L:C/scroll
readout (rich-text spans, change-detected per side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:17:07 -04:00
Levi Neuwirth a962d18d4d status band: geometry + local L:C / scroll readout (Q#S2, Q#S3)
A 26px strip reserved at the surface bottom: background quad riding
the bg batch, one-line cosmic-text buffer as a second TextArea in
the same prepare pass, right-aligned by measured width. L:C derives
from the optimistic caret (it must not lag a round trip during
typing bursts) and the scroll readout is the TUI's All/Top/Bot/NN%
formula, ported verbatim and pinned.

text_area_bottom() is now the single source for every
bottom-of-text computation: visible-line estimate, text clip
bounds, the minimap height (via a minimap_height() helper), and
the edge-scroll bottom band all subtract the strip through it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:57:17 -04:00
Levi Neuwirth 70a26d4ac1 framing: GPU status band — local freshness + v8 StatusFacts
Q#S1 splits facts by authority: L:C and scroll derive locally (the
optimistic caret must not lag a round trip); name, modified, and
exact diag counts arrive via a new additive StatusFacts variant
(protocol v8). The reserved ModeLine(Vec<Cell>) stays grid-shaped
and unused. Q#S2 quad + second TextArea; Q#S3 one text_area_bottom
helper for the five bottom-assuming geometry sites. Three bets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:45:12 -04:00
Levi Neuwirth b4871ffe7c
Merge pull request #66 from levineuwirth/session-mouse-deferred-set
Mouse deferred set: Shift-click, triple-click (v7), minimap jump, edge auto-scroll
2026-06-12 13:28:35 -04:00
Levi Neuwirth c9ec6ef02b scroll-to-cursor only when the cursor moved — fixes minimap snap-back
The daemon attaches a CursorByte to every frame it produces,
including the frames the GPU's own Viewport sends trigger. The
CursorByte handler followed the cursor unconditionally, so a
minimap jump away from a stationary cursor snapped straight back:
jump → Viewport → frame + re-announced CursorByte →
scroll_to_cursor → snap, looping on every scrub move (the reported
jitter). Wheel-scrolling past the cursor's screen had the same
latent loop. The handler now compares the arriving position to
own_cursor and follows only genuine movement — scrolling away from
a cursor that isn't moving is the user's prerogative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:10:48 -04:00
Levi Neuwirth f8027a0161 minimap: live thumb + post-jump styled-redraw hold
Two Q#M6 validation findings. The thumb never tracked scrolling —
minimap_vertex_bytes hardcoded first_visible_line = 0 since the
minimap's first session; jumping finally made the frozen thumb
obvious. It now follows scroll_top.

The flash (framing bet #2, called): a far jump rebuilds every
visible line from a span set that covers the old viewport, so the
first frame after the jump drew unstyled text until the daemon's
restyle landed a round trip later. When a scroll rebuild reuses no
shaped line, the redraw is now held until fresh styling arrives
(refresh_changed_lines / reshape clear the hold) or a 25ms
deadline fires in about_to_wait — the styled frame is usually the
first one visible, and plain-text buffers still feel instantaneous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 13:00:22 -04:00
Levi Neuwirth 212c17664c drag auto-scroll at window edges (Q#M7)
While a drag sits within 24px of the text area's top or the
surface bottom, about_to_wait ticks every 35ms: one line of scroll
toward the pointer plus a re-run of the drag hit-test at the
current pointer position — CursorMoved alone would stall the
selection the moment the mouse stops past the edge. Armed/disarmed
from the drag's vertical position; the loop returns to plain Wait
whenever it isn't scrolling, so the tick costs nothing outside the
gesture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:42:48 -04:00
Levi Neuwirth d47b7f5cd5 minimap click-to-jump + scrub (Q#M6)
A press inside the minimap band is consumed before text
hit-testing and never becomes a Pointer event: pixel y maps back
through the painter's linear line interpolation, the viewport
centers on that line via the existing scroll_by_lines plumbing
(clamp / rebuild / viewport send), and holding the button scrubs —
CursorMoved keeps jumping even if the pointer wanders out of the
band, until release. Band + inverse-mapping geometry extracted as
pure fns and pinned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:26:39 -04:00
Levi Neuwirth a358df8cf2 triple-click selects the line (Q#M4, protocol v7)
PointerKind::TripleDown — the cheap additive bump shape returns:
PROTOCOL_VERSION 7, SUPPORTED [6, 7], the new variant kept off
pre-v7 wires by a frontend send-gate that downgrades it to the
plain Down a third click produced before. The GPU's click history
deepens to a chain count (1 → Down, 2 → DoubleDown, 3 →
TripleDown, then restart). Daemon side, select_line_at_cursor
selects the line including its trailing newline, so consecutive
triple-click lines abut.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:24:08 -04:00
Levi Neuwirth e8e494e1c1 Shift-click extends the selection (Q#M5)
dispatch_pointer consults the mods it has carried since v5: a Down
with SHIFT keeps the existing anchor (or, with no selection,
anchors at the pre-click cursor) and only moves the cursor — the
universal extend convention. Zero wire change. Frontend-side, a
Shift-click neither advances nor inherits the multi-click chain,
so two Shift-clicks can't become a word select.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:16:08 -04:00
Levi Neuwirth aea08a58ec framing: mouse deferred set — triple-click, Shift-click, minimap jump, edge auto-scroll
Q#M4 TripleDown variant (protocol v7, the cheap additive kind —
SUPPORTED becomes [6,7], ladder restarts on the v6 floor) + daemon
select_line_at_cursor incl. trailing newline. Q#M5 Shift-click
extension daemon-side, zero wire change (mods already carried).
Q#M6 minimap jump GPU-local, consumed before text hit-testing,
scrubbable. Q#M7 edge auto-scroll via ControlFlow::WaitUntil tick
re-firing the drag hit-test. Three bets recorded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 12:12:18 -04:00
Levi Neuwirth 6853a0feac
Merge pull request #65 from levineuwirth/session-gpu-diagnostic-parity
GPU diagnostic parity: quad squiggle bars + minimap marks
2026-06-12 10:37:02 -04:00
Levi Neuwirth afd5e80466 producer: widen zero-width diagnostics to a visible byte
User validation found nothing rendered in pmacs-gpu for the
missing-comma error — rust-analyzer's zero-width EOL anchor. The
TUI's anchor-cell fix lives in DiagnosticView, but the semantic
wire ships Decorations straight from line/col conversion: a
zero-width range clips to None at the frontend and overlaps no
glyph, so the GPU drew nothing. Widen at the producer (one byte;
forward mid-line, backward at EOL where forward covers only the
glyph-less newline) so every semantic frontend gets a paintable
range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:24:31 -04:00
Levi Neuwirth c2f6905b08 minimap diagnostic marks: severity color rides FileStyleSummary
The GPU's gutter-sign equivalent (framing Q#D3). The producer folds
diagnostics into the per-line summary: each touched line's dominant
style gets the severity's canonical underline_color (most severe
wins), skipped while the URI's store entry is stale — same
discipline as the decorations producer. The minimap stroke prefers
underline_color over the syntax fg, so error/warning lines read at
a glance.

Diagnostics publish without a CRDT generation bump, so the
summary's generation-keyed cache gains a second key: a new per-URI
epoch on DiagnosticStore (bumped on set/clear, not mark_stale). A
republish re-emits the summary; everything else stays suppressed
(framing bet #3 — the gate widens precisely, not naively).

DiagnosticSeverity::underline_color() becomes the canonical palette
(TUI squiggles, col-0 markers, and minimap marks all share it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:16:45 -04:00
Levi Neuwirth 81288187e0 pmacs-gpu: diagnostic squiggles as quad bars; retire the fg recolor
Diagnostics were rendered by overriding glyph fg color — clobbering
the syntax color of the token the diagnostic points at, the same
flaw the TUI fixed via protocol v6 underline_color. Now each
diagnostic decoration draws a 2px severity-colored bar hugging the
bottom of its glyph extents (push_glyph_extent_rects grows a bar_px
mode), same palette as before.

With no fg-affecting decoration kinds left, the fg-fingerprint
reshape gate is gone: every decoration change takes the cheap
request_redraw path (quads rebuild per frame), so diagnostic
publishes no longer pay set_rich_text + shape_until_scroll at all —
decorations drop out of the rich-chunk pipeline entirely
(projected_rich_chunks / clipped_chunks_for_range lose the param).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:10:00 -04:00
Levi Neuwirth a11d9ab374 framing: pmacs-gpu diagnostic parity — quad squiggles, minimap marks
Q#D1 quad-pipeline 2px bars (glyphon draws no underlines), Q#D2
retire the diagnostic fg recolor, Q#D3 minimap marks via v6
underline_color in FileStyleSummary, Q#D4 defer counts (no GPU
status band). Three categorical bets recorded, incl. the summary
recompute-gate (generation-only today; diagnostic publishes must
refresh marks without naive traffic growth).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:00:06 -04:00
Levi Neuwirth 50709412d0
Merge pull request #64 from levineuwirth/session-m46-diagnostic-surface
M4.6 TUI diagnostic surface: mode-line counts, colored squiggles (protocol v6), line markers
2026-06-12 10:00:05 -04:00