improvise-7mro: the seed settles on a projected cell, and the matrix holds replies three ways

An awaited seed waits for a cell the host projected (id present), never the
blank a relabel leaves between a frame and its cells. The advance's reopen
keeps #282's shape: on this base the welded clear-buffer empties it anyway.

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:
Edward Langley
2026-09-23 19:24:09 -07:00
parent 2007a21557
commit fb246a8331
3 changed files with 177 additions and 95 deletions
+1 -3
View File
@@ -182,15 +182,13 @@ impl ClientState {
/// An untouched editor opened on a cell not projected yet takes its seed
/// once the host projects that cell, and never over typing (improvise-7mro).
fn settle_awaited_seed(&mut self) {
let (row, col) = self.view.cursor().selected;
let waiting = self.view.mode.is_editing()
&& self
.view
.opened_editor
.as_ref()
.is_some_and(|opened| opened.awaiting && !opened.touched);
if waiting && !crate::resolve::unprojected(self, row, col) {
let seed = crate::resolve::edit_seed(self).unwrap_or_default();
if let (true, Some(seed)) = (waiting, crate::resolve::projected_seed(self)) {
self.view.buffers.insert("edit".to_string(), seed.clone());
self.view.opened_editor = Some(crate::OpenedEditor::on(seed));
}
+10 -4
View File
@@ -1041,6 +1041,13 @@ pub(crate) fn edit_seed(state: &ClientState) -> Option<String> {
cell_at_cursor(state)?.edit_text.clone()
}
/// What an editor awaiting its seed takes once the host projects the cell under
/// the cursor: `None` until then, a relabel's blank included (improvise-7mro).
pub(crate) fn projected_seed(state: &ClientState) -> Option<String> {
let cell = cell_at_cursor(state).filter(|cell| cell.id.is_some())?;
Some(cell.edit_text.clone().unwrap_or_default())
}
/// The edit text the host projected at an absolute grid position.
fn seed_at(state: &ClientState, row: usize, col: usize) -> String {
projected_at(state, row, col)
@@ -1051,10 +1058,9 @@ fn seed_at(state: &ClientState, row: usize, col: usize) -> String {
/// `EnterEditAtCursor`: re-open the editor where the advance landed, unless `i`
/// would decline that cell. The host asks `uneditable`; this asks the same id.
fn reopened_at(state: &ClientState, row: usize, col: usize, mode: AppMode) -> Vec<Command> {
match (declined_at(state, row, col), unprojected(state, row, col)) {
(Some(refusal), _) => vec![Command::SetStatus(refusal)],
(None, true) => awaiting_seed(mode),
(None, false) => opened_on(seed_at(state, row, col), mode),
match declined_at(state, row, col) {
Some(refusal) => vec![Command::SetStatus(refusal)],
None => opened_on(seed_at(state, row, col), mode),
}
}
+166 -88
View File
@@ -2672,12 +2672,27 @@ type Pipelined = (
&'static [&'static str],
);
/// When the host's replies reach the client.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Replies {
/// Each before the next key.
Delivered,
/// All held to the end, so every key reads the first window.
Held,
/// Held until the last two keys, which then run delivered.
HeldThenTyped,
}
/// `wrapping_host`'s cells that differ from how it was built, as `Month/Measure`.
fn written_cells(app: &App) -> Vec<String> {
let model = &app.model_state.workbook.model;
let mut written = Vec::new();
for month in ["Jan", "Feb"] {
for (measure, built) in [("Revenue", Some(10.0)), ("Profit", None), ("Cost", Some(4.0))] {
for (measure, built) in [
("Revenue", Some(10.0)),
("Profit", None),
("Cost", Some(4.0)),
] {
let key = improvise_command::model::cell::CellKey::new(vec![
("Month".to_string(), month.to_string()),
("_Measure".to_string(), measure.to_string()),
@@ -2691,8 +2706,8 @@ fn written_cells(app: &App) -> Vec<String> {
written
}
/// One key on the client, its frame sent to the host; the reply comes back
/// for the caller to deliver or hold. An upstream key reaches a host that answers nothing.
/// One key on the client, its frame sent to the host; the reply comes back for
/// the caller to deliver or hold. An upstream key reaches a host that answers nothing.
fn sent(
host: &mut improvise_command::ui::host::Host,
session: improvise_command::ui::project::SessionId,
@@ -2708,58 +2723,93 @@ fn sent(
}
}
/// improvise-7mro: the offset-aliasing matrix, replies delivered and replies
/// held. See `context/test-evidence/clients.md`.
/// A host over `make()` and a client on it, the cursor placed and answered.
fn placed_client(
make: fn() -> App,
window: (usize, usize),
(at, row_offset, col_offset): ((usize, usize), usize, usize),
) -> (
improvise_command::ui::host::Host,
improvise_command::ui::project::SessionId,
ClientState,
) {
let mut host = improvise_command::ui::host::Host::new(make());
let (session, welcome) = host.connect(window.0, window.1);
let mut client = ClientState {
window,
..ClientState::default()
};
deliver(vec![welcome], session, &mut client);
let placed = vec![
improvise_protocol::Command::SetSelected {
row: at.0,
col: at.1,
},
improvise_protocol::Command::SetRowOffset(row_offset),
improvise_protocol::Command::SetColOffset(col_offset),
];
client.fold_outgoing(&placed);
deliver(
host.handle(session, ClientFrame::Commands(placed)),
session,
&mut client,
);
(host, session, client)
}
/// improvise-7mro: the offset-aliasing matrix, with the host's replies delivered,
/// held, and held until the typing. See `context/test-evidence/clients.md`.
#[test]
fn keys_sent_before_a_scroll_is_answered_agree_with_the_tui() {
let cases: [Pipelined; 7] = [
("wrap", (2, 6), ((2, 0), 1, 0), &["i", "Enter", "8", "Enter"]),
("horizontal wrap", (2, 1), ((2, 0), 1, 0), &["i", "Enter", "8", "Enter"]),
(
"wrap",
(2, 6),
((2, 0), 1, 0),
&["i", "Enter", "8", "Enter"],
),
(
"horizontal wrap",
(2, 1),
((2, 0), 1, 0),
&["i", "Enter", "8", "Enter"],
),
("G", (2, 6), ((0, 0), 0, 0), &["G", "i", "7", "Enter"]),
("$", (2, 1), ((0, 0), 0, 0), &["$", "i", "7", "Enter"]),
("gg", (2, 6), ((2, 0), 1, 0), &["g", "g", "i", "7", "Enter"]),
("$ x", (2, 1), ((0, 0), 0, 0), &["$", "x"]),
("gg p", (2, 6), ((2, 0), 1, 0), &["y", "y", "g", "g", "p"]),
];
for (name, window, (at, row_offset, col_offset), keys) in cases {
for (name, window, start, keys) in cases {
let mut tui = wrapping_host();
tui.cursor_mut().selected = at;
tui.cursor_mut().selected = start.0;
for &k in keys {
reduce_key(&mut tui, BrowserKey::new(k).to_key_event().unwrap());
}
assert_eq!(written_cells(&tui).len(), 1, "setup: {name}: the TUI wrote one cell");
for held in [false, true] {
let what = format!("{name} ({keys:?}, replies held: {held})");
let mut host = improvise_command::ui::host::Host::new(wrapping_host());
let (session, welcome) = host.connect(window.0, window.1);
let mut client = ClientState {
window,
..ClientState::default()
assert_eq!(
written_cells(&tui).len(),
1,
"setup: {name}: the TUI wrote one cell"
);
for replies in [Replies::Delivered, Replies::Held, Replies::HeldThenTyped] {
let what = format!("{name} ({keys:?}, replies {replies:?})");
let (mut host, session, mut client) = placed_client(wrapping_host, window, start);
let typed_from = match replies {
Replies::Delivered => 0,
Replies::Held => keys.len(),
Replies::HeldThenTyped => keys.len().saturating_sub(2),
};
deliver(vec![welcome], session, &mut client);
let placed = vec![
improvise_protocol::Command::SetSelected {
row: at.0,
col: at.1,
},
improvise_protocol::Command::SetRowOffset(row_offset),
improvise_protocol::Command::SetColOffset(col_offset),
];
client.fold_outgoing(&placed);
deliver(
host.handle(session, ClientFrame::Commands(placed)),
session,
&mut client,
);
let mut replies = Vec::new();
for &k in keys {
let reply = sent(&mut host, session, &mut client, k);
match held {
true => replies.extend(reply),
false => deliver(reply, session, &mut client),
let mut held = Vec::new();
for (i, &k) in keys.iter().enumerate() {
if i == typed_from {
deliver(std::mem::take(&mut held), session, &mut client);
}
held.extend(sent(&mut host, session, &mut client, k));
if i >= typed_from {
deliver(std::mem::take(&mut held), session, &mut client);
}
}
if held {
if replies == Replies::Held {
assert_eq!(
written_cells(host.app()),
Vec::<String>::new(),
@@ -2772,7 +2822,7 @@ fn keys_sent_before_a_scroll_is_answered_agree_with_the_tui() {
client.view.mode,
client.view.status_msg
);
deliver(replies, session, &mut client);
deliver(std::mem::take(&mut held), session, &mut client);
let retry = keys.last().expect("a key");
let reply = sent(&mut host, session, &mut client, retry);
deliver(reply, session, &mut client);
@@ -2782,8 +2832,13 @@ fn keys_sent_before_a_scroll_is_answered_agree_with_the_tui() {
written_cells(&tui),
"{what}: the browser wrote a different cell from the TUI"
);
if !held {
if replies != Replies::Held {
assert_eq!(document_text(host.app()), document_text(&tui), "{what}");
assert_eq!(
client.view.buffers.get("edit"),
tui.view_state.buffers.get("edit"),
"{what}: the editor's buffer"
);
}
assert_eq!(
client.view.cursor().selected,
@@ -2796,58 +2851,81 @@ fn keys_sent_before_a_scroll_is_answered_agree_with_the_tui() {
"{what}: the mode; browser status {:?}",
client.view.status_msg
);
assert_eq!(
client.view.buffers.get("edit"),
tui.view_state.buffers.get("edit"),
"{what}: the editor's buffer"
);
}
}
}
/// improvise-7mro: a cell inside the held window whose offsets the client has
/// moved is read at its own position. See `context/test-evidence/clients.md`.
/// Four regions by two measures, every cell stored and no two alike, so a cell
/// read at the wrong row or column is a different value.
fn four_by_two_host() -> App {
let mut wb = Workbook::new("Test");
wb.add_category("Region").unwrap();
for region in ["R0", "R1", "R2", "R3"] {
wb.add_item("Region", region).unwrap();
}
for measure in ["A", "B"] {
wb.add_item("_Measure", measure).unwrap();
}
wb.active_view_mut().set_axis("Region", Axis::Row);
wb.active_view_mut().set_axis("_Measure", Axis::Column);
for (r, region) in ["R0", "R1", "R2", "R3"].into_iter().enumerate() {
for (base, measure) in [(10.0, "A"), (20.0, "B")] {
let key = improvise_command::model::cell::CellKey::new(vec![
("Region".to_string(), region.to_string()),
("_Measure".to_string(), measure.to_string()),
]);
wb.model
.set_cell(key, improvise_protocol::CellValue::Number(base + r as f64));
}
}
let mut app = App::new(wb, None);
app.rebuild_layout();
app
}
/// improvise-7mro: a cell inside the held window, read while the client's offsets
/// differ from the frame's origin. See `context/test-evidence/clients.md`.
#[test]
fn a_held_cell_is_read_at_its_position_and_not_its_cache_index() {
let keys = ["k", "k", "j", "i"];
let mut tui = wrapping_host();
tui.cursor_mut().selected = (2, 0);
for k in keys {
reduce_key(&mut tui, BrowserKey::new(k).to_key_event().unwrap());
for keys in [
&["k", "k", "j", "i", "7", "Enter"][..],
&["k", "k", "j", "i", "Tab"][..],
] {
let mut tui = four_by_two_host();
tui.cursor_mut().selected = (3, 0);
for &k in keys {
reduce_key(&mut tui, BrowserKey::new(k).to_key_event().unwrap());
}
let (mut host, session, mut client) =
placed_client(four_by_two_host, (2, 6), ((3, 0), 2, 0));
let mut held = Vec::new();
for (i, &k) in keys.iter().enumerate() {
if i == 3 {
let cursor = client.view.cursor();
assert_eq!(
(
cursor.selected,
cursor.row_offset,
client.grid.row_frame.first
),
((2, 0), 1, 2),
"setup: inside the held window, the offsets not its origin"
);
}
held.extend(sent(&mut host, session, &mut client, k));
}
assert_eq!(document_text(host.app()), document_text(&tui), "{keys:?}");
assert_eq!(
client.view.cursor().selected,
tui.view_state.cursor().selected
);
assert_eq!(
client.view.buffers.get("edit"),
tui.view_state.buffers.get("edit")
);
assert_eq!(
client.view.opened_editor, tui.view_state.opened_editor,
"{keys:?}: the editor opened on another cell's seed"
);
}
assert_eq!(tui.view_state.cursor().selected, (1, 0), "setup: on Profit/Jan");
let mut host = improvise_command::ui::host::Host::new(wrapping_host());
let (session, welcome) = host.connect(2, 6);
let mut client = ClientState {
window: (2, 6),
..ClientState::default()
};
deliver(vec![welcome], session, &mut client);
let placed = vec![
improvise_protocol::Command::SetSelected { row: 2, col: 0 },
improvise_protocol::Command::SetRowOffset(1),
];
client.fold_outgoing(&placed);
deliver(
host.handle(session, ClientFrame::Commands(placed)),
session,
&mut client,
);
for k in keys {
let _held = sent(&mut host, session, &mut client, k);
}
let cursor = client.view.cursor();
assert_eq!(
(cursor.selected, cursor.row_offset, client.grid.row_frame.first),
((1, 0), 0, 1),
"setup: inside the held window, offsets ahead of it"
);
assert_eq!(
format!("{:?}", client.view.mode),
format!("{:?}", tui.view_state.mode),
"the client read another row's cell: {:?}",
client.view.buffers.get("edit")
);
assert_eq!(client.view.status_msg, tui.view_state.status_msg);
}