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::addr::GridAddr;
10use crate::engine::named_range::{NameScope, NamedDefinition};
11use crate::engine::row_visibility::RowVisibilitySource;
12use crate::engine::vertex::VertexId;
13use crate::reference::CellRef;
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: GridAddr,
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<GridAddr>,
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: GridAddr,
96        new_coord: GridAddr,
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
192/// Complete, operation-local mutation capture used by `Engine` correctness paths.
193///
194/// Unlike `ChangeLog`, this sink is always enabled and never evicts. It is crate-private so
195/// audit retention remains a property of `ChangeLog`, not of graph mutation.
196#[derive(Debug)]
197pub(crate) struct MutationCapture {
198    events: Vec<ChangeEvent>,
199    compound_depth: usize,
200    current_meta: ChangeEventMeta,
201}
202
203impl MutationCapture {
204    pub(crate) fn new(current_meta: ChangeEventMeta) -> Self {
205        Self {
206            events: Vec::new(),
207            compound_depth: 0,
208            current_meta,
209        }
210    }
211
212    pub(crate) fn len(&self) -> usize {
213        self.events.len()
214    }
215
216    pub(crate) fn events(&self) -> &[ChangeEvent] {
217        &self.events
218    }
219
220    pub(crate) fn close_compounds(&mut self) {
221        while self.compound_depth > 0 {
222            self.end_compound();
223        }
224    }
225
226    fn push(&mut self, event: ChangeEvent) {
227        self.events.push(event);
228    }
229}
230
231impl ChangeLogger for MutationCapture {
232    fn record(&mut self, event: ChangeEvent) {
233        self.push(event);
234    }
235
236    fn set_enabled(&mut self, _: bool) {}
237
238    fn begin_compound(&mut self, description: String) {
239        self.compound_depth += 1;
240        self.push(ChangeEvent::CompoundStart {
241            description,
242            depth: self.compound_depth,
243        });
244    }
245
246    fn end_compound(&mut self) {
247        if self.compound_depth == 0 {
248            return;
249        }
250        self.push(ChangeEvent::CompoundEnd {
251            depth: self.compound_depth,
252        });
253        self.compound_depth -= 1;
254    }
255}
256
257impl ChangeLog {
258    pub fn new() -> Self {
259        Self {
260            events: Vec::new(),
261            metas: Vec::new(),
262            enabled: true,
263            max_changelog_events: None,
264            compound_depth: 0,
265            seqs: Vec::new(),
266            groups: Vec::new(),
267            next_seq: 0,
268            group_stack: Vec::new(),
269            next_group_id: 1,
270            current_meta: ChangeEventMeta::default(),
271        }
272    }
273
274    pub fn with_max_changelog_events(max: usize) -> Self {
275        let mut out = Self::new();
276        out.max_changelog_events = Some(max);
277        out
278    }
279
280    pub fn set_max_changelog_events(&mut self, max: Option<usize>) {
281        self.max_changelog_events = max;
282        self.enforce_cap();
283    }
284
285    fn enforce_cap(&mut self) {
286        let Some(max) = self.max_changelog_events else {
287            return;
288        };
289        if max == 0 {
290            self.clear_retained();
291            return;
292        }
293        if self.events.len() <= max {
294            return;
295        }
296        let drop_n = self.events.len() - max;
297        self.events.drain(0..drop_n);
298        self.metas.drain(0..drop_n);
299        self.seqs.drain(0..drop_n);
300        self.groups.drain(0..drop_n);
301    }
302
303    fn clear_retained(&mut self) {
304        self.events.clear();
305        self.metas.clear();
306        self.seqs.clear();
307        self.groups.clear();
308    }
309
310    fn replay_record(&mut self, event: ChangeEvent, meta: &ChangeEventMeta, retain: bool) {
311        if !self.enabled {
312            return;
313        }
314        let seq = self.next_seq;
315        self.next_seq += 1;
316        if retain {
317            self.events.push(event);
318            self.metas.push(meta.clone());
319            self.seqs.push(seq);
320            self.groups.push(self.group_stack.last().copied());
321        }
322    }
323
324    fn replay_begin_compound(&mut self, description: String, meta: &ChangeEventMeta, retain: bool) {
325        self.compound_depth += 1;
326        if self.compound_depth == 1 {
327            let gid = self.next_group_id;
328            self.next_group_id += 1;
329            self.group_stack.push(gid);
330        } else if let Some(&gid) = self.group_stack.last() {
331            self.group_stack.push(gid);
332        }
333        self.replay_record(
334            ChangeEvent::CompoundStart {
335                description,
336                depth: self.compound_depth,
337            },
338            meta,
339            retain,
340        );
341    }
342
343    fn replay_end_compound(&mut self, meta: &ChangeEventMeta, retain: bool) {
344        if self.compound_depth == 0 {
345            return;
346        }
347        self.replay_record(
348            ChangeEvent::CompoundEnd {
349                depth: self.compound_depth,
350            },
351            meta,
352            retain,
353        );
354        self.compound_depth -= 1;
355        self.group_stack.pop();
356    }
357
358    fn replay_capture(&mut self, capture: MutationCapture, retain: bool) {
359        for event in capture.events {
360            match event {
361                ChangeEvent::CompoundStart { description, .. } => {
362                    self.replay_begin_compound(description, &capture.current_meta, retain);
363                }
364                ChangeEvent::CompoundEnd { .. } => {
365                    self.replay_end_compound(&capture.current_meta, retain);
366                }
367                event => self.replay_record(event, &capture.current_meta, retain),
368            }
369        }
370        if retain {
371            self.enforce_cap();
372        }
373    }
374
375    pub(crate) fn current_meta(&self) -> ChangeEventMeta {
376        self.current_meta.clone()
377    }
378
379    pub(crate) fn publish_capture(&mut self, capture: MutationCapture) {
380        self.replay_capture(capture, true);
381    }
382
383    pub(crate) fn discard_capture(&mut self, capture: MutationCapture) {
384        self.replay_capture(capture, false);
385    }
386
387    pub fn record(&mut self, event: ChangeEvent) {
388        if self.enabled {
389            let seq = self.next_seq;
390            self.next_seq += 1;
391            let current_group = self.group_stack.last().copied();
392            self.events.push(event);
393            self.metas.push(self.current_meta.clone());
394            self.seqs.push(seq);
395            self.groups.push(current_group);
396            self.enforce_cap();
397        }
398    }
399
400    /// Record an event with explicit metadata (used for replay/redo).
401    pub fn record_with_meta(&mut self, event: ChangeEvent, meta: ChangeEventMeta) {
402        if self.enabled {
403            let seq = self.next_seq;
404            self.next_seq += 1;
405            let current_group = self.group_stack.last().copied();
406            self.events.push(event);
407            self.metas.push(meta);
408            self.seqs.push(seq);
409            self.groups.push(current_group);
410            self.enforce_cap();
411        }
412    }
413
414    /// Begin a compound operation (multiple changes from single action)
415    pub fn begin_compound(&mut self, description: String) {
416        self.compound_depth += 1;
417        if self.compound_depth == 1 {
418            // allocate new group id
419            let gid = self.next_group_id;
420            self.next_group_id += 1;
421            self.group_stack.push(gid);
422        } else {
423            // nested: reuse top id
424            if let Some(&gid) = self.group_stack.last() {
425                self.group_stack.push(gid);
426            }
427        }
428        if self.enabled {
429            self.record(ChangeEvent::CompoundStart {
430                description,
431                depth: self.compound_depth,
432            });
433        }
434    }
435
436    /// End a compound operation
437    pub fn end_compound(&mut self) {
438        if self.compound_depth > 0 {
439            if self.enabled {
440                self.record(ChangeEvent::CompoundEnd {
441                    depth: self.compound_depth,
442                });
443            }
444            self.compound_depth -= 1;
445            self.group_stack.pop();
446        }
447    }
448
449    pub fn events(&self) -> &[ChangeEvent] {
450        &self.events
451    }
452
453    pub fn event_meta(&self, index: usize) -> Option<&ChangeEventMeta> {
454        self.metas.get(index)
455    }
456
457    pub fn set_actor_id(&mut self, actor_id: Option<String>) {
458        self.current_meta.actor_id = actor_id;
459    }
460
461    pub fn set_correlation_id(&mut self, correlation_id: Option<String>) {
462        self.current_meta.correlation_id = correlation_id;
463    }
464
465    pub fn set_reason(&mut self, reason: Option<String>) {
466        self.current_meta.reason = reason;
467    }
468
469    /// Truncate log (and metadata) to len
470    pub fn truncate(&mut self, len: usize) {
471        self.events.truncate(len);
472        self.metas.truncate(len);
473        self.seqs.truncate(len);
474        self.groups.truncate(len);
475    }
476
477    pub fn clear(&mut self) {
478        self.clear_retained();
479        self.compound_depth = 0;
480        self.group_stack.clear();
481    }
482
483    pub fn len(&self) -> usize {
484        self.events.len()
485    }
486
487    pub fn is_empty(&self) -> bool {
488        self.events.is_empty()
489    }
490
491    /// Extract events from index to end
492    pub fn take_from(&mut self, index: usize) -> Vec<ChangeEvent> {
493        let events = self.events.split_off(index);
494        let _ = self.metas.split_off(index);
495        let _ = self.seqs.split_off(index);
496        let _ = self.groups.split_off(index);
497        events
498    }
499
500    /// Temporarily disable logging (for rollback operations)
501    pub fn set_enabled(&mut self, enabled: bool) {
502        self.enabled = enabled;
503    }
504
505    /// Get current compound depth (for testing)
506    pub fn compound_depth(&self) -> usize {
507        self.compound_depth
508    }
509
510    /// Return (sequence_number, group_id) metadata for event index
511    pub fn meta(&self, index: usize) -> Option<(u64, Option<u64>)> {
512        self.seqs
513            .get(index)
514            .copied()
515            .zip(self.groups.get(index).copied())
516    }
517
518    /// Collect indices belonging to the last (innermost) complete group. Fallback: last single event.
519    pub fn last_group_indices(&self) -> Vec<usize> {
520        if let Some(&last_gid) = self.groups.iter().rev().flatten().next() {
521            let idxs: Vec<usize> = self
522                .groups
523                .iter()
524                .enumerate()
525                .filter_map(|(i, g)| if *g == Some(last_gid) { Some(i) } else { None })
526                .collect();
527            if !idxs.is_empty() {
528                return idxs;
529            }
530        }
531        self.events.len().checked_sub(1).into_iter().collect()
532    }
533}
534
535/// Trait for pluggable logging strategies
536pub trait ChangeLogger {
537    fn record(&mut self, event: ChangeEvent);
538    fn set_enabled(&mut self, enabled: bool);
539    fn begin_compound(&mut self, description: String);
540    fn end_compound(&mut self);
541}
542
543impl ChangeLogger for ChangeLog {
544    fn record(&mut self, event: ChangeEvent) {
545        ChangeLog::record(self, event);
546    }
547
548    fn set_enabled(&mut self, enabled: bool) {
549        self.enabled = enabled;
550    }
551
552    fn begin_compound(&mut self, description: String) {
553        ChangeLog::begin_compound(self, description);
554    }
555
556    fn end_compound(&mut self) {
557        ChangeLog::end_compound(self);
558    }
559}
560
561/// Null logger for when change tracking not needed
562pub struct NullChangeLogger;
563
564impl ChangeLogger for NullChangeLogger {
565    fn record(&mut self, _: ChangeEvent) {}
566    fn set_enabled(&mut self, _: bool) {}
567    fn begin_compound(&mut self, _: String) {}
568    fn end_compound(&mut self) {}
569}