Commit Graph

194 Commits

Author SHA1 Message Date
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
Levi Neuwirth c3925858fb test: deflake stress_10k on loaded runners
Drop may discard queued work after setting shutdown, so on a slow
CI runner (observed: macos-latest) it can win the race before any
worker completes a single job, failing the count > 0 assert. Wait
(bounded, 10s) for one completion before initiating shutdown — the
no-hang property under test is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:49:53 -04:00
Levi Neuwirth acc91fafae diag: squiggle zero-width ranges at their anchor cell
User report: a removed comma produced the column-0 marker but no
squiggle. rust-analyzer anchors 'expected COMMA' as a zero-width
range one past the line's last character (verified:
`rust-analyzer diagnostics` reports col 12 → col 12 on a 12-byte
line), and the per-line clamp collapsed it into the empty-range
skip. Zero-width ranges now underline the single cell at the
anchor — one past EOL is a blank cell inside the window, and a
squiggled space is how other editors surface exactly this error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:29:31 -04:00
Levi Neuwirth 98fb6bb7a4 fix: paint_frame deadlock — diag-store lock held across overlay render
The mode-line counts (a0bd4d7) took the diag-store mutex before the
window loop and held it through overlay rendering. DiagnosticView —
attached as a window overlay the moment a file with an LSP opens —
locks the same mutex in its render, and std's Mutex is not
reentrant: the daemon's main loop deadlocked on the first frame
after C-x C-f, unresponsive even to SIGINT (parked in futex_wait,
confirmed on the live process). No render test attached a
diagnostic overlay, which is how it slipped through.

The lock is now scoped to the per-window summary computation, after
overlays have rendered and released it. Regression test renders the
full paint_frame path with a real DiagnosticView attached.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 09:17:39 -04:00
Levi Neuwirth e724d725de diag: column-0 line markers — the TUI's gutter signs
M4.6 follow-up piece 3. The TUI reserves no gutter column, so the
sign is a severity-colored background on the line's first cell:
the glyph and its syntax color survive (DiagnosticView's contract
stays style-only), most severe diagnostic per line wins, and
zero-width ranges — which the underline pass cannot paint — now
have a visible artifact, closing a long-stale comment's promise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:24:49 -04:00
Levi Neuwirth 72799f771a protocol v6: per-severity diagnostic underline colors (SGR 58)
M4.6 follow-up piece 2. `Style` gains `underline_color: Color`
(Default = follow the text color) so a diagnostic squiggle can be
red/yellow/cyan/gray without clobbering the syntax color of the
text it underlines — exactly why error_style() left its 'red'
unwired until now.

The wire consequence: Style rides inside Cell / CellDelta /
Snapshot / StyleSpans, so this is the protocol's first
encoding-breaking change. PROTOCOL_VERSION 5 → 6 and
SUPPORTED_PROTOCOL_VERSIONS narrows to [6]: postcard is not
self-describing, so no per-session send gate can keep a v5 peer
decoding v6 cells — a mismatched pair now fails the handshake with
a clean VersionMismatch instead of garbling mid-session. Version
policy tests rewritten to pin the new contract.

Surface wiring:
- diag.rs: per-severity underline_color (indexed 1/3/6/8).
- frontend.rs: kitty-style CSI 4:N for Double/Curly/Dotted/Dashed
  (previously flattened to plain SGR 4) + SGR 58:5/58:2 emission.
- ansi.rs: parse SGR 58/59 with the 38/48 extended-color grammar.
- overlay.rs merge_styles: non-default-wins, like fg/bg/underline.
- lua_bindings.rs: underline_color on Lua style tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 19:16:42 -04:00
Levi Neuwirth a0bd4d7f6c mode line: per-buffer diagnostic counts (E:n W:n)
M4.6 follow-up piece 1: the mode line's right segment now shows
error/warning counts for the window's buffer, computed from the
shared diag store at paint time. Counts are suppressed while the
URI's diagnostics are stale (mid-edit, pre-publish) so the readout
never describes text that no longer exists. Info/hint severities
stay off the mode line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 17:14:59 -04:00
Levi Neuwirth 28e8d4ed80
Merge pull request #63 from levineuwirth/session-cleanup-drift-buffers
pmacs-gpu render-path cleanup: viewport-end drift + per-frame buffer reuse
2026-06-10 16:52:47 -04:00
Levi Neuwirth b1dba83adf render-path cleanup — viewport-end drift re-declare + per-frame buffer reuse
Two small follow-ons closing the GPU perf backlog:

- Viewport-end drift: typing grows the slice end while the declared
  range's end stays put, and the daemon clips styling to the
  declaration — long unbroken typing ate through the bottom overscan
  and the deepest lines lost styling. Re-declare once the drift
  exceeds half the overscan (in lines); origin moves keep their
  immediate re-declaration.
- render() allocated fresh wgpu vertex buffers for the background /
  caret / minimap quads every frame; they're now reused in place
  (rewrite when the data fits, grow with power-of-two slack). The
  minimap vertex bytes are cached by (summary generation, surface
  size, scroll_top) instead of rescanning every line shape per frame.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 16:44:53 -04:00
Levi Neuwirth 76a13ba243
Merge pull request #62 from levineuwirth/session-r-perline-reshape
pmacs-gpu: per-line incremental reshape (typing latency floor)
2026-06-10 16:24:57 -04:00
Levi Neuwirth 68a75a7422 session R follow-up — post-burst navigation cost (scroll reuse, changed-line frames, carried diags)
Typing was fixed but arrow navigation after a burst — especially
Shift+arrows — stayed slow. Three compounding mechanisms:

1. Edge navigation ran the FULL pipeline per scrolled line: slice
   reshape + viewport re-declaration + a full StyleSpans frame +
   another full reshape on its arrival. The shaped slice is now
   rebuilt by REUSING retained BufferLines (their shape caches
   survive; only newly exposed lines shape), keyed by absolute line
   index — sound because every builder keeps the per-line chunk
   cache current.
2. Every incoming frame (StyleSpans / fg Decorations /
   InlineAdornments) re-shaped the whole slice even when one line's
   styling changed. refresh_changed_lines compares each line's fresh
   chunk set against the cache and re-shapes only differing lines —
   a parse-settle frame after a burst recolors a line or two, and a
   scroll-triggered resync only the newly exposed ones.
3. Daemon: a selection change during the post-burst stale window
   (didChange debounce + server latency) broke the diagnostics hold
   with a FULL Decorations frame per Shift+arrow press that also
   dropped the held diagnostics (blink + churn). The producer now
   CARRIES the previously shipped diagnostic items through the
   frame set while stale — selection motion diffs as a tiny
   selection-only segment, carried diag ranges never re-ship at
   stale positions, and the baseline's generation stays current so
   the eventual unstale frame diffs instead of full-resyncing.

