Skip to main content

sim_lib_web_bridge/
history.rs

1//! History, snapshots, the session log, and review primitives -- all as values.
2//!
3//! Undo/redo is not a bespoke command stack: each edit is recorded as a forward
4//! operation and its inverse operation (the same `set-value` op pointing at the
5//! prior value), reusing event/effect ledger semantics. Undoing replays the
6//! inverse through `realize`; redoing replays the forward. Snapshots, the edit
7//! log, and annotations are plain SIM values, so prior states can be inspected
8//! and restored as data.
9
10use std::collections::BTreeMap;
11
12use sim_kernel::{Error, Expr, Result, Symbol};
13
14use crate::transport::Transport;
15
16fn set_value_op(value: Expr) -> Expr {
17    Expr::Map(vec![
18        (
19            Expr::Symbol(Symbol::new("op")),
20            Expr::Symbol(Symbol::new("set-value")),
21        ),
22        (Expr::Symbol(Symbol::new("value")), value),
23    ])
24}
25
26/// One ledger entry: the resource and the forward/inverse operations.
27#[derive(Clone, Debug)]
28struct LedgerEntry {
29    resource: Symbol,
30    forward: Expr,
31    inverse: Expr,
32}
33
34/// An undo/redo history recorded as inverse operations in a value-backed ledger.
35#[derive(Default)]
36pub struct History {
37    past: Vec<LedgerEntry>,
38    future: Vec<LedgerEntry>,
39}
40
41impl History {
42    /// An empty history.
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    /// Commit a new value for `resource` through `transport`, recording the
48    /// forward and inverse operations. Clears the redo stack.
49    pub fn commit<T: Transport>(
50        &mut self,
51        transport: &mut T,
52        resource: &Symbol,
53        new_value: Expr,
54    ) -> Result<()> {
55        let old_value = transport.read(resource)?;
56        transport.realize(resource, &set_value_op(new_value.clone()))?;
57        self.past.push(LedgerEntry {
58            resource: resource.clone(),
59            forward: set_value_op(new_value),
60            inverse: set_value_op(old_value),
61        });
62        self.future.clear();
63        Ok(())
64    }
65
66    /// Undo the most recent edit by replaying its inverse operation. Returns the
67    /// resource that changed, or `None` if there is nothing to undo.
68    pub fn undo<T: Transport>(&mut self, transport: &mut T) -> Result<Option<Symbol>> {
69        let Some(entry) = self.past.pop() else {
70            return Ok(None);
71        };
72        transport.realize(&entry.resource, &entry.inverse)?;
73        let resource = entry.resource.clone();
74        self.future.push(entry);
75        Ok(Some(resource))
76    }
77
78    /// Redo the most recently undone edit by replaying its forward operation.
79    pub fn redo<T: Transport>(&mut self, transport: &mut T) -> Result<Option<Symbol>> {
80        let Some(entry) = self.future.pop() else {
81            return Ok(None);
82        };
83        transport.realize(&entry.resource, &entry.forward)?;
84        let resource = entry.resource.clone();
85        self.past.push(entry);
86        Ok(Some(resource))
87    }
88
89    /// Whether there is an edit to undo.
90    pub fn can_undo(&self) -> bool {
91        !self.past.is_empty()
92    }
93
94    /// Whether there is an edit to redo.
95    pub fn can_redo(&self) -> bool {
96        !self.future.is_empty()
97    }
98
99    /// The ledger as a value: the ordered list of recorded operations (the
100    /// object edit history / session event log).
101    pub fn as_value(&self) -> Expr {
102        Expr::List(
103            self.past
104                .iter()
105                .map(|entry| {
106                    Expr::Map(vec![
107                        (
108                            Expr::Symbol(Symbol::new("resource")),
109                            Expr::Symbol(entry.resource.clone()),
110                        ),
111                        (Expr::Symbol(Symbol::new("op")), entry.forward.clone()),
112                        (Expr::Symbol(Symbol::new("inverse")), entry.inverse.clone()),
113                    ])
114                })
115                .collect(),
116        )
117    }
118}
119
120/// Named snapshots of values (workspaces or objects), kept as data.
121#[derive(Default)]
122pub struct Snapshots {
123    named: BTreeMap<String, Expr>,
124    order: Vec<String>,
125}
126
127impl Snapshots {
128    /// An empty snapshot store.
129    pub fn new() -> Self {
130        Self::default()
131    }
132
133    /// Take (or replace) a named snapshot of `value`.
134    pub fn take(&mut self, name: &str, value: Expr) {
135        if !self.named.contains_key(name) {
136            self.order.push(name.to_owned());
137        }
138        self.named.insert(name.to_owned(), value);
139    }
140
141    /// Restore a named snapshot.
142    pub fn restore(&self, name: &str) -> Option<Expr> {
143        self.named.get(name).cloned()
144    }
145
146    /// The snapshot names in creation order.
147    pub fn names(&self) -> &[String] {
148        &self.order
149    }
150}
151
152/// Append-only session event log, kept as a value.
153#[derive(Default)]
154pub struct SessionLog {
155    events: Vec<Expr>,
156}
157
158impl SessionLog {
159    /// An empty log.
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// Append an event value.
165    pub fn append(&mut self, event: Expr) {
166        self.events.push(event);
167    }
168
169    /// The log as a value.
170    pub fn as_value(&self) -> Expr {
171        Expr::List(self.events.clone())
172    }
173
174    /// The number of logged events.
175    pub fn len(&self) -> usize {
176        self.events.len()
177    }
178
179    /// Whether the log is empty.
180    pub fn is_empty(&self) -> bool {
181        self.events.is_empty()
182    }
183}
184
185/// Attach a review comment to an object, returning the object with an appended
186/// annotation. Annotations are object-review primitives kept as data.
187pub fn annotate(object: &Expr, author: &str, comment: &str) -> Result<Expr> {
188    let Expr::Map(entries) = object else {
189        return Err(Error::HostError(
190            "annotations attach to map-shaped objects".to_owned(),
191        ));
192    };
193    let mut entries = entries.clone();
194    let annotation = Expr::Map(vec![
195        (
196            Expr::Symbol(Symbol::new("author")),
197            Expr::Symbol(Symbol::new(author)),
198        ),
199        (
200            Expr::Symbol(Symbol::new("text")),
201            Expr::String(comment.to_owned()),
202        ),
203    ]);
204    let key = Expr::Symbol(Symbol::new("annotations"));
205    if let Some(slot) = entries.iter_mut().find(|(entry_key, _)| entry_key == &key) {
206        if let Expr::List(list) = &mut slot.1 {
207            list.push(annotation);
208        } else {
209            slot.1 = Expr::List(vec![annotation]);
210        }
211    } else {
212        entries.push((key, Expr::List(vec![annotation])));
213    }
214    Ok(Expr::Map(entries))
215}