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    /// The figure the step behind the position touched, or `None` when there is nothing behind.
158    ///
159    /// **Answerable without walking, like [`can_undo`](Session::can_undo)**, which is why the walk
160    /// keeps its signature (§4 of the application model asks before it gives the step back). A
161    /// caller that has to know where to leave the selection asks this first and keeps the answer
162    /// across the one call that moves the position.
163    ///
164    /// **The answer is the step's and not the command's.** For every command that names a figure it
165    /// is that `id`, and for `Command::Add` it is the identity the diagram assigned — the one case
166    /// the command alone cannot name, which is why the step records it (§5 of the editing model).
167    #[must_use]
168    pub fn affected_by_undo(&self) -> Option<ShapeId> {
169        self.position
170            .checked_sub(1)
171            .and_then(|index| self.steps.get(index))
172            .map(|step| step.affected)
173    }
174
175    /// The figure the step ahead of the position touched, or `None` when there is nothing ahead.
176    ///
177    /// **The other direction of [`affected_by_undo`](Session::affected_by_undo)**, asked the same
178    /// way and recorded the same way.
179    #[must_use]
180    pub fn affected_by_redo(&self) -> Option<ShapeId> {
181        self.steps.get(self.position).map(|step| step.affected)
182    }
183
184    /// Gives the step behind the position back: what the session this returns holds is one step
185    /// earlier than what this one holds.
186    ///
187    /// **Nothing here is changed, and the same is true of [`redo`](Session::redo) and
188    /// [`perform`](Session::perform).** What comes back is a session standing one place earlier
189    /// over the same steps: a new position rather than a new history, every step a node this one
190    /// was already holding, and no diagram copied to produce it. **At the oldest position there is
191    /// nothing behind, and what comes back is this session unchanged** — no error, no report and no
192    /// panic, which is the answer the diagram gives a figure it does not hold and the reason
193    /// `can_undo` is asked rather than a result waited for.
194    #[must_use]
195    pub fn undo(&self) -> Self {
196        if !self.can_undo() {
197            return self.clone();
198        }
199
200        Self {
201            initial_diagram: self.initial_diagram.clone(),
202            steps: self.steps.clone(),
203            position: self.position - 1,
204        }
205    }
206
207    /// Carries the step ahead of the position out again: the diagram it holds is what the session
208    /// this returns holds again.
209    ///
210    /// **This session is not changed**, for the reason [`undo`](Session::undo) gives. **At the
211    /// newest position there is nothing ahead, and what comes back is this session unchanged** — the
212    /// same silence in the other direction, and asked about by
213    /// [`can_redo`](Session::can_redo) rather than waited for.
214    #[must_use]
215    pub fn redo(&self) -> Self {
216        if !self.can_redo() {
217            return self.clone();
218        }
219
220        Self {
221            initial_diagram: self.initial_diagram.clone(),
222            steps: self.steps.clone(),
223            position: self.position + 1,
224        }
225    }
226
227    /// Carries `command` out against the diagram this session holds, and hands back the session
228    /// standing over what it left: records the diagram the command leaves as a step, and discards
229    /// whatever was ahead of the position.
230    ///
231    /// **This session is not changed, and discarding what comes back changes nothing.** That is
232    /// what keeping a session out of reach of a `&mut` buys, and it is why this is the one method
233    /// here whose result has to be kept rather than merely read.
234    ///
235    /// **The step is the diagram the command left, and that order is what fixes what the field
236    /// holds.** The state the first command found is the one state no step holds, and it is the
237    /// field — which is why nothing writes the field again, and why the diagram in hand is either
238    /// that field or a step and never a third thing. **The other order would work too**, and it
239    /// would make the field hold the newest state rather than the first: a field whose contents
240    /// change with every command, and a newest state kept outside the history, which is what makes
241    /// the answer at the end of the history a case of its own instead of a step like any other.
242    ///
243    /// **The diagram in hand is the one the step is handed, so no diagram is copied to record it.**
244    /// `apply_to` builds the state and hands it back, and that value is both what the step holds and
245    /// what the next call to [`diagram`](Session::diagram) reads — a whole diagram on the write
246    /// path, which is what the other order would have cost, since a step written before the command
247    /// it belongs to has to be handed a copy of what that command went on to replace.
248    ///
249    /// **What the command becomes is carried out here and nowhere else.** The mapping from a
250    /// meaning to one of the diagram's seven operations is the one in [`Command`], and the session
251    /// is what carries it out because only what was asked knows whether what it was asked to do
252    /// was one gesture or several: a figure dragged across a screen is one thing to the person
253    /// dragging it and many displacements to the diagram. Nothing reads that answer yet — §8 of
254    /// the model says what is left to write — and the field it will be read from is the step's
255    /// command, which is recorded beside the diagram from this first version.
256    ///
257    /// **Nothing is inverted, and the steps are what make that unnecessary.** `Diagram::remove`
258    /// freezes every reference naming the figure it takes and states that nothing re-hangs them,
259    /// so an inverse of that command cannot be derived — it could only be reconstructed from what
260    /// the caller happened to capture beforehand. Every state the history passes through is kept
261    /// instead, and giving a change back is one of them becoming the diagram again.
262    ///
263    /// **What was ahead of the position is discarded**, which is what makes the history a line
264    /// rather than a tree. Branching earns its keep when a state somebody discarded is worth
265    /// returning to, and nothing here says one is: a caller that moves on from a state it gave back
266    /// has said the state was not worth keeping. What would reopen it is the first caller that
267    /// wants one back, and §6 of the model says so. **It is `kept_first` rather than a
268    /// `Vec::truncate`**, because the sequence holding the steps is not one that can be truncated
269    /// in place; the walk is where the cost of that choice is written down rather than paid
270    /// silently here.
271    ///
272    /// **A command that changed nothing records no step** (#285). The diagram's operations cannot
273    /// fail, so a command naming a figure it does not hold changes nothing, and neither does one
274    /// whose change gives the figure back as it was — a box at its smallest shrunk again, a line
275    /// at its shortest shrunk again, a connector resized, a figure brought forward or to the front
276    /// when it is already at the front, or backward or to the back when it is already at the back.
277    /// **What comes back is this session**: the same steps, the same position and the same diagram, so what was ahead
278    /// of the position stays ahead and a no-op discards no branch. The command knows which figure
279    /// it touched and answers whether that figure changed; nothing here compares two diagrams,
280    /// which a diagram has no way of doing.
281    #[must_use]
282    pub fn perform(&self, command: Command) -> Self {
283        let Some((made, affected)) = command.apply_to(self.diagram()) else {
284            return self.clone();
285        };
286
287        let mut steps = kept_first(&self.steps, self.position);
288        steps.push_back_mut(Step::new(command, affected, made));
289
290        Self {
291            initial_diagram: self.initial_diagram.clone(),
292            steps,
293            position: self.position + 1,
294        }
295    }
296
297    /// Adds `shape` to the diagram this session holds, assigns it a [`ShapeId`], records the
298    /// resulting diagram as a step, and hands back the new session beside the assigned identity.
299    ///
300    /// **This session is not changed**, for the reason every method here gives: a new session is
301    /// returned holding the step ahead and the original is left intact.
302    ///
303    /// **The step records `Command::Add { shape }` and the diagram `add` produced**, so
304    /// [`undo`](Session::undo) gives the diagram back without the added figure and
305    /// [`redo`](Session::redo) restores it.
306    ///
307    /// **What was ahead of the position is discarded**, exactly as in
308    /// [`perform`](Session::perform): a branch is cut when a new figure is added from behind the
309    /// end of the history.
310    #[must_use]
311    pub fn add(&self, shape: Shape) -> (Self, ShapeId) {
312        let (made, id) = self.diagram().add(shape.clone());
313        let mut steps = kept_first(&self.steps, self.position);
314        steps.push_back_mut(Step::new(Command::Add { shape }, id, made));
315
316        (
317            Self {
318                initial_diagram: self.initial_diagram.clone(),
319                steps,
320                position: self.position + 1,
321            },
322            id,
323        )
324    }
325}