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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user