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::row_visibility::RowVisibilitySource;
11use crate::engine::vertex::VertexId;
12use crate::reference::CellRef;
13use formualizer_common::Coord as AbsCoord;
14use formualizer_common::LiteralValue;
15use formualizer_parse::parser::ASTNode;
16
17#[derive(Debug, Clone, PartialEq)]
18pub struct SpillSnapshot {
19    /// Declared target cells (row-major rectangle) owned by this spill anchor.
20    pub target_cells: Vec<CellRef>,
21    /// Row-major rectangular values corresponding to the target rectangle.
22    pub values: Vec<Vec<LiteralValue>>,
23}
24
25/// Per-event metadata attached by the caller.
26///
27/// This is intentionally lightweight (Strings) to avoid leaking application types
28/// into the engine layer.
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
30pub struct ChangeEventMeta {
31    pub actor_id: Option<String>,
32    pub correlation_id: Option<String>,
33    pub reason: Option<String>,
34}
35
36/// Represents a single change to the dependency graph
37#[derive(Debug, Clone, PartialEq)]
38pub enum ChangeEvent {
39    // Simple events
40    SetValue {
41        addr: CellRef,
42        old_value: Option<LiteralValue>,
43        old_formula: Option<ASTNode>,
44        new: LiteralValue,
45    },
46    SetFormula {
47        addr: CellRef,
48        old_value: Option<LiteralValue>,
49        old_formula: Option<ASTNode>,
50        new: ASTNode,
51    },
52    SetRowVisibility {
53        sheet_id: SheetId,
54        row0: u32,
55        source: RowVisibilitySource,
56        old_hidden: bool,
57        new_hidden: bool,
58    },
59    /// Vertex creation snapshot (for undo). Minimal for now.
60    AddVertex {
61        id: VertexId,
62        coord: AbsCoord,
63        sheet_id: SheetId,
64        value: Option<LiteralValue>,
65        formula: Option<ASTNode>,
66        kind: Option<crate::engine::vertex::VertexKind>,
67        flags: Option<u8>,
68    },
69    RemoveVertex {
70        id: VertexId,
71        // Need to capture more for rollback!
72        old_value: Option<LiteralValue>,
73        old_formula: Option<ASTNode>,
74        old_dependencies: Vec<VertexId>, // outgoing
75        old_dependents: Vec<VertexId>,   // incoming
76        coord: Option<AbsCoord>,
77        sheet_id: Option<SheetId>,
78        kind: Option<crate::engine::vertex::VertexKind>,
79        flags: Option<u8>,
80    },
81
82    // Compound operation markers
83    CompoundStart {
84        description: String, // e.g., "InsertRows(sheet=0, before=5, count=2)"
85        depth: usize,
86    },
87    CompoundEnd {
88        depth: usize,
89    },
90
91    // Granular events for compound operations
92    VertexMoved {
93        id: VertexId,
94        sheet_id: SheetId,
95        old_coord: AbsCoord,
96        new_coord: AbsCoord,
97    },
98    FormulaAdjusted {
99        id: VertexId,
100        /// Cell address for replay. May be None for non-cell formula vertices.
101        addr: Option<CellRef>,
102        old_ast: ASTNode,
103        new_ast: ASTNode,
104    },
105    NamedRangeAdjusted {
106        name: String,
107        scope: NameScope,
108        old_definition: NamedDefinition,
109        new_definition: NamedDefinition,
110    },
111    EdgeAdded {
112        from: VertexId,
113        to: VertexId,
114    },
115    EdgeRemoved {
116        from: VertexId,
117        to: VertexId,
118    },
119
120    // Named range operations
121    DefineName {
122        name: String,
123        scope: NameScope,
124        definition: NamedDefinition,
125    },
126    UpdateName {
127        name: String,
128        scope: NameScope,
129        old_definition: NamedDefinition,
130        new_definition: NamedDefinition,
131    },
132    DeleteName {
133        name: String,
134        scope: NameScope,
135        old_definition: Option<NamedDefinition>,
136    },
137
138    // Spill region changes (dynamic arrays)
139    SpillCommitted {
140        anchor: VertexId,
141        old: Option<SpillSnapshot>,
142        new: SpillSnapshot,
143    },
144    SpillCleared {
145        anchor: VertexId,
146        old: SpillSnapshot,
147    },
148    /// Workbook-level per-cell staged formula delta used to keep deferred edits
149    /// undoable.
150    ///
151    /// Replaces the former `StagedFormulaStateChanged` full before/after snapshot
152    /// pair (which made interactive `set_formula` O(N) per edit and O(N^2) in
153    /// changelog memory — see #126). Each edit records only the affected cell's
154    /// staged text transition, so a sequence of N edits costs O(N) total.
155    ///
156    /// - `old`: the staged formula text for the cell before the edit, if any.
157    /// - `new`: the staged formula text for the cell after the edit, if any.
158    ///
159    /// Undo restores `old` (re-stage if `Some`, clear if `None`); redo applies
160    /// `new` (re-stage if `Some`, clear if `None`).
161    StagedFormulaCellChanged {
162        sheet: String,
163        row: u32,
164        col: u32,
165        old: Option<String>,
166        new: Option<String>,
167    },
168}
169
170/// Audit trail for tracking all changes to the dependency graph
171#[derive(Debug, Default)]
172pub struct ChangeLog {
173    events: Vec<ChangeEvent>,
174    metas: Vec<ChangeEventMeta>,
175    enabled: bool,
176    /// Optional cap on retained events; when exceeded, oldest events are evicted (FIFO).
177    max_changelog_events: Option<usize>,
178    /// Track compound operations for atomic rollback
179    compound_depth: usize,
180    /// Monotonic sequence number per event
181    seqs: Vec<u64>,
182    /// Optional group id (compound) per event
183    groups: Vec<Option<u64>>,
184    next_seq: u64,
185    /// Stack of active group ids for nested compounds
186    group_stack: Vec<u64>,
187    next_group_id: u64,
188
189    current_meta: ChangeEventMeta,
190}
191
192impl ChangeLog {
193    pub fn new() -> Self {
194        Self {
195            events: Vec::new(),
196            metas: Vec::new(),
197            enabled: true,
198            max_changelog_events: None,
199            compound_depth: 0,
200            seqs: Vec::new(),
201            groups: Vec::new(),
202            next_seq: 0,
203            group_stack: Vec::new(),
204            next_group_id: 1,
205            current_meta: ChangeEventMeta::default(),
206        }
207    }
208
209    pub fn with_max_changelog_events(max: usize) -> Self {
210        let mut out = Self::new();
211        out.max_changelog_events = Some(max);
212        out
213    }
214
215    pub fn set_max_changelog_events(&mut self, max: Option<usize>) {
216        self.max_changelog_events = max;
217        self.enforce_cap();
218    }
219
220    fn enforce_cap(&mut self) {
221        let Some(max) = self.max_changelog_events else {
222            return;
223        };
224        if max == 0 {
225            self.clear();
226            return;
227        }
228        if self.events.len() <= max {
229            return;
230        }
231        let drop_n = self.events.len() - max;
232        self.events.drain(0..drop_n);
233        self.metas.drain(0..drop_n);
234        self.seqs.drain(0..drop_n);
235        self.groups.drain(0..drop_n);
236    }
237
238    pub fn record(&mut self, event: ChangeEvent) {
239        if self.enabled {
240            let seq = self.next_seq;
241            self.next_seq += 1;
242            let current_group = self.group_stack.last().copied();
243            self.events.push(event);
244            self.metas.push(self.current_meta.clone());
245            self.seqs.push(seq);
246            self.groups.push(current_group);
247            self.enforce_cap();
248        }
249    }
250
251    /// Record an event with explicit metadata (used for replay/redo).
252    pub fn record_with_meta(&mut self, event: ChangeEvent, meta: ChangeEventMeta) {
253        if self.enabled {
254            let seq = self.next_seq;
255            self.next_seq += 1;
256            let current_group = self.group_stack.last().copied();
257            self.events.push(event);
258            self.metas.push(meta);
259            self.seqs.push(seq);
260            self.groups.push(current_group);
261            self.enforce_cap();
262        }
263    }
264
265    /// Begin a compound operation (multiple changes from single action)
266    pub fn begin_compound(&mut self, description: String) {
267        self.compound_depth += 1;
268        if self.compound_depth == 1 {
269            // allocate new group id
270            let gid = self.next_group_id;
271            self.next_group_id += 1;
272            self.group_stack.push(gid);
273        } else {
274            // nested: reuse top id
275            if let Some(&gid) = self.group_stack.last() {
276                self.group_stack.push(gid);
277            }
278        }
279        if self.enabled {
280            self.record(ChangeEvent::CompoundStart {
281                description,
282                depth: self.compound_depth,
283            });
284        }
285    }
286
287    /// End a compound operation
288    pub fn end_compound(&mut self) {
289        if self.compound_depth > 0 {
290            if self.enabled {
291                self.record(ChangeEvent::CompoundEnd {
292                    depth: self.compound_depth,
293                });
294            }
295            self.compound_depth -= 1;
296            self.group_stack.pop();
297        }
298    }
299
300    pub fn events(&self) -> &[ChangeEvent] {
301        &self.events
302    }
303
304    pub fn event_meta(&self, index: usize) -> Option<&ChangeEventMeta> {
305        self.metas.get(index)
306    }
307
308    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
309        self.current_meta.actor_id = actor_id;
310    }
311
312    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
313        self.current_meta.correlation_id = correlation_id;
314    }
315
316    pub fn set_reason(&mut self, reason: Option<String>) {
317        self.current_meta.reason = reason;
318    }
319
320    /// Truncate log (and metadata) to len
321    pub fn truncate(&mut self, len: usize) {
322        self.events.truncate(len);
323        self.metas.truncate(len);
324        self.seqs.truncate(len);
325        self.groups.truncate(len);
326    }
327
328    pub fn clear(&mut self) {
329        self.events.clear();
330        self.metas.clear();
331        self.seqs.clear();
332        self.groups.clear();
333        self.compound_depth = 0;
334        self.group_stack.clear();
335    }
336
337    pub fn len(&self) -> usize {
338        self.events.len()
339    }
340
341    pub fn is_empty(&self) -> bool {
342        self.events.is_empty()
343    }
344
345    /// Extract events from index to end
346    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
347        let events = self.events.split_off(index);
348        let _ = self.metas.split_off(index);
349        let _ = self.seqs.split_off(index);
350        let _ = self.groups.split_off(index);
351        events
352    }
353
354    /// Temporarily disable logging (for rollback operations)
355    pub fn set_enabled(&mut self, enabled: bool) {
356        self.enabled = enabled;
357    }
358
359    /// Get current compound depth (for testing)
360    pub fn compound_depth(&self) -> usize {
361        self.compound_depth
362    }
363
364    /// Return (sequence_number, group_id) metadata for event index
365    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
366        self.seqs
367            .get(index)
368            .copied()
369            .zip(self.groups.get(index).copied())
370    }
371
372    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
373    pub fn last_group_indices(&self) -> Vec<usize> {
374        if let Some(&last_gid) = self.groups.iter().rev().flatten().next() {
375            let idxs: Vec<usize> = self
376                .groups
377                .iter()
378                .enumerate()
379                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
380                .collect();
381            if !idxs.is_empty() {
382                return idxs;
383            }
384        }
385        self.events.len().checked_sub(1).into_iter().collect()
386    }
387}
388
389/// Trait for pluggable logging strategies
390pub trait ChangeLogger {
391    fn record(&mut self, event: ChangeEvent);
392    fn set_enabled(&mut self, enabled: bool);
393    fn begin_compound(&mut self, description: String);
394    fn end_compound(&mut self);
395}
396
397impl ChangeLogger for ChangeLog {
398    fn record(&mut self, event: ChangeEvent) {
399        ChangeLog::record(self, event);
400    }
401
402    fn set_enabled(&mut self, enabled: bool) {
403        self.enabled = enabled;
404    }
405
406    fn begin_compound(&mut self, description: String) {
407        ChangeLog::begin_compound(self, description);
408    }
409
410    fn end_compound(&mut self) {
411        ChangeLog::end_compound(self);
412    }
413}
414
415/// Null logger for when change tracking not needed
416pub struct NullChangeLogger;
417
418impl ChangeLogger for NullChangeLogger {
419    fn record(&mut self, _: ChangeEvent) {}
420    fn set_enabled(&mut self, _: bool) {}
421    fn begin_compound(&mut self, _: String) {}
422    fn end_compound(&mut self) {}
423}