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::{Cx, 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        cx: &mut Cx,
52        transport: &mut T,
53        resource: &Symbol,
54        new_value: Expr,
55    ) -> Result<()> {
56        let old_value = transport.read(cx, resource)?;
57        transport.realize(cx, resource, &set_value_op(new_value.clone()))?;
58        self.past.push(LedgerEntry {
59            resource: resource.clone(),
60            forward: set_value_op(new_value),
61            inverse: set_value_op(old_value),
62        });
63        self.future.clear();
64        Ok(())
65    }
66
67    /// Undo the most recent edit by replaying its inverse operation. Returns the
68    /// resource that changed, or `None` if there is nothing to undo.
69    pub fn undo<T: Transport>(&mut self, cx: &mut Cx, transport: &mut T) -> Result<Option<Symbol>> {
70        let Some(entry) = self.past.pop() else {
71            return Ok(None);
72        };
73        transport.realize(cx, &entry.resource, &entry.inverse)?;
74        let resource = entry.resource.clone();
75        self.future.push(entry);
76        Ok(Some(resource))
77    }
78
79    /// Redo the most recently undone edit by replaying its forward operation.
80    pub fn redo<T: Transport>(&mut self, cx: &mut Cx, transport: &mut T) -> Result<Option<Symbol>> {
81        let Some(entry) = self.future.pop() else {
82            return Ok(None);
83        };
84        transport.realize(cx, &entry.resource, &entry.forward)?;
85        let resource = entry.resource.clone();
86        self.past.push(entry);
87        Ok(Some(resource))
88    }
89
90    /// Whether there is an edit to undo.
91    pub fn can_undo(&self) -> bool {
92        !self.past.is_empty()
93    }
94
95    /// Whether there is an edit to redo.
96    pub fn can_redo(&self) -> bool {
97        !self.future.is_empty()
98    }
99
100    /// The ledger as a value: the ordered list of recorded operations (the
101    /// object edit history / session event log).
102    pub fn as_value(&self) -> Expr {
103        Expr::List(
104            self.past
105                .iter()
106                .map(|entry| {
107                    Expr::Map(vec![
108                        (
109                            Expr::Symbol(Symbol::new("resource")),
110                            Expr::Symbol(entry.resource.clone()),
111                        ),
112                        (Expr::Symbol(Symbol::new("op")), entry.forward.clone()),
113                        (Expr::Symbol(Symbol::new("inverse")), entry.inverse.clone()),
114                    ])
115                })
116                .collect(),
117        )
118    }
119}
120
121/// Named snapshots of values (workspaces or objects), kept as data.
122#[derive(Default)]
123pub struct Snapshots {
124    named: BTreeMap<String, Expr>,
125    order: Vec<String>,
126}
127
128impl Snapshots {
129    /// An empty snapshot store.
130    pub fn new() -> Self {
131        Self::default()
132    }
133
134    /// Take (or replace) a named snapshot of `value`.
135    pub fn take(&mut self, name: &str, value: Expr) {
136        if !self.named.contains_key(name) {
137            self.order.push(name.to_owned());
138        }
139        self.named.insert(name.to_owned(), value);
140    }
141
142    /// Restore a named snapshot.
143    pub fn restore(&self, name: &str) -> Option<Expr> {
144        self.named.get(name).cloned()
145    }
146
147    /// The snapshot names in creation order.
148    pub fn names(&self) -> &[String] {
149        &self.order
150    }
151}
152
153/// Append-only session event log, kept as a value.
154#[derive(Default)]
155pub struct SessionLog {
156    events: Vec<Expr>,
157}
158
159impl SessionLog {
160    /// An empty log.
161    pub fn new() -> Self {
162        Self::default()
163    }
164
165    /// Append an event value.
166    pub fn append(&mut self, event: Expr) {
167        self.events.push(event);
168    }
169
170    /// The log as a value.
171    pub fn as_value(&self) -> Expr {
172        Expr::List(self.events.clone())
173    }
174
175    /// The number of logged events.
176    pub fn len(&self) -> usize {
177        self.events.len()
178    }
179
180    /// Whether the log is empty.
181    pub fn is_empty(&self) -> bool {
182        self.events.is_empty()
183    }
184}
185
186/// Attach a review comment to an object, returning the object with an appended
187/// annotation. Annotations are object-review primitives kept as data.
188pub fn annotate(object: &Expr, author: &str, comment: &str) -> Result<Expr> {
189    let Expr::Map(entries) = object else {
190        return Err(Error::HostError(
191            "annotations attach to map-shaped objects".to_owned(),
192        ));
193    };
194    let mut entries = entries.clone();
195    let annotation = Expr::Map(vec![
196        (
197            Expr::Symbol(Symbol::new("author")),
198            Expr::Symbol(Symbol::new(author)),
199        ),
200        (
201            Expr::Symbol(Symbol::new("text")),
202            Expr::String(comment.to_owned()),
203        ),
204    ]);
205    let key = Expr::Symbol(Symbol::new("annotations"));
206    if let Some(slot) = entries.iter_mut().find(|(entry_key, _)| entry_key == &key) {
207        if let Expr::List(list) = &mut slot.1 {
208            list.push(annotation);
209        } else {
210            slot.1 = Expr::List(vec![annotation]);
211        }
212    } else {
213        entries.push((key, Expr::List(vec![annotation])));
214    }
215    Ok(Expr::Map(entries))
216}