1use crate::{
6 commit::Commit,
7 document::{Document, FoldError},
8 rev::Rev,
9 trail::{Direction, Entry, JournalAs, Trail},
10};
11
12#[derive(Default)]
23pub struct Repo {
24 document: Document,
25 log: Vec<Commit>,
26}
27
28impl Repo {
29 pub fn at(document: Document) -> Self {
34 Self {
35 document,
36 log: Vec::new(),
37 }
38 }
39
40 pub fn folding(commits: &[Commit]) -> Result<Self, FoldError> {
46 let mut repo = Self::default();
47 for commit in commits {
48 repo.fold_one(commit.clone())?;
49 }
50 Ok(repo)
51 }
52
53 pub fn fold_one(&mut self, commit: Commit) -> Result<&Document, FoldError> {
65 self.document = self.document.try_apply(&commit)?;
66 self.log.push(commit);
67 Ok(&self.document)
68 }
69
70 pub fn document(&self) -> &Document {
72 &self.document
73 }
74
75 pub fn rev(&self) -> Rev {
76 self.document.rev()
77 }
78
79 pub fn log(&self) -> &[Commit] {
83 &self.log
84 }
85
86 pub fn revs_after(&self, rev: Rev) -> Vec<Rev> {
92 let taken = self.rev().get().saturating_sub(rev.get()) as usize;
93 std::iter::successors(Some(rev.next()), |rev| Some(rev.next()))
94 .take(taken)
95 .collect()
96 }
97
98 pub fn submit(&mut self, commit: Commit, trail: &mut Trail) -> Result<Rev, FoldError> {
109 let rev = self.fold_one(commit)?.rev();
110 trail.record(rev, JournalAs::Edit);
111 Ok(rev)
112 }
113
114 pub fn restore(&mut self, trail: &mut Trail, step: Restoring<'_>) -> Rev {
130 let Restoring {
131 entry,
132 direction,
133 target,
134 label,
135 } = step;
136 let rev = self.rev().next();
137 let mut adopted = target;
138 adopted.restored_at(rev, &self.document);
139 self.document = adopted;
140 self.log.push(Commit::new(
141 format!("{} {label}", direction.verb()),
142 Vec::new(),
143 ));
144 trail.record(rev, direction.journal(entry.rev));
145 rev
146 }
147}
148
149pub struct Restoring<'a> {
153 pub entry: Entry,
154 pub direction: Direction,
155 pub target: Document,
159 pub label: &'a str,
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::fixtures::{block_id, commit, pin_id, projection, rev};
166 use crate::opcode::{Crud, OpCodes};
167 use crate::trail::UndoRefusal;
168 use crate::{
169 block_model::{Block, BlockUpdate, Icon, Label, Pin},
170 geometry::{FracVal, GridPoint, GridRect, GridSize, PinSlot},
171 id::BlockId,
172 values::{LabelSide, PinDir, Role},
173 };
174 use std::collections::BTreeMap;
175
176 fn rect(x: i32, y: i32) -> GridRect {
177 GridRect {
178 top_left: GridPoint { x, y },
179 size: GridSize { w: 4, h: 4 },
180 }
181 }
182
183 fn label_init(name: String) -> Label {
184 Label {
185 name,
186 side: LabelSide::default(),
187 offset: FracVal::default(),
188 hidden: false,
189 }
190 }
191
192 fn block_create(n: u32) -> OpCodes {
193 OpCodes::Block(
194 block_id(n),
195 Crud::Create(Block {
196 parent: BlockId::NULL,
197 rect: rect(0, 0),
198 locked: false,
199 role: Role::default(),
200 title: label_init(format!("b{n}")),
201 type_label: label_init(String::new()),
202 icon: Icon::default(),
203 }),
204 )
205 }
206
207 fn resize(n: u32, to: GridRect) -> OpCodes {
208 OpCodes::Block(block_id(n), Crud::Update(BlockUpdate::Rect(to)))
209 }
210
211 fn orphan_pin(n: u32) -> OpCodes {
214 OpCodes::Pin(
215 pin_id(n),
216 Crud::Create(Pin {
217 owner: block_id(200),
218 name: format!("p{n}"),
219 type_name: String::new(),
220 tag: String::new(),
221 tag_hidden: false,
222 rect: rect(0, 0),
223 slot: PinSlot::default(),
224 dir: PinDir::default(),
225 port_accent: Role::default(),
226 flip_lr: false,
227 }),
228 )
229 }
230
231 fn one_block() -> Session {
234 let mut live = Session::default();
235 live.edit("Added a block", vec![block_create(1)]);
236 live
237 }
238
239 #[test]
243 fn submit_assigns_contiguous_revs() {
244 let mut live = Session::default();
245 assert_eq!(
246 live.repo.rev(),
247 Rev::ZERO,
248 "an empty document holds no position",
249 );
250
251 let assigned: Vec<Rev> = (1..=3)
252 .map(|n| live.edit("Added a block", vec![block_create(n)]))
253 .collect();
254 let repo = &live.repo;
255
256 assert_eq!(
257 assigned.iter().map(|rev| rev.get()).collect::<Vec<_>>(),
258 [1, 2, 3],
259 );
260 assert_eq!(repo.rev(), assigned[2], "the head is the last assignment");
261 assert_eq!(repo.log().len(), 3);
262 assert_eq!(repo.revs_after(Rev::ZERO), assigned);
263 assert_eq!(repo.revs_after(assigned[1]), [assigned[2]]);
264 assert!(repo.revs_after(repo.rev()).is_empty(), "caught up");
265 assert!(
266 repo.revs_after(repo.rev().next()).is_empty(),
267 "and a rev beyond the head does not panic",
268 );
269 }
270
271 #[test]
272 fn a_commit_the_fold_refuses_takes_no_rev_and_trails_nothing() {
273 let mut live = Session::default();
274 let before = live.repo.document().clone();
275
276 assert!(
277 live.repo
278 .submit(commit("Orphan pin", vec![orphan_pin(3)]), &mut live.trail)
279 .is_err(),
280 );
281 assert_eq!(live.repo.rev(), Rev::ZERO, "a refusal mints no rev");
282 assert!(live.repo.log().is_empty(), "and logs nothing");
283 assert!(!live.trail.can_undo(), "and stands on nothing");
284 assert_eq!(live.repo.document(), &before, "the document is untouched");
285
286 let next = live.edit("Added a block", vec![block_create(1)]);
287 assert_eq!(
288 next,
289 Rev::ZERO.next(),
290 "the next commit takes the rev the refusal did not consume",
291 );
292 }
293
294 #[test]
297 fn a_step_naming_anything_but_the_top_is_refused() {
298 let mut live = one_block();
299 let first = live.trail.next_undo().expect("the create stands");
300 let second = live.edit("Moved it", vec![resize(1, rect(5, 5))]);
301 assert_eq!(
302 live.trail.undo_revs(),
303 [first, second],
304 "precondition: the named edit sits below the top",
305 );
306
307 assert_eq!(
308 live.stepping(first, Direction::Undo),
309 Err(UndoRefusal::Stale { top: second }),
310 );
311 assert_eq!(live.trail.undo_depth(), 2, "and the refusal took no step");
312 assert!(
313 live.stepping(second, Direction::Undo).is_ok(),
314 "the step naming the top is taken",
315 );
316 }
317
318 #[test]
320 fn a_step_on_a_spent_stack_is_refused() {
321 let mut live = one_block();
322 let only = live.trail.next_undo().expect("the setup edit stands");
323 live.undo();
324 assert_eq!(live.trail.next_undo(), None);
325 assert_eq!(
326 live.stepping(only, Direction::Undo),
327 Err(UndoRefusal::Spent),
328 );
329 }
330
331 #[test]
335 fn undo_and_redo_travel_as_ordinary_commits_and_round_trip_the_document() {
336 let mut live = one_block();
337 let placed = live.repo.document().clone();
338 let edit = live.edit("Moved it", vec![resize(1, rect(5, 5))]);
339 let moved = live.repo.document().clone();
340 assert_ne!(placed, moved, "the edit must be observable");
341
342 let undone = live.undo();
343 assert_eq!(undone, edit.next(), "the step took the next rev");
344 assert_eq!(
345 live.repo.document(),
346 &placed,
347 "undo restored the document the create left",
348 );
349 assert_eq!(
350 live.repo.log().last().map(Commit::label),
351 Some("Undo Moved it"),
352 "a step's commit is its label and nothing else",
353 );
354 assert!(
355 live.repo
356 .log()
357 .last()
358 .is_some_and(|step| step.ops().is_empty()),
359 "a step carries no ops: the rev copy is what it adopts",
360 );
361 assert_eq!(
362 live.trail.standing(),
363 edit.prev().expect("an edit before it")
364 );
365 assert!(live.trail.can_redo());
366
367 let redone = live.redo();
368 assert_eq!(redone, undone.next());
369 assert_eq!(
370 live.repo.document(),
371 &moved,
372 "and a round trip lands on the value the edit produced",
373 );
374 assert_eq!(
375 live.trail.standing(),
376 edit,
377 "and the trail stands where it started",
378 );
379 assert!(live.trail.can_undo());
380 assert!(!live.trail.can_redo(), "the redo future is spent");
381 }
382
383 #[test]
384 fn a_fresh_edit_abandons_the_redo_future() {
385 let mut live = one_block();
386 live.edit("Moved it", vec![resize(1, rect(5, 5))]);
387 live.undo();
388 assert!(live.trail.can_redo());
389
390 live.edit("Moved it elsewhere", vec![resize(1, rect(7, 7))]);
391 assert!(!live.trail.can_redo(), "a new edit forks the history");
392 }
393
394 #[test]
395 fn folding_a_log_reproduces_the_document_that_wrote_it() {
396 let mut live = Session::default();
397 for n in 1..=3 {
398 live.edit("Added a block", vec![block_create(n)]);
399 }
400
401 let replayed = Repo::folding(live.repo.log()).expect("the log folds");
402 assert_eq!(replayed.rev(), live.repo.rev());
403 assert_eq!(
404 replayed.document(),
405 live.repo.document(),
406 "a replayed log reproduces the document exactly",
407 );
408 }
409
410 type Projection = Vec<(BlockId, BlockId, GridRect, String)>;
413
414 #[derive(Default)]
422 struct Session {
423 repo: Repo,
424 trail: Trail,
425 kinds: Vec<JournalAs>,
426 revs: BTreeMap<Rev, Document>,
427 labels: BTreeMap<Rev, String>,
428 }
429
430 impl Session {
431 fn edit(&mut self, label: &str, ops: Vec<OpCodes>) -> Rev {
432 let rev = self
433 .repo
434 .submit(commit(label, ops), &mut self.trail)
435 .expect("the edit folds");
436 self.took(rev, JournalAs::Edit, label.to_owned());
437 rev
438 }
439
440 fn took(&mut self, rev: Rev, kind: JournalAs, label: String) {
441 self.kinds.push(kind);
442 self.revs.insert(rev, self.repo.document().clone());
443 self.labels.insert(rev, label);
444 }
445
446 fn at(&self, rev: Rev) -> Document {
447 self.revs.get(&rev).cloned().unwrap_or_default()
448 }
449
450 fn stepping(&mut self, edit: Rev, direction: Direction) -> Result<Rev, UndoRefusal> {
451 let entry = self.trail.stepping(edit, direction)?;
452 let target = self.at(entry.restores);
453 let label = self.labels[&edit].clone();
454 let rev = self.repo.restore(
455 &mut self.trail,
456 Restoring {
457 entry,
458 direction,
459 target,
460 label: &label,
461 },
462 );
463 self.took(
464 rev,
465 direction.journal(edit),
466 format!("{} {label}", direction.verb()),
467 );
468 Ok(rev)
469 }
470
471 fn undo(&mut self) -> Rev {
472 let of = self.trail.next_undo().expect("a step to take back");
473 self.stepping(of, Direction::Undo)
474 .expect("the top names itself")
475 }
476
477 fn redo(&mut self) -> Rev {
478 let of = self.trail.next_redo().expect("a step to put back");
479 self.stepping(of, Direction::Redo)
480 .expect("the top names itself")
481 }
482
483 fn reopened(&self) -> Session {
487 let mut reopened = Session::default();
488 for (nth, kind) in self.kinds.iter().enumerate() {
489 let at = rev(nth as u64 + 1);
490 reopened.trail.record(at, *kind);
491 reopened.kinds.push(*kind);
492 reopened.revs.insert(at, self.at(at));
493 reopened.labels.insert(at, self.labels[&at].clone());
494 }
495 reopened.repo = Repo::at(self.at(self.repo.rev()));
496 reopened
497 }
498 }
499
500 #[test]
503 fn a_replayed_log_rebuilds_the_trail_its_session_had() {
504 let mut live = Session::default();
505 live.edit("Added a block", vec![block_create(1)]);
506 live.edit("Moved it", vec![resize(1, rect(5, 5))]);
507 live.edit("Added another", vec![block_create(2)]);
508 live.undo();
509 live.undo();
510 live.redo();
511 assert_eq!(
512 (live.trail.undo_depth(), live.trail.redo_depth()),
513 (2, 1),
514 "precondition: the session closes with depth both ways",
515 );
516
517 let reopened = live.reopened();
518 assert_eq!(
519 reopened.trail, live.trail,
520 "the reopened trail is the one the session closed with",
521 );
522 assert_eq!(reopened.repo.document(), live.repo.document(),);
523 }
524
525 #[test]
529 fn walking_a_reopened_trail_moves_the_document_as_the_session_would_have() {
530 let mut live = Session::default();
531 live.edit("Added a block", vec![block_create(1)]);
532 live.edit("Moved it", vec![resize(1, rect(5, 5))]);
533 live.edit("Added another", vec![block_create(2)]);
534 live.undo();
535
536 let mut reopened = live.reopened();
537 let drained = drain(&mut live);
538 assert!(
539 drained.windows(2).any(|pair| pair[0] != pair[1]),
540 "the walk must move the document or it proves nothing",
541 );
542 assert_eq!(drain(&mut reopened), drained);
543 }
544
545 fn drain(live: &mut Session) -> Vec<Projection> {
550 let mut seen = vec![projection(live.repo.document())];
551 while live.trail.can_undo() {
552 live.undo();
553 seen.push(projection(live.repo.document()));
554 }
555 while live.trail.can_redo() {
556 live.redo();
557 seen.push(projection(live.repo.document()));
558 }
559 seen
560 }
561
562 #[test]
565 fn a_reopened_undo_can_still_be_redone() {
566 let mut live = Session::default();
567 live.edit("Added a block", vec![block_create(1)]);
568 live.edit("Moved it", vec![resize(1, rect(5, 5))]);
569 let moved = projection(live.repo.document());
570 live.undo();
571
572 let mut reopened = live.reopened();
573 assert!(
574 reopened.trail.can_redo(),
575 "the reopened document lost its future",
576 );
577 reopened.redo();
578 assert_eq!(
579 projection(reopened.repo.document()),
580 moved,
581 "redoing after a restart did not restore what the undo took back",
582 );
583 }
584
585 #[test]
588 fn a_reopened_edit_after_an_undo_has_no_redo_future() {
589 let mut live = Session::default();
590 live.edit("Added a block", vec![block_create(1)]);
591 live.edit("Moved it", vec![resize(1, rect(5, 5))]);
592 live.undo();
593 live.edit("Moved it elsewhere", vec![resize(1, rect(7, 7))]);
594 assert!(!live.trail.can_redo(), "precondition: the fork spent it");
595
596 assert!(
597 !live.reopened().trail.can_redo(),
598 "the reconstructed trail offers a redo the session had abandoned",
599 );
600 }
601
602 #[test]
607 fn a_seeded_past_replays_as_edits_and_is_undoable() {
608 let mut seeded = Repo::default();
609 for n in 1..=3 {
610 seeded
611 .fold_one(commit("Added a block", vec![block_create(n)]))
612 .expect("the seed folds");
613 }
614 let mut reopened = Session::default();
615 for past in seeded.log() {
616 let rev = reopened
617 .repo
618 .fold_one(past.clone())
619 .expect("the log folds")
620 .rev();
621 reopened.trail.record(rev, JournalAs::Edit);
622 }
623 assert_eq!(reopened.trail.undo_revs(), [rev(1), rev(2), rev(3)]);
624 assert_eq!(reopened.repo.document(), seeded.document(),);
625 }
626}