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}