fix(panel): the record is self-contained --- viewport, anchoring, exact bytes

Answers review of 48057d7. The G5k routing fix held; the record still
leaned on ambient state in three places.

TerminalLocal now records the accepted content VIEWPORT. Replay fetched
the current panel_grid_size and returned when it was None --- which is
exactly what a hidden or absent panel produces, so a cancellation could
not finish the drag it was cancelling, and a size-changing cancellation
would have finished against the successor's geometry.

A press that anchors NOTHING no longer arms. The document path returned
Some(Document) unconditionally even when panel_cell_byte found no byte,
and the terminal path returned Local while discarding begin_selection's
answer. Both now report what actually began.

The child rows assert EXACT BYTES rather than a count: a wrong event or
encoding passed the old length checks. The literals are written out
rather than built with the encoder's own formula, which would only
assert that the encoder agrees with itself. G5k(b) pins the ruling that
the SGR framing comes from the record while the modifier bits still
report live state, so its release carries code 4 rather than 0.

New rows: P2's effect half (a refused press reaches no target), P9 (a
document press that anchors nothing does not arm), P11 (a recorded local
completion still runs with the panel HIDDEN, which is what the recorded
viewport is for).

THREE ROWS IN THIS ROUND WERE VACUOUS BEFORE THE MUTATIONS CAUGHT THEM,
and the fixtures now assert their way past each cause. The panel grid in
this fixture is FOUR rows, so content is rows 0..=2: my first P9 and P10
cells were out of grid and refused before reaching the path they claimed
to test, and P9's earlier cell clamped to byte 0 instead of failing to
anchor. Both rows now assert the disposition is Accepted before
asserting anything about the effect.

P10 IS DELIBERATELY ABSENT AND RECORDED AS UNWITNESSED. The
begin_selection gate has no reachable false branch through the daemon:
classify has already established the buffer is the side window's live
terminal, and anchor_at resolves every in-grid cell of a live view ---
measured on the fixture, not assumed. The gate is kept as insurance and
the gap is written where the row would have been, rather than covered by
something that would pass whether or not the gate existed.

P2's effect half is likewise not falsifiable by any mutation I could
construct, because a Refused disposition carries no resolution, so no
path can apply it. That is a stronger guarantee than a test, and it is
stated rather than dressed up as coverage.

Also corrects the last false ledger tense.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
Levi Neuwirth 2026-08-20 22:22:09 +02:00
parent 48057d7667
commit 2ea39aaa57
No known key found for this signature in database
3 changed files with 310 additions and 46 deletions

View File

@ -336,7 +336,8 @@ from #171 and #215 — the correction the 1b lane missed, honoured here.
checkpoint above; this bullet records what it was):
§5b and this lane gave `dispatch_semantic_panel_pointer`'s `bool`
different meanings — accepted-as-a-gesture versus consumed-here. A
mode-line press therefore **arms the latch** on this branch today.
mode-line press therefore **armed the latch** --- past tense: the
fix landed with Q#BP-R4, and P1 pins it.
**Q#BP-R4** rules a three-state `PanelPointerOutcome`, classified
**before** target effects. Only an `Accepted` `Down(Left)` arms;
left `Drag`/`Up` require a live record; an accepted `Up` performs

View File

@ -7842,6 +7842,16 @@ mod tests {
);
}
/// The exact SGR bytes a left press at cell (1, 2) produces:
/// `ESC [ < 0 ; col+1 ; row+1 M`. Written as a LITERAL rather than
/// built with the encoder's own formula, which would assert only
/// that the encoder agrees with itself.
const SGR_PRESS_1_2: &[u8] = b"\x1b[<0;3;2M";
/// The matching release, `m` rather than `M`.
const SGR_RELEASE_1_2: &[u8] = b"\x1b[<0;3;2m";
/// The same release with SHIFT held: the button code gains 4.
const SGR_RELEASE_1_2_SHIFT: &[u8] = b"\x1b[<4;3;2m";
/// A panel session whose side window holds a live TERMINAL, with
/// the send tap armed.
///
@ -7966,8 +7976,11 @@ mod tests {
press,
none,
);
let after_press = child_stream(&editor);
assert_eq!(after_press.len(), 1, "fixture: the press reached the child");
assert_eq!(
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child, in SGR"
);
assert!(
states[&fid]
.accepted_gesture()
@ -7994,11 +8007,11 @@ mod tests {
);
assert_eq!(
child_stream(&editor).len(),
1,
"the release must still reach the child: it holds a button \
down that only this release can lift, and re-reading the \
modes here is what strands it"
child_stream(&editor),
vec![SGR_RELEASE_1_2.to_vec()],
"the release must still reach the child AS A RELEASE at the \
gesture's cell: it holds a button down that only this can \
lift, and re-reading the modes here is what strands it"
);
let key = crate::terminal::view::TerminalViewKey::new(fid, panel, buffer_id);
assert!(
@ -8035,9 +8048,9 @@ mod tests {
none,
);
assert_eq!(
child_stream(&editor).len(),
1,
"fixture: press reached the child"
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child, in SGR"
);
send_panel(
@ -8053,10 +8066,12 @@ mod tests {
);
assert_eq!(
child_stream(&editor).len(),
1,
child_stream(&editor),
vec![SGR_RELEASE_1_2_SHIFT.to_vec()],
"Shift is the LOCAL-HANDLING override for a NEW gesture, not \
a way to abandon one already delivered to the child"
a way to abandon one already delivered to the child. The SGR \
framing comes from the record; the modifier bits still report \
live state, so the code is 4 rather than 0"
);
let key = crate::terminal::view::TerminalViewKey::new(fid, panel, buffer_id);
assert!(
@ -8228,9 +8243,9 @@ mod tests {
none,
);
assert_eq!(
child_stream(&editor).len(),
1,
"fixture: press reached the child"
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child, in SGR"
);
send_panel(
@ -8245,12 +8260,12 @@ mod tests {
none,
);
let sent = child_stream(&editor);
assert_eq!(
sent.len(),
1,
"the chrome release must still terminate the child's gesture \
--- the terminal path returns before replay, so without the \
child_stream(&editor),
vec![SGR_RELEASE_1_2.to_vec()],
"the chrome release must terminate the child's gesture AT THE \
GESTURE'S OWN CELL, not the chrome cell it landed on (R-c2). \
The terminal path returns before replay, so without the \
recorded completion the child holds the button forever"
);
assert!(
@ -8259,6 +8274,192 @@ mod tests {
);
}
/// P9 — a document press that ANCHORS NOTHING does not arm.
///
/// `panel_cell_byte` is `None` past the end of a short line and on
/// an empty panel, and the press then places no cursor and opens no
/// selection. Arming over it records a gesture whose completion has
/// nothing to complete — and, once cancellations deliver effects,
/// one that fires a release for a press that did nothing.
#[test]
fn r4_p9_a_document_press_that_anchors_nothing_does_not_arm() {
let fid = FrontendId(802);
let (mut editor, mut states, mut render, _document, panel, epochs) =
panel_session_at(LEGACY_PANEL_VERSION, fid);
let buffer_id = editor.core.borrow().windows[&panel].buffer_id;
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
// The panel grid is 4 rows, so content is rows 0..=2 and row 3
// is the mode line. The `*panel*` buffer is created EMPTY --- one
// zero-length line --- so row 1 is IN CONTENT and still past the
// buffer's only line.
//
// Two cells that do NOT work, and the row asserts its way past
// both: a column past the end of row 0 clamps to the line end
// and yields byte 0, and any row >= 4 is out of grid, so the
// press is refused before the document path is reached at all.
let unanchorable = pmacs_protocol::CellCoord::new(1, 0);
assert!(
editor
.classify_panel_pointer(fid, buffer_id, unanchorable, press)
.outcome()
== crate::editor::PanelPointerOutcome::Accepted,
"fixture: the cell must be ACCEPTED content, or this row \
passes because the press was refused for an unrelated reason"
);
assert!(
editor
.panel_cell_byte_for_test(panel, unanchorable)
.is_none(),
"fixture: this cell must genuinely have no byte behind it"
);
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
unanchorable,
press,
none,
);
assert!(
!states[&fid].has_accepted_gesture(),
"a press that anchored nothing must not arm"
);
}
// P10 IS DELIBERATELY ABSENT, and this note is why.
//
// The press path also gates arming on `begin_selection` actually
// starting a drag, so a local terminal press that begins nothing
// records nothing. That gate has NO WITNESS because it has no
// reachable false branch through the daemon: `begin_selection`
// returns `false` only when the session is missing or the cell maps
// to no retained row, and by the time the daemon reaches the press
// path `classify_panel_pointer` has already established that the
// buffer IS the side window's live terminal, while `anchor_at`
// resolves every in-grid cell of a live view — measured, not
// assumed: a press at every content row of the 4-row fixture
// anchors.
//
// The gate is kept as insurance against a future view that can
// refuse, and it is recorded as UNWITNESSED rather than covered by
// a row that would pass whether or not the gate existed. A row at
// an out-of-grid cell looks like it tests this and does not: such a
// press is `Refused` long before the local path.
/// P11 — a recorded LOCAL completion still runs with the panel
/// HIDDEN.
///
/// `panel_grid_size` is `None` for a hidden panel, so a completion
/// that fetched the current geometry could not finish a drag during
/// exactly the cancellations that need finishing. The viewport is
/// recorded at the press for this reason, and the row hides the
/// panel between the press and the completion to prove it.
#[test]
fn r4_p11_a_recorded_local_completion_survives_a_hidden_panel() {
let fid = FrontendId(804);
let (mut editor, mut states, mut render, panel, buffer_id, epochs) =
terminal_panel_session(fid, false);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
let cell = pmacs_protocol::CellCoord::new(1, 2);
let key = crate::terminal::view::TerminalViewKey::new(fid, panel, buffer_id);
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
cell,
press,
none,
);
assert!(
editor
.terminal_manager
.borrow()
.view_is_dragging_for_test(key),
"fixture: a local drag is live"
);
let record = states[&fid]
.accepted_gesture()
.copied()
.expect("fixture: the gesture is armed");
// The panel goes away. `panel_grid_size` is now `None`.
editor.hide_panel_for_test(fid);
assert!(
editor.core.borrow().panel_grid_size(fid).is_none(),
"fixture: the panel is hidden, so ambient geometry is gone"
);
editor.complete_panel_gesture(fid, &record, none);
assert!(
!editor
.terminal_manager
.borrow()
.view_is_dragging_for_test(key),
"the completion must still finish the drag --- it replays \
against the viewport RECORDED at the press, because the \
ambient one is exactly what a cancellation removes"
);
}
/// P2, effect half — a REFUSED press reaches no target at all.
///
/// §5b's four `g5_substrate_a_refused_*` rows read the latch and the
/// cancellation count; none of them reads the target. A refusal that
/// armed nothing while still sending the child a press, or starting
/// a local drag, would pass every one of them.
#[test]
fn r4_p2_a_refused_press_reaches_no_target() {
let fid = FrontendId(801);
let (mut editor, mut states, mut render, panel, buffer_id, epochs) =
terminal_panel_session(fid, true);
let press = pmacs_protocol::MouseKind::Down(pmacs_protocol::MouseButton::Left);
let none = pmacs_protocol::Modifiers::default();
let outside = {
let core = editor.core.borrow();
let grid = core.panel_grid_size(fid).expect("grid");
pmacs_protocol::CellCoord::new(grid.rows, 0)
};
let key = crate::terminal::view::TerminalViewKey::new(fid, panel, buffer_id);
send_panel(
&mut editor,
&mut states,
&mut render,
fid,
epochs,
buffer_id,
outside,
press,
none,
);
assert!(
child_stream(&editor).is_empty(),
"a refused press must send the child NOTHING --- a press it \
receives is one it will expect a release for"
);
assert!(
!editor
.terminal_manager
.borrow()
.view_is_dragging_for_test(key),
"and it must not begin a local drag either"
);
assert!(!states[&fid].has_accepted_gesture(), "and it must not arm");
}
/// P5 — an accepted in-content release delivers EXACTLY ONE child
/// release, never the ordinary one plus a record-driven one.
#[test]
@ -8283,9 +8484,9 @@ mod tests {
none,
);
assert_eq!(
child_stream(&editor).len(),
1,
"fixture: press reached the child"
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child, in SGR"
);
send_panel(
@ -8301,11 +8502,11 @@ mod tests {
);
assert_eq!(
child_stream(&editor).len(),
1,
"EXACTLY ONE release: the in-content path already completed \
the gesture, so running the recorded completion as well \
sends the child two Ups for one Down"
child_stream(&editor),
vec![SGR_RELEASE_1_2.to_vec()],
"EXACTLY ONE release, and its exact bytes: the in-content path \
already completed the gesture, so running the recorded \
completion as well sends the child two Ups for one Down"
);
}
@ -8338,9 +8539,9 @@ mod tests {
none,
);
assert_eq!(
child_stream(&editor).len(),
1,
"fixture: press reached the child"
child_stream(&editor),
vec![SGR_PRESS_1_2.to_vec()],
"fixture: the press reached the child, in SGR"
);
send_panel(

View File

@ -472,6 +472,15 @@ pub enum PanelGestureDomain {
window: WindowId,
/// The terminal buffer whose local selection is being built.
buffer_id: crate::buffer::BufferId,
/// The CONTENT viewport the press was accepted against.
///
/// Recorded, not re-fetched. `panel_grid_size` is `None` while
/// the panel is hidden or absent, so a completion that asked
/// for it could not finish a drag during exactly the
/// cancellations that need finishing — and a size-changing
/// cancellation would finish against the SUCCESSOR's geometry,
/// putting the selection somewhere the user never dragged.
viewport: CellSize,
},
}
@ -2901,6 +2910,24 @@ impl EditorState {
.is_some_and(|now| now != current)
}
/// The byte behind a panel cell, for P9.
#[doc(hidden)]
#[must_use]
pub fn panel_cell_byte_for_test(&self, win_id: WindowId, coord: CellCoord) -> Option<u64> {
self.panel_cell_byte(win_id, coord)
}
/// Hide this frontend's panel, for P11.
///
/// `panel_grid_size` returns `None` once hidden, which is the state
/// a recorded completion has to survive.
#[doc(hidden)]
pub fn hide_panel_for_test(&self, frontend_id: FrontendId) {
if let Some(view) = self.core.borrow_mut().views.get_mut(&frontend_id) {
view.panel_hidden = true;
}
}
/// Classify an authenticated panel gesture, WITHOUT applying it
/// (Q#BP-R4).
///
@ -3083,12 +3110,17 @@ impl EditorState {
TerminalGestureRoute::Local => Some(PanelGestureDomain::TerminalLocal {
window: target.side,
buffer_id: target.buffer_id,
viewport,
}),
};
}
self.replay_panel_document_gesture(frontend_id, target.side, coord, kind, mods);
Some(PanelGestureDomain::Document {
// A press that placed no anchor began NOTHING, and arming over
// it records a gesture whose completion has nothing to
// complete. `panel_cell_byte` is `None` past the end of a short
// line and on an empty panel, which is not a rare shape.
self.replay_panel_document_gesture(frontend_id, target.side, coord, kind, mods)
.then_some(PanelGestureDomain::Document {
window: target.side,
})
}
@ -3137,11 +3169,11 @@ impl EditorState {
self.send_terminal_bytes(buffer_id, &bytes);
}
}
PanelGestureDomain::TerminalLocal { window, buffer_id } => {
let Some(size) = self.core.borrow().panel_grid_size(frontend_id) else {
return;
};
let viewport = CellSize::new(size.rows.saturating_sub(1), size.cols);
PanelGestureDomain::TerminalLocal {
window,
buffer_id,
viewport,
} => {
let key = TerminalViewKey::new(frontend_id, window, buffer_id);
let mut manager = self.terminal_manager.borrow_mut();
match kind {
@ -3192,6 +3224,9 @@ impl EditorState {
/// frontend's input can interleave between a `Down` and its tail, so
/// a replay reading `active_window_mut()` would act on whatever
/// happened to be active at that moment (R-b).
/// Returns whether a left press ANCHORED a gesture. Every other
/// kind returns `false`: only a press can begin one, so only a
/// press has an answer to give.
fn replay_panel_document_gesture(
&mut self,
frontend_id: FrontendId,
@ -3199,7 +3234,7 @@ impl EditorState {
coord: CellCoord,
kind: pmacs_protocol::MouseKind,
mods: pmacs_protocol::Modifiers,
) {
) -> bool {
use pmacs_protocol::{MouseButton as PButton, MouseKind as PKind};
match kind {
@ -3210,7 +3245,8 @@ impl EditorState {
self.core.borrow_mut().break_command_chain(frontend_id);
let is_double = self.is_double_click(frontend_id, side, coord);
let Some(byte) = self.panel_cell_byte(side, coord) else {
return;
// No anchor: nothing began, so nothing may arm.
return false;
};
let extending = mods.contains(pmacs_protocol::Modifiers::SHIFT);
let prev = self.core.borrow().windows[&side].cursor;
@ -3233,7 +3269,9 @@ impl EditorState {
// window-targeted writers instead.
if self.core.borrow_mut().select_word_at_cursor() {
self.mouse_click = None;
return;
// A double-click word selection IS an anchored
// gesture: its release still has to complete.
return true;
}
}
if extending {
@ -3249,6 +3287,11 @@ impl EditorState {
cell: coord,
at: Instant::now(),
});
// Anchored. Stated here rather than inferred from
// selection state afterwards, which a later change
// could stop setting without anyone noticing arming had
// gone quiet.
return true;
}
PKind::Drag(PButton::Left) => {
self.mouse_click = None;
@ -3289,6 +3332,9 @@ impl EditorState {
| PKind::Up(_)
| PKind::Drag(_) => {}
}
// Only a left press can anchor, and it returns `true` above.
// Every other kind reaching here handled something already live.
false
}
/// Byte under a panel cell, resolved against the SIDE window's own
@ -4208,6 +4254,7 @@ impl EditorState {
if claims_control {
self.claim_terminal_controller(key);
}
let mut began_local_gesture = false;
let mut manager = self.terminal_manager.borrow_mut();
match kind {
TerminalMouseKind::ScrollUp => {
@ -4217,7 +4264,12 @@ impl EditorState {
let _ = manager.scroll_view(key, viewport_size, -SCROLL_LINES);
}
TerminalMouseKind::Down(TerminalMouseButton::Left) => {
let _ = manager.begin_selection(key, viewport_size, coord);
// The ONE local kind that begins a gesture, so its
// answer decides whether there is anything to arm. A
// press against a view that cannot start a drag began
// nothing, and arming over it records a gesture whose
// completion has nothing to finish.
began_local_gesture = manager.begin_selection(key, viewport_size, coord);
}
TerminalMouseKind::Drag(TerminalMouseButton::Left) => {
let _ = manager.update_selection(key, viewport_size, coord);
@ -4235,6 +4287,16 @@ impl EditorState {
}
// The local branch: scrollback, local selection or the menu. The
// child heard nothing.
//
// A left press reports `Local` only when it actually began a
// drag; otherwise it began nothing and the caller must not arm.
// Every other kind is not an arming event, so `Local` is the
// honest answer for it either way.
if matches!(kind, TerminalMouseKind::Down(TerminalMouseButton::Left))
&& !began_local_gesture
{
return TerminalGestureRoute::None;
}
TerminalGestureRoute::Local
}