Skip to main content

formualizer_eval/engine/graph/editor/
change_log.rs

1//! Standalone change logging infrastructure for tracking graph mutations
2//!
3//! This module provides:
4//! - ChangeLog: Audit trail of all graph changes
5//! - ChangeEvent: Granular representation of individual changes
6//! - ChangeLogger: Trait for pluggable logging strategies
7
8use crate::SheetId;
9use crate::engine::named_range::{NameScope, NamedDefinition};
10use crate::engine::vertex::VertexId;
11use crate::reference::CellRef;
12use formualizer_common::Coord as AbsCoord;
13use formualizer_common::LiteralValue;
14use formualizer_parse::parser::ASTNode;
15
16/// Represents a single change to the dependency graph
17#[derive(Debug, Clone, PartialEq)]
18pub enum ChangeEvent {
19    // Simple events
20    SetValue {
21        addr: CellRef,
22        old: Option<LiteralValue>,
23        new: LiteralValue,
24    },
25    SetFormula {
26        addr: CellRef,
27        old: Option<ASTNode>,
28        new: ASTNode,
29    },
30    /// Vertex creation snapshot (for undo). Minimal for now.
31    AddVertex {
32        id: VertexId,
33        coord: AbsCoord,
34        sheet_id: SheetId,
35        value: Option<LiteralValue>,
36        formula: Option<ASTNode>,
37        kind: Option<crate::engine::vertex::VertexKind>,
38        flags: Option<u8>,
39    },
40    RemoveVertex {
41        id: VertexId,
42        // Need to capture more for rollback!
43        old_value: Option<LiteralValue>,
44        old_formula: Option<ASTNode>,
45        old_dependencies: Vec<VertexId>, // outgoing
46        old_dependents: Vec<VertexId>,   // incoming
47        coord: Option<AbsCoord>,
48        sheet_id: Option<SheetId>,
49        kind: Option<crate::engine::vertex::VertexKind>,
50        flags: Option<u8>,
51    },
52
53    // Compound operation markers
54    CompoundStart {
55        description: String, // e.g., "InsertRows(sheet=0, before=5, count=2)"
56        depth: usize,
57    },
58    CompoundEnd {
59        depth: usize,
60    },
61
62    // Granular events for compound operations
63    VertexMoved {
64        id: VertexId,
65        old_coord: AbsCoord,
66        new_coord: AbsCoord,
67    },
68    FormulaAdjusted {
69        id: VertexId,
70        old_ast: ASTNode,
71        new_ast: ASTNode,
72    },
73    NamedRangeAdjusted {
74        name: String,
75        scope: NameScope,
76        old_definition: NamedDefinition,
77        new_definition: NamedDefinition,
78    },
79    EdgeAdded {
80        from: VertexId,
81        to: VertexId,
82    },
83    EdgeRemoved {
84        from: VertexId,
85        to: VertexId,
86    },
87
88    // Named range operations
89    DefineName {
90        name: String,
91        scope: NameScope,
92        definition: NamedDefinition,
93    },
94    UpdateName {
95        name: String,
96        scope: NameScope,
97        old_definition: NamedDefinition,
98        new_definition: NamedDefinition,
99    },
100    DeleteName {
101        name: String,
102        scope: NameScope,
103        old_definition: Option<NamedDefinition>,
104    },
105}
106
107/// Audit trail for tracking all changes to the dependency graph
108#[derive(Debug, Default)]
109pub struct ChangeLog {
110    events: Vec<ChangeEvent>,
111    enabled: bool,
112    /// Track compound operations for atomic rollback
113    compound_depth: usize,
114    /// Monotonic sequence number per event
115    seqs: Vec<u64>,
116    /// Optional group id (compound) per event
117    groups: Vec<Option<u64>>,
118    next_seq: u64,
119    /// Stack of active group ids for nested compounds
120    group_stack: Vec<u64>,
121    next_group_id: u64,
122}
123
124impl ChangeLog {
125    pub fn new() -> Self {
126        Self {
127            events: Vec::new(),
128            enabled: true,
129            compound_depth: 0,
130            seqs: Vec::new(),
131            groups: Vec::new(),
132            next_seq: 0,
133            group_stack: Vec::new(),
134            next_group_id: 1,
135        }
136    }
137
138    pub fn record(&mut self, event: ChangeEvent) {
139        if self.enabled {
140            let seq = self.next_seq;
141            self.next_seq += 1;
142            let current_group = self.group_stack.last().copied();
143            self.events.push(event);
144            self.seqs.push(seq);
145            self.groups.push(current_group);
146        }
147    }
148
149    /// Begin a compound operation (multiple changes from single action)
150    pub fn begin_compound(&mut self, description: String) {
151        self.compound_depth += 1;
152        if self.compound_depth == 1 {
153            // allocate new group id
154            let gid = self.next_group_id;
155            self.next_group_id += 1;
156            self.group_stack.push(gid);
157        } else {
158            // nested: reuse top id
159            if let Some(&gid) = self.group_stack.last() {
160                self.group_stack.push(gid);
161            }
162        }
163        if self.enabled {
164            self.record(ChangeEvent::CompoundStart {
165                description,
166                depth: self.compound_depth,
167            });
168        }
169    }
170
171    /// End a compound operation
172    pub fn end_compound(&mut self) {
173        if self.compound_depth > 0 {
174            if self.enabled {
175                self.record(ChangeEvent::CompoundEnd {
176                    depth: self.compound_depth,
177                });
178            }
179            self.compound_depth -= 1;
180            self.group_stack.pop();
181        }
182    }
183
184    pub fn events(&self) -> &[ChangeEvent] {
185        &self.events
186    }
187
188    /// Truncate log (and metadata) to len
189    pub fn truncate(&mut self, len: usize) {
190        self.events.truncate(len);
191        self.seqs.truncate(len);
192        self.groups.truncate(len);
193    }
194
195    pub fn clear(&mut self) {
196        self.events.clear();
197        self.seqs.clear();
198        self.groups.clear();
199        self.compound_depth = 0;
200        self.group_stack.clear();
201    }
202
203    pub fn len(&self) -> usize {
204        self.events.len()
205    }
206
207    pub fn is_empty(&self) -> bool {
208        self.events.is_empty()
209    }
210
211    /// Extract events from index to end
212    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
213        self.events.split_off(index)
214    }
215
216    /// Temporarily disable logging (for rollback operations)
217    pub fn set_enabled(&mut self, enabled: bool) {
218        self.enabled = enabled;
219    }
220
221    /// Get current compound depth (for testing)
222    pub fn compound_depth(&self) -> usize {
223        self.compound_depth
224    }
225
226    /// Return (sequence_number, group_id) metadata for event index
227    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
228        self.seqs
229            .get(index)
230            .copied()
231            .zip(self.groups.get(index).copied())
232    }
233
234    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
235    pub fn last_group_indices(&self) -> Vec<usize> {
236        if let Some(&last_gid) = self.groups.iter().rev().flatten().next() {
237            let idxs: Vec<usize> = self
238                .groups
239                .iter()
240                .enumerate()
241                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
242                .collect();
243            if !idxs.is_empty() {
244                return idxs;
245            }
246        }
247        self.events.len().checked_sub(1).into_iter().collect()
248    }
249}
250
251/// Trait for pluggable logging strategies
252pub trait ChangeLogger {
253    fn record(&mut self, event: ChangeEvent);
254    fn set_enabled(&mut self, enabled: bool);
255    fn begin_compound(&mut self, description: String);
256    fn end_compound(&mut self);
257}
258
259impl ChangeLogger for ChangeLog {
260    fn record(&mut self, event: ChangeEvent) {
261        ChangeLog::record(self, event);
262    }
263
264    fn set_enabled(&mut self, enabled: bool) {
265        self.enabled = enabled;
266    }
267
268    fn begin_compound(&mut self, description: String) {
269        ChangeLog::begin_compound(self, description);
270    }
271
272    fn end_compound(&mut self) {
273        ChangeLog::end_compound(self);
274    }
275}
276
277/// Null logger for when change tracking not needed
278pub struct NullChangeLogger;
279
280impl ChangeLogger for NullChangeLogger {
281    fn record(&mut self, _: ChangeEvent) {}
282    fn set_enabled(&mut self, _: bool) {}
283    fn begin_compound(&mut self, _: String) {}
284    fn end_compound(&mut self) {}
285}