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
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
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
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
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>