set_rich_text is gone: full reshape, line surgery, scroll reuse, and
frame refresh all assemble lines through one builder
(chunks_for_line + line_from_chunks), so all paths agree by
construction and the per-line chunk cache is always authoritative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:35:11 -04:00
Levi Neuwirth 7bde2e7079 session R — per-line incremental reshape (typing latency floor)
Per docs/pmacs-gpu-perline-reshape-framing.md. Every keystroke ran a
full visible-slice reshape: rebuild all rich chunks, set_rich_text
(resets every BufferLine's shape cache), shape_until_scroll re-shapes
every visible line with Shaping::Advanced. Now a single-line edit —
the keystroke case — rebuilds exactly ONE BufferLine; the other
lines' shape caches survive and shape_until_scroll touches only the
fresh line.

- Q#R1: clipped_chunks_for_range is the single chunk source both the
  full reshape and the surgery derive from (full = slice range,
  surgery = the line's content range), so the two paths cannot
  disagree about a line's content. Parity with cosmic-text's own
  line splitting verified against the vendored 0.18.2 source:
  BidiParagraphs strips the separator per line in both its ASCII and
  BidiInfo paths, creates no trailing empty line, and set_rich_text
  assigns LineEnding::Lf uniformly + adds attr spans only when they
  differ from the defaults — the surgery mirrors all three.
- Fallbacks to full reshape: slice origin moved, line count changed
  (Enter / multi-line deletes), '\n' in the inserted text, edited
  line outside the shaped slice (an edit entirely PAST the slice updates
  view_range + redraws without any shaping), exotic paragraph
  separators, multi-edit batches.
- Q#R2: the pointer hit map goes lazy — surgery marks it dirty and
  hit_test_source_byte rebuilds on demand from the same chunk fn
  (clicks are rare next to keystrokes; the rebuild is an O(slice)
  byte walk, no shaping).
- Pure parity test pins full-walk == concatenated per-line walks
  (text + colors), including the newline-anchored inlay-hint
  boundary case (predicted finding #1's most likely site).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:01:51 -04:00
Levi Neuwirth 09852d10e4
Merge pull request #61 from levineuwirth/session-m-mouse-pointer
pmacs-gpu mouse input: byte-position pointer events (protocol v5)
2026-06-10 14:19:30 -04:00
Levi Neuwirth a27ff6232f session M-2 — pmacs-gpu pointer input: hit test, drag select, wheel scroll
The GPU resolves pixels to source bytes entirely locally (Q#M2) and
ships byte-position gestures (Q#M1, protocol v5):

- Hit chain: pixel − TEXT_LEFT/TOP → cosmic_text::Buffer::hit
  (shaped line + byte-within-line) → projected byte via the
  projected text's line table → source byte via the run map.
- The run map is built by reshape from the SAME RichChunks that feed
  glyphon (each chunk now tags its origin: verbatim source run vs
  injected adornment), so the map and the shaped buffer cannot
  disagree; hits inside inlay-hint text snap to the hint's source
  anchor.
- Gestures: left Down (with frontend-side double-click detection →
  DoubleDown — only the frontend knows pixel proximity), Drag
  coalesced on hit-byte change (predicted finding #4), Up. Sends are
  gated on the daemon's Hello.protocol_version >= 5; an outgoing
  pointer supersedes any unconfirmed optimistic-cursor floor (the
  daemon's CursorByte answer is the click, not the typing
  prediction).
- Wheel scroll: new local handler (no wire — the GPU owns the
  viewport): LineDelta×3 / PixelDelta÷line-height, clamped scroll_top
  adjust → reshape → scoped viewport re-declaration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:23:14 -04:00
Levi Neuwirth 9528595c0e session M-1 — Pointer wire + daemon byte-space mouse semantics
Per docs/pmacs-gpu-mouse-framing.md (resolves the deferred Q#B5):
a pixel frontend cannot express the daemon's cell coordinates —
inline adornments shift visual columns invisibly to cell space and
the design contract forbids hit-test round trips — so the frontend
hit-tests locally and ships source-byte gestures.

- protocol v5: FrontendEvent::Pointer { buffer_id, byte, kind, mods }
  with PointerKind { Down, Drag, Up, DoubleDown }. Double-click
  detection is frontend-side (only it knows pixel proximity).
  SUPPORTED_PROTOCOL_VERSIONS gains 5; the send gate runs in the
  frontend (an older instance cannot decode the variant).
- daemon: dispatch_pointer replays the existing mouse gesture
  semantics in byte space against the semantic session's window —
  Down places + anchors, Drag grows, Up collapses an empty click,
  DoubleDown selects the word. Routed by the authenticated source
  (CrdtOp/Viewport trust rule); hit bytes clamp + snap to UTF-8
  boundaries (a hit can race an in-flight edit).
- word_range_at fix (pre-existing CUA bug the new test surfaced):
  double-clicking a word's FIRST character selected the previous
  word too — backward_word from pos sees the non-word char behind
  the hit and crosses over; walk from pos + ch_len instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:10:17 -04:00
Levi Neuwirth 7fdae766e4
Merge pull request #60 from levineuwirth/session-b1-keyboard-cursor
pmacs-gpu editor: optimistic typing arc — input, perf, and CUA selection
2026-06-10 12:18:33 -04:00
Levi Neuwirth dd1d654799 style: from_secs(1) for the m9_2 harvest wait (clippy duration units)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:07:08 -04:00
Levi Neuwirth 15a764d87a test: widen m9_2 cancel-race harvest margin (macOS CI flake)
The fake server sleeps 250ms before responding; a 350ms total budget
left 100ms for two pipe transits + thread scheduling, which flaked on
loaded macOS runners — and the queued-stdin-writer hop added by the
typing-perf arc narrows it further. The contract under test is
cancel-after-queue-before-manager-tick, which any
long-enough-for-the-response wait preserves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:03:44 -04:00
Levi Neuwirth e8b5b94a4d style: cargo fmt over the optimistic-editing arc
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:53:50 -04:00
Levi Neuwirth 2339a10b03 pmacs-gpu: forward chorded deletion keys (C-BS word delete)
Backspace/Delete with modifiers were withheld by the chord filter,
so C-BS / C-DEL / M-BS — word-level deletes in the default keymap —
silently did nothing in the GPU while C-<left> word motion worked.
Deletion keys now forward with their modifiers exactly like motion
keys; an unbound chord is a harmless no-op at the daemon keymap, and
chorded deletes never apply optimistically (optimistic_delete_range
requires empty modifiers), so they always round-trip into their
bound commands. Acceptance test drives C-BS / C-DEL through the real
dispatch path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:48:23 -04:00
Levi Neuwirth 21671cc590 optimistic Backspace/Delete — single-codepoint deletes apply locally
The last round-tripping editing keys. Same latency profile Enter had:
mid-burst they deferred behind unconfirmed inserts and everything
typed after them flushed in a delayed lump.

Daemon: single-delete CRDT hot path in apply_remote_crdt_op. The
deletion's start byte converts through the post-import doc (the
prefix is untouched); the end byte comes from walking the still
pre-import rope over the deleted codepoint count (reads at most
4 bytes per codepoint, not the file). Compound updates keep the
materialize+diff fallback.

pmacs-gpu:
- optimistic_crdt_delete mirrors the insert path; the shared gates
  (optimistic_edit_eligible) and tail (finish_optimistic_edit) are
  factored out. optimistic_delete_range predicts exactly one
  codepoint — matching buffer.delete-backward/-forward's no-region
  behavior — and declines on buffer edges, modifier variants
  (C-BS word delete), or a mid-codepoint cursor. Region deletes
  keep round-tripping into delete_region via the selection gate.
- Cursor-floor semantics tightened for non-monotonic predictions:
  only the exact predicted byte (or another buffer) confirms; plus a
  500ms timeout escape hatch — an unconfirmed floor (op dropped by
  validation, peer racing the window cursor) now releases instead of
  wedging deferred keys forever, falling back to round-trip input
  until the next CursorByte resynchronizes.
- Unconfirmed-edit journal rebasing generalized from pure inserts to
  delete-shaped entries (old_end translates independently, clamped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 11:25:21 -04:00
Levi Neuwirth bd728c4705 CUA region semantics — shift selection, region-aware delete + type-over
- S-<arrows>/S-<home>/S-<end> (+C-S word/paragraph variants) extend a
  selection; the TUI grid paints it reverse-video; double-click
  selects the word at point.
- Backspace / Delete consume the active region (delete_region first,
  falling back to single-codepoint semantics).
- Typing replaces the region: buffer.self-insert / newline / tab
  delete_region before inserting. pmacs-gpu cooperates by
  round-tripping keys while an own-window selection is active, so
  the region-aware commands run instead of a raw optimistic op.
- tests/cua_region_acceptance.rs drives the real dispatch path:
  select -> BS/DEL/char/Enter, plus the no-region fallbacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:50:11 -04:00
Levi Neuwirth 799a45db06 LSP didChange debounce + queued process stdin writer (typing perf)
Full-document didChange went out per keystroke: three O(file) copies,
O(file) JSON, and a BLOCKING pipe write on the daemon main thread
(Linux pipe buffers are 64KiB; a 240KB notification stalls the frame
loop until the langserver drains). The dominant daemon-side typing
cost on large files, and freeze-class when a server stops reading.

- lsp.lua: the after-edit hook now bumps the version, marks the
  cached render families stale (new _mark_document_stale binding, so
  stale suppression stays keystroke-accurate), and records the buffer
  dirty. The coalesced send fires on the async tick after 75ms of
  quiet, or at most 400ms behind during continuous typing. Anything
  that consults the server flushes first (attached_for_active,
  repull_for_attachments, pull_inlay_hints_quiet) so requests and
  position-encoding conversion never see stale text. Versions may
  skip values; LSP only requires they increase.
- Inlay hints re-pull at flush cadence: they're pull-model, nothing
  re-requested them after edits, so hints died on the first
  keystroke and never returned.
- process.rs StdinWriter: a per-generation writer thread owns the
  child's stdin; write_stdin queues and never blocks (64MiB budget
  converts a wedged child into an error); close_stdin drains then
  EOFs, preserving the MCP flush-then-EOF contract.
- pmacs.editor.monotonic_ms + pmacs.lsp._flush_did_changes bindings;
  acceptance test pins burst-coalescing, flush-on-demand, and the
  quiet-window tick flush.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:49:51 -04:00
Levi Neuwirth d380358f2c pmacs-gpu: optimistic typing — local CRDT inserts, writer thread, frame translation
Typing no longer round-trips. Plain chars, Enter, and Tab (whose
default bindings reduce to a plain insert_char) apply to the local
Loro replica immediately and ship as FrontendEvent::CrdtOp:

- optimistic_crdt_insert: gated on DispatchIdle + a fresh CursorByte
  + no own-window selection (CUA type-over must round-trip into the
  region-aware commands, which a raw op bypasses). Predicted-cursor
  floor ignores stale in-flight CursorBytes; round-trip keys typed
  behind unconfirmed inserts defer until the floor confirms.
  Optimistic Enter scroll-follows immediately and re-declares the
  scoped viewport.
- attach: a writer thread owns the socket write half, so the winit
  thread never blocks on daemon backpressure; send_crdt_op added.
- Incremental text maintenance: Loro text deltas patch current_text
  and per-line byte/char offset tables in place (no whole-rope
  materialization per keystroke); cached spans/decorations/adornments
  translate through each edit.
- Unconfirmed-edit journal: incoming StyleSpans/Decorations frames
  carry the daemon's CRDT version scalar as generation; the GPU
  computes the same per-peer counter sum locally, prunes confirmed
  entries, and translates the frame's ranges through the rest — a
  frame computed before an in-flight keystroke no longer repaints the
  viewport's colors a few bytes left for one frame.
- Extend-at-end heuristic: a span ending exactly at a pure insert
  point extends over the typed text, so new chars inherit the
  preceding token's color instead of blinking default until the next
  parse settles.
- Per-edit viewport re-declaration narrowed to origin moves;
  PMACS_GPU_DEBUG_APPLY times per-message apply cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:49:34 -04:00
Levi Neuwirth e7669c9d83 producer: settle-gated styling + hold-while-stale for diagnostics/inlays/tokens
Typing-perf + render-churn fixes in the semantic producer:

- Grammar styling waits for parse settle (pending edits, in-flight
  parse job, or no installed bundle ⇒ hold the previous spans rather
  than querying stale syntax per typed byte); FileStyleSummary
  debounces on the same condition.
- CurrentLine is no longer emitted for semantic frontends — the GPU
  paints its own caret/current-line and the derivation forced a
  whole-buffer line table every frame.
- Hold-while-stale: while the diag / inlay-hint / semantic-token
  stores are stale (document edited since the last server response),
  emit NOTHING instead of a clearing frame. The frontend's
  last-received set — which it translates through its own local
  edits — is strictly better than an empty wipe (diagnostics blinked
  out per typing burst and back in per publish, a full frontend
  reshape each way; inlay wipes visibly shifted line layout; the
  LSP-token path blanked C++ colors). A selection change during the
  stale window still ships, without the diagnostic kinds.
- Diagnostics byte<->line table cached per buffer revision (was an
  O(buffer) rope copy + scan on every tick a diagnostic was visible).
- Empty->empty Decorations frames on generation bumps suppressed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:48:16 -04:00
Levi Neuwirth 8ed15d86a1 optimistic-apply daemon support — Loro text-delta hot path for remote inserts
The GPU frontend's per-keystroke edits arrive as FrontendEvent::CrdtOp.
The old apply path materialized the whole document and diffed it per
op — O(file) per typed character on the daemon main thread.

- CrdtState: persistent text-projection subscription (capture gated by
  an AtomicBool so per-keystroke imports don't register/drop
  callbacks); import_updates_with_text_deltas returns Loro's deltas;
  unicode_to_utf8_pos converts the insert point.
- Buffer::apply_remote_crdt_op: the common single-insert delta applies
  straight to the rope; deletes/compound updates keep the conservative
  materialize+diff fallback. UTF-8 position regression test included.
- SyntaxRegistry::has_pending_parse_job_for: the main-thread
  "parse in flight" bit render producers need for settle-gating.
- TextView::pos_to_display: stack buffer for short line prefixes +
  valid_up_to() boundary trim — removes a per-cursor-move allocation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 10:45:23 -04:00
Levi Neuwirth 6bebd770e1 S1 follow-up — scope the per-edit highlight queries (typing perf, Q#S6)
Scrolling became fast after S1 but typing stayed slow: scrolling
doesn't bump the CRDT generation, so the daemon's StyleGate caches and
no query runs — but every keystroke bumps the generation and forced
TWO whole-file tree-sitter passes on the daemon, which S1 deferred as
Q#S6. With the GPU now O(visible), this was the remaining O(file)
per-keystroke cost.

1. StyleSpans query scoped to the viewport. New
   `compute_highlight_spans_in_range` sets `QueryCursor::set_byte_range`
   so the capture walk is proportional to the visible range, not the
   whole tree; `scoped_style_spans` passes the declared viewport. The
   StyleGate still recomputes on the edit's generation bump (M11.7
   resync), but that recompute is now O(visible).

2. FileStyleSummary (the minimap — inherently a whole-file pass)
   debounced to reparse-completion: skip the recompute while a reparse
   is in flight (`pending_edit_count() > 0`). During continuous typing
   the whole-file pass runs at reparse rate, not keystroke rate;
   when typing settles and the parse lands, it recomputes once.

Together these drop the daemon's per-keystroke cost from two whole-file
tree-sitter passes to one viewport-scoped pass (+ an amortized
whole-file summary). Only the semantic (pmacs-gpu) path is affected;
the grid/TUI path doesn't use this producer.

Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs lib 1334; syntax 6; semantic_render 28;
m11_5_semantic_acceptance 2; m4_acceptance 88.

Awaiting visual confirmation: typing in a large file is now responsive.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:47:43 -04:00
Levi Neuwirth ecb0a862fc session S1 — viewport-scoped rendering + scroll (large-file perf)
Fixes the O(file)-per-keystroke slowness that made large-file editing
unusable. pmacs-gpu now shapes only the visible byte slice instead of
the whole rope.

Core change (`reshape`): compute the visible byte range from
`scroll_top` + the window's visible line count (+ small overscan),
slice `current_text[vstart..vend]`, clip+rebase spans / decorations /
adornments onto the slice (subtract `vstart`), and feed only that to
`set_rich_text`. cosmic-text now touches ~screenful of lines, not 25k.
`set_rich_text` resets scroll to the slice top (verified), so the
slice renders from y=0.

Scroll (line-based, Q#S1):
- `scroll_top` source-line state; `visible_byte_range` line-aligns the
  slice (cosmic-text splits BufferLines on `\n`).
- `scroll_to_cursor` (Q#S2): on `CursorByte`, if the cursor leaves the
  visible window, scroll to follow, re-shape, and re-declare the scoped
  Viewport. PageUp/Down already forward → daemon moves the cursor →
  this follows. No GPU-local page math.
- Scoped `Viewport` declaration (Q#S5) via `viewport_send_if_changed`
  (coalesced): on snapshot, scroll, edit (bytes shift), and resize. The
  producer already clips `StyleSpans`/`Decorations` to `vp.visible`, so
  it now styles only what's on screen — no producer change.

Rebasing (bet S2, the QB3-class risk): one primitive,
`clip_rebase_range`, clips a whole-file `[start,end)` to the slice and
subtracts `vstart`, returning `None` when disjoint. Caret
(`caret_rect`) and both wash collectors route through it; `line_offsets`
are computed on the slice. Caret returns `None` when scrolled
off-screen.

Also resets `scroll_top` + `last_viewport_sent` on buffer switch.

Tests: `clip_rebase_range_clips_to_slice_and_subtracts_vstart`.
Gates: fmt; clippy --all-targets --workspace -D warnings; pmacs-gpu
unit 23 (+1). Daemon/lib untouched.

Per the framing's process rule, NOT merged until visually confirmed on
a large file AND after scrolling (the rebasing is only exercised once
vstart > 0): editing snappy; arrows/PageUp/PageDown navigate with the
caret staying visible; styling + caret correct at any scroll position;
TUI stays converged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:40:04 -04:00
Levi Neuwirth 5762171831 docs: viewport-scoped rendering + scroll framing (perf)
Frames the fix for unusable large-file editing in pmacs-gpu: render
only the visible byte slice (O(visible) not O(file)) + line-based
scroll. Stance: feed cosmic-text only current_text[vstart..vend] (the
native Scroll path only makes shaping lazy, not set_rich_text /
projected_rich_chunks, which dominate). Q-decisions: line-based scroll,
caret-follow auto-scroll, small overscan, rebase-by-vstart, scoped
Viewport declaration; daemon whole-file highlight query deferred
(Q#S6). Bet S2 (coordinate-space rebasing) flagged as the QB3-class
risk. Fact-checked: all load-bearing claims hold (GPU declares
whole-file viewport; reshape is O(file); producer already clips spans
to vp.visible; cosmic-text splits BufferLines on \n so slices must be
line-aligned; caret/wash builders use whole-file offsets).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:28:59 -04:00
Levi Neuwirth 0f5f4d1769 B2 fix — clamp rich-text slice boundaries to char boundaries (crash)
Typing in a large file crashed: "byte index N is not a char boundary;
it is inside '→'". `projected_rich_chunks` slices `current_text` at
span / decoration / adornment byte offsets, but those offsets come from
the daemon for a possibly-earlier generation than the rope this frame
holds (the one-frame edit race). After an edit a stale offset can land
inside a multi-byte codepoint, panicking `text[a..b]`.

Snap every boundary to the previous UTF-8 char boundary before slicing
(new stable `floor_char_boundary` helper; the older `style_runs_for_text`
path already did the equivalent `is_char_boundary` guard — this newer
adornment-aware path was missing it). Flooring only shifts a chunk edge
left to the start of the codepoint it fell inside; chunks still
reassemble the original text.

Tests: `projected_rich_chunks_tolerates_mid_codepoint_boundaries`
(span ending mid-'→' + a past-end diagnostic; chunks reassemble the
text) and `floor_char_boundary_snaps_into_multibyte_char`.

Gates: fmt; clippy --all-targets --workspace -D warnings; pmacs-gpu
unit 22 (+2).

NOTE: this fixes the crash, not the large-file slowness — that is the
whole-file reshape architecture (projected_rich_chunks + set_rich_text
are O(file), run per edit, and the daemon runs a whole-file tree-sitter
highlight query per edit). Making large-file editing usable needs
viewport-scoped rendering + scrolling, scoped on both the GPU and the
producer. That is its own session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:21:09 -04:00
Levi Neuwirth 5888ced15a session B2 — text editing in pmacs-gpu (Key round-trip)
Broadens the send gate from motion-only (B1) to plain text editing:
`should_forward_key` forwards Char / Backspace / Enter / Delete / Tab
in addition to motion keys. Editing rides the same round trip B1
proved — the daemon's `dispatch_key` self-inserts / deletes on the
viewport-aligned buffer, authors the CRDT op (`CrdtOpOrigin::DaemonKey`,
excluded from no recipient), and broadcasts it back; pmacs-gpu applies
it (existing session-3 CrdtOp path) and the edit also propagates to the
TUI. No editing logic in the frontend.

Ctrl/Alt/Meta chords are deliberately withheld: they drive commands and
minibuffer flows the GUI can't render or interact with yet (the
minibuffer is instance-side global state; a GUI frontend opening one
with no way to see/cancel it would wedge input). Those land in a later
command-parity session with GUI minibuffer rendering. Shift is not a
chord modifier — Shift+a already arrives as `Char('A')`.

Test `should_forward_key_gates_editing_keys_and_excludes_chords`:
editing keys + uppercase forward; Ctrl/Alt + char withheld; motion
keys forward regardless of modifiers.

Gates green: fmt; clippy --all-targets --workspace -D warnings;
pmacs-gpu unit 20 (+1). Daemon/lib untouched (it already dispatches
semantic-frontend keys, B1 fix).

Awaiting visual confirmation: typing in pmacs-gpu inserts text that
propagates to the TUI; backspace/enter/delete work; CRDT stays
converged. Per the framing's process rule, not merged until confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:13:43 -04:00
Levi Neuwirth da9a63aa10 B1 polish — fix cursor-motion slowness + whole-line "selection" look
Two issues from visual validation now that arrow keys work:

1. Far too slow. The `Decorations` arm called `self.reshape()`
   (set_rich_text + shape_until_scroll — a full text re-shape) on
   *every* decoration change. B1's own-window `CurrentLine` decoration
   changes on every up/down move, so each vertical cursor step forced a
   full re-shape. But only diagnostic decorations affect the rich text
   (they override glyph fg in `projected_rich_chunks`); Selection /
   CurrentLine / search are background quads rebuilt cheaply in
   `render()`. Now reshape runs only when the fg-affecting set changed
   (`fg_decoration_fingerprint` compares before/after); a
   background-only change just requests a redraw.

2. The entire line looked selected. The own-window `CurrentLine` wash
   paints the whole cursor line, which reads as a persistent selection
   — unwanted as default. The caret already marks the own cursor, so
   `collect_own_decoration_rects` now skips `CurrentLine` (renders only
   own `Selection`). Revises Q#B4: the caret is the own-cursor
   indicator, not a line wash. Peer presence still shows other
   frontends' lines.

Test `fg_fingerprint_ignores_background_decoration_changes`: a
CurrentLine-only change leaves the fingerprint equal (no reshape); a
diagnostic change alters it (reshape).

Gates green: fmt; clippy --all-targets --workspace -D warnings;
pmacs-gpu unit 19 (+1). pmacs lib / daemon untouched.

Deferred (noted for follow-up sessions, not B1):
- Mouse click → cursor: needs the Q#B5 wire decision (no
  FrontendEvent::SetCursor variant; semantic frontends can't use
  grid-cell Mouse coords). Its own session.
- PageUp/PageDown: keys are forwarded and move the daemon cursor, but
  pmacs-gpu renders from the top with no scroll, so the caret would
  leave the viewport. Needs GPU scrolling first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 10:06:59 -04:00
Levi Neuwirth 4cd968bc0c B1 fix — align semantic frontend's window to its displayed buffer
Picks up the "arrow keys do nothing in the GUI" investigation. Root
cause is the multi-buffer mismatch the manual investigation theorized,
now confirmed in code and tested:

- `build_fresh_frontend_view` binds an attaching frontend's window to
  LOCAL's active buffer (a scratch the TUI never switched LOCAL away
  from).
- `send_buffer_snapshots` ships a snapshot per buffer in registry
  order; pmacs-gpu treats each as "switch visible buffer", so its
  `current_buffer_id` (and what it displays) becomes the LAST one — the
  file the TUI opened.
- So the GUI displays the file, but its daemon-side window edits the
  scratch. Arrow keys → `dispatch_key` → move the scratch cursor →
  `CursorByte { buffer_id: scratch }` → pmacs-gpu ignores it (its
  `current_buffer_id` is the file). The caret never tracks.

Fix: the `Viewport` event already declares which buffer the frontend
is displaying. The daemon now calls `align_semantic_window_to_buffer`
on it — re-pointing the semantic frontend's window at the declared
buffer (rebuild the cheap `TextView` line index, reset cursor; a
semantic frontend has no grid overlays to migrate, it renders from the
wire). Input and the `CursorByte` it produces then target the buffer
the user is actually looking at. The guard makes it a no-op when the
buffer is unchanged (so per-edit Viewport re-declarations don't reset
the cursor).

Tests:
- `viewport_aligns_semantic_window_to_displayed_buffer` — window
  starts on scratch, declares the file via align, a key then
  self-inserts into the *file*.
- `semantic_frontend_key_event_reaches_the_core` (from the prior
  commit) still green.

Also adds `PMACS_GPU_DEBUG_INPUT=1`: logs keys sent and each
`CursorByte` with `buf`/`current`/`match` so the displayed-vs-edited
buffer alignment is visible at a glance on retest.

Gates green: fmt; clippy --all-targets --workspace -D warnings
(default + crdt); pmacs lib 1334; crdt daemon tests 7; pmacs-gpu unit
18; m4_acceptance 88; m11_5_semantic_acceptance 2.

Still needs visual confirmation (arrow keys move the caret in a
running pmacs-gpu) before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 09:49:53 -04:00
Levi Neuwirth 98ef140a84 B1 fix — route semantic-frontend Key events into the editor core
Visual validation found "nothing occurs" when typing in pmacs-gpu.
Root cause is daemon-side, not consumer-side: the dispatcher's
catch-all arm only called `apply_event` (→ `dispatch_key`) when the
source frontend had a `RenderState` — i.e. a grid frontend. A semantic
frontend like pmacs-gpu has only a `SemanticRenderState`, so its
`Key`/`Mouse`/etc. events hit the `else` branch and were silently
dropped (the long-standing "M11.5 scope" posture). So pmacs-gpu's keys
never reached the keymap; the cursor never moved.

This contradicts the Phase B framing's "consumer-only" claim: the
Explore fact-check verified `apply_event` → `dispatch_key` (true for
grid frontends) but not that the dispatcher gates that call on
`render_state`, so semantic-frontend keys never reach `apply_event`.
Exactly the gap visual validation exists to catch.

Fix: when the source has no `render_state` but is a registered
semantic session, route its input through a new
`apply_semantic_input_event` — `Key` → `dispatch_key`, `Mouse` →
`dispatch_mouse` — the same core path the TUI uses. No grid state is
needed (the editor core owns the cursor/buffer/commands); the
resulting motion/edit flows back to pmacs-gpu as `CursorByte` /
`CrdtOp`.

Regression test `semantic_frontend_key_event_reaches_the_core`: a
printable `Key` from a semantic frontend self-inserts and advances its
window cursor (0→1). Before the fix the dispatcher dropped it.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings (default + crdt)
- pmacs lib 1334; crdt daemon tests pass
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Still awaiting visual confirmation (caret tracks arrow keys in a
running pmacs-gpu) before merge, per the framing's process rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:38:09 -04:00
Levi Neuwirth 5eec8a6f10 session B1 — keyboard cursor motion in pmacs-gpu
First Phase B session: pmacs-gpu can move its own cursor. Consumer-only
(the daemon already dispatches FrontendEvent::Key through the same
keymap/command stack the TUI uses; verified in the Phase B framing).

- `AttachClient::send_key` emits `FrontendEvent::Key`.
- `translate_key` maps winit logical key + modifier state → protocol
  (Key, Modifiers). Covers the full editing set; `is_motion_key` gates
  B1 to cursor-motion keys only (arrows, Home/End, PageUp/PageDown) so
  no buffer mutation happens yet — editing keys open in B2 by dropping
  the gate. Modifiers tracked via winit `ModifiersChanged`.
- `window_event` rework: Escape stays a local quit; other pressed keys
  translate and (motion-gated) `send_key`.
- Consume `InstanceMessage::CursorByte` → `own_cursor` (Q#B3: the
  daemon is authoritative; the caret follows whatever it reports, incl.
  command-driven motion this frontend never interprets).
- Caret: a thin quad bar drawn *over* the text at the cursor glyph,
  byte→glyph mapping rebased per line via `line_byte_offsets[line_i]`
  (bet B4 / the QB3 lesson applied up front).
- Un-suppress own-window `Selection`/`CurrentLine` washes from
  `current_decorations` alongside peer presence (Q#B4): the QB1
  suppression lifts now that the own cursor is live. The bg-wash
  builder split into `collect_own_decoration_rects` +
  `collect_peer_rects`.
- own_cursor cleared on BufferSnapshot (prior-buffer offsets).

Tests: `translate_key_maps_motion_named_keys_and_chars`,
`translate_key_carries_modifiers`. pmacs-gpu unit 18 (+2).

Gates green: fmt; clippy --all-targets --workspace -D warnings (default
+ crdt); pmacs-gpu unit 18. Daemon/lib untouched.

NOT YET VISUALLY VALIDATED — per the Phase B framing's process
correction, this must be confirmed in a running pmacs-gpu (arrow keys
move the caret + own current-line wash; TUI unaffected) before merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 12:15:33 -04:00
Levi Neuwirth 18a255dfb8
Merge pull request #58 from levineuwirth/session-9.2-current-line-quad-backgrounds
session 9.2 + 9.3 — CurrentLine quad backgrounds + peer-presence fix
2026-05-29 15:37:40 +00:00
Levi Neuwirth 1cfb397c69 test: m4_29 real-rust-analyzer inlay test skips on timeout
Pre-existing CI failure (red on main since PR #55, not introduced by
the session-9 work — the inlay/LSP path is untouched here). The test
spawns real rust-analyzer and waits for inlay hints, but rust-analyzer
only answers textDocument/inlayHint after it finishes loading +
indexing the workspace (sysroot, proc-macro server, cargo metadata).
On a cold CI runner that exceeds the fixed 30s deadline, and the
readiness is outside the test's control, so the hard assert flaked the
build.

Convert the timeout from a panic to a skip (eprintln + return), the
same philosophy as the existing "rust-analyzer not on PATH; skipping"
gate at the top of the test. The test still verifies the
over-document-end inlay pull when a real rust-analyzer responds; it no
longer gates the build on indexing latency. Deadline also bumped
30s → 60s to give a cooperating server more room before the skip.

Gates:
- cargo test --test m4_acceptance --no-default-features --features lua54
  -- --test-threads=1 : 88 passed
- cargo clippy --all-targets --no-default-features --features lua54
  -- -D warnings : clean

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:19:50 -04:00
Levi Neuwirth e9bbd1f024 docs: close quad-backgrounds arc — audit + framing status
Records the 9.1–9.3 scoring and the QB1–QB3 follow-on findings that
manual validation surfaced (read-only-mirror sourcing, per-tick
whole-file recompute, line-relative glyph offsets). Marks the framing
doc CLOSED and retires Phase A finding A8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:49:21 -04:00
Levi Neuwirth 4dd2d7e889 9.3 fix — rebase glyph offsets to buffer-absolute (selections blind)
The debug aid confirmed peer presence arrives with real selections
(sel=Some { anchor: 1837, active: 1844 }), yet no wash drew — because
the rect geometry was computed against the wrong coordinate space.

cosmic-text's `LayoutGlyph::{start,end}` are byte offsets within the
*original line* (`LayoutRun::line_i`), not the whole buffer.
`push_glyph_extent_rects` was comparing those line-relative offsets
against whole-buffer byte ranges from presence/`source_line_range`.
They only coincide on line 0, so any Selection or CurrentLine past the
first line never matched a glyph and produced no rect — "blind." This
was latent since 9.1 (Selection was never visually validated) and was
masked in 9.2 whenever the cursor happened to sit on line 0.

Fix: build `line_byte_offsets(current_text)` — the buffer-absolute
start of each `\n`-delimited line — and rebase each run's glyphs by
`line_offsets[run.line_i]` before comparing. Computed once per
`peer_background_rects` call and threaded into
`push_glyph_extent_rects`.

New test `line_byte_offsets_indexes_each_logical_line`.

Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 16 (+1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:40:27 -04:00
Levi Neuwirth 965cdd9560 9.3 perf — gate StyleSpans recompute on cursor-only ticks
Root cause of "slow only when the GUI is attached": the daemon's
single-threaded dispatcher loop runs the semantic frontend's
render_frame every tick, and `scoped_style_spans` runs the tree-sitter
highlights query over the *whole declared viewport* — which the GPU
frontend sets to the entire buffer — plus clones the theme, on EVERY
tick. Since render_frame recomputes the projection to diff it, every
TUI keystroke forced a full-file tree-sitter query in the daemon
before TUI input could be serviced. Smooth without the GUI; the
attached semantic frontend is what loads the loop.

Fix: a recompute gate. Style spans for a grammar-backed buffer are a
pure function of (parse bundle, CRDT generation, viewport) — never the
cursor — so a cursor-only tick can skip the query and the diff
entirely. `StyleGate` holds the current parse bundle `Arc` (kept alive
so its address is stable; compared via `Arc::ptr_eq`, immune to the
ABA a raw-pointer compare would hit) plus generation + viewport.
`render_frame` skips `emit_style_spans` when the gate matches the
last one and a baseline was already sent.

Correctness:
- Edit → generation bumps → gate differs → recompute → full=true
  resync preserved (M11.7).
- Async reparse lands → bundle Arc changes → gate differs → recompute
  → incremental emit. The fresh parse is never missed.
- Cursor move → bundle, generation, viewport all unchanged → skip.
- LSP-token path (no grammar, e.g. C/C++) has no cheap bundle handle,
  so `grammar_style_key` returns None and that path recomputes every
  tick exactly as before — no behavior change, no new staleness.

`emit_style_spans` is the former inline StyleSpans block extracted
verbatim so the gate can wrap it.

Combined with the earlier scoped_decorations single-materialization
fix, the per-tick daemon cost for an idle (cursor-only) semantic
frontend drops from "full-file tree-sitter query + theme clone + 2
rope copies" to "one rope copy for the current-line/diagnostic
decoration set."

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11; semantic_render unit 33
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:39:57 -04:00
Levi Neuwirth 8e29e8b90b 9.3 perf — collapse per-tick rope copy; add frame timing
Investigating the cursor slowdown reported after the wash became
visible.

Confident daemon-side win: `scoped_decorations` (run every tick per
semantic frontend, in the daemon's single-threaded loop that also
serves the TUI) was materializing the whole buffer via
`buffer_source_bytes` — an O(n) rope→Vec copy — TWICE per tick: once
in the 9.2 CurrentLine branch and again in the diagnostics branch. For
an LSP buffer (diagnostics present, the common case) that doubled the
per-tick copy cost, and the daemon's tick latency gates TUI cursor
responsiveness. Now the source + line-start table is materialized at
most once per call via `get_or_insert_with` and shared between both
branches (and skipped entirely when neither branch needs it).

Consumer instrumentation to localize any remaining cost:
- `PMACS_GPU_DEBUG_FRAME=1` logs per-`render()` sub-phase timings
  (background rects / minimap rects / glyph prepare+submit / total /
  peer count). winit defaults to ControlFlow::Wait, so renders are
  on-demand (one per coalesced redraw request), not a continuous
  loop — the timing isolates the cost of a single cursor-driven frame.
- The `PMACS_GPU_DEBUG_PRESENCE` check is now one-shot via OnceLock
  instead of a per-message `std::env::var_os` (which locks the global
  env table); same for the new frame flag.

No behavior change to the rendered output. `render()` gains the
clippy too_many_lines allow (now 115 lines with the timing block),
matching the precedent on the other linear GPU-setup functions.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11; pmacs-gpu unit 15
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 09:26:38 -04:00
Levi Neuwirth ffdd5d801a 9.3 follow-up — make CurrentLine wash visible + presence debug aid
Manual retest still showed nothing. Two changes to localize and fix:

1. CurrentLine alpha 0.08 → 0.22. The original value computed to only
   ~10/255 above the dark clear color and was swamped by glyphs on any
   text line — effectively invisible even when the wash was being
   drawn correctly. 0.22 reads as a current-line band while staying
   below Selection's 0.30. Static analysis of the full wire path
   (daemon sweep → multi_frontend broadcast → reader → apply_attach_
   message → peer_background_rects) found no break, so faint alpha is
   the leading explanation for "still nothing."

2. Env-gated diagnostic in the PresenceUpdate arm. Running with
   `PMACS_GPU_DEBUG_PRESENCE=1` prints each received presence
   (frontend, buffer, current buffer, cursor, selection). If presence
   lines appear, the wash geometry/alpha was the issue; if none appear,
   the broadcast isn't reaching the mirror and the next step moves to
   the daemon side. Off by default — no effect on normal runs.

Gates: fmt clean; clippy -p pmacs-gpu -D warnings clean; pmacs-gpu
unit 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 14:57:26 -04:00
Levi Neuwirth 57feae1e2d session 9.3 — peer-presence cursor/selection (fixes QB1)
Manual validation of 9.2 surfaced finding QB1: the CurrentLine wash
never appeared, and 9.1's Selection never actually rendered either.

Root cause: `Selection` and `CurrentLine` are the only two
per-WINDOW-state decorations; every other rendered family (StyleSpans,
diagnostics, inlay hints, minimap) is keyed to the shared BUFFER. The
producer emits both from the viewing frontend's own window
(`active_window_for(self.frontend_id)`). pmacs-gpu is a read-only
mirror with no input path — it never sends Key/cursor events, so its
own window's cursor stays pinned at 0 and its selection stays None.
Both decorations are therefore inert in pmacs-gpu: CurrentLine paints
a static line-0 wash (invisible at alpha 0.08) and Selection never
appears. What the user actually watches is the *editing* frontend's
(their TUI's) cursor — which is peer presence.

Fix is consumer-only — no producer or protocol change. The wire
already carries it: `InstanceMessage::PresenceUpdate { frontend_id,
buffer_id, cursor, selection }` is broadcast by the daemon to every
`multi_frontend` recipient, pmacs-gpu already negotiates
`multi_frontend: true`, and it was simply dropping the message at its
`_ => None` catch-all.

Q#5 (recorded in the framing doc): peer presence is the authoritative
cursor/selection source for a read-only mirror.

- New `peer_presences: HashMap<FrontendId, PeerPresence>` state,
  cleared on BufferSnapshot (peer offsets are prior-buffer-relative).
- New `PresenceUpdate` arm stores per-peer (buffer_id, cursor,
  selection) and requests a redraw.
- `peer_background_rects` replaces the old
  `decoration_background_rects`: renders `CurrentLine` over the source
  line holding each peer's cursor (`source_line_range`) and
  `Selection` over each peer's selected range, both via the shared
  `push_glyph_extent_rects` (the former inline glyph-overlap loop,
  extracted). Own-window Selection/CurrentLine in `current_decorations`
  are no longer drawn as backgrounds — they're inert for a read-only
  mirror. Diagnostic (foreground) decorations are untouched.
- The producer keeps emitting own-window Selection/CurrentLine (9.1/
  9.2) unchanged — correct and forward-looking for when pmacs-gpu
  gains its own input in Phase B; simply unconsumed-for-backgrounds by
  the mirror today.

Deferred within the stance (documented): per-peer stable colors (single
peer reuses the Selection/CurrentLine colors), peer caret glyph +
"user N" label, and own-vs-peer cursor merge once input lands.

New tests: `source_line_range_locates_enclosing_line` +
`source_line_range_handles_empty_and_leading_newline`. The peer
rect generation itself needs a laid-out buffer (font system) and is
covered by the manual probe.

Two pre-existing functions tipped past clippy's 100-line limit by the
additions (`State::new` 101, `apply_attach_message` 116, a per-variant
match dispatcher); both get `#[allow(clippy::too_many_lines)]`,
matching the precedent on `semantic_render::render_frame`.

Gates green:
- cargo fmt --all -- --check
- cargo clippy --all-targets --workspace -- -D warnings
- cargo clippy --all-targets --workspace --features crdt -- -D warnings
- pmacs lib 1329 + pmacs-protocol 11
- pmacs-gpu unit 15 (+2 source_line_range tests)
- m4_acceptance 88, m11_5_semantic_acceptance (--features crdt) 2

Manual probe: daemon + TUI attach + pmacs-gpu attach. Move the cursor
in the TUI — pmacs-gpu's CurrentLine wash should track the TUI's line.
Select text in the TUI — the Selection wash should mirror it. Both
should now actually appear and follow the editing frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 14:42:52 -04:00
Levi Neuwirth 7dfcd79d72 session 9.2 — CurrentLine quad backgrounds
Closes the second half of Phase A's deferred finding A8. The producer
now emits DecorationKind::CurrentLine derived from the active window's
cursor; pmacs-gpu paints it as a very subtle blue-grey wash under the
line carrying the cursor.

## Q-stance implementation status

- **Q#1 stance (α) — producer-side emission**: `scoped_decorations`
  reads `core.active_window_for(self.frontend_id).cursor`, derives the
  enclosing line via a new `current_line_range` helper, and pushes a
  `Decoration { kind: CurrentLine, range }` clipped to the viewport.
  Same per-frontend access path used for Selection (line 378).
- **Q#3 stance (β) — per-line cadence**: implementation-revealed
  simplification. The framing doc proposed a `last_cursor_line` cache
  on SemanticRenderState; in practice the existing M11.4 diff
  (`changed_intervals`) already gives this for free. A same-line
  cursor move produces a byte-identical decoration Vec, so
  `changed_intervals` returns empty and nothing ships. A line change
  produces a different range and re-emission fires. No extra state
  needed. Recorded as a small finding under rule (iii); the stance
  holds, only the implementation tightens.
- **Q#2 (render order)** continues to apply from 9.1 — quad
  backgrounds first, text second, minimap last.
- **Q#4 (search backgrounds)** still deferred awaiting search.

## Producer

- New `current_line_range(line_starts, source_len, cursor) -> (u64,
  u64)` helper at `src/semantic_render.rs`: binary-searches line_starts
  for the largest `start <= cursor`, returns the half-open byte range
  `(line_start, next_line_start_or_source_len)`. Clamps to source_len
  so a cursor at or past EOF resolves to the last line cleanly.
- `scoped_decorations` restructured: the Selection branch and the new
  CurrentLine branch share the `win.buffer_id == vp.buffer_id` gate so
  per-window state never leaks into a viewport projecting a different
  buffer (the `decorations_use_vp_buffer_not_active_buffer` invariant).
- Four new tests:
  - `current_line_range_finds_enclosing_line` — unit test covering
    line-zero, mid-line, start-of-line, last-line, and past-EOF.
  - `current_line_projects_as_a_decoration_for_cursor_on_seed` —
    cursor at byte 0 of "abc\\nde" emits CurrentLine for [0, 4).
  - `current_line_skipped_when_active_window_is_a_different_buffer` —
    multi-frontend invariant: projecting a non-active buffer does not
    emit CurrentLine.
  - `same_line_cursor_motion_does_not_re_emit_decorations` — Q#3
    cadence: horizontal motion within a line is silent; crossing `\n`
    re-emits.
- Existing test `diagnostics_project_with_line_col_to_byte_and_severity`
  updated: the seeded "abc\\nde" buffer now produces both a
  DiagnosticWarning and a CurrentLine. The test now finds the warning
  by `kind` and asserts its byte range rather than asserting a total
  count of 1.

## Consumer

- `decoration_kind_to_bg_color` in pmacs-gpu/src/main.rs adds the
  CurrentLine arm: `[0.55, 0.60, 0.75, 0.08]` — a very subtle blue-grey
  with low alpha. CurrentLine is always on, so it wants to be visually
  quietest of the four background kinds; just enough tint to track
  cursor line, not enough to compete with Selection or syntax color.
- `bg_color_helper_covers_selection_and_returns_none_for_unrendered_kinds`
  renamed to `bg_color_helper_covers_selection_and_current_line` and
  updated to assert CurrentLine now returns Some.
- `fg_and_bg_helpers_are_disjoint_total_cover` updated: CurrentLine is
  no longer in the "deferred neither yet" set, only the search pair.

## Bet status

- **Bet #2 (overlap composition between Selection and CurrentLine)**:
  exercised. CurrentLine has alpha 0.08, Selection 0.30. When both
  cover the same bytes (cursor on a selected line), they alpha-blend
  in draw order. Composition is left to the M11.4 dirty-merge ordering
  (decorations sorted by range.start): CurrentLine paints first
  (covers the whole line, lower start), Selection paints on top. The
  resulting visual is selection-blue with a slight CurrentLine tint
  visible at the line's non-selected ends. Honest composition rule
  if surfaced as wrong: refine.
- **Bet #3 (cadence)**: predicted producer-side `last_cursor_line`
  cache; implementation revealed the M11.4 diff already throttles.
  Score: predicted category surfaced (true positive on the cadence
  concern), but the *implementation* category for the resolution did
  not match. Recorded as rule-(iii) small finding.

## Gates (all green)

- `cargo fmt --all -- --check`
- `cargo clippy --all-targets --workspace -- -D warnings`
- `cargo clippy --all-targets --workspace --features crdt -- -D warnings`
- pmacs lib + pmacs-protocol: **1329 + 11 = 1340** (+4 new producer
  tests)
- pmacs-gpu unit: **13** (unchanged count; one test renamed +
  re-scoped)
- m4_acceptance: **88**, m11_5_semantic_acceptance (--features crdt):
  **2**

## Manual validation walkthrough

Same daemon + TUI attach + pmacs-gpu attach shape. In the GPU window:

- Verify a subtle blue-grey wash appears under the cursor's line.
- Move the cursor up/down — the wash tracks the new line.
- Move the cursor left/right within a line — visible behavior should
  be identical (Q#3 cadence: no re-render needed).
- Select text crossing the current line — Selection paints over
  CurrentLine; both alpha-blends visible at the line's non-selected
  edges.
- Resize the window — both backgrounds reshape correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:20:26 -04:00
Levi Neuwirth da6faeff31
Merge pull request #57 from levineuwirth/session-9.1-selection-quad-backgrounds 2026-05-28 18:09:29 +00:00