Skip to main content

formualizer_eval/engine/graph/editor/
vertex_editor.rs

1use crate::SheetId;
2use crate::engine::graph::DependencyGraph;
3use crate::engine::graph::editor::reference_adjuster::{
4    MoveReferenceAdjuster, ReferenceAdjuster, ReferenceContext, RelativeReferenceAdjuster,
5    ShiftOperation,
6};
7use crate::engine::named_range::{NameScope, NamedDefinition};
8use crate::engine::{ChangeEvent, ChangeLogger, VertexId, VertexKind};
9use crate::reference::{CellRef, Coord};
10use formualizer_common::Coord as AbsCoord;
11use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
12use formualizer_parse::parser::ASTNode;
13use rustc_hash::FxHashMap;
14use std::sync::atomic::{AtomicU64, Ordering};
15
16/// Metadata for creating a new vertex
17#[derive(Debug, Clone)]
18pub struct VertexMeta {
19    pub coord: AbsCoord,
20    pub sheet_id: SheetId,
21    pub kind: VertexKind,
22    pub flags: u8,
23}
24
25impl VertexMeta {
26    pub fn new(row: u32, col: u32, sheet_id: SheetId, kind: VertexKind) -> Self {
27        Self {
28            coord: AbsCoord::new(row, col),
29            sheet_id,
30            kind,
31            flags: 0,
32        }
33    }
34
35    pub fn with_flags(mut self, flags: u8) -> Self {
36        self.flags = flags;
37        self
38    }
39
40    pub fn dirty(mut self) -> Self {
41        self.flags |= 0x01;
42        self
43    }
44
45    pub fn volatile(mut self) -> Self {
46        self.flags |= 0x02;
47        self
48    }
49}
50
51/// Patch for updating vertex metadata
52#[derive(Debug, Clone)]
53pub struct VertexMetaPatch {
54    pub kind: Option<VertexKind>,
55    pub coord: Option<AbsCoord>,
56    pub dirty: Option<bool>,
57    pub volatile: Option<bool>,
58}
59
60/// Patch for updating vertex data
61#[derive(Debug, Clone)]
62pub struct VertexDataPatch {
63    pub value: Option<LiteralValue>,
64    pub formula: Option<ASTNode>,
65}
66
67/// Summary of metadata update
68#[derive(Debug, Clone, Default)]
69pub struct MetaUpdateSummary {
70    pub coord_changed: bool,
71    pub kind_changed: bool,
72    pub flags_changed: bool,
73}
74
75/// Summary of data update
76#[derive(Debug, Clone, Default)]
77pub struct DataUpdateSummary {
78    pub value_changed: bool,
79    pub formula_changed: bool,
80    pub dependents_marked_dirty: Vec<VertexId>,
81}
82
83/// Summary of shift operations (row/column insert/delete)
84#[derive(Debug, Clone, Default)]
85pub struct ShiftSummary {
86    pub vertices_moved: Vec<VertexId>,
87    pub vertices_deleted: Vec<VertexId>,
88    pub references_adjusted: usize,
89    pub formulas_updated: usize,
90}
91
92/// Summary of range operations
93#[derive(Debug, Clone, Default)]
94pub struct RangeSummary {
95    pub cells_affected: usize,
96    pub vertices_created: Vec<VertexId>,
97    pub vertices_updated: Vec<VertexId>,
98    pub cells_moved: usize,
99}
100
101/// Transaction ID for tracking active transactions
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub struct TransactionId(u64);
104
105impl TransactionId {
106    fn new() -> Self {
107        static COUNTER: AtomicU64 = AtomicU64::new(0);
108        TransactionId(COUNTER.fetch_add(1, Ordering::Relaxed))
109    }
110}
111
112/// Represents an active transaction
113#[derive(Debug)]
114struct Transaction {
115    id: TransactionId,
116    start_index: usize, // Index in change_log where transaction started
117}
118
119/// Custom error type for vertex editor operations
120#[derive(Debug, Clone)]
121pub enum EditorError {
122    TargetOccupied { cell: CellRef },
123    OutOfBounds { row: u32, col: u32 },
124    InvalidName { name: String, reason: String },
125    TransactionFailed { reason: String },
126    TransactionUnsupported { reason: String },
127    NoActiveTransaction,
128    VertexNotFound { id: VertexId },
129    Excel(ExcelError),
130}
131
132impl From<ExcelError> for EditorError {
133    fn from(e: ExcelError) -> Self {
134        EditorError::Excel(e)
135    }
136}
137
138impl std::fmt::Display for EditorError {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        match self {
141            EditorError::TargetOccupied { cell } => {
142                write!(
143                    f,
144                    "Target cell occupied at row {}, col {}",
145                    cell.coord.row(),
146                    cell.coord.col()
147                )
148            }
149            EditorError::OutOfBounds { row, col } => {
150                write!(f, "Cell position out of bounds: row {row}, col {col}")
151            }
152            EditorError::InvalidName { name, reason } => {
153                write!(f, "Invalid name '{name}': {reason}")
154            }
155            EditorError::TransactionFailed { reason } => {
156                write!(f, "Transaction failed: {reason}")
157            }
158            EditorError::TransactionUnsupported { reason } => {
159                write!(f, "Transaction unsupported: {reason}")
160            }
161            EditorError::NoActiveTransaction => {
162                write!(f, "No active transaction")
163            }
164            EditorError::VertexNotFound { id } => {
165                write!(f, "Vertex not found: {id:?}")
166            }
167            EditorError::Excel(e) => write!(f, "Excel error: {e:?}"),
168        }
169    }
170}
171
172impl std::error::Error for EditorError {}
173
174/// Builder/controller object that provides exclusive access to the dependency graph
175/// for all mutation operations. This ensures consistency and proper change tracking.
176/// # Example Usage
177///
178/// ```rust
179/// use formualizer_eval::engine::{DependencyGraph, VertexEditor, VertexMeta, VertexKind};
180/// use formualizer_common::LiteralValue;
181/// use formualizer_eval::reference::{CellRef, Coord};
182///
183/// let mut graph = DependencyGraph::new();
184/// let mut editor = VertexEditor::new(&mut graph);
185///
186/// // Batch operations for better performance
187/// editor.begin_batch();
188///
189/// // Create a new cell vertex
190/// let meta = VertexMeta::new(1, 1, 0, VertexKind::Cell).dirty();
191/// let vertex_id = editor.add_vertex(meta);
192///
193/// // Set cell values
194/// let cell_ref = CellRef {
195///     sheet_id: 0,
196///     coord: Coord::new(2, 3, true, true)
197/// };
198/// editor.set_cell_value(cell_ref, LiteralValue::Number(42.0));
199///
200/// // Commit batch operations
201/// editor.commit_batch();
202///
203/// ```
204/// Optional hook for reading Arrow-truth spill values for ChangeLog snapshots.
205///
206/// VertexEditor is structure-only; in canonical mode, callers should provide this
207/// reader so spill undo/redo uses Arrow overlays rather than graph value caches.
208pub trait SpillValueReader {
209    fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue>;
210}
211
212pub struct VertexEditor<'g> {
213    graph: &'g mut DependencyGraph,
214    change_logger: Option<&'g mut dyn ChangeLogger>,
215    spill_value_reader: Option<&'g dyn SpillValueReader>,
216    batch_mode: bool,
217}
218
219impl<'g> VertexEditor<'g> {
220    /// Create a new vertex editor without change logging
221    pub fn new(graph: &'g mut DependencyGraph) -> Self {
222        Self {
223            graph,
224            change_logger: None,
225            spill_value_reader: None,
226            batch_mode: false,
227        }
228    }
229
230    /// Create a new vertex editor with change logging
231    pub fn with_logger<L: ChangeLogger + 'g>(
232        graph: &'g mut DependencyGraph,
233        logger: &'g mut L,
234    ) -> Self {
235        Self {
236            graph,
237            change_logger: Some(logger as &'g mut dyn ChangeLogger),
238            spill_value_reader: None,
239            batch_mode: false,
240        }
241    }
242
243    /// Create a new vertex editor with change logging and an Arrow-truth spill reader
244    pub fn with_logger_and_spill_reader<L: ChangeLogger + 'g>(
245        graph: &'g mut DependencyGraph,
246        logger: &'g mut L,
247        spill_value_reader: &'g dyn SpillValueReader,
248    ) -> Self {
249        Self {
250            graph,
251            change_logger: Some(logger as &'g mut dyn ChangeLogger),
252            spill_value_reader: Some(spill_value_reader),
253            batch_mode: false,
254        }
255    }
256
257    /// Start batch mode to defer expensive operations until commit
258    pub fn begin_batch(&mut self) {
259        if !self.batch_mode {
260            self.graph.begin_batch();
261            self.batch_mode = true;
262        }
263    }
264
265    /// End batch mode and commit all deferred operations
266    pub fn commit_batch(&mut self) {
267        if self.batch_mode {
268            self.graph.end_batch();
269            self.batch_mode = false;
270        }
271    }
272
273    /// Helper method to log a change event
274    fn log_change(&mut self, event: ChangeEvent) {
275        if let Some(logger) = &mut self.change_logger {
276            logger.record(event);
277        }
278    }
279
280    fn snapshot_spill_for_anchor(
281        &self,
282        anchor: VertexId,
283    ) -> Option<crate::engine::graph::editor::change_log::SpillSnapshot> {
284        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
285        if cells.is_empty() {
286            return None;
287        }
288
289        // Defensive bound for log payloads.
290        let max = self.graph.get_config().spill.max_spill_cells as usize;
291        let mut cells = cells;
292        if cells.len() > max {
293            cells.truncate(max);
294        }
295
296        let first = *cells.first().expect("non-empty spill cells");
297        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
298        let row0 = first.coord.row();
299        let col0 = first.coord.col();
300
301        let mut max_row = row0;
302        let mut max_col = col0;
303        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
304        for cell in &cells {
305            max_row = max_row.max(cell.coord.row());
306            max_col = max_col.max(cell.coord.col());
307            let v = if let Some(reader) = self.spill_value_reader {
308                reader
309                    .read_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
310                    .unwrap_or(LiteralValue::Empty)
311            } else {
312                self.graph
313                    .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
314                    .unwrap_or(LiteralValue::Empty)
315            };
316            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
317        }
318
319        let rows = (max_row - row0 + 1) as usize;
320        let cols = (max_col - col0 + 1) as usize;
321        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
322        for r in 0..rows {
323            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
324            for c in 0..cols {
325                row.push(
326                    by_coord
327                        .get(&(row0 + r as u32, col0 + c as u32))
328                        .cloned()
329                        .unwrap_or(LiteralValue::Empty),
330                );
331            }
332            values.push(row);
333        }
334
335        Some(crate::engine::graph::editor::change_log::SpillSnapshot {
336            target_cells: cells,
337            values,
338        })
339    }
340
341    /// Commit a spill region and log it for replay/undo.
342    pub fn commit_spill_region(
343        &mut self,
344        anchor: VertexId,
345        target_cells: Vec<CellRef>,
346        values: Vec<Vec<LiteralValue>>,
347    ) -> Result<(), EditorError> {
348        let old = self.snapshot_spill_for_anchor(anchor);
349        self.graph
350            .commit_spill_region_atomic_with_fault(
351                anchor,
352                target_cells.clone(),
353                values.clone(),
354                None,
355            )
356            .map_err(EditorError::Excel)?;
357        self.log_change(ChangeEvent::SpillCommitted {
358            anchor,
359            old,
360            new: crate::engine::graph::editor::change_log::SpillSnapshot {
361                target_cells,
362                values,
363            },
364        });
365        Ok(())
366    }
367
368    /// Clear a spill region (if any) and log it for replay/undo.
369    pub fn clear_spill_region(&mut self, anchor: VertexId) {
370        let Some(old) = self.snapshot_spill_for_anchor(anchor) else {
371            return;
372        };
373        self.graph.clear_spill_region(anchor);
374        self.log_change(ChangeEvent::SpillCleared { anchor, old });
375    }
376
377    /// Check if change logging is enabled
378    pub fn has_logger(&self) -> bool {
379        self.change_logger.is_some()
380    }
381
382    fn get_formula_ast(&self, id: VertexId) -> Option<ASTNode> {
383        self.graph.get_formula_id(id).and_then(|ast_id| {
384            self.graph
385                .data_store()
386                .retrieve_ast(ast_id, self.graph.sheet_reg())
387        })
388    }
389
390    fn snapshot_named_definitions(&self) -> FxHashMap<(NameScope, String), NamedDefinition> {
391        let mut out: FxHashMap<(NameScope, String), NamedDefinition> = FxHashMap::default();
392        for (name, nr) in self.graph.named_ranges_iter() {
393            out.insert((NameScope::Workbook, name.clone()), nr.definition.clone());
394        }
395        for ((sheet_id, name), nr) in self.graph.sheet_named_ranges_iter() {
396            out.insert(
397                (NameScope::Sheet(*sheet_id), name.clone()),
398                nr.definition.clone(),
399            );
400        }
401        out
402    }
403
404    // Transaction support
405
406    // Transaction support has been moved to TransactionContext
407    // which coordinates ChangeLog, TransactionManager, and VertexEditor
408
409    /// Apply the inverse of a change event (used by TransactionContext for rollback)
410    pub fn apply_inverse(&mut self, change: ChangeEvent) -> Result<(), EditorError> {
411        match change {
412            ChangeEvent::SetValue {
413                addr,
414                old_value,
415                old_formula,
416                new: _,
417            } => {
418                // Restore previous state. Setting a value can overwrite a formula.
419                if let Some(old_formula) = old_formula {
420                    self.set_cell_formula(addr, old_formula);
421                } else if let Some(old_value) = old_value {
422                    self.set_cell_value(addr, old_value);
423                } else if let Some(&id) = self.graph.get_vertex_id_for_address(&addr) {
424                    self.remove_vertex(id)?;
425                }
426            }
427            ChangeEvent::SetFormula {
428                addr,
429                old_value,
430                old_formula,
431                new: _,
432            } => {
433                // Restore previous state. Setting a formula can overwrite a value.
434                if let Some(old_formula) = old_formula {
435                    self.set_cell_formula(addr, old_formula);
436                } else if let Some(old_value) = old_value {
437                    self.set_cell_value(addr, old_value);
438                } else if let Some(&id) = self.graph.get_vertex_id_for_address(&addr) {
439                    self.remove_vertex(id)?;
440                }
441            }
442            ChangeEvent::SetRowVisibility { .. } => {
443                // Engine-level sidecar metadata; handled by Engine replay/rollback paths.
444            }
445            ChangeEvent::AddVertex { id, .. } => {
446                // Inverse of AddVertex is removal
447                let _ = self.remove_vertex(id); // ignore errors for now
448            }
449            ChangeEvent::RemoveVertex {
450                id: _,
451                old_value,
452                old_formula,
453                old_dependencies,
454                old_dependents,
455                coord,
456                sheet_id,
457                kind,
458                ..
459            } => {
460                if let (Some(c), Some(sid)) = (coord, sheet_id) {
461                    let meta =
462                        VertexMeta::new(c.row(), c.col(), sid, kind.unwrap_or(VertexKind::Cell));
463                    let new_id = self.try_add_vertex(meta)?;
464                    if let Some(v) = old_value {
465                        let cell_ref = self.graph.make_cell_ref_internal(sid, c.row(), c.col());
466                        self.set_cell_value(cell_ref, v);
467                    }
468                    if let Some(f) = old_formula {
469                        let cell_ref = self.graph.make_cell_ref_internal(sid, c.row(), c.col());
470                        self.set_cell_formula(cell_ref, f);
471                    }
472                    for dep in old_dependencies {
473                        self.graph.add_dependency_edge(new_id, dep)?;
474                    }
475                    for parent in old_dependents {
476                        self.graph.add_dependency_edge(parent, new_id)?;
477                    }
478                }
479            }
480            ChangeEvent::DefineName { name, scope, .. } => {
481                // Inverse is delete name
482                self.graph.delete_name(&name, scope)?;
483            }
484            ChangeEvent::UpdateName {
485                name,
486                scope,
487                old_definition,
488                ..
489            } => {
490                // Restore old definition
491                self.graph.update_name(&name, old_definition, scope)?;
492            }
493            ChangeEvent::DeleteName {
494                name,
495                scope,
496                old_definition,
497            } => {
498                if let Some(def) = old_definition {
499                    self.graph.define_name(&name, def, scope)?;
500                } else {
501                    return Err(EditorError::TransactionFailed {
502                        reason: "Missing old definition for name deletion rollback".to_string(),
503                    });
504                }
505            }
506            ChangeEvent::SpillCommitted { anchor, old, .. } => {
507                // Restore previous spill region.
508                if let Some(old) = old {
509                    self.graph
510                        .commit_spill_region_atomic_with_fault(
511                            anchor,
512                            old.target_cells,
513                            old.values,
514                            None,
515                        )
516                        .map_err(EditorError::Excel)?;
517                } else {
518                    self.graph.clear_spill_region(anchor);
519                }
520            }
521            ChangeEvent::SpillCleared { anchor, old } => {
522                // Re-commit the previous spill region.
523                self.graph
524                    .commit_spill_region_atomic_with_fault(
525                        anchor,
526                        old.target_cells,
527                        old.values,
528                        None,
529                    )
530                    .map_err(EditorError::Excel)?;
531            }
532            ChangeEvent::StagedFormulaCellChanged { .. } => {
533                // Workbook-level deferred state is replayed by Engine undo/redo wrappers.
534            }
535            // Granular events for compound operations
536            ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
537                // These are markers, no inverse needed
538            }
539            ChangeEvent::VertexMoved {
540                id,
541                sheet_id: _,
542                old_coord,
543                ..
544            } => {
545                // Move back to old position
546                self.move_vertex(id, old_coord)?;
547            }
548            ChangeEvent::FormulaAdjusted { id, old_ast, .. } => {
549                // Restore old formula directly by vertex id.
550                self.graph
551                    .update_vertex_formula(id, old_ast)
552                    .map_err(EditorError::Excel)?;
553                self.graph.mark_vertex_dirty(id);
554            }
555            ChangeEvent::NamedRangeAdjusted {
556                name,
557                scope,
558                old_definition,
559                ..
560            } => {
561                // Restore old definition
562                self.graph.update_name(&name, old_definition, scope)?;
563            }
564            ChangeEvent::EdgeAdded { from, to } => {
565                // Remove the edge
566                // TODO: Need specific edge removal method
567                return Err(EditorError::TransactionFailed {
568                    reason: "Cannot rollback edge addition yet".to_string(),
569                });
570            }
571            ChangeEvent::EdgeRemoved { from, to } => {
572                // Re-add the edge
573                // TODO: Need specific edge addition method
574                return Err(EditorError::TransactionFailed {
575                    reason: "Cannot rollback edge removal yet".to_string(),
576                });
577            }
578        }
579        Ok(())
580    }
581
582    /// Add a vertex to the graph.
583    ///
584    /// This compatibility API preserves the historical sentinel return on failure. New
585    /// transactional callers should use [`Self::try_add_vertex`] to retain typed admission errors.
586    pub fn add_vertex(&mut self, meta: VertexMeta) -> VertexId {
587        self.try_add_vertex(meta)
588            .unwrap_or_else(|_| VertexId::new(0))
589    }
590
591    pub fn try_add_vertex(&mut self, meta: VertexMeta) -> Result<VertexId, EditorError> {
592        // For now, use the existing set_cell_value method to create vertices
593        // This is a simplified implementation that works with the current API
594        let sheet_name = self.graph.sheet_name(meta.sheet_id).to_string();
595
596        // VertexEditor/VertexMeta use internal 0-based coordinates, while the
597        // graph mutation API is 1-based and owns common admission.
598        let id = self
599            .graph
600            .set_cell_value(
601                &sheet_name,
602                meta.coord.row() + 1,
603                meta.coord.col() + 1,
604                LiteralValue::Empty,
605            )
606            .map_err(EditorError::Excel)?
607            .affected_vertices
608            .into_iter()
609            .next()
610            .ok_or_else(|| EditorError::TransactionFailed {
611                reason: "vertex addition produced no affected vertex".to_string(),
612            })?;
613
614        if self.has_logger() && id.0 != 0 {
615            self.log_change(ChangeEvent::AddVertex {
616                id,
617                coord: meta.coord,
618                sheet_id: meta.sheet_id,
619                value: Some(LiteralValue::Empty),
620                formula: None,
621                kind: Some(meta.kind),
622                flags: Some(meta.flags),
623            });
624        }
625        Ok(id)
626    }
627
628    /// Remove a vertex from the graph with proper cleanup
629    pub fn remove_vertex(&mut self, id: VertexId) -> Result<(), EditorError> {
630        // Check if vertex exists
631        if !self.graph.vertex_exists(id) {
632            return Err(EditorError::Excel(
633                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
634            ));
635        }
636
637        // If this vertex anchors a spill, clear ownership + spilled children first.
638        // This keeps the spill registry consistent even if the anchor is removed.
639        let spill_snapshot = self.snapshot_spill_for_anchor(id);
640        let did_spill_clear = spill_snapshot.is_some();
641        if let Some(old_spill) = spill_snapshot {
642            if let Some(logger) = &mut self.change_logger {
643                logger.begin_compound(format!("RemoveVertexWithSpillClear id={}", id.0));
644            }
645            self.graph.clear_spill_region(id);
646            self.log_change(ChangeEvent::SpillCleared {
647                anchor: id,
648                old: old_spill,
649            });
650        }
651
652        // Get dependents before removing edges (delta-aware; no rebuild needed)
653        let dependents = self.graph.get_dependents(id);
654
655        // Capture old state (dependencies & dependents) BEFORE edge removal
656        let (
657            old_value,
658            old_formula,
659            old_dependencies,
660            old_dependents,
661            coord,
662            sheet_id_opt,
663            kind,
664            flags,
665        ) = if self.has_logger() {
666            let coord = self.graph.get_coord(id);
667            let sheet_id = self.graph.get_sheet_id(id);
668            let kind = self.graph.get_vertex_kind(id);
669            // flags not publicly exposed; set to 0 for now (future: expose getter)
670            let flags = 0u8;
671            (
672                self.graph.get_value(id),
673                self.get_formula_ast(id),
674                self.graph.get_dependencies(id), // outgoing deps
675                dependents.clone(),              // captured earlier
676                Some(coord),
677                Some(sheet_id),
678                Some(kind),
679                Some(flags),
680            )
681        } else {
682            (None, None, vec![], vec![], None, None, None, None)
683        };
684
685        // Remove from cell mapping if it exists
686        if let Some(cell_ref) = self.graph.get_cell_ref_for_vertex(id) {
687            self.graph.remove_cell_mapping(&cell_ref);
688        }
689
690        // Remove all formula/value payloads owned by this vertex.  Tombstoned vertices remain in
691        // the SoA store for stable IDs/debugging, but they must not continue to participate in
692        // formula evaluation through `vertex_formulas`.
693        self.graph.vertex_formulas.remove(&id);
694        self.graph.vertex_values.remove(&id);
695        self.graph.clear_formula_vertex_dirty(id);
696        self.graph.mark_volatile(id, false);
697        self.graph.store.set_kind(id, VertexKind::Empty);
698        self.graph.store.set_dynamic(id, false);
699
700        // Remove all edges
701        self.graph.remove_all_edges(id);
702
703        // Mark all dependents as having #REF! error
704        for dep_id in &dependents {
705            self.graph.mark_as_ref_error(*dep_id);
706        }
707
708        // Mark as deleted in store (tombstone)
709        self.graph.mark_deleted(id, true);
710
711        // Log change event
712        self.log_change(ChangeEvent::RemoveVertex {
713            id,
714            old_value,
715            old_formula,
716            old_dependencies,
717            old_dependents,
718            coord,
719            sheet_id: sheet_id_opt,
720            kind,
721            flags,
722        });
723
724        if did_spill_clear && let Some(logger) = &mut self.change_logger {
725            logger.end_compound();
726        }
727
728        Ok(())
729    }
730
731    /// Convenience: remove vertex at a given cell ref if exists
732    pub fn remove_vertex_at(&mut self, cell: CellRef) -> Result<(), EditorError> {
733        if let Some(id) = self.graph.get_vertex_for_cell(&cell) {
734            self.remove_vertex(id)
735        } else {
736            Ok(())
737        }
738    }
739
740    /// Move a vertex to a new position
741    pub fn move_vertex(&mut self, id: VertexId, new_coord: AbsCoord) -> Result<(), EditorError> {
742        // Check if vertex exists
743        if !self.graph.vertex_exists(id) {
744            return Err(EditorError::Excel(
745                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
746            ));
747        }
748
749        // Get old cell reference
750        let old_cell_ref = self.graph.get_cell_ref_for_vertex(id);
751
752        // Create new cell reference
753        let sheet_id = self.graph.get_sheet_id(id);
754        let new_cell_ref = CellRef::new(
755            sheet_id,
756            Coord::new(new_coord.row(), new_coord.col(), true, true),
757        );
758
759        // Update coordinate in store
760        self.graph.set_coord(id, new_coord);
761
762        // Update edge cache coordinate if needed
763        self.graph.update_edge_coord(id, new_coord);
764
765        // Update cell mapping
766        self.graph
767            .update_cell_mapping(id, old_cell_ref, new_cell_ref);
768
769        // Mark dependents as dirty
770        self.graph.mark_dependents_dirty(id);
771
772        Ok(())
773    }
774
775    /// Update vertex metadata
776    pub fn patch_vertex_meta(
777        &mut self,
778        id: VertexId,
779        patch: VertexMetaPatch,
780    ) -> Result<MetaUpdateSummary, EditorError> {
781        if !self.graph.vertex_exists(id) {
782            return Err(EditorError::Excel(
783                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
784            ));
785        }
786
787        let mut summary = MetaUpdateSummary::default();
788
789        if let Some(coord) = patch.coord {
790            self.graph.set_coord(id, coord);
791            self.graph.update_edge_coord(id, coord);
792            summary.coord_changed = true;
793        }
794
795        if let Some(kind) = patch.kind {
796            self.graph.set_kind(id, kind);
797            summary.kind_changed = true;
798        }
799
800        if let Some(dirty) = patch.dirty {
801            self.graph.set_dirty(id, dirty);
802            summary.flags_changed = true;
803        }
804
805        if let Some(volatile) = patch.volatile {
806            self.graph.mark_volatile(id, volatile);
807            summary.flags_changed = true;
808        }
809
810        Ok(summary)
811    }
812
813    /// Update vertex data (value or formula)
814    pub fn patch_vertex_data(
815        &mut self,
816        id: VertexId,
817        patch: VertexDataPatch,
818    ) -> Result<DataUpdateSummary, EditorError> {
819        if !self.graph.vertex_exists(id) {
820            return Err(EditorError::Excel(
821                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
822            ));
823        }
824
825        let mut summary = DataUpdateSummary::default();
826
827        if let Some(value) = patch.value {
828            self.graph.update_vertex_value(id, value);
829            summary.value_changed = true;
830
831            // Mark dependents as dirty. get_dependents is delta-aware, so no
832            // CSR rebuild is required even when edits are pending (#125).
833            let dependents = self.graph.get_dependents(id);
834            for dep in &dependents {
835                self.graph.set_dirty(*dep, true);
836            }
837            summary.dependents_marked_dirty = dependents;
838        }
839
840        if let Some(_formula) = patch.formula {
841            // This would need proper formula update implementation
842            // For now, we'll mark as changed
843            summary.formula_changed = true;
844        }
845
846        Ok(summary)
847    }
848
849    /// Add an edge between two vertices
850    pub fn add_edge(&mut self, from: VertexId, to: VertexId) -> bool {
851        if from == to {
852            return false; // Prevent self-loops
853        }
854
855        // TODO: Add edge through proper API when available
856        // For now, return true to indicate intent
857        true
858    }
859
860    /// Remove an edge between two vertices
861    pub fn remove_edge(&mut self, _from: VertexId, _to: VertexId) -> bool {
862        // TODO: Remove edge through proper API when available
863        true
864    }
865
866    /// Insert rows at the specified position, shifting existing rows down
867    pub fn insert_rows(
868        &mut self,
869        sheet_id: SheetId,
870        before: u32,
871        count: u32,
872    ) -> Result<ShiftSummary, EditorError> {
873        if count == 0 {
874            return Ok(ShiftSummary::default());
875        }
876
877        let mut summary = ShiftSummary::default();
878
879        // Begin batch for efficiency
880        self.begin_batch();
881
882        // 1. Collect vertices to shift (those at or after the insert point)
883        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
884            .graph
885            .vertices_in_sheet(sheet_id)
886            .filter_map(|id| {
887                let coord = self.graph.get_coord(id);
888                if coord.row() >= before {
889                    Some((id, coord))
890                } else {
891                    None
892                }
893            })
894            .collect();
895
896        if let Some(logger) = &mut self.change_logger {
897            logger.begin_compound(format!(
898                "InsertRows sheet={sheet_id} before={before} count={count}"
899            ));
900        }
901        // 2. Shift vertices down (emit VertexMoved)
902        for (id, old_coord) in vertices_to_shift {
903            let new_coord = AbsCoord::new(old_coord.row() + count, old_coord.col());
904            if self.has_logger() {
905                self.log_change(ChangeEvent::VertexMoved {
906                    id,
907                    sheet_id,
908                    old_coord,
909                    new_coord,
910                });
911            }
912            self.move_vertex(id, new_coord)?;
913            summary.vertices_moved.push(id);
914        }
915
916        // 3. Adjust formulas using ReferenceAdjuster
917        let op = ShiftOperation::InsertRows {
918            sheet_id,
919            before,
920            count,
921        };
922        let adjuster = ReferenceAdjuster::new();
923
924        // Get all formulas and adjust them
925        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
926
927        for id in formula_vertices {
928            if let Some(ast) = self.get_formula_ast(id)
929                && let Some(adjusted) = adjuster.adjust_ast_if_changed_in_context(
930                    &ast,
931                    &op,
932                    &ReferenceContext::new(self.graph.get_sheet_id(id), self.graph.sheet_reg()),
933                )
934            {
935                if self.has_logger() {
936                    self.log_change(ChangeEvent::FormulaAdjusted {
937                        id,
938                        addr: self.graph.get_cell_ref_for_vertex(id),
939                        old_ast: ast.clone(),
940                        new_ast: adjusted.clone(),
941                    });
942                }
943                self.graph.update_vertex_formula(id, adjusted)?;
944                self.graph.mark_vertex_dirty(id);
945                summary.formulas_updated += 1;
946            }
947        }
948
949        // 4. Adjust named ranges
950        let old_names = if self.has_logger() {
951            Some(self.snapshot_named_definitions())
952        } else {
953            None
954        };
955        self.graph.adjust_named_ranges(&op)?;
956        if let Some(old_names) = old_names {
957            let new_names = self.snapshot_named_definitions();
958            for ((scope, name), old_definition) in old_names {
959                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
960                    && *new_definition != old_definition
961                {
962                    self.log_change(ChangeEvent::NamedRangeAdjusted {
963                        name,
964                        scope,
965                        old_definition,
966                        new_definition: new_definition.clone(),
967                    });
968                }
969            }
970        }
971
972        // 5. Log change event
973        if let Some(logger) = &mut self.change_logger {
974            logger.end_compound();
975        }
976
977        self.commit_batch();
978
979        Ok(summary)
980    }
981
982    /// Delete rows at the specified position, shifting remaining rows up
983    pub fn delete_rows(
984        &mut self,
985        sheet_id: SheetId,
986        start: u32,
987        count: u32,
988    ) -> Result<ShiftSummary, EditorError> {
989        if count == 0 {
990            return Ok(ShiftSummary::default());
991        }
992
993        let mut summary = ShiftSummary::default();
994
995        self.begin_batch();
996
997        if let Some(logger) = &mut self.change_logger {
998            logger.begin_compound(format!(
999                "DeleteRows sheet={sheet_id} start={start} count={count}"
1000            ));
1001        }
1002
1003        // 1. Delete vertices in the range
1004        let vertices_to_delete: Vec<VertexId> = self
1005            .graph
1006            .vertices_in_sheet(sheet_id)
1007            .filter(|&id| {
1008                let coord = self.graph.get_coord(id);
1009                coord.row() >= start && coord.row() < start + count
1010            })
1011            .collect();
1012
1013        for id in vertices_to_delete {
1014            self.remove_vertex(id)?;
1015            summary.vertices_deleted.push(id);
1016        }
1017        // 2. Shift remaining vertices up (emit VertexMoved)
1018        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
1019            .graph
1020            .vertices_in_sheet(sheet_id)
1021            .filter_map(|id| {
1022                let coord = self.graph.get_coord(id);
1023                if coord.row() >= start + count {
1024                    Some((id, coord))
1025                } else {
1026                    None
1027                }
1028            })
1029            .collect();
1030
1031        for (id, old_coord) in vertices_to_shift {
1032            let new_coord = AbsCoord::new(old_coord.row() - count, old_coord.col());
1033            if self.has_logger() {
1034                self.log_change(ChangeEvent::VertexMoved {
1035                    id,
1036                    sheet_id,
1037                    old_coord,
1038                    new_coord,
1039                });
1040            }
1041            self.move_vertex(id, new_coord)?;
1042            summary.vertices_moved.push(id);
1043        }
1044
1045        // 3. Adjust formulas
1046        let op = ShiftOperation::DeleteRows {
1047            sheet_id,
1048            start,
1049            count,
1050        };
1051        let adjuster = ReferenceAdjuster::new();
1052
1053        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
1054
1055        for id in formula_vertices {
1056            if let Some(ast) = self.get_formula_ast(id)
1057                && let Some(adjusted) = adjuster.adjust_ast_if_changed_in_context(
1058                    &ast,
1059                    &op,
1060                    &ReferenceContext::new(self.graph.get_sheet_id(id), self.graph.sheet_reg()),
1061                )
1062            {
1063                if self.has_logger() {
1064                    self.log_change(ChangeEvent::FormulaAdjusted {
1065                        id,
1066                        addr: self.graph.get_cell_ref_for_vertex(id),
1067                        old_ast: ast.clone(),
1068                        new_ast: adjusted.clone(),
1069                    });
1070                }
1071                self.graph.update_vertex_formula(id, adjusted)?;
1072                self.graph.mark_vertex_dirty(id);
1073                summary.formulas_updated += 1;
1074            }
1075        }
1076
1077        // 4. Adjust named ranges
1078        let old_names = if self.has_logger() {
1079            Some(self.snapshot_named_definitions())
1080        } else {
1081            None
1082        };
1083        self.graph.adjust_named_ranges(&op)?;
1084        if let Some(old_names) = old_names {
1085            let new_names = self.snapshot_named_definitions();
1086            for ((scope, name), old_definition) in old_names {
1087                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1088                    && *new_definition != old_definition
1089                {
1090                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1091                        name,
1092                        scope,
1093                        old_definition,
1094                        new_definition: new_definition.clone(),
1095                    });
1096                }
1097            }
1098        }
1099
1100        // 5. Log change event
1101        if let Some(logger) = &mut self.change_logger {
1102            logger.end_compound();
1103        }
1104
1105        self.commit_batch();
1106
1107        Ok(summary)
1108    }
1109
1110    /// Insert columns at the specified position, shifting existing columns right
1111    pub fn insert_columns(
1112        &mut self,
1113        sheet_id: SheetId,
1114        before: u32,
1115        count: u32,
1116    ) -> Result<ShiftSummary, EditorError> {
1117        if count == 0 {
1118            return Ok(ShiftSummary::default());
1119        }
1120
1121        let mut summary = ShiftSummary::default();
1122
1123        // Begin batch for efficiency
1124        self.begin_batch();
1125
1126        // 1. Collect vertices to shift (those at or after the insert point)
1127        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
1128            .graph
1129            .vertices_in_sheet(sheet_id)
1130            .filter_map(|id| {
1131                let coord = self.graph.get_coord(id);
1132                if coord.col() >= before {
1133                    Some((id, coord))
1134                } else {
1135                    None
1136                }
1137            })
1138            .collect();
1139
1140        if let Some(logger) = &mut self.change_logger {
1141            logger.begin_compound(format!(
1142                "InsertColumns sheet={sheet_id} before={before} count={count}"
1143            ));
1144        }
1145        // 2. Shift vertices right (emit VertexMoved)
1146        for (id, old_coord) in vertices_to_shift {
1147            let new_coord = AbsCoord::new(old_coord.row(), old_coord.col() + count);
1148            if self.has_logger() {
1149                self.log_change(ChangeEvent::VertexMoved {
1150                    id,
1151                    sheet_id,
1152                    old_coord,
1153                    new_coord,
1154                });
1155            }
1156            self.move_vertex(id, new_coord)?;
1157            summary.vertices_moved.push(id);
1158        }
1159
1160        // 3. Adjust formulas using ReferenceAdjuster
1161        let op = ShiftOperation::InsertColumns {
1162            sheet_id,
1163            before,
1164            count,
1165        };
1166        let adjuster = ReferenceAdjuster::new();
1167
1168        // Get all formulas and adjust them
1169        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
1170
1171        for id in formula_vertices {
1172            if let Some(ast) = self.get_formula_ast(id)
1173                && let Some(adjusted) = adjuster.adjust_ast_if_changed_in_context(
1174                    &ast,
1175                    &op,
1176                    &ReferenceContext::new(self.graph.get_sheet_id(id), self.graph.sheet_reg()),
1177                )
1178            {
1179                if self.has_logger() {
1180                    self.log_change(ChangeEvent::FormulaAdjusted {
1181                        id,
1182                        addr: self.graph.get_cell_ref_for_vertex(id),
1183                        old_ast: ast.clone(),
1184                        new_ast: adjusted.clone(),
1185                    });
1186                }
1187                self.graph.update_vertex_formula(id, adjusted)?;
1188                self.graph.mark_vertex_dirty(id);
1189                summary.formulas_updated += 1;
1190            }
1191        }
1192
1193        // 4. Adjust named ranges
1194        let old_names = if self.has_logger() {
1195            Some(self.snapshot_named_definitions())
1196        } else {
1197            None
1198        };
1199        self.graph.adjust_named_ranges(&op)?;
1200        if let Some(old_names) = old_names {
1201            let new_names = self.snapshot_named_definitions();
1202            for ((scope, name), old_definition) in old_names {
1203                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1204                    && *new_definition != old_definition
1205                {
1206                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1207                        name,
1208                        scope,
1209                        old_definition,
1210                        new_definition: new_definition.clone(),
1211                    });
1212                }
1213            }
1214        }
1215
1216        // 5. Log change event
1217        if let Some(logger) = &mut self.change_logger {
1218            logger.end_compound();
1219        }
1220
1221        self.commit_batch();
1222
1223        Ok(summary)
1224    }
1225
1226    /// Delete columns at the specified position, shifting remaining columns left
1227    pub fn delete_columns(
1228        &mut self,
1229        sheet_id: SheetId,
1230        start: u32,
1231        count: u32,
1232    ) -> Result<ShiftSummary, EditorError> {
1233        if count == 0 {
1234            return Ok(ShiftSummary::default());
1235        }
1236
1237        let mut summary = ShiftSummary::default();
1238
1239        self.begin_batch();
1240
1241        if let Some(logger) = &mut self.change_logger {
1242            logger.begin_compound(format!(
1243                "DeleteColumns sheet={sheet_id} start={start} count={count}"
1244            ));
1245        }
1246
1247        // 1. Delete vertices in the range
1248        let vertices_to_delete: Vec<VertexId> = self
1249            .graph
1250            .vertices_in_sheet(sheet_id)
1251            .filter(|&id| {
1252                let coord = self.graph.get_coord(id);
1253                coord.col() >= start && coord.col() < start + count
1254            })
1255            .collect();
1256
1257        for id in vertices_to_delete {
1258            self.remove_vertex(id)?;
1259            summary.vertices_deleted.push(id);
1260        }
1261        // 2. Shift remaining vertices left (emit VertexMoved)
1262        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
1263            .graph
1264            .vertices_in_sheet(sheet_id)
1265            .filter_map(|id| {
1266                let coord = self.graph.get_coord(id);
1267                if coord.col() >= start + count {
1268                    Some((id, coord))
1269                } else {
1270                    None
1271                }
1272            })
1273            .collect();
1274
1275        for (id, old_coord) in vertices_to_shift {
1276            let new_coord = AbsCoord::new(old_coord.row(), old_coord.col() - count);
1277            if self.has_logger() {
1278                self.log_change(ChangeEvent::VertexMoved {
1279                    id,
1280                    sheet_id,
1281                    old_coord,
1282                    new_coord,
1283                });
1284            }
1285            self.move_vertex(id, new_coord)?;
1286            summary.vertices_moved.push(id);
1287        }
1288
1289        // 3. Adjust formulas
1290        let op = ShiftOperation::DeleteColumns {
1291            sheet_id,
1292            start,
1293            count,
1294        };
1295        let adjuster = ReferenceAdjuster::new();
1296
1297        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
1298
1299        for id in formula_vertices {
1300            if let Some(ast) = self.get_formula_ast(id)
1301                && let Some(adjusted) = adjuster.adjust_ast_if_changed_in_context(
1302                    &ast,
1303                    &op,
1304                    &ReferenceContext::new(self.graph.get_sheet_id(id), self.graph.sheet_reg()),
1305                )
1306            {
1307                if self.has_logger() {
1308                    self.log_change(ChangeEvent::FormulaAdjusted {
1309                        id,
1310                        addr: self.graph.get_cell_ref_for_vertex(id),
1311                        old_ast: ast.clone(),
1312                        new_ast: adjusted.clone(),
1313                    });
1314                }
1315                self.graph.update_vertex_formula(id, adjusted)?;
1316                self.graph.mark_vertex_dirty(id);
1317                summary.formulas_updated += 1;
1318            }
1319        }
1320
1321        // 4. Adjust named ranges
1322        let old_names = if self.has_logger() {
1323            Some(self.snapshot_named_definitions())
1324        } else {
1325            None
1326        };
1327        self.graph.adjust_named_ranges(&op)?;
1328        if let Some(old_names) = old_names {
1329            let new_names = self.snapshot_named_definitions();
1330            for ((scope, name), old_definition) in old_names {
1331                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1332                    && *new_definition != old_definition
1333                {
1334                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1335                        name,
1336                        scope,
1337                        old_definition,
1338                        new_definition: new_definition.clone(),
1339                    });
1340                }
1341            }
1342        }
1343
1344        // 5. Log change event
1345        if let Some(logger) = &mut self.change_logger {
1346            logger.end_compound();
1347        }
1348
1349        self.commit_batch();
1350
1351        Ok(summary)
1352    }
1353
1354    /// Shift rows down/up within a sheet (Excel's insert/delete rows)
1355    pub fn shift_rows(&mut self, sheet_id: SheetId, start_row: u32, delta: i32) {
1356        if delta == 0 {
1357            return;
1358        }
1359
1360        // Log change event for undo/redo
1361        let change_event = ChangeEvent::SetValue {
1362            addr: CellRef {
1363                sheet_id,
1364                coord: Coord::new(start_row, 0, true, true),
1365            },
1366            old_value: None,
1367            old_formula: None,
1368            new: LiteralValue::Text(format!("Row shift: start={start_row}, delta={delta}")),
1369        };
1370        self.log_change(change_event);
1371
1372        // TODO: Implement actual row shifting logic
1373        // This would require coordination with the vertex store and dependency tracking
1374    }
1375
1376    /// Shift columns left/right within a sheet (Excel's insert/delete columns)
1377    pub fn shift_columns(&mut self, sheet_id: SheetId, start_col: u32, delta: i32) {
1378        if delta == 0 {
1379            return;
1380        }
1381
1382        // Log change event
1383        let change_event = ChangeEvent::SetValue {
1384            addr: CellRef {
1385                sheet_id,
1386                coord: Coord::new(0, start_col, true, true),
1387            },
1388            old_value: None,
1389            old_formula: None,
1390            new: LiteralValue::Text(format!("Column shift: start={start_col}, delta={delta}")),
1391        };
1392        self.log_change(change_event);
1393
1394        // TODO: Implement actual column shifting logic
1395        // This would require coordination with the vertex store and dependency tracking
1396    }
1397
1398    /// Set a cell value, creating the vertex if it doesn't exist
1399    pub fn set_cell_value(&mut self, cell_ref: CellRef, value: LiteralValue) -> VertexId {
1400        self.set_cell_value_with_old_state(cell_ref, value, None, None)
1401    }
1402
1403    /// Like [`set_cell_value`](Self::set_cell_value), but lets the caller
1404    /// supply old state captured from an external source of truth (e.g. the
1405    /// Arrow store, whose values are invisible here when the graph value cache
1406    /// is disabled) for the change-log event.
1407    ///
1408    /// Precedence matches the historical append-then-patch flow
1409    /// (`ChangeLog::patch_last_cell_event_old_state`): state the editor
1410    /// captures from the graph wins; caller-supplied state only fills fields
1411    /// the graph left `None`.
1412    pub fn set_cell_value_with_old_state(
1413        &mut self,
1414        cell_ref: CellRef,
1415        value: LiteralValue,
1416        fallback_old_value: Option<LiteralValue>,
1417        fallback_old_formula: Option<ASTNode>,
1418    ) -> VertexId {
1419        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1420
1421        // Capture old state before modification (value + formula); fall back
1422        // to caller-supplied state for anything the graph cannot see.
1423        let old_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1424        let old_value = old_id
1425            .and_then(|id| self.graph.get_value(id))
1426            .or(fallback_old_value);
1427        let old_formula = old_id
1428            .and_then(|id| self.get_formula_ast(id))
1429            .or(fallback_old_formula);
1430
1431        // If this cell currently anchors a spill, clear the spill first and log it.
1432        // This keeps spill ownership maps and children consistent under undo/redo.
1433        let spill_snapshot =
1434            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1435        let did_spill_clear = spill_snapshot.is_some();
1436        if let Some((anchor, old_spill)) = spill_snapshot {
1437            if let Some(logger) = &mut self.change_logger {
1438                logger.begin_compound(format!(
1439                    "SetValueWithSpillClear sheet={} row={} col={}",
1440                    cell_ref.sheet_id,
1441                    cell_ref.coord.row(),
1442                    cell_ref.coord.col()
1443                ));
1444            }
1445            self.graph.clear_spill_region(anchor);
1446            self.log_change(ChangeEvent::SpillCleared {
1447                anchor,
1448                old: old_spill,
1449            });
1450        }
1451
1452        // Use the existing DependencyGraph API
1453        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1454        match self.graph.set_cell_value(
1455            &sheet_name,
1456            cell_ref.coord.row() + 1,
1457            cell_ref.coord.col() + 1,
1458            value.clone(),
1459        ) {
1460            Ok(summary) => {
1461                // Log change event
1462                let change_event = ChangeEvent::SetValue {
1463                    addr: cell_ref,
1464                    old_value,
1465                    old_formula,
1466                    new: value,
1467                };
1468                self.log_change(change_event);
1469
1470                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1471                    logger.end_compound();
1472                }
1473
1474                summary
1475                    .affected_vertices
1476                    .into_iter()
1477                    .next()
1478                    .unwrap_or(VertexId::new(0))
1479            }
1480            Err(_) => VertexId::new(0),
1481        }
1482    }
1483
1484    /// Set a cell formula, creating the vertex if it doesn't exist
1485    pub fn set_cell_formula(&mut self, cell_ref: CellRef, formula: ASTNode) -> VertexId {
1486        self.set_cell_formula_with_old_state(cell_ref, formula, None, None)
1487    }
1488
1489    /// Like [`set_cell_formula`](Self::set_cell_formula), but lets the caller
1490    /// supply old state captured from an external source of truth (e.g. the
1491    /// Arrow store) for the change-log event. Same precedence as
1492    /// [`set_cell_value_with_old_state`](Self::set_cell_value_with_old_state):
1493    /// graph-captured state wins, caller state only fills `None` fields.
1494    pub fn set_cell_formula_with_old_state(
1495        &mut self,
1496        cell_ref: CellRef,
1497        formula: ASTNode,
1498        fallback_old_value: Option<LiteralValue>,
1499        fallback_old_formula: Option<ASTNode>,
1500    ) -> VertexId {
1501        self.set_cell_formula_with_old_state_and_plan(
1502            cell_ref,
1503            formula,
1504            fallback_old_value,
1505            fallback_old_formula,
1506            None,
1507        )
1508    }
1509
1510    pub(crate) fn set_cell_formula_with_prepared_plan(
1511        &mut self,
1512        cell_ref: CellRef,
1513        formula: ASTNode,
1514        fallback_old_value: Option<LiteralValue>,
1515        fallback_old_formula: Option<ASTNode>,
1516        ast_id: crate::engine::arena::AstNodeId,
1517        plan: crate::engine::ingest_pipeline::DependencyPlanRow,
1518    ) -> VertexId {
1519        self.set_cell_formula_with_old_state_and_plan(
1520            cell_ref,
1521            formula,
1522            fallback_old_value,
1523            fallback_old_formula,
1524            Some((ast_id, plan)),
1525        )
1526    }
1527
1528    fn set_cell_formula_with_old_state_and_plan(
1529        &mut self,
1530        cell_ref: CellRef,
1531        formula: ASTNode,
1532        fallback_old_value: Option<LiteralValue>,
1533        fallback_old_formula: Option<ASTNode>,
1534        prepared: Option<(
1535            crate::engine::arena::AstNodeId,
1536            crate::engine::ingest_pipeline::DependencyPlanRow,
1537        )>,
1538    ) -> VertexId {
1539        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1540
1541        // Capture old state before modification (value + formula); fall back
1542        // to caller-supplied state for anything the graph cannot see.
1543        let old_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1544        let old_value = old_id
1545            .and_then(|id| self.graph.get_value(id))
1546            .or(fallback_old_value);
1547        let old_formula = old_id
1548            .and_then(|id| self.get_formula_ast(id))
1549            .or(fallback_old_formula);
1550
1551        // If this cell currently anchors a spill, clear it before updating the formula.
1552        let spill_snapshot =
1553            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1554        let did_spill_clear = spill_snapshot.is_some();
1555        if let Some((anchor, old_spill)) = spill_snapshot {
1556            if let Some(logger) = &mut self.change_logger {
1557                logger.begin_compound(format!(
1558                    "SetFormulaWithSpillClear sheet={} row={} col={}",
1559                    cell_ref.sheet_id,
1560                    cell_ref.coord.row(),
1561                    cell_ref.coord.col()
1562                ));
1563            }
1564            self.graph.clear_spill_region(anchor);
1565            self.log_change(ChangeEvent::SpillCleared {
1566                anchor,
1567                old: old_spill,
1568            });
1569        }
1570
1571        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1572        let result = if let Some((ast_id, plan)) = prepared {
1573            self.graph.set_cell_formula_with_plan(
1574                &sheet_name,
1575                cell_ref.coord.row() + 1,
1576                cell_ref.coord.col() + 1,
1577                ast_id,
1578                &plan,
1579                plan.volatile,
1580                plan.dynamic,
1581            )
1582        } else {
1583            self.graph.set_cell_formula(
1584                &sheet_name,
1585                cell_ref.coord.row() + 1,
1586                cell_ref.coord.col() + 1,
1587                formula.clone(),
1588            )
1589        };
1590        match result {
1591            Ok(summary) => {
1592                // Log change event
1593                let change_event = ChangeEvent::SetFormula {
1594                    addr: cell_ref,
1595                    old_value,
1596                    old_formula,
1597                    new: formula,
1598                };
1599                self.log_change(change_event);
1600
1601                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1602                    logger.end_compound();
1603                }
1604
1605                summary
1606                    .affected_vertices
1607                    .into_iter()
1608                    .next()
1609                    .unwrap_or(VertexId::new(0))
1610            }
1611            Err(_) => VertexId::new(0),
1612        }
1613    }
1614
1615    // Range operations
1616
1617    /// Set values for a rectangular range of cells
1618    pub fn set_range_values(
1619        &mut self,
1620        sheet_id: SheetId,
1621        start_row: u32,
1622        start_col: u32,
1623        values: &[Vec<LiteralValue>],
1624    ) -> Result<RangeSummary, EditorError> {
1625        let mut summary = RangeSummary::default();
1626
1627        self.begin_batch();
1628        // One multi-source dirty propagation for the whole rectangle instead
1629        // of a full BFS per cell (the loop body cannot error, so the scope
1630        // always closes before returning).
1631        self.graph.begin_deferred_dirty();
1632
1633        for (row_offset, row_values) in values.iter().enumerate() {
1634            for (col_offset, value) in row_values.iter().enumerate() {
1635                let row = start_row + row_offset as u32;
1636                let col = start_col + col_offset as u32;
1637                let cell_ref = self.graph.make_cell_ref_internal(sheet_id, row, col);
1638                let existing_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1639
1640                let id = self.set_cell_value(cell_ref, value.clone());
1641                match existing_id {
1642                    Some(existing_id) => summary.vertices_updated.push(existing_id),
1643                    None if id.0 != 0 => summary.vertices_created.push(id),
1644                    None => {}
1645                }
1646                summary.cells_affected += 1;
1647            }
1648        }
1649
1650        let _ = self.graph.end_deferred_dirty();
1651        self.commit_batch();
1652
1653        Ok(summary)
1654    }
1655
1656    /// Clear all cells in a rectangular range
1657    pub fn clear_range(
1658        &mut self,
1659        sheet_id: SheetId,
1660        start_row: u32,
1661        start_col: u32,
1662        end_row: u32,
1663        end_col: u32,
1664    ) -> Result<RangeSummary, EditorError> {
1665        let mut summary = RangeSummary::default();
1666
1667        self.begin_batch();
1668
1669        // Collect vertices in range
1670        let vertices_in_range: Vec<_> = self
1671            .graph
1672            .vertices_in_sheet(sheet_id)
1673            .filter(|&id| {
1674                let coord = self.graph.get_coord(id);
1675                let row = coord.row();
1676                let col = coord.col();
1677                row >= start_row && row <= end_row && col >= start_col && col <= end_col
1678            })
1679            .collect();
1680
1681        for id in vertices_in_range {
1682            self.remove_vertex(id)?;
1683            summary.cells_affected += 1;
1684        }
1685
1686        self.commit_batch();
1687
1688        Ok(summary)
1689    }
1690
1691    /// Copy a range to a new location
1692    pub fn copy_range(
1693        &mut self,
1694        sheet_id: SheetId,
1695        from_start_row: u32,
1696        from_start_col: u32,
1697        from_end_row: u32,
1698        from_end_col: u32,
1699        to_sheet_id: SheetId,
1700        to_row: u32,
1701        to_col: u32,
1702    ) -> Result<RangeSummary, EditorError> {
1703        let row_offset = to_row as i32 - from_start_row as i32;
1704        let col_offset = to_col as i32 - from_start_col as i32;
1705
1706        let mut summary = RangeSummary::default();
1707        let mut cell_data = Vec::new();
1708
1709        // Collect source data
1710        let vertices_in_range: Vec<_> = self
1711            .graph
1712            .vertices_in_sheet(sheet_id)
1713            .filter(|&id| {
1714                let coord = self.graph.get_coord(id);
1715                let row = coord.row();
1716                let col = coord.col();
1717                row >= from_start_row
1718                    && row <= from_end_row
1719                    && col >= from_start_col
1720                    && col <= from_end_col
1721            })
1722            .collect();
1723
1724        for id in vertices_in_range {
1725            let coord = self.graph.get_coord(id);
1726            let row = coord.row();
1727            let col = coord.col();
1728
1729            // Get value or formula
1730            if let Some(formula) = self.get_formula_ast(id) {
1731                cell_data.push((
1732                    row - from_start_row,
1733                    col - from_start_col,
1734                    CellData::Formula(formula),
1735                ));
1736            } else if let Some(value) = self.graph.get_value(id) {
1737                cell_data.push((
1738                    row - from_start_row,
1739                    col - from_start_col,
1740                    CellData::Value(value),
1741                ));
1742            }
1743        }
1744
1745        self.begin_batch();
1746
1747        // Apply to destination with relative adjustment
1748        for (row_idx, col_idx, data) in cell_data {
1749            let dest_row = (to_row as i32 + row_idx as i32) as u32;
1750            let dest_col = (to_col as i32 + col_idx as i32) as u32;
1751
1752            match data {
1753                CellData::Value(value) => {
1754                    let cell_ref =
1755                        self.graph
1756                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1757
1758                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1759                        self.graph.update_vertex_value(existing_id, value);
1760                        self.graph.mark_vertex_dirty(existing_id);
1761                        summary.vertices_updated.push(existing_id);
1762                    } else {
1763                        let meta =
1764                            VertexMeta::new(dest_row, dest_col, to_sheet_id, VertexKind::Cell);
1765                        let id = self.try_add_vertex(meta)?;
1766                        self.graph.update_vertex_value(id, value);
1767                        summary.vertices_created.push(id);
1768                    }
1769                }
1770                CellData::Formula(formula) => {
1771                    // Adjust relative references in formula
1772                    let adjuster = RelativeReferenceAdjuster::new(row_offset, col_offset);
1773                    let adjusted = adjuster.adjust_formula(&formula);
1774
1775                    let cell_ref =
1776                        self.graph
1777                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1778
1779                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1780                        self.graph.update_vertex_formula(existing_id, adjusted)?;
1781                        summary.vertices_updated.push(existing_id);
1782                    } else {
1783                        let meta = VertexMeta::new(
1784                            dest_row,
1785                            dest_col,
1786                            to_sheet_id,
1787                            VertexKind::FormulaScalar,
1788                        );
1789                        let id = self.try_add_vertex(meta)?;
1790                        self.graph.update_vertex_formula(id, adjusted)?;
1791                        summary.vertices_created.push(id);
1792                    }
1793                }
1794            }
1795
1796            summary.cells_affected += 1;
1797        }
1798
1799        self.commit_batch();
1800
1801        Ok(summary)
1802    }
1803
1804    /// Move a range to a new location (copy + clear source)
1805    pub fn move_range(
1806        &mut self,
1807        sheet_id: SheetId,
1808        from_start_row: u32,
1809        from_start_col: u32,
1810        from_end_row: u32,
1811        from_end_col: u32,
1812        to_sheet_id: SheetId,
1813        to_row: u32,
1814        to_col: u32,
1815    ) -> Result<RangeSummary, EditorError> {
1816        // First copy the range
1817        let mut summary = self.copy_range(
1818            sheet_id,
1819            from_start_row,
1820            from_start_col,
1821            from_end_row,
1822            from_end_col,
1823            to_sheet_id,
1824            to_row,
1825            to_col,
1826        )?;
1827
1828        // Then clear the source range
1829        let clear_summary = self.clear_range(
1830            sheet_id,
1831            from_start_row,
1832            from_start_col,
1833            from_end_row,
1834            from_end_col,
1835        )?;
1836
1837        summary.cells_moved = clear_summary.cells_affected;
1838
1839        // Update external references to moved cells
1840        let row_offset = to_row as i32 - from_start_row as i32;
1841        let col_offset = to_col as i32 - from_start_col as i32;
1842
1843        // Find all formulas that reference the moved range
1844        let all_formula_vertices: Vec<_> = self.graph.vertices_with_formulas().collect();
1845
1846        let from_sheet_name = self.graph.sheet_name(sheet_id).to_string();
1847        let to_sheet_name = self.graph.sheet_name(to_sheet_id).to_string();
1848        let adjuster = MoveReferenceAdjuster::new(
1849            sheet_id,
1850            from_sheet_name,
1851            from_start_row,
1852            from_start_col,
1853            from_end_row,
1854            from_end_col,
1855            to_sheet_id,
1856            to_sheet_name,
1857            row_offset,
1858            col_offset,
1859        );
1860
1861        for formula_id in all_formula_vertices {
1862            if let Some(formula) = self.get_formula_ast(formula_id) {
1863                let formula_sheet_id = self.graph.get_vertex_sheet_id(formula_id);
1864                if let Some(adjusted) = adjuster.adjust_if_references(&formula, formula_sheet_id) {
1865                    self.graph.update_vertex_formula(formula_id, adjusted)?;
1866                }
1867            }
1868        }
1869
1870        Ok(summary)
1871    }
1872
1873    /// Define a named range
1874    pub fn define_name(
1875        &mut self,
1876        name: &str,
1877        definition: NamedDefinition,
1878        scope: NameScope,
1879    ) -> Result<(), EditorError> {
1880        self.graph.define_name(name, definition.clone(), scope)?;
1881
1882        self.log_change(ChangeEvent::DefineName {
1883            name: name.to_string(),
1884            scope,
1885            definition,
1886        });
1887
1888        Ok(())
1889    }
1890
1891    /// Helper to create definitions from coordinates for a single cell
1892    pub fn define_name_for_cell(
1893        &mut self,
1894        name: &str,
1895        sheet_name: &str,
1896        row: u32,
1897        col: u32,
1898        scope: NameScope,
1899    ) -> Result<(), EditorError> {
1900        let sheet_id = self
1901            .graph
1902            .sheet_id(sheet_name)
1903            .ok_or_else(|| EditorError::InvalidName {
1904                name: sheet_name.to_string(),
1905                reason: "Sheet not found".to_string(),
1906            })?;
1907        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
1908        self.define_name(name, NamedDefinition::Cell(cell_ref), scope)
1909    }
1910
1911    /// Helper to create definitions from coordinates for a range
1912    pub fn define_name_for_range(
1913        &mut self,
1914        name: &str,
1915        sheet_name: &str,
1916        start_row: u32,
1917        start_col: u32,
1918        end_row: u32,
1919        end_col: u32,
1920        scope: NameScope,
1921    ) -> Result<(), EditorError> {
1922        let sheet_id = self
1923            .graph
1924            .sheet_id(sheet_name)
1925            .ok_or_else(|| EditorError::InvalidName {
1926                name: sheet_name.to_string(),
1927                reason: "Sheet not found".to_string(),
1928            })?;
1929        let start = CellRef::new(
1930            sheet_id,
1931            Coord::from_excel(start_row, start_col, true, true),
1932        );
1933        let end = CellRef::new(sheet_id, Coord::from_excel(end_row, end_col, true, true));
1934        let range_ref = crate::reference::RangeRef::new(start, end);
1935        self.define_name(name, NamedDefinition::Range(range_ref), scope)
1936    }
1937
1938    /// Update an existing named range definition
1939    pub fn update_name(
1940        &mut self,
1941        name: &str,
1942        new_definition: NamedDefinition,
1943        scope: NameScope,
1944    ) -> Result<(), EditorError> {
1945        // Get the old definition for the change log
1946        let old_definition = self
1947            .graph
1948            .resolve_name(
1949                name,
1950                match scope {
1951                    NameScope::Sheet(id) => id,
1952                    NameScope::Workbook => 0,
1953                },
1954            )
1955            .cloned();
1956
1957        self.graph
1958            .update_name(name, new_definition.clone(), scope)?;
1959
1960        if let Some(old_def) = old_definition {
1961            self.log_change(ChangeEvent::UpdateName {
1962                name: name.to_string(),
1963                scope,
1964                old_definition: old_def,
1965                new_definition,
1966            });
1967        }
1968
1969        Ok(())
1970    }
1971
1972    /// Delete a named range
1973    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), EditorError> {
1974        // Capture old definition *before* deletion so undo can restore it.
1975        let old_def = if self.has_logger() {
1976            self.graph
1977                .resolve_name(
1978                    name,
1979                    match scope {
1980                        NameScope::Sheet(id) => id,
1981                        NameScope::Workbook => 0,
1982                    },
1983                )
1984                .cloned()
1985        } else {
1986            None
1987        };
1988
1989        self.graph.delete_name(name, scope)?;
1990        self.log_change(ChangeEvent::DeleteName {
1991            name: name.to_string(),
1992            scope,
1993            old_definition: old_def,
1994        });
1995
1996        Ok(())
1997    }
1998}
1999
2000/// Helper enum for cell data
2001enum CellData {
2002    Value(LiteralValue),
2003    Formula(ASTNode),
2004}
2005
2006impl<'g> Drop for VertexEditor<'g> {
2007    fn drop(&mut self) {
2008        // Ensure batch operations are committed when the editor is dropped
2009        if self.batch_mode {
2010            self.commit_batch();
2011        }
2012    }
2013}
2014
2015#[cfg(test)]
2016mod tests {
2017    use super::*;
2018    use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog};
2019    use crate::reference::Coord;
2020
2021    fn create_test_graph() -> DependencyGraph {
2022        DependencyGraph::new()
2023    }
2024
2025    #[test]
2026    fn test_vertex_editor_creation() {
2027        let mut graph = create_test_graph();
2028        let editor = VertexEditor::new(&mut graph);
2029        assert!(!editor.has_logger());
2030        assert!(!editor.batch_mode);
2031    }
2032
2033    #[test]
2034    fn test_vertex_editor_with_logger() {
2035        let mut graph = create_test_graph();
2036        let mut log = ChangeLog::new();
2037        let editor = VertexEditor::with_logger(&mut graph, &mut log);
2038        assert!(editor.has_logger());
2039        assert!(!editor.batch_mode);
2040    }
2041
2042    #[test]
2043    fn test_add_vertex() {
2044        let mut graph = create_test_graph();
2045        let mut editor = VertexEditor::new(&mut graph);
2046
2047        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell).dirty();
2048        let vertex_id = editor.add_vertex(meta);
2049
2050        // Verify vertex was created (simplified check)
2051        assert!(vertex_id.0 > 0);
2052    }
2053
2054    #[test]
2055    fn test_batch_operations() {
2056        let mut graph = create_test_graph();
2057        let mut editor = VertexEditor::new(&mut graph);
2058
2059        assert!(!editor.batch_mode);
2060        editor.begin_batch();
2061        assert!(editor.batch_mode);
2062
2063        // Add multiple vertices in batch mode
2064        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2065        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::Cell);
2066
2067        let id1 = editor.add_vertex(meta1);
2068        let id2 = editor.add_vertex(meta2);
2069
2070        // Add edge between them
2071        assert!(editor.add_edge(id1, id2));
2072
2073        editor.commit_batch();
2074        assert!(!editor.batch_mode);
2075    }
2076
2077    #[test]
2078    fn test_remove_vertex() {
2079        let mut graph = create_test_graph();
2080        let mut editor = VertexEditor::new(&mut graph);
2081
2082        let meta = VertexMeta::new(3, 4, 0, VertexKind::Cell).dirty();
2083        let vertex_id = editor.add_vertex(meta);
2084
2085        // Now removal returns Result
2086        assert!(editor.remove_vertex(vertex_id).is_ok());
2087    }
2088
2089    #[test]
2090    fn test_remove_vertex_clears_spill_registry_for_anchor() {
2091        let mut graph = create_test_graph();
2092        let sheet_id = graph.sheet_id_mut("Sheet1");
2093
2094        // Create anchor vertex at A1 (0-based internal coord 0,0).
2095        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
2096        let anchor_vid = {
2097            let mut editor = VertexEditor::new(&mut graph);
2098            editor.set_cell_value(anchor_cell, LiteralValue::Number(0.0))
2099        };
2100
2101        let target_cells = vec![
2102            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
2103            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
2104            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
2105            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
2106        ];
2107        let values = vec![
2108            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
2109            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
2110        ];
2111
2112        graph
2113            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
2114            .unwrap();
2115
2116        assert!(graph.spill_registry_has_anchor(anchor_vid));
2117        for cell in &target_cells {
2118            assert_eq!(
2119                graph.spill_registry_anchor_for_cell(*cell),
2120                Some(anchor_vid)
2121            );
2122        }
2123
2124        {
2125            let mut editor = VertexEditor::new(&mut graph);
2126            editor.remove_vertex(anchor_vid).unwrap();
2127        }
2128
2129        assert!(!graph.spill_registry_has_anchor(anchor_vid));
2130        for cell in &target_cells {
2131            assert_eq!(graph.spill_registry_anchor_for_cell(*cell), None);
2132        }
2133        assert_eq!(graph.spill_registry_counts(), (0, 0));
2134    }
2135
2136    #[test]
2137    fn test_edge_operations() {
2138        let mut graph = create_test_graph();
2139        let mut editor = VertexEditor::new(&mut graph);
2140
2141        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2142        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::FormulaScalar);
2143
2144        let id1 = editor.add_vertex(meta1);
2145        let id2 = editor.add_vertex(meta2);
2146
2147        // Add edge
2148        assert!(editor.add_edge(id1, id2));
2149
2150        // Prevent self-loop
2151        assert!(!editor.add_edge(id1, id1));
2152
2153        // Remove edge
2154        assert!(editor.remove_edge(id1, id2));
2155    }
2156
2157    #[test]
2158    fn test_set_cell_value() {
2159        let mut graph = create_test_graph();
2160        let mut log = ChangeLog::new();
2161
2162        let cell_ref = CellRef {
2163            sheet_id: 0,
2164            coord: Coord::new(2, 3, true, true),
2165        };
2166        let value = LiteralValue::Number(42.0);
2167
2168        let vertex_id = {
2169            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2170            editor.set_cell_value(cell_ref, value.clone())
2171        };
2172
2173        // Verify vertex was created (simplified check)
2174        assert!(vertex_id.0 > 0);
2175
2176        // Verify change log
2177        assert_eq!(log.len(), 1);
2178        match &log.events()[0] {
2179            ChangeEvent::SetValue { addr, new, .. } => {
2180                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2181                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2182                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2183                assert_eq!(new, &value);
2184            }
2185            _ => panic!("Expected SetValue event"),
2186        }
2187    }
2188
2189    #[test]
2190    fn test_set_cell_formula() {
2191        let mut graph = create_test_graph();
2192        let mut log = ChangeLog::new();
2193
2194        let cell_ref = CellRef {
2195            sheet_id: 0,
2196            coord: Coord::new(1, 1, true, true),
2197        };
2198
2199        use formualizer_parse::parser::ASTNodeType;
2200        let formula = formualizer_parse::parser::ASTNode {
2201            node_type: ASTNodeType::Literal(LiteralValue::Number(100.0)),
2202            source_token: None,
2203            contains_volatile: false,
2204        };
2205
2206        let vertex_id = {
2207            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2208            editor.set_cell_formula(cell_ref, formula.clone())
2209        };
2210
2211        // Verify vertex was created (simplified check)
2212        assert!(vertex_id.0 > 0);
2213
2214        // Verify change log
2215        assert_eq!(log.len(), 1);
2216        match &log.events()[0] {
2217            ChangeEvent::SetFormula { addr, .. } => {
2218                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2219                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2220                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2221            }
2222            _ => panic!("Expected SetFormula event"),
2223        }
2224    }
2225
2226    #[test]
2227    fn test_shift_rows() {
2228        let mut graph = create_test_graph();
2229        let mut log = ChangeLog::new();
2230
2231        {
2232            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2233
2234            // Create vertices at different rows
2235            let cell1 = CellRef {
2236                sheet_id: 0,
2237                coord: Coord::new(5, 1, true, true),
2238            };
2239            let cell2 = CellRef {
2240                sheet_id: 0,
2241                coord: Coord::new(10, 1, true, true),
2242            };
2243            let cell3 = CellRef {
2244                sheet_id: 0,
2245                coord: Coord::new(15, 1, true, true),
2246            };
2247
2248            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2249            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2250            editor.set_cell_value(cell3, LiteralValue::Number(3.0));
2251        }
2252
2253        // Clear change log to focus on shift operation
2254        log.clear();
2255
2256        {
2257            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2258            // Shift rows starting at row 10, moving down by 2
2259            editor.shift_rows(0, 10, 2);
2260        }
2261
2262        // Verify change log contains the shift operation
2263        assert_eq!(log.len(), 1);
2264        match &log.events()[0] {
2265            ChangeEvent::SetValue { addr, new, .. } => {
2266                assert_eq!(addr.sheet_id, 0);
2267                assert_eq!(addr.coord.row(), 10);
2268                if let LiteralValue::Text(msg) = new {
2269                    assert!(msg.contains("Row shift"));
2270                    assert!(msg.contains("start=10"));
2271                    assert!(msg.contains("delta=2"));
2272                }
2273            }
2274            _ => panic!("Expected SetValue event for row shift"),
2275        }
2276    }
2277
2278    #[test]
2279    fn test_shift_columns() {
2280        let mut graph = create_test_graph();
2281        let mut log = ChangeLog::new();
2282
2283        {
2284            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2285
2286            // Create vertices at different columns
2287            let cell1 = CellRef {
2288                sheet_id: 0,
2289                coord: Coord::new(1, 5, true, true),
2290            };
2291            let cell2 = CellRef {
2292                sheet_id: 0,
2293                coord: Coord::new(1, 10, true, true),
2294            };
2295
2296            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2297            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2298        }
2299
2300        // Clear change log
2301        log.clear();
2302
2303        {
2304            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2305            // Shift columns starting at col 8, moving right by 3
2306            editor.shift_columns(0, 8, 3);
2307        }
2308
2309        // Verify change log
2310        assert_eq!(log.len(), 1);
2311        match &log.events()[0] {
2312            ChangeEvent::SetValue { addr, new, .. } => {
2313                assert_eq!(addr.sheet_id, 0);
2314                assert_eq!(addr.coord.col(), 8);
2315                if let LiteralValue::Text(msg) = new {
2316                    assert!(msg.contains("Column shift"));
2317                    assert!(msg.contains("start=8"));
2318                    assert!(msg.contains("delta=3"));
2319                }
2320            }
2321            _ => panic!("Expected SetValue event for column shift"),
2322        }
2323    }
2324
2325    #[test]
2326    fn test_move_vertex() {
2327        let mut graph = create_test_graph();
2328        let mut editor = VertexEditor::new(&mut graph);
2329
2330        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell);
2331        let vertex_id = editor.add_vertex(meta);
2332
2333        // Move vertex returns Result
2334        assert!(editor.move_vertex(vertex_id, AbsCoord::new(8, 12)).is_ok());
2335
2336        // Moving to same position should work
2337        assert!(editor.move_vertex(vertex_id, AbsCoord::new(8, 12)).is_ok());
2338    }
2339
2340    #[test]
2341    fn test_vertex_meta_builder() {
2342        let meta = VertexMeta::new(1, 2, 3, VertexKind::FormulaScalar)
2343            .dirty()
2344            .volatile()
2345            .with_flags(0x08);
2346
2347        assert_eq!(meta.coord.row(), 1);
2348        assert_eq!(meta.coord.col(), 2);
2349        assert_eq!(meta.sheet_id, 3);
2350        assert_eq!(meta.kind, VertexKind::FormulaScalar);
2351        assert_eq!(meta.flags, 0x08); // Last with_flags call overwrites previous flags
2352    }
2353
2354    #[test]
2355    fn test_change_log_management() {
2356        let mut graph = create_test_graph();
2357        let mut log = ChangeLog::new();
2358
2359        {
2360            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2361            let cell_ref = CellRef {
2362                sheet_id: 0,
2363                coord: Coord::new(0, 0, true, true),
2364            };
2365            editor.set_cell_value(cell_ref, LiteralValue::Number(1.0));
2366            editor.set_cell_value(cell_ref, LiteralValue::Number(2.0));
2367        }
2368
2369        assert_eq!(log.len(), 2);
2370
2371        log.clear();
2372        assert_eq!(log.len(), 0);
2373    }
2374
2375    #[test]
2376    fn test_editor_drop_commits_batch() {
2377        let mut graph = create_test_graph();
2378        {
2379            let mut editor = VertexEditor::new(&mut graph);
2380            editor.begin_batch();
2381
2382            let meta = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2383            editor.add_vertex(meta);
2384
2385            // Editor will be dropped here, should commit batch
2386        }
2387
2388        // If we reach here without hanging, the batch was properly committed
2389    }
2390}