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