# Conflicts: # context/test-evidence/clients.md
This commit is contained in:
@@ -2,9 +2,9 @@
|
||||
//! The two properties this dispatch owes: `context/code/clients.md`.
|
||||
|
||||
use crate::{
|
||||
BasisCache, CategoryTreeCache, Command, DocumentCache, FormulaCache, GridCache, Projection,
|
||||
Rejection, Snapshot, TileBarCache, ViewListCache, ViewState, WidgetCache, reduce_view,
|
||||
settled_mode,
|
||||
BasisCache, CategoryTreeCache, Command, DocumentCache, EditorSession, FormulaCache, GridCache,
|
||||
Projection, Rejection, Snapshot, TileBarCache, ViewListCache, ViewState, WidgetCache,
|
||||
reduce_view, settled_mode,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -43,6 +43,12 @@ pub struct ClientState {
|
||||
/// replay the rest. Why a queue: `context/code/clients.md`.
|
||||
#[serde(skip)]
|
||||
pub in_flight: std::collections::VecDeque<Sent>,
|
||||
/// The status text last observed, and the editor it was written in while
|
||||
/// that editor is still open (improvise-1x9h, `context/code/clients.md`).
|
||||
#[serde(skip)]
|
||||
pub status_seen: String,
|
||||
#[serde(skip)]
|
||||
pub status_owner: Option<EditorSession>,
|
||||
}
|
||||
|
||||
/// One frame this client has sent. `awaited` is true exactly when the host
|
||||
@@ -78,6 +84,7 @@ impl ClientState {
|
||||
}
|
||||
self.settle_mode();
|
||||
self.settle_editor();
|
||||
self.settle_status();
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
@@ -98,6 +105,7 @@ impl ClientState {
|
||||
self.replay();
|
||||
self.settle_mode();
|
||||
self.settle_editor();
|
||||
self.settle_status();
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
@@ -131,6 +139,7 @@ impl ClientState {
|
||||
}
|
||||
self.settle_mode();
|
||||
self.settle_editor();
|
||||
self.settle_status();
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
@@ -181,9 +190,18 @@ impl ClientState {
|
||||
self.last_rejection = Some(rejection);
|
||||
self.settle_mode();
|
||||
self.settle_editor();
|
||||
self.settle_status();
|
||||
self.revision += 1;
|
||||
}
|
||||
|
||||
/// Whether the status belongs to the editor still open, so a renderer must
|
||||
/// not expire it (improvise-1x9h). The TUI asks the same `EditorSession`.
|
||||
pub fn status_stands(&self) -> bool {
|
||||
!self.view.status_msg.is_empty()
|
||||
&& self.status_owner.is_some()
|
||||
&& self.status_owner == EditorSession::of(&self.view)
|
||||
}
|
||||
|
||||
/// The settled slice plus every frame still in flight, in order. What a
|
||||
/// dropped frame leaves behind is the *rest* of them, not nothing.
|
||||
fn replay(&mut self) {
|
||||
@@ -248,6 +266,18 @@ impl ClientState {
|
||||
}
|
||||
}
|
||||
|
||||
/// A new message is owned by the editor open as it arrives; an old one loses
|
||||
/// its owner for good once that editor is not the open one.
|
||||
fn settle_status(&mut self) {
|
||||
let now = EditorSession::of(&self.view);
|
||||
if self.view.status_msg != self.status_seen {
|
||||
self.status_seen = self.view.status_msg.clone();
|
||||
self.status_owner = now;
|
||||
} else if self.status_owner != now {
|
||||
self.status_owner = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// The widget dispatch: a projection names the widget it is addressed to.
|
||||
/// The `CategoryTreeCache`/`TileBarCache` overlap is deliberate (gotcha 32).
|
||||
fn apply_projection(&mut self, projection: &Projection) {
|
||||
@@ -662,6 +692,101 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// improvise-1x9h: a message written while an editor is open stands until
|
||||
/// that editor closes, and never again after. Evidence file.
|
||||
#[test]
|
||||
fn a_message_stands_while_the_editor_it_was_written_in_is_open() {
|
||||
let mut state = ClientState::default();
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::formula_edit()));
|
||||
state.fold(&Command::SetStatus("Formula error".into()));
|
||||
assert!(
|
||||
state.status_stands(),
|
||||
"a refusal beside its editor must stand"
|
||||
);
|
||||
state.fold(&Command::SetBuffer {
|
||||
name: "formula".into(),
|
||||
value: "Total = ".into(),
|
||||
});
|
||||
assert!(state.status_stands(), "typing is the same editor");
|
||||
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::FormulaPanel));
|
||||
assert!(!state.status_stands(), "the refusal outlived its editor");
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::formula_edit()));
|
||||
assert_eq!(state.view.status_msg, "Formula error", "nothing cleared it");
|
||||
assert!(
|
||||
!state.status_stands(),
|
||||
"an equal editor opened later is another opening, and the message is not its"
|
||||
);
|
||||
}
|
||||
|
||||
/// A relay that rebases the view off the editor ends its message's ownership,
|
||||
/// as a fold would (improvise-0dbk). Evidence file.
|
||||
#[test]
|
||||
fn a_rebase_off_the_editor_ends_the_message_standing() {
|
||||
let mut state = ClientState::default();
|
||||
state.fold(&Command::SetSelected { row: 1, col: 1 });
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::editing()));
|
||||
state.fold(&Command::SetStatus("a note about this cell".into()));
|
||||
assert!(state.status_stands(), "precondition: the note stands");
|
||||
state.fold_outgoing(&commit_like("unused")[..1]);
|
||||
assert!(
|
||||
!state.in_flight.is_empty(),
|
||||
"the relay must rebase, not apply"
|
||||
);
|
||||
|
||||
state.fold(&Command::ResetCursor);
|
||||
assert_eq!(
|
||||
state.view.cursor().selected,
|
||||
(0, 0),
|
||||
"the rebase moved the cursor"
|
||||
);
|
||||
state.fold_outgoing(&[Command::SetSelected { row: 1, col: 1 }]);
|
||||
assert!(
|
||||
!state.status_stands(),
|
||||
"an equal editor after a rebase took the old message back"
|
||||
);
|
||||
}
|
||||
|
||||
/// The editor is the one it was WRITTEN in: not any editor, and not the next
|
||||
/// cell's after a commit advances (improvise-1x9h, sx7h). Evidence file.
|
||||
#[test]
|
||||
fn a_message_does_not_stand_beside_an_editor_it_was_not_written_in() {
|
||||
let mut state = ClientState::default();
|
||||
state.fold(&Command::SetStatus("saved".into()));
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::editing()));
|
||||
assert!(
|
||||
!state.status_stands(),
|
||||
"written in Normal, it is the timer's"
|
||||
);
|
||||
|
||||
let mut state = ClientState::default();
|
||||
state.fold(&Command::SetSelected { row: 1, col: 1 });
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::editing()));
|
||||
state.fold(&Command::SetStatus("a note about this cell".into()));
|
||||
assert!(state.status_stands());
|
||||
state.fold(&Command::SetSelected { row: 2, col: 1 });
|
||||
assert!(
|
||||
!state.status_stands(),
|
||||
"the note followed the editor down a row"
|
||||
);
|
||||
|
||||
let mut state = ClientState::default();
|
||||
state.fold(&Command::SetSelected { row: 1, col: 1 });
|
||||
state.fold(&Command::ChangeMode(crate::AppMode::editing()));
|
||||
state.fold(&Command::SetOpenedEditor(Some(crate::OpenedEditor::on(
|
||||
"4".into(),
|
||||
))));
|
||||
state.fold(&Command::SetStatus("a note about this cell".into()));
|
||||
assert!(state.status_stands());
|
||||
state.fold(&Command::SetOpenedEditor(Some(crate::OpenedEditor::on(
|
||||
"47".into(),
|
||||
))));
|
||||
assert!(
|
||||
!state.status_stands(),
|
||||
"a re-opening on the same cell is another editor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_fold_bumps_the_revision_so_a_renderer_can_poll_it() {
|
||||
let mut state = ClientState::default();
|
||||
|
||||
@@ -561,6 +561,27 @@ impl ViewState {
|
||||
}
|
||||
}
|
||||
|
||||
/// What one opening of a minibuffer sets. Equal at two moments means the same
|
||||
/// editor is still open; both front ends ask this. `context/code/tui.md`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct EditorSession {
|
||||
mode: AppMode,
|
||||
cell: (usize, usize),
|
||||
seed: Option<String>,
|
||||
}
|
||||
|
||||
impl EditorSession {
|
||||
/// The editor open in `view`, or `None` outside a text-entry mode.
|
||||
pub fn of(view: &ViewState) -> Option<Self> {
|
||||
view.mode.minibuffer()?;
|
||||
Some(Self {
|
||||
mode: view.mode.clone(),
|
||||
cell: view.cursor().selected,
|
||||
seed: view.opened_editor.as_ref().map(|o| o.seed.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod boundary_tests {
|
||||
/// improvise-cqi AC 2: the part of the boundary the compiler can hold.
|
||||
|
||||
@@ -16,6 +16,9 @@ pub struct RenderModel {
|
||||
/// The mode's own label, off `AppMode` — the client does not spell it.
|
||||
pub mode: String,
|
||||
pub status: String,
|
||||
/// Whether `status` belongs to the editor still open, so the page must not
|
||||
/// expire it on a timer (improvise-1x9h).
|
||||
pub status_stands: bool,
|
||||
/// The document's active view, as this client knows it (gotcha 29).
|
||||
pub view_name: String,
|
||||
/// The title bar: the document's name, whether it has unsaved changes, and
|
||||
@@ -239,6 +242,7 @@ pub fn render_model(state: &ClientState) -> RenderModel {
|
||||
revision: state.revision,
|
||||
mode: improvise_protocol::mode_label(&state.view.mode).to_string(),
|
||||
status: state.view.status_msg.clone(),
|
||||
status_stands: state.status_stands(),
|
||||
view_name: state.view.active_view.clone(),
|
||||
doc_name: state.document.name.clone(),
|
||||
doc_dirty: state.document.dirty,
|
||||
|
||||
@@ -195,6 +195,26 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A rejection's sentence is written outside a fold, so it must still be owned
|
||||
/// by the editor it arrived in (improvise-1x9h). Evidence file.
|
||||
#[test]
|
||||
fn a_rejection_that_arrives_in_an_open_editor_stands_beside_it() {
|
||||
let mut client = Client::default();
|
||||
client
|
||||
.state_mut()
|
||||
.fold(&Command::ChangeMode(improvise_protocol::AppMode::editing()));
|
||||
client.fold_frame(ServerFrame::Rejected(Rejection::NoDocumentPath));
|
||||
assert!(
|
||||
client.state().view.mode.is_editing(),
|
||||
"a refused save closes nothing"
|
||||
);
|
||||
assert!(!client.state().view.status_msg.is_empty());
|
||||
assert!(
|
||||
client.state().status_stands(),
|
||||
"the refusal was not observed where it arrived, so it would expire"
|
||||
);
|
||||
}
|
||||
|
||||
/// A rejection that refused an **edit** must say nothing was applied; the
|
||||
/// two `Save` answers refused none. Evidence: clients.md (improvise-8uj8).
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user