The advertised-key guard judges by what the fold moved, off every edge (6562)
heard() compared command types, so a clamped move counted as acting. It now folds the outcome and compares the view slice (status and disarm aside); a Model command counts as sent. The fixture is a 3x3 grid with the cursor at (1,1), three tiles with the cursor at 1, help page 1. Direction is out of reach for any did-it-move check, so every grid key is also pressed from the centre and must land where its binding says. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
@@ -2039,6 +2039,35 @@ mod tests {
|
||||
|
||||
// ── what is bound and what is not ───────────────────────────────
|
||||
|
||||
/// A three-by-three grid with `id` at (1,1) and the cursor on it: no move
|
||||
/// from there is clamped by an edge (improvise-6562).
|
||||
fn centred_on(id: CellId) -> ClientState {
|
||||
let mut centre = CellDisplay::number("1200");
|
||||
centre.id = Some(id);
|
||||
let mut cells: Vec<_> = (0..3)
|
||||
.flat_map(|r| (0..3).map(move |c| ((r, c), CellDisplay::number("7"))))
|
||||
.collect();
|
||||
cells[4].1 = centre;
|
||||
let mut client = ClientState::default();
|
||||
client.bootstrap(Snapshot {
|
||||
view: ViewState::default(),
|
||||
seed: vec![
|
||||
Projection::Grid(GridUpdate::RowFrame(AxisFrame::flat([
|
||||
"North", "South", "East",
|
||||
]))),
|
||||
Projection::Grid(GridUpdate::ColFrame(AxisFrame::flat([
|
||||
"Revenue", "Cost", "Profit",
|
||||
]))),
|
||||
Projection::Grid(GridUpdate::Cells(cells)),
|
||||
Projection::Grid(GridUpdate::Extent(GridExtent { rows: 3, cols: 3 })),
|
||||
Projection::Basis(published()),
|
||||
],
|
||||
});
|
||||
client.window = (3, 3);
|
||||
client.view.cursor_mut().selected = (1, 1);
|
||||
client
|
||||
}
|
||||
|
||||
/// A client whose every widget has something in it, so an `Upstream` names
|
||||
/// an **unhandled command** and never a state that said no (improvise-rmvx).
|
||||
fn furnished(mode: AppMode) -> ClientState {
|
||||
@@ -2048,7 +2077,7 @@ mod tests {
|
||||
/// `furnished`, with `cell` under the cursor. A history in both directions
|
||||
/// and a yanked value, so no `Cmd` is refused by an empty stack (3t5x).
|
||||
fn furnished_on(mode: AppMode, cell: CellId) -> ClientState {
|
||||
let mut state = client_on(cell);
|
||||
let mut state = centred_on(cell);
|
||||
let frame = |name: &str| crate::ViewFrame {
|
||||
view_name: name.into(),
|
||||
mode: AppMode::Normal,
|
||||
@@ -2062,8 +2091,8 @@ mod tests {
|
||||
state.view.mode = mode;
|
||||
state.grid.page_coords = vec![crate::PageAxis {
|
||||
cat: "Quarter".into(),
|
||||
selected: "Q1".into(),
|
||||
items: Some(vec!["Q1".into(), "Q2".into()]),
|
||||
selected: "Q2".into(),
|
||||
items: Some(vec!["Q1".into(), "Q2".into(), "Q3".into()]),
|
||||
}];
|
||||
// THREE of everything, every panel cursor seated in the MIDDLE: an edge
|
||||
// makes `move-panel-cursor` answer `Ignored` for the wrong reason.
|
||||
@@ -2095,10 +2124,15 @@ mod tests {
|
||||
axes: Vec::new(),
|
||||
})
|
||||
.collect();
|
||||
state.tiles.tiles = vec![crate::TileEntry {
|
||||
category: "Region".into(),
|
||||
axis: Axis::Row,
|
||||
}];
|
||||
state.tiles.tiles = ["Region", "Product", "Quarter"]
|
||||
.into_iter()
|
||||
.map(|category| crate::TileEntry {
|
||||
category: category.into(),
|
||||
axis: Axis::Row,
|
||||
})
|
||||
.collect();
|
||||
state.view.tile_cat_idx = 1;
|
||||
state.view.help_page = 1;
|
||||
state.view.cat_panel_cursor = 1;
|
||||
state.view.formula_cursor = 1;
|
||||
state.view.view_panel_cursor = 1;
|
||||
@@ -2124,19 +2158,24 @@ mod tests {
|
||||
Silent,
|
||||
}
|
||||
|
||||
/// Judged by what moved, never by the variant: a chord's second key is
|
||||
/// `Local` even when all it does is disarm the prefix.
|
||||
fn heard(outcome: KeyOutcome) -> Heard {
|
||||
/// Judged by what the fold MOVED, never by a command's type: a clamped move
|
||||
/// or a lone disarm changes nothing (improvise-6562). `Model` is sent.
|
||||
fn heard(state: &mut ClientState, outcome: KeyOutcome) -> Heard {
|
||||
let commands = match outcome {
|
||||
KeyOutcome::Upstream(_) | KeyOutcome::Ignored => return Heard::Silent,
|
||||
KeyOutcome::Request(_) => return Heard::Acts,
|
||||
KeyOutcome::Local(commands) => commands,
|
||||
};
|
||||
let quiet = |view: &ViewState| ViewState {
|
||||
status_msg: String::new(),
|
||||
transient_prefix: None,
|
||||
..view.clone()
|
||||
};
|
||||
let before = quiet(&state.view);
|
||||
commands.iter().for_each(|c| state.fold(c));
|
||||
let sent = commands.iter().any(|c| matches!(c, Command::Model { .. }));
|
||||
let said = commands.iter().any(|c| matches!(c, Command::SetStatus(_)));
|
||||
let did = commands
|
||||
.iter()
|
||||
.any(|c| !matches!(c, Command::SetStatus(_) | Command::SetTransientPrefix(None)));
|
||||
match (did, said) {
|
||||
match (sent || quiet(&state.view) != before, said) {
|
||||
(true, _) => Heard::Acts,
|
||||
(false, true) => Heard::Declines,
|
||||
(false, false) => Heard::Silent,
|
||||
@@ -2196,7 +2235,8 @@ mod tests {
|
||||
commands.iter().for_each(|c| state.fold(c));
|
||||
}
|
||||
}
|
||||
heard(resolve(&state, *last))
|
||||
let outcome = resolve(&state, *last);
|
||||
heard(&mut state, outcome)
|
||||
}
|
||||
|
||||
/// improvise-rmvx, wk18, 3t5x. Every advertised key does what it says, or
|
||||
@@ -2312,6 +2352,52 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// improvise-6562: "it moved" cannot see a move the WRONG way, so every grid
|
||||
/// key is pressed from the centre and must land where it says. Evidence file.
|
||||
#[test]
|
||||
fn every_grid_move_lands_where_its_binding_says() {
|
||||
let keymap = Keymaps::shared().get(&ModeKey::Normal).expect("Normal");
|
||||
let mut moves = Vec::new();
|
||||
bound_commands(keymap, &mut moves);
|
||||
// A move's destination is its own args; a jump's is its name.
|
||||
let mut expected: Vec<(String, Vec<String>, (usize, usize))> = moves
|
||||
.into_iter()
|
||||
.filter(|(name, _)| name == "move-selection")
|
||||
.map(|(name, args)| {
|
||||
let d = |i: usize| args[i].parse::<isize>().expect("a delta");
|
||||
let to = ((1 + d(0)) as usize, (1 + d(1)) as usize);
|
||||
(name, args, to)
|
||||
})
|
||||
.collect();
|
||||
for (name, to) in [
|
||||
("jump-first-row", (0, 1)),
|
||||
("jump-last-row", (2, 1)),
|
||||
("jump-first-col", (1, 0)),
|
||||
("jump-last-col", (1, 2)),
|
||||
] {
|
||||
expected.push((name.to_string(), Vec::new(), to));
|
||||
}
|
||||
let mut pressed = 0;
|
||||
for (name, args, to) in expected {
|
||||
let wanted = (!args.is_empty()).then_some(args.as_slice());
|
||||
for sequence in sequences_running(keymap, &name, wanted) {
|
||||
let mut state = furnished(AppMode::Normal);
|
||||
for key in &sequence {
|
||||
if let KeyOutcome::Local(commands) = resolve(&state, *key) {
|
||||
commands.iter().for_each(|c| state.fold(c));
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
state.view.cursor().selected,
|
||||
to,
|
||||
"{sequence:?} runs `{name} {args:?}` from (1,1)"
|
||||
);
|
||||
pressed += 1;
|
||||
}
|
||||
}
|
||||
assert!(pressed >= 14, "only {pressed} keys pressed; the walk went blind");
|
||||
}
|
||||
|
||||
/// improvise-rmvx. `step` checks `declined` first, so a name answered
|
||||
/// there makes an arm in `composed` UNREACHABLE. Evidence file.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user