feat(gui-1b): B2's horizontal leg --- the panel origin actually moves
`PKind::ScrollLeft | PKind::ScrollRight` were CLAIMED AND DROPPED in the panel replay, with a comment assigning the axis to Stage 1b. That is the "frontend emits, receiver discards" shape the panel-replay lane was opened to fix, inherited for the horizontal axis. This closes it. `scroll_window_columns` moves the side window's `view_left` by B7's bound, stated exactly: `0 ..= widest - viewport`, saturating at zero, so the final display column stays visible --- clamping at the widest line's full width would let the origin pass every glyph and blank the viewport. Wrap pins the origin to zero, matching `horizontal_follow`. It returns whether the origin actually moved, which is lifetime clause 2's "effective move". The widest-line rule is SHARED. `display_width::widest_line_columns` lives beside the module's other column helpers and both frontends use it, for the same reason `scroll::follow_left` is shared: two frontends that compute the right bound differently disagree about where the document ends. B2's row asserts the EFFECT --- `view_left` before and after --- not an emission, and it carries the discriminating setup the bound requires: a panel whose content fits has a maximum origin of zero, so the move is absorbed by the clamp and a dropped event reads identical to correct behaviour. The fixture gets a line wider than the viewport. Mutation: restore the claimed-and-dropped arm, and the row fires. Two mistakes of mine in this commit's history, both caught before it: - I reverted a mutation with `git checkout -- src/editor.rs` on a file holding UNCOMMITTED work, and destroyed the whole B2 implementation. Re-applied, and the mutation check redone against a file snapshot --- the discipline I had used earlier in the CRDT lane and dropped here. - Inserting the new test above an existing one STOLE ITS `#[test]` and its doc comment, so `r4_p1_a_chrome_press_neither_arms_nor_moves_point` silently stopped being a test. Clippy's "never used" caught it. Both are restored, and the suite count confirms 1994 tests rather than 1993.
This commit is contained in:
parent
0f29530dd8
commit
d3d720ba8a
|
|
@ -8377,6 +8377,17 @@ impl State {
|
||||||
/// budget rows and is recorded rather than pre-optimised: a cache
|
/// budget rows and is recorded rather than pre-optimised: a cache
|
||||||
/// needs an invalidation key, and the wrong key is a worse defect
|
/// needs an invalidation key, and the wrong key is a worse defect
|
||||||
/// than a measurable scan.
|
/// than a measurable scan.
|
||||||
|
///
|
||||||
|
/// **Which "display line" this measures is a boundary B3's witness
|
||||||
|
/// must settle.** This counts SOURCE-TEXT display columns — tab
|
||||||
|
/// stops and Unicode width — and therefore excludes rendered
|
||||||
|
/// projections such as inline adornments and math substitutions,
|
||||||
|
/// which can occupy a different width on screen than the bytes they
|
||||||
|
/// stand for. That is consistent with the TUI-derived column rule
|
||||||
|
/// the two frontends share, and it is a choice, not an oversight:
|
||||||
|
/// "widest display line" is readable the other way. Recorded here
|
||||||
|
/// so the witness states which meaning governs rather than
|
||||||
|
/// discovering it.
|
||||||
fn widest_display_columns(&self) -> u32 {
|
fn widest_display_columns(&self) -> u32 {
|
||||||
let widest = self
|
let widest = self
|
||||||
.current_text
|
.current_text
|
||||||
|
|
|
||||||
|
|
@ -7673,6 +7673,70 @@ mod tests {
|
||||||
|
|
||||||
/// P1 — a press on the band's MODE LINE begins nothing.
|
/// P1 — a press on the band's MODE LINE begins nothing.
|
||||||
///
|
///
|
||||||
|
/// GUI Stage 1b B2 — a horizontal panel notch reaches the daemon's
|
||||||
|
/// window-targeted `view_left` path and MOVES it.
|
||||||
|
///
|
||||||
|
/// This arm used to be claimed and dropped, which is the
|
||||||
|
/// "frontend emits, receiver discards" shape §2a named. The row
|
||||||
|
/// asserts the **effect**, not the emission: `view_left` before and
|
||||||
|
/// after.
|
||||||
|
#[test]
|
||||||
|
fn b2_a_horizontal_panel_notch_moves_the_side_windows_view_left() {
|
||||||
|
let fid = FrontendId(791);
|
||||||
|
let (mut editor, mut states, mut render, _document, panel, epochs) =
|
||||||
|
panel_session_at(PROTOCOL_VERSION, fid);
|
||||||
|
// A CONTENT cell, not chrome: row 0 is inside the grid.
|
||||||
|
let (buffer_id, cell) = {
|
||||||
|
let core = editor.core.borrow();
|
||||||
|
(
|
||||||
|
core.windows[&panel].buffer_id,
|
||||||
|
pmacs_protocol::CellCoord::new(0, 0),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// **The discriminating setup.** B7's bound is
|
||||||
|
// `widest − viewport`, so a panel whose content fits has a
|
||||||
|
// maximum origin of zero and the move is absorbed by the
|
||||||
|
// clamp — correct behaviour that would read here as a dropped
|
||||||
|
// event. The row needs content wider than the viewport.
|
||||||
|
foreign_edit(&editor, buffer_id, "x".repeat(400).as_bytes());
|
||||||
|
let before = editor
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.windows
|
||||||
|
.get(&panel)
|
||||||
|
.map_or(0, |w| w.view_left);
|
||||||
|
|
||||||
|
let generation = live_generation(PanelArm::Mapped, &editor, &mut states, fid);
|
||||||
|
dispatch_panel_event(
|
||||||
|
&mut editor,
|
||||||
|
fid,
|
||||||
|
PROTOCOL_VERSION,
|
||||||
|
&mut states,
|
||||||
|
&mut render,
|
||||||
|
arm_pointer(
|
||||||
|
PanelArm::Mapped,
|
||||||
|
fid,
|
||||||
|
epochs,
|
||||||
|
buffer_id,
|
||||||
|
generation,
|
||||||
|
cell,
|
||||||
|
pmacs_protocol::MouseKind::ScrollRight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let after = editor
|
||||||
|
.core
|
||||||
|
.borrow()
|
||||||
|
.windows
|
||||||
|
.get(&panel)
|
||||||
|
.map_or(0, |w| w.view_left);
|
||||||
|
assert_ne!(
|
||||||
|
after, before,
|
||||||
|
"a horizontal panel notch must move the side window's origin, \
|
||||||
|
not be claimed and dropped"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// The merge made this arm the latch, because `Consumed` and
|
/// The merge made this arm the latch, because `Consumed` and
|
||||||
/// `Accepted` were the same `true`. The row reads the cursor as well
|
/// `Accepted` were the same `true`. The row reads the cursor as well
|
||||||
/// as the latch: a chrome press must not move point either.
|
/// as the latch: a chrome press must not move point either.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,24 @@ pub fn advance_char(column: u32, ch: char) -> u32 {
|
||||||
column.saturating_add(width)
|
column.saturating_add(width)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Widest line in `text`, in display columns.
|
||||||
|
///
|
||||||
|
/// GUI Stage 1b B7's upper-bound input, shared by both frontends so
|
||||||
|
/// they cannot disagree about where the right bound is — the same
|
||||||
|
/// reason `scroll::follow_left` is shared.
|
||||||
|
///
|
||||||
|
/// **This measures SOURCE-TEXT display columns**: tab stops and Unicode
|
||||||
|
/// width. Rendered projections — inline adornments, math substitutions
|
||||||
|
/// — can occupy a different width on screen and are deliberately not
|
||||||
|
/// counted, matching the column rule the rest of this module states.
|
||||||
|
#[must_use]
|
||||||
|
pub fn widest_line_columns(text: &str) -> u32 {
|
||||||
|
text.split('\n')
|
||||||
|
.map(|line| line.chars().fold(0, advance_char))
|
||||||
|
.max()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
/// Display width of the valid UTF-8 prefix of `bytes`.
|
/// Display width of the valid UTF-8 prefix of `bytes`.
|
||||||
///
|
///
|
||||||
/// Invalid input is conservatively truncated at the first invalid byte. This
|
/// Invalid input is conservatively truncated at the first invalid byte. This
|
||||||
|
|
|
||||||
|
|
@ -3356,23 +3356,81 @@ impl EditorState {
|
||||||
}
|
}
|
||||||
self.open_context_menu(side, coord.row, coord.col, (coord.row, coord.col));
|
self.open_context_menu(side, coord.row, coord.col, (coord.row, coord.col));
|
||||||
}
|
}
|
||||||
// Claimed and dropped, for two different reasons kept in one
|
// GUI Stage 1b B2 — the horizontal leg, which this arm used
|
||||||
// arm because their bodies are identical: horizontal panel
|
// to claim and drop. One notch is `SCROLL_COLUMNS`; the
|
||||||
// scrolling belongs to GUI arc Stage 1b's B-rows rather than
|
// frontend banks fractions and sends whole notches, so the
|
||||||
// parent 48, bare `Move` neither focuses nor claims, and the
|
// step is applied here exactly once.
|
||||||
// remaining buttons have no panel semantics at all.
|
PKind::ScrollLeft => {
|
||||||
PKind::ScrollLeft
|
self.scroll_window_columns(side, -SCROLL_COLUMNS);
|
||||||
| PKind::ScrollRight
|
}
|
||||||
| PKind::Move
|
PKind::ScrollRight => {
|
||||||
| PKind::Down(_)
|
self.scroll_window_columns(side, SCROLL_COLUMNS);
|
||||||
| PKind::Up(_)
|
}
|
||||||
| PKind::Drag(_) => {}
|
// Bare `Move` neither focuses nor claims, and the remaining
|
||||||
|
// buttons have no panel semantics at all.
|
||||||
|
PKind::Move | PKind::Down(_) | PKind::Up(_) | PKind::Drag(_) => {}
|
||||||
}
|
}
|
||||||
// Only a left press can anchor, and it returns `true` above.
|
// Only a left press can anchor, and it returns `true` above.
|
||||||
// Every other kind reaching here handled something already live.
|
// Every other kind reaching here handled something already live.
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GUI Stage 1b B2/B3/B7 — move one window's horizontal origin by
|
||||||
|
/// `columns`, the daemon-side effect a panel-document horizontal
|
||||||
|
/// wheel reaches.
|
||||||
|
///
|
||||||
|
/// This closes the leg §2a named: `ScrollLeft`/`ScrollRight` were
|
||||||
|
/// **claimed and dropped** here, which is the "frontend emits,
|
||||||
|
/// receiver discards" shape the panel-replay lane was opened to fix
|
||||||
|
/// and that B1–B3 inherited for the horizontal axis.
|
||||||
|
///
|
||||||
|
/// The bound is B7's, stated exactly — `0 ..= widest − viewport`,
|
||||||
|
/// **saturating at zero** — so the final display column stays
|
||||||
|
/// visible; clamping at the widest line's full width would let the
|
||||||
|
/// origin pass every glyph and blank the viewport. **Wrap pins the
|
||||||
|
/// origin to zero**, matching `horizontal_follow`.
|
||||||
|
///
|
||||||
|
/// Returns whether the origin actually moved, which is clause 2's
|
||||||
|
/// "effective move".
|
||||||
|
fn scroll_window_columns(&mut self, win_id: WindowId, columns: i32) -> bool {
|
||||||
|
let mut core = self.core.borrow_mut();
|
||||||
|
let Some(window) = core.windows.get(&win_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if window.last_wrap == crate::view::WrapMode::Wrap {
|
||||||
|
if let Some(window) = core.windows.get_mut(&win_id) {
|
||||||
|
window.view_left = 0;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let buffer_id = window.buffer_id;
|
||||||
|
let viewport_cols = window.last_content_cols;
|
||||||
|
let old_left = window.view_left;
|
||||||
|
let registry = core.registry.clone();
|
||||||
|
let widest = {
|
||||||
|
let reg = registry.borrow();
|
||||||
|
let Ok(buf) = reg.get(buffer_id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let len = buf.len();
|
||||||
|
let mut bytes = vec![0u8; len as usize];
|
||||||
|
buf.snapshot_rope().slice(0, len, &mut bytes);
|
||||||
|
crate::display_width::widest_line_columns(&String::from_utf8_lossy(&bytes))
|
||||||
|
};
|
||||||
|
let max_left = widest.saturating_sub(viewport_cols);
|
||||||
|
let next = i64::from(old_left)
|
||||||
|
.saturating_add(i64::from(columns))
|
||||||
|
.clamp(0, i64::from(max_left));
|
||||||
|
let next = u32::try_from(next).unwrap_or(0);
|
||||||
|
if next == old_left {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(window) = core.windows.get_mut(&win_id) {
|
||||||
|
window.view_left = next;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Byte under a panel cell, resolved against the SIDE window's own
|
/// Byte under a panel cell, resolved against the SIDE window's own
|
||||||
/// `view_top` and fold map.
|
/// `view_top` and fold map.
|
||||||
///
|
///
|
||||||
|
|
@ -4654,6 +4712,11 @@ impl EditorState {
|
||||||
/// readline / Emacs default and is what most terminal users expect.
|
/// readline / Emacs default and is what most terminal users expect.
|
||||||
const SCROLL_LINES: i32 = 3;
|
const SCROLL_LINES: i32 = 3;
|
||||||
|
|
||||||
|
/// Columns one horizontal wheel notch moves a window's origin — B7's
|
||||||
|
/// "three columns per wheel tick", the horizontal twin of
|
||||||
|
/// [`SCROLL_LINES`].
|
||||||
|
const SCROLL_COLUMNS: i32 = 3;
|
||||||
|
|
||||||
/// Gutter marker drawn on a collapsed region's head row (Arc 6 Stage 2,
|
/// Gutter marker drawn on a collapsed region's head row (Arc 6 Stage 2,
|
||||||
/// Q#FD20). Occupies the gutter's leading pad cell — the same cell the
|
/// Q#FD20). Occupies the gutter's leading pad cell — the same cell the
|
||||||
/// diagnostic sign uses — so it adds no column and changes no width; it
|
/// diagnostic sign uses — so it adds no column and changes no width; it
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue