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}