Skip to main content

monospace_editing/
session.rs

1//! The session: a diagram being edited, the steps behind and ahead of it, and how far along them
2//! it stands. See the _Vocabulary_ rows for `Session` and for `Position`, and §4 and §6 of
3//! [`docs/editing-model.md`](../../../docs/editing-model.md).
4
5use monospace_diagram::{Diagram, Shape, ShapeId};
6use rpds::VectorSync;
7
8use crate::utilities::kept_first;
9use crate::{Command, Step};
10
11/// A diagram being edited, the steps behind and ahead of it, and a position among them.
12///
13/// **Nothing here changes a session, and the type system says so rather than a rule this file
14/// keeps.** No method takes `&mut self`: every one of them is asked over `&self` and hands a new
15/// [`Session`] back, so a caller that forgets to keep what it was given changes nothing, and a
16/// caller that keeps it still cannot have changed this one. **The guarantee §4 of the model makes —
17/// the diagram a session owns is changed through the session and nowhere else — is what the absence
18/// of a `&mut self` anywhere in this surface means**, and it is a stronger answer than the one a
19/// private field was: nothing in this crate can break it, because there is no borrow that would let
20/// it. **The price is that a caller has to keep what it is given**: discarding the result of
21/// [`perform`](Session::perform) changes nothing at all, which is a warning rather than a failure.
22/// The field below is written once on top of that: a command changes the diagram by way of the
23/// steps, never by way of the field.
24///
25/// **The field holds the diagram the session was created over, and the steps hold every state after
26/// it.** A step is the diagram a command *left*, so the state before the first command is the one
27/// state no step holds, and it is the field — which is why nothing writes the field again, and why
28/// the diagram in hand is either that field or a step and never anything else. One diagram per step
29/// and one to start with, which is the model's "memory proportional to the diagram times the number
30/// of steps", and each of them held once.
31///
32/// **What a session is made of is shareable, and the sharing is what pays for the copies.** Two
33/// sessions standing at different positions in one history hold the same `VectorSync<Step>` and the
34/// same `Diagram`s, and a new one costs a new position and at most a new leaf. Nothing in the layer
35/// needs two sessions at once today — the demonstration walks one — but this is the property a
36/// caller gets for free the first time one does, and it is why the steps are a persistent sequence
37/// and not a `Vec`.
38///
39/// **Giving a step back moves the position and changes nothing else.** [`undo`](Session::undo) and
40/// [`redo`](Session::redo) are one expression each for that reason, and neither removes a step or
41/// rebuilds a diagram: the state that comes back is the state that was there, held since the command
42/// that made it. Nothing is inverted, and every step stays where it is while the history walks over
43/// them, which is what makes the walk go both ways.
44///
45/// **The history is a line, and `position` is where in it the session stands.** It is how many
46/// steps are behind rather than an index into a pair of stacks, so it cannot disagree with either
47/// of them: there is something behind exactly when `position` is above zero, and something ahead
48/// exactly when it is below `steps.len()`. A command carried out from behind the end discards what
49/// was ahead, which is the only end a line can discard from without changing what giving a step
50/// back means.
51///
52/// **A command that changes nothing records nothing and discards nothing** (#285): the session
53/// comes back as it was, so a branch is only ever cut by a command that moved the drawing.
54///
55/// **The steps are held in the same persistent sequence the diagram holds its figures in**, so a
56/// session standing at one place in a history shares every step with one standing at another
57/// rather than copying them — and a step is a whole diagram beside a command, which is what makes
58/// the difference between sharing and copying the difference between 949 B and 40 122 B per step.
59/// That sequence has no truncation, so what was ahead is dropped by a walk of `drop_last_mut`
60/// rather than in one step: each of those is a trie descent where a `Vec` was one, and it is paid
61/// only on the branch that carries a command out from behind the end. See `kept_first`, which is
62/// where that walk is written and tested.
63///
64/// # The absence, shown rather than described
65///
66/// The rule that nothing here changes a session is held by nothing in this file, so it
67/// is worth one documentation example that says it out loud: everything up to the last line compiles, and the
68/// last line is a method this crate does not have.
69///
70/// ```compile_fail
71/// use monospace_core::{Buffer, Pos, Size, Window};
72/// use monospace_diagram::Diagram;
73/// use monospace_editing::Session;
74///
75/// let session = Session::new(Diagram::new());
76/// session.diagram().draw(&mut Buffer::new(Window {
77///     origin: Pos { x: 0, y: 0 },
78///     size: Size { width: 1, height: 1 },
79/// }));
80///
81/// session.diagram_mut();
82/// ```
83#[derive(Clone)]
84pub struct Session {
85    /// The diagram this session was created over, and the one it hands out until a command has been
86    /// carried out.
87    ///
88    /// **Written once, in [`new`](Session::new), and never again** — a stronger statement than the
89    /// field being private, and it is what lets what the session hands out be read off the position
90    /// alone: this field is the whole of the answer at the oldest position, and one step is the
91    /// whole of it everywhere else. Private rather than `pub(crate)` because there is no longer
92    /// anything another module needs to do to it.
93    initial_diagram: Diagram,
94
95    /// The steps behind `position` and the steps ahead of it, in the order they were made. **Step
96    /// `j` is the diagram as it was left by command `j + 1`**, so the step behind the position is
97    /// the state the session hands out and the step ahead of it is what a command starting from
98    /// here would put behind. The state before the first command is not here, and `position == 0`
99    /// is what says so.
100    ///
101    /// Held in the same persistent sequence the diagram holds its figures in, so two sessions over
102    /// one history are sharing it rather than copying it — see [`perform`](Session::perform), which
103    /// is the one place it is written to.
104    steps: VectorSync<Step>,
105
106    /// How many of `steps` are behind the state the session hands out.
107    position: usize,
108}
109
110impl Session {
111    /// A session over `diagram`, holding it and no history behind it.
112    ///
113    /// The diagram is taken by value, so the caller has handed it over rather than lent it, and a
114    /// copy the caller kept is a diagram of its own that this session never sees. **Where it came
115    /// from is not asked and not refused**: a diagram is a value and any of them can be opened,
116    /// which §8 of the model lists as deliberately unresolved rather than as missing.
117    #[must_use]
118    pub fn new(initial_diagram: Diagram) -> Session {
119        Self {
120            initial_diagram,
121            steps: VectorSync::new_sync(),
122            position: 0,
123        }
124    }
125
126    /// The diagram this session holds, borrowed.
127    ///
128    /// **The step behind the position, and the diagram this session was created over when nothing is
129    /// behind it** — read rather than rebuilt, and whole. Nothing is copied and nothing can be
130    /// changed through it, which is what makes it the answer for a caller that repaints on every
131    /// event. A caller that wants to keep one copies it — `Diagram` is `Clone` — and the copy is a
132    /// diagram like any other: changeable, free of the session and unconnected to it.
133    #[must_use]
134    pub fn diagram(&self) -> &Diagram {
135        let behind = self.position.checked_sub(1).and_then(|i| self.steps.get(i));
136        behind.map_or(&self.initial_diagram, |step| &step.diagram)
137    }
138
139    /// Whether there is a step behind the position, and so whether [`undo`](Session::undo) would
140    /// change anything.
141    ///
142    /// **Answerable without carrying it out**, which is what lets a caller grey out a control
143    /// rather than guess — the same answer the diagram gives a reference to a figure that is not
144    /// there.
145    #[must_use]
146    pub fn can_undo(&self) -> bool {
147        self.position > 0
148    }
149
150    /// Whether there is a step ahead of the position, and so whether [`redo`](Session::redo)
151    /// would change anything.
152    #[must_use]
153    pub fn can_redo(&self) -> bool {
154        self.position < self.steps.len()
155    }
156
157    /// Gives the step behind the position back: what the session this returns holds is one step
158    /// earlier than what this one holds.
159    ///
160    /// **Nothing here is changed, and the same is true of [`redo`](Session::redo) and
161    /// [`perform`](Session::perform).** What comes back is a session standing one place earlier
162    /// over the same steps: a new position rather than a new history, every step a node this one
163    /// was already holding, and no diagram copied to produce it. **At the oldest position there is
164    /// nothing behind, and what comes back is this session unchanged** — no error, no report and no
165    /// panic, which is the answer the diagram gives a figure it does not hold and the reason
166    /// `can_undo` is asked rather than a result waited for.
167    #[must_use]
168    pub fn undo(&self) -> Self {
169        if !self.can_undo() {
170            return self.clone();
171        }
172
173        Self {
174            initial_diagram: self.initial_diagram.clone(),
175            steps: self.steps.clone(),
176            position: self.position - 1,
177        }
178    }
179
180    /// Carries the step ahead of the position out again: the diagram it holds is what the session
181    /// this returns holds again.
182    ///
183    /// **This session is not changed**, for the reason [`undo`](Session::undo) gives. **At the
184    /// newest position there is nothing ahead, and what comes back is this session unchanged** — the
185    /// same silence in the other direction, and asked about by
186    /// [`can_redo`](Session::can_redo) rather than waited for.
187    #[must_use]
188    pub fn redo(&self) -> Self {
189        if !self.can_redo() {
190            return self.clone();
191        }
192
193        Self {
194            initial_diagram: self.initial_diagram.clone(),
195            steps: self.steps.clone(),
196            position: self.position + 1,
197        }
198    }
199
200    /// Carries `command` out against the diagram this session holds, and hands back the session
201    /// standing over what it left: records the diagram the command leaves as a step, and discards
202    /// whatever was ahead of the position.
203    ///
204    /// **This session is not changed, and discarding what comes back changes nothing.** That is
205    /// what keeping a session out of reach of a `&mut` buys, and it is why this is the one method
206    /// here whose result has to be kept rather than merely read.
207    ///
208    /// **The step is the diagram the command left, and that order is what fixes what the field
209    /// holds.** The state the first command found is the one state no step holds, and it is the
210    /// field — which is why nothing writes the field again, and why the diagram in hand is either
211    /// that field or a step and never a third thing. **The other order would work too**, and it
212    /// would make the field hold the newest state rather than the first: a field whose contents
213    /// change with every command, and a newest state kept outside the history, which is what makes
214    /// the answer at the end of the history a case of its own instead of a step like any other.
215    ///
216    /// **The diagram in hand is the one the step is handed, so no diagram is copied to record it.**
217    /// `apply_to` builds the state and hands it back, and that value is both what the step holds and
218    /// what the next call to [`diagram`](Session::diagram) reads — a whole diagram on the write
219    /// path, which is what the other order would have cost, since a step written before the command
220    /// it belongs to has to be handed a copy of what that command went on to replace.
221    ///
222    /// **What the command becomes is carried out here and nowhere else.** The mapping from a
223    /// meaning to one of the diagram's seven operations is the one in [`Command`], and the session
224    /// is what carries it out because only what was asked knows whether what it was asked to do
225    /// was one gesture or several: a figure dragged across a screen is one thing to the person
226    /// dragging it and many displacements to the diagram. Nothing reads that answer yet — §8 of
227    /// the model says what is left to write — and the field it will be read from is the step's
228    /// command, which is recorded beside the diagram from this first version.
229    ///
230    /// **Nothing is inverted, and the steps are what make that unnecessary.** `Diagram::remove`
231    /// freezes every reference naming the figure it takes and states that nothing re-hangs them,
232    /// so an inverse of that command cannot be derived — it could only be reconstructed from what
233    /// the caller happened to capture beforehand. Every state the history passes through is kept
234    /// instead, and giving a change back is one of them becoming the diagram again.
235    ///
236    /// **What was ahead of the position is discarded**, which is what makes the history a line
237    /// rather than a tree. Branching earns its keep when a state somebody discarded is worth
238    /// returning to, and nothing here says one is: a caller that moves on from a state it gave back
239    /// has said the state was not worth keeping. What would reopen it is the first caller that
240    /// wants one back, and §6 of the model says so. **It is `kept_first` rather than a
241    /// `Vec::truncate`**, because the sequence holding the steps is not one that can be truncated
242    /// in place; the walk is where the cost of that choice is written down rather than paid
243    /// silently here.
244    ///
245    /// **A command that changed nothing records no step** (#285). The diagram's operations cannot
246    /// fail, so a command naming a figure it does not hold changes nothing, and neither does one
247    /// whose change gives the figure back as it was — a box at its smallest shrunk again, a line
248    /// at its shortest shrunk again, a connector resized, a figure brought forward or to the front
249    /// when it is already at the front, or backward or to the back when it is already at the back.
250    /// **What comes back is this session**: the same steps, the same position and the same diagram, so what was ahead
251    /// of the position stays ahead and a no-op discards no branch. The command knows which figure
252    /// it touched and answers whether that figure changed; nothing here compares two diagrams,
253    /// which a diagram has no way of doing.
254    #[must_use]
255    pub fn perform(&self, command: Command) -> Self {
256        let Some(made) = command.apply_to(self.diagram()) else {
257            return self.clone();
258        };
259
260        let mut steps = kept_first(&self.steps, self.position);
261        steps.push_back_mut(Step::new(command, made));
262
263        Self {
264            initial_diagram: self.initial_diagram.clone(),
265            steps,
266            position: self.position + 1,
267        }
268    }
269
270    /// Adds `shape` to the diagram this session holds, assigns it a [`ShapeId`], records the
271    /// resulting diagram as a step, and hands back the new session beside the assigned identity.
272    ///
273    /// **This session is not changed**, for the reason every method here gives: a new session is
274    /// returned holding the step ahead and the original is left intact.
275    ///
276    /// **The step records `Command::Add { shape }` and the diagram `add` produced**, so
277    /// [`undo`](Session::undo) gives the diagram back without the added figure and
278    /// [`redo`](Session::redo) restores it.
279    ///
280    /// **What was ahead of the position is discarded**, exactly as in
281    /// [`perform`](Session::perform): a branch is cut when a new figure is added from behind the
282    /// end of the history.
283    #[must_use]
284    pub fn add(&self, shape: Shape) -> (Self, ShapeId) {
285        let (made, id) = self.diagram().add(shape.clone());
286        let mut steps = kept_first(&self.steps, self.position);
287        steps.push_back_mut(Step::new(Command::Add { shape }, made));
288
289        (
290            Self {
291                initial_diagram: self.initial_diagram.clone(),
292                steps,
293                position: self.position + 1,
294            },
295            id,
296        )
297    }
298}