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::AddVertex { id, .. } => {
442                // Inverse of AddVertex is removal
443                let _ = self.remove_vertex(id); // ignore errors for now
444            }
445            ChangeEvent::RemoveVertex {
446                id: _,
447                old_value,
448                old_formula,
449                old_dependencies,
450                old_dependents,
451                coord,
452                sheet_id,
453                kind,
454                ..
455            } => {
456                if let (Some(c), Some(sid)) = (coord, sheet_id) {
457                    let meta =
458                        VertexMeta::new(c.row(), c.col(), sid, kind.unwrap_or(VertexKind::Cell));
459                    let new_id = self.add_vertex(meta);
460                    if let Some(v) = old_value {
461                        let cell_ref = self.graph.make_cell_ref_internal(sid, c.row(), c.col());
462                        self.set_cell_value(cell_ref, v);
463                    }
464                    if let Some(f) = old_formula {
465                        let cell_ref = self.graph.make_cell_ref_internal(sid, c.row(), c.col());
466                        self.set_cell_formula(cell_ref, f);
467                    }
468                    for dep in old_dependencies {
469                        self.graph.add_dependency_edge(new_id, dep);
470                    }
471                    for parent in old_dependents {
472                        self.graph.add_dependency_edge(parent, new_id);
473                    }
474                }
475            }
476            ChangeEvent::DefineName { name, scope, .. } => {
477                // Inverse is delete name
478                self.graph.delete_name(&name, scope)?;
479            }
480            ChangeEvent::UpdateName {
481                name,
482                scope,
483                old_definition,
484                ..
485            } => {
486                // Restore old definition
487                self.graph.update_name(&name, old_definition, scope)?;
488            }
489            ChangeEvent::DeleteName {
490                name,
491                scope,
492                old_definition,
493            } => {
494                if let Some(def) = old_definition {
495                    self.graph.define_name(&name, def, scope)?;
496                } else {
497                    return Err(EditorError::TransactionFailed {
498                        reason: "Missing old definition for name deletion rollback".to_string(),
499                    });
500                }
501            }
502            ChangeEvent::SpillCommitted { anchor, old, .. } => {
503                // Restore previous spill region.
504                if let Some(old) = old {
505                    self.graph
506                        .commit_spill_region_atomic_with_fault(
507                            anchor,
508                            old.target_cells,
509                            old.values,
510                            None,
511                        )
512                        .map_err(EditorError::Excel)?;
513                } else {
514                    self.graph.clear_spill_region(anchor);
515                }
516            }
517            ChangeEvent::SpillCleared { anchor, old } => {
518                // Re-commit the previous spill region.
519                self.graph
520                    .commit_spill_region_atomic_with_fault(
521                        anchor,
522                        old.target_cells,
523                        old.values,
524                        None,
525                    )
526                    .map_err(EditorError::Excel)?;
527            }
528            // Granular events for compound operations
529            ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
530                // These are markers, no inverse needed
531            }
532            ChangeEvent::VertexMoved {
533                id,
534                sheet_id: _,
535                old_coord,
536                ..
537            } => {
538                // Move back to old position
539                self.move_vertex(id, old_coord)?;
540            }
541            ChangeEvent::FormulaAdjusted { id, old_ast, .. } => {
542                // Restore old formula directly by vertex id.
543                self.graph
544                    .update_vertex_formula(id, old_ast)
545                    .map_err(EditorError::Excel)?;
546                self.graph.mark_vertex_dirty(id);
547            }
548            ChangeEvent::NamedRangeAdjusted {
549                name,
550                scope,
551                old_definition,
552                ..
553            } => {
554                // Restore old definition
555                self.graph.update_name(&name, old_definition, scope)?;
556            }
557            ChangeEvent::EdgeAdded { from, to } => {
558                // Remove the edge
559                // TODO: Need specific edge removal method
560                return Err(EditorError::TransactionFailed {
561                    reason: "Cannot rollback edge addition yet".to_string(),
562                });
563            }
564            ChangeEvent::EdgeRemoved { from, to } => {
565                // Re-add the edge
566                // TODO: Need specific edge addition method
567                return Err(EditorError::TransactionFailed {
568                    reason: "Cannot rollback edge removal yet".to_string(),
569                });
570            }
571        }
572        Ok(())
573    }
574
575    /// Add a vertex to the graph
576    pub fn add_vertex(&mut self, meta: VertexMeta) -> VertexId {
577        // For now, use the existing set_cell_value method to create vertices
578        // This is a simplified implementation that works with the current API
579        let sheet_name = self.graph.sheet_name(meta.sheet_id).to_string();
580
581        let id = match meta.kind {
582            VertexKind::Cell => {
583                // Create with empty value initially.
584                // NOTE: VertexEditor/VertexMeta use internal 0-based coords, while
585                // DependencyGraph::set_cell_value is a public 1-based API. Convert here.
586                match self.graph.set_cell_value(
587                    &sheet_name,
588                    meta.coord.row() + 1,
589                    meta.coord.col() + 1,
590                    LiteralValue::Empty,
591                ) {
592                    Ok(summary) => summary
593                        .affected_vertices
594                        .into_iter()
595                        .next()
596                        .unwrap_or(VertexId::new(0)),
597                    Err(_) => VertexId::new(0),
598                }
599            }
600            _ => {
601                // For now, treat other kinds as cells.
602                // A full implementation would handle different vertex kinds properly.
603                // Convert internal 0-based coords to public 1-based API.
604                match self.graph.set_cell_value(
605                    &sheet_name,
606                    meta.coord.row() + 1,
607                    meta.coord.col() + 1,
608                    LiteralValue::Empty,
609                ) {
610                    Ok(summary) => summary
611                        .affected_vertices
612                        .into_iter()
613                        .next()
614                        .unwrap_or(VertexId::new(0)),
615                    Err(_) => VertexId::new(0),
616                }
617            }
618        };
619
620        if self.has_logger() && id.0 != 0 {
621            self.log_change(ChangeEvent::AddVertex {
622                id,
623                coord: meta.coord,
624                sheet_id: meta.sheet_id,
625                value: Some(LiteralValue::Empty),
626                formula: None,
627                kind: Some(meta.kind),
628                flags: Some(meta.flags),
629            });
630        }
631        id
632    }
633
634    /// Remove a vertex from the graph with proper cleanup
635    pub fn remove_vertex(&mut self, id: VertexId) -> Result<(), EditorError> {
636        // Check if vertex exists
637        if !self.graph.vertex_exists(id) {
638            return Err(EditorError::Excel(
639                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
640            ));
641        }
642
643        // If this vertex anchors a spill, clear ownership + spilled children first.
644        // This keeps the spill registry consistent even if the anchor is removed.
645        let spill_snapshot = self.snapshot_spill_for_anchor(id);
646        let did_spill_clear = spill_snapshot.is_some();
647        if let Some(old_spill) = spill_snapshot {
648            if let Some(logger) = &mut self.change_logger {
649                logger.begin_compound(format!("RemoveVertexWithSpillClear id={}", id.0));
650            }
651            self.graph.clear_spill_region(id);
652            self.log_change(ChangeEvent::SpillCleared {
653                anchor: id,
654                old: old_spill,
655            });
656        }
657
658        // Get dependents before removing edges
659        // Note: get_dependents may require CSR rebuild if delta has changes
660        let dependents = self.graph.get_dependents(id);
661
662        // Capture old state (dependencies & dependents) BEFORE edge removal
663        let (
664            old_value,
665            old_formula,
666            old_dependencies,
667            old_dependents,
668            coord,
669            sheet_id_opt,
670            kind,
671            flags,
672        ) = if self.has_logger() {
673            let coord = self.graph.get_coord(id);
674            let sheet_id = self.graph.get_sheet_id(id);
675            let kind = self.graph.get_vertex_kind(id);
676            // flags not publicly exposed; set to 0 for now (future: expose getter)
677            let flags = 0u8;
678            (
679                self.graph.get_value(id),
680                self.get_formula_ast(id),
681                self.graph.get_dependencies(id), // outgoing deps
682                dependents.clone(),              // captured earlier
683                Some(coord),
684                Some(sheet_id),
685                Some(kind),
686                Some(flags),
687            )
688        } else {
689            (None, None, vec![], vec![], None, None, None, None)
690        };
691
692        // Remove from cell mapping if it exists
693        if let Some(cell_ref) = self.graph.get_cell_ref_for_vertex(id) {
694            self.graph.remove_cell_mapping(&cell_ref);
695        }
696
697        // Remove all edges
698        self.graph.remove_all_edges(id);
699
700        // Mark all dependents as having #REF! error
701        for dep_id in &dependents {
702            self.graph.mark_as_ref_error(*dep_id);
703        }
704
705        // Mark as deleted in store (tombstone)
706        self.graph.mark_deleted(id, true);
707
708        // Log change event
709        self.log_change(ChangeEvent::RemoveVertex {
710            id,
711            old_value,
712            old_formula,
713            old_dependencies,
714            old_dependents,
715            coord,
716            sheet_id: sheet_id_opt,
717            kind,
718            flags,
719        });
720
721        if did_spill_clear && let Some(logger) = &mut self.change_logger {
722            logger.end_compound();
723        }
724
725        Ok(())
726    }
727
728    /// Convenience: remove vertex at a given cell ref if exists
729    pub fn remove_vertex_at(&mut self, cell: CellRef) -> Result<(), EditorError> {
730        if let Some(id) = self.graph.get_vertex_for_cell(&cell) {
731            self.remove_vertex(id)
732        } else {
733            Ok(())
734        }
735    }
736
737    /// Move a vertex to a new position
738    pub fn move_vertex(&mut self, id: VertexId, new_coord: AbsCoord) -> Result<(), EditorError> {
739        // Check if vertex exists
740        if !self.graph.vertex_exists(id) {
741            return Err(EditorError::Excel(
742                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
743            ));
744        }
745
746        // Get old cell reference
747        let old_cell_ref = self.graph.get_cell_ref_for_vertex(id);
748
749        // Create new cell reference
750        let sheet_id = self.graph.get_sheet_id(id);
751        let new_cell_ref = CellRef::new(
752            sheet_id,
753            Coord::new(new_coord.row(), new_coord.col(), true, true),
754        );
755
756        // Update coordinate in store
757        self.graph.set_coord(id, new_coord);
758
759        // Update edge cache coordinate if needed
760        self.graph.update_edge_coord(id, new_coord);
761
762        // Update cell mapping
763        self.graph
764            .update_cell_mapping(id, old_cell_ref, new_cell_ref);
765
766        // Mark dependents as dirty
767        self.graph.mark_dependents_dirty(id);
768
769        Ok(())
770    }
771
772    /// Update vertex metadata
773    pub fn patch_vertex_meta(
774        &mut self,
775        id: VertexId,
776        patch: VertexMetaPatch,
777    ) -> Result<MetaUpdateSummary, EditorError> {
778        if !self.graph.vertex_exists(id) {
779            return Err(EditorError::Excel(
780                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
781            ));
782        }
783
784        let mut summary = MetaUpdateSummary::default();
785
786        if let Some(coord) = patch.coord {
787            self.graph.set_coord(id, coord);
788            self.graph.update_edge_coord(id, coord);
789            summary.coord_changed = true;
790        }
791
792        if let Some(kind) = patch.kind {
793            self.graph.set_kind(id, kind);
794            summary.kind_changed = true;
795        }
796
797        if let Some(dirty) = patch.dirty {
798            self.graph.set_dirty(id, dirty);
799            summary.flags_changed = true;
800        }
801
802        if let Some(volatile) = patch.volatile {
803            self.graph.mark_volatile(id, volatile);
804            summary.flags_changed = true;
805        }
806
807        Ok(summary)
808    }
809
810    /// Update vertex data (value or formula)
811    pub fn patch_vertex_data(
812        &mut self,
813        id: VertexId,
814        patch: VertexDataPatch,
815    ) -> Result<DataUpdateSummary, EditorError> {
816        if !self.graph.vertex_exists(id) {
817            return Err(EditorError::Excel(
818                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
819            ));
820        }
821
822        let mut summary = DataUpdateSummary::default();
823
824        if let Some(value) = patch.value {
825            self.graph.update_vertex_value(id, value);
826            summary.value_changed = true;
827
828            // Force edge rebuild if needed to get accurate dependents
829            // get_dependents may require rebuild when delta has changes
830            if self.graph.edges_delta_size() > 0 {
831                self.graph.rebuild_edges();
832            }
833
834            // Mark dependents as dirty
835            let dependents = self.graph.get_dependents(id);
836            for dep in &dependents {
837                self.graph.set_dirty(*dep, true);
838            }
839            summary.dependents_marked_dirty = dependents;
840        }
841
842        if let Some(_formula) = patch.formula {
843            // This would need proper formula update implementation
844            // For now, we'll mark as changed
845            summary.formula_changed = true;
846        }
847
848        Ok(summary)
849    }
850
851    /// Add an edge between two vertices
852    pub fn add_edge(&mut self, from: VertexId, to: VertexId) -> bool {
853        if from == to {
854            return false; // Prevent self-loops
855        }
856
857        // TODO: Add edge through proper API when available
858        // For now, return true to indicate intent
859        true
860    }
861
862    /// Remove an edge between two vertices
863    pub fn remove_edge(&mut self, _from: VertexId, _to: VertexId) -> bool {
864        // TODO: Remove edge through proper API when available
865        true
866    }
867
868    /// Insert rows at the specified position, shifting existing rows down
869    pub fn insert_rows(
870        &mut self,
871        sheet_id: SheetId,
872        before: u32,
873        count: u32,
874    ) -> Result<ShiftSummary, EditorError> {
875        if count == 0 {
876            return Ok(ShiftSummary::default());
877        }
878
879        let mut summary = ShiftSummary::default();
880
881        // Begin batch for efficiency
882        self.begin_batch();
883
884        // 1. Collect vertices to shift (those at or after the insert point)
885        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
886            .graph
887            .vertices_in_sheet(sheet_id)
888            .filter_map(|id| {
889                let coord = self.graph.get_coord(id);
890                if coord.row() >= before {
891                    Some((id, coord))
892                } else {
893                    None
894                }
895            })
896            .collect();
897
898        if let Some(logger) = &mut self.change_logger {
899            logger.begin_compound(format!(
900                "InsertRows sheet={sheet_id} before={before} count={count}"
901            ));
902        }
903        // 2. Shift vertices down (emit VertexMoved)
904        for (id, old_coord) in vertices_to_shift {
905            let new_coord = AbsCoord::new(old_coord.row() + count, old_coord.col());
906            if self.has_logger() {
907                self.log_change(ChangeEvent::VertexMoved {
908                    id,
909                    sheet_id,
910                    old_coord,
911                    new_coord,
912                });
913            }
914            self.move_vertex(id, new_coord)?;
915            summary.vertices_moved.push(id);
916        }
917
918        // 3. Adjust formulas using ReferenceAdjuster
919        let op = ShiftOperation::InsertRows {
920            sheet_id,
921            before,
922            count,
923        };
924        let adjuster = ReferenceAdjuster::new();
925
926        // Get all formulas and adjust them
927        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
928
929        for id in formula_vertices {
930            if let Some(ast) = self.get_formula_ast(id) {
931                let adjusted = adjuster.adjust_ast(&ast, &op);
932                // Only update if the formula actually changed
933                if format!("{ast:?}") != format!("{adjusted:?}") {
934                    if self.has_logger() {
935                        self.log_change(ChangeEvent::FormulaAdjusted {
936                            id,
937                            addr: self.graph.get_cell_ref_for_vertex(id),
938                            old_ast: ast.clone(),
939                            new_ast: adjusted.clone(),
940                        });
941                    }
942                    self.graph.update_vertex_formula(id, adjusted)?;
943                    self.graph.mark_vertex_dirty(id);
944                    summary.formulas_updated += 1;
945                }
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 adjusted = adjuster.adjust_ast(&ast, &op);
1058                if format!("{ast:?}") != format!("{adjusted:?}") {
1059                    if self.has_logger() {
1060                        self.log_change(ChangeEvent::FormulaAdjusted {
1061                            id,
1062                            addr: self.graph.get_cell_ref_for_vertex(id),
1063                            old_ast: ast.clone(),
1064                            new_ast: adjusted.clone(),
1065                        });
1066                    }
1067                    self.graph.update_vertex_formula(id, adjusted)?;
1068                    self.graph.mark_vertex_dirty(id);
1069                    summary.formulas_updated += 1;
1070                }
1071            }
1072        }
1073
1074        // 4. Adjust named ranges
1075        let old_names = if self.has_logger() {
1076            Some(self.snapshot_named_definitions())
1077        } else {
1078            None
1079        };
1080        self.graph.adjust_named_ranges(&op)?;
1081        if let Some(old_names) = old_names {
1082            let new_names = self.snapshot_named_definitions();
1083            for ((scope, name), old_definition) in old_names {
1084                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1085                    && *new_definition != old_definition
1086                {
1087                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1088                        name,
1089                        scope,
1090                        old_definition,
1091                        new_definition: new_definition.clone(),
1092                    });
1093                }
1094            }
1095        }
1096
1097        // 5. Log change event
1098        if let Some(logger) = &mut self.change_logger {
1099            logger.end_compound();
1100        }
1101
1102        self.commit_batch();
1103
1104        Ok(summary)
1105    }
1106
1107    /// Insert columns at the specified position, shifting existing columns right
1108    pub fn insert_columns(
1109        &mut self,
1110        sheet_id: SheetId,
1111        before: u32,
1112        count: u32,
1113    ) -> Result<ShiftSummary, EditorError> {
1114        if count == 0 {
1115            return Ok(ShiftSummary::default());
1116        }
1117
1118        let mut summary = ShiftSummary::default();
1119
1120        // Begin batch for efficiency
1121        self.begin_batch();
1122
1123        // 1. Collect vertices to shift (those at or after the insert point)
1124        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
1125            .graph
1126            .vertices_in_sheet(sheet_id)
1127            .filter_map(|id| {
1128                let coord = self.graph.get_coord(id);
1129                if coord.col() >= before {
1130                    Some((id, coord))
1131                } else {
1132                    None
1133                }
1134            })
1135            .collect();
1136
1137        if let Some(logger) = &mut self.change_logger {
1138            logger.begin_compound(format!(
1139                "InsertColumns sheet={sheet_id} before={before} count={count}"
1140            ));
1141        }
1142        // 2. Shift vertices right (emit VertexMoved)
1143        for (id, old_coord) in vertices_to_shift {
1144            let new_coord = AbsCoord::new(old_coord.row(), old_coord.col() + count);
1145            if self.has_logger() {
1146                self.log_change(ChangeEvent::VertexMoved {
1147                    id,
1148                    sheet_id,
1149                    old_coord,
1150                    new_coord,
1151                });
1152            }
1153            self.move_vertex(id, new_coord)?;
1154            summary.vertices_moved.push(id);
1155        }
1156
1157        // 3. Adjust formulas using ReferenceAdjuster
1158        let op = ShiftOperation::InsertColumns {
1159            sheet_id,
1160            before,
1161            count,
1162        };
1163        let adjuster = ReferenceAdjuster::new();
1164
1165        // Get all formulas and adjust them
1166        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
1167
1168        for id in formula_vertices {
1169            if let Some(ast) = self.get_formula_ast(id) {
1170                let adjusted = adjuster.adjust_ast(&ast, &op);
1171                // Only update if the formula actually changed
1172                if format!("{ast:?}") != format!("{adjusted:?}") {
1173                    if self.has_logger() {
1174                        self.log_change(ChangeEvent::FormulaAdjusted {
1175                            id,
1176                            addr: self.graph.get_cell_ref_for_vertex(id),
1177                            old_ast: ast.clone(),
1178                            new_ast: adjusted.clone(),
1179                        });
1180                    }
1181                    self.graph.update_vertex_formula(id, adjusted)?;
1182                    self.graph.mark_vertex_dirty(id);
1183                    summary.formulas_updated += 1;
1184                }
1185            }
1186        }
1187
1188        // 4. Adjust named ranges
1189        let old_names = if self.has_logger() {
1190            Some(self.snapshot_named_definitions())
1191        } else {
1192            None
1193        };
1194        self.graph.adjust_named_ranges(&op)?;
1195        if let Some(old_names) = old_names {
1196            let new_names = self.snapshot_named_definitions();
1197            for ((scope, name), old_definition) in old_names {
1198                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1199                    && *new_definition != old_definition
1200                {
1201                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1202                        name,
1203                        scope,
1204                        old_definition,
1205                        new_definition: new_definition.clone(),
1206                    });
1207                }
1208            }
1209        }
1210
1211        // 5. Log change event
1212        if let Some(logger) = &mut self.change_logger {
1213            logger.end_compound();
1214        }
1215
1216        self.commit_batch();
1217
1218        Ok(summary)
1219    }
1220
1221    /// Delete columns at the specified position, shifting remaining columns left
1222    pub fn delete_columns(
1223        &mut self,
1224        sheet_id: SheetId,
1225        start: u32,
1226        count: u32,
1227    ) -> Result<ShiftSummary, EditorError> {
1228        if count == 0 {
1229            return Ok(ShiftSummary::default());
1230        }
1231
1232        let mut summary = ShiftSummary::default();
1233
1234        self.begin_batch();
1235
1236        if let Some(logger) = &mut self.change_logger {
1237            logger.begin_compound(format!(
1238                "DeleteColumns sheet={sheet_id} start={start} count={count}"
1239            ));
1240        }
1241
1242        // 1. Delete vertices in the range
1243        let vertices_to_delete: Vec<VertexId> = self
1244            .graph
1245            .vertices_in_sheet(sheet_id)
1246            .filter(|&id| {
1247                let coord = self.graph.get_coord(id);
1248                coord.col() >= start && coord.col() < start + count
1249            })
1250            .collect();
1251
1252        for id in vertices_to_delete {
1253            self.remove_vertex(id)?;
1254            summary.vertices_deleted.push(id);
1255        }
1256        // 2. Shift remaining vertices left (emit VertexMoved)
1257        let vertices_to_shift: Vec<(VertexId, AbsCoord)> = self
1258            .graph
1259            .vertices_in_sheet(sheet_id)
1260            .filter_map(|id| {
1261                let coord = self.graph.get_coord(id);
1262                if coord.col() >= start + count {
1263                    Some((id, coord))
1264                } else {
1265                    None
1266                }
1267            })
1268            .collect();
1269
1270        for (id, old_coord) in vertices_to_shift {
1271            let new_coord = AbsCoord::new(old_coord.row(), old_coord.col() - count);
1272            if self.has_logger() {
1273                self.log_change(ChangeEvent::VertexMoved {
1274                    id,
1275                    sheet_id,
1276                    old_coord,
1277                    new_coord,
1278                });
1279            }
1280            self.move_vertex(id, new_coord)?;
1281            summary.vertices_moved.push(id);
1282        }
1283
1284        // 3. Adjust formulas
1285        let op = ShiftOperation::DeleteColumns {
1286            sheet_id,
1287            start,
1288            count,
1289        };
1290        let adjuster = ReferenceAdjuster::new();
1291
1292        let formula_vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
1293
1294        for id in formula_vertices {
1295            if let Some(ast) = self.get_formula_ast(id) {
1296                let adjusted = adjuster.adjust_ast(&ast, &op);
1297                if format!("{ast:?}") != format!("{adjusted:?}") {
1298                    if self.has_logger() {
1299                        self.log_change(ChangeEvent::FormulaAdjusted {
1300                            id,
1301                            addr: self.graph.get_cell_ref_for_vertex(id),
1302                            old_ast: ast.clone(),
1303                            new_ast: adjusted.clone(),
1304                        });
1305                    }
1306                    self.graph.update_vertex_formula(id, adjusted)?;
1307                    self.graph.mark_vertex_dirty(id);
1308                    summary.formulas_updated += 1;
1309                }
1310            }
1311        }
1312
1313        // 4. Adjust named ranges
1314        let old_names = if self.has_logger() {
1315            Some(self.snapshot_named_definitions())
1316        } else {
1317            None
1318        };
1319        self.graph.adjust_named_ranges(&op)?;
1320        if let Some(old_names) = old_names {
1321            let new_names = self.snapshot_named_definitions();
1322            for ((scope, name), old_definition) in old_names {
1323                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1324                    && *new_definition != old_definition
1325                {
1326                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1327                        name,
1328                        scope,
1329                        old_definition,
1330                        new_definition: new_definition.clone(),
1331                    });
1332                }
1333            }
1334        }
1335
1336        // 5. Log change event
1337        if let Some(logger) = &mut self.change_logger {
1338            logger.end_compound();
1339        }
1340
1341        self.commit_batch();
1342
1343        Ok(summary)
1344    }
1345
1346    /// Shift rows down/up within a sheet (Excel's insert/delete rows)
1347    pub fn shift_rows(&mut self, sheet_id: SheetId, start_row: u32, delta: i32) {
1348        if delta == 0 {
1349            return;
1350        }
1351
1352        // Log change event for undo/redo
1353        let change_event = ChangeEvent::SetValue {
1354            addr: CellRef {
1355                sheet_id,
1356                coord: Coord::new(start_row, 0, true, true),
1357            },
1358            old_value: None,
1359            old_formula: None,
1360            new: LiteralValue::Text(format!("Row shift: start={start_row}, delta={delta}")),
1361        };
1362        self.log_change(change_event);
1363
1364        // TODO: Implement actual row shifting logic
1365        // This would require coordination with the vertex store and dependency tracking
1366    }
1367
1368    /// Shift columns left/right within a sheet (Excel's insert/delete columns)
1369    pub fn shift_columns(&mut self, sheet_id: SheetId, start_col: u32, delta: i32) {
1370        if delta == 0 {
1371            return;
1372        }
1373
1374        // Log change event
1375        let change_event = ChangeEvent::SetValue {
1376            addr: CellRef {
1377                sheet_id,
1378                coord: Coord::new(0, start_col, true, true),
1379            },
1380            old_value: None,
1381            old_formula: None,
1382            new: LiteralValue::Text(format!("Column shift: start={start_col}, delta={delta}")),
1383        };
1384        self.log_change(change_event);
1385
1386        // TODO: Implement actual column shifting logic
1387        // This would require coordination with the vertex store and dependency tracking
1388    }
1389
1390    /// Set a cell value, creating the vertex if it doesn't exist
1391    pub fn set_cell_value(&mut self, cell_ref: CellRef, value: LiteralValue) -> VertexId {
1392        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1393
1394        // Capture old state before modification (value + formula).
1395        let old_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1396        let old_value = old_id.and_then(|id| self.graph.get_value(id));
1397        let old_formula = old_id.and_then(|id| self.get_formula_ast(id));
1398
1399        // If this cell currently anchors a spill, clear the spill first and log it.
1400        // This keeps spill ownership maps and children consistent under undo/redo.
1401        let spill_snapshot =
1402            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1403        let did_spill_clear = spill_snapshot.is_some();
1404        if let Some((anchor, old_spill)) = spill_snapshot {
1405            if let Some(logger) = &mut self.change_logger {
1406                logger.begin_compound(format!(
1407                    "SetValueWithSpillClear sheet={} row={} col={}",
1408                    cell_ref.sheet_id,
1409                    cell_ref.coord.row(),
1410                    cell_ref.coord.col()
1411                ));
1412            }
1413            self.graph.clear_spill_region(anchor);
1414            self.log_change(ChangeEvent::SpillCleared {
1415                anchor,
1416                old: old_spill,
1417            });
1418        }
1419
1420        // Use the existing DependencyGraph API
1421        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1422        match self.graph.set_cell_value(
1423            &sheet_name,
1424            cell_ref.coord.row() + 1,
1425            cell_ref.coord.col() + 1,
1426            value.clone(),
1427        ) {
1428            Ok(summary) => {
1429                // Log change event
1430                let change_event = ChangeEvent::SetValue {
1431                    addr: cell_ref,
1432                    old_value,
1433                    old_formula,
1434                    new: value,
1435                };
1436                self.log_change(change_event);
1437
1438                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1439                    logger.end_compound();
1440                }
1441
1442                summary
1443                    .affected_vertices
1444                    .into_iter()
1445                    .next()
1446                    .unwrap_or(VertexId::new(0))
1447            }
1448            Err(_) => VertexId::new(0),
1449        }
1450    }
1451
1452    /// Set a cell formula, creating the vertex if it doesn't exist
1453    pub fn set_cell_formula(&mut self, cell_ref: CellRef, formula: ASTNode) -> VertexId {
1454        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1455
1456        // Capture old state before modification (value + formula).
1457        let old_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1458        let old_value = old_id.and_then(|id| self.graph.get_value(id));
1459        let old_formula = old_id.and_then(|id| self.get_formula_ast(id));
1460
1461        // If this cell currently anchors a spill, clear it before updating the formula.
1462        let spill_snapshot =
1463            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1464        let did_spill_clear = spill_snapshot.is_some();
1465        if let Some((anchor, old_spill)) = spill_snapshot {
1466            if let Some(logger) = &mut self.change_logger {
1467                logger.begin_compound(format!(
1468                    "SetFormulaWithSpillClear sheet={} row={} col={}",
1469                    cell_ref.sheet_id,
1470                    cell_ref.coord.row(),
1471                    cell_ref.coord.col()
1472                ));
1473            }
1474            self.graph.clear_spill_region(anchor);
1475            self.log_change(ChangeEvent::SpillCleared {
1476                anchor,
1477                old: old_spill,
1478            });
1479        }
1480
1481        // Use the existing DependencyGraph API
1482        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1483        match self.graph.set_cell_formula(
1484            &sheet_name,
1485            cell_ref.coord.row() + 1,
1486            cell_ref.coord.col() + 1,
1487            formula.clone(),
1488        ) {
1489            Ok(summary) => {
1490                // Log change event
1491                let change_event = ChangeEvent::SetFormula {
1492                    addr: cell_ref,
1493                    old_value,
1494                    old_formula,
1495                    new: formula,
1496                };
1497                self.log_change(change_event);
1498
1499                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1500                    logger.end_compound();
1501                }
1502
1503                summary
1504                    .affected_vertices
1505                    .into_iter()
1506                    .next()
1507                    .unwrap_or(VertexId::new(0))
1508            }
1509            Err(_) => VertexId::new(0),
1510        }
1511    }
1512
1513    // Range operations
1514
1515    /// Set values for a rectangular range of cells
1516    pub fn set_range_values(
1517        &mut self,
1518        sheet_id: SheetId,
1519        start_row: u32,
1520        start_col: u32,
1521        values: &[Vec<LiteralValue>],
1522    ) -> Result<RangeSummary, EditorError> {
1523        let mut summary = RangeSummary::default();
1524
1525        self.begin_batch();
1526
1527        for (row_offset, row_values) in values.iter().enumerate() {
1528            for (col_offset, value) in row_values.iter().enumerate() {
1529                let row = start_row + row_offset as u32;
1530                let col = start_col + col_offset as u32;
1531
1532                // Check if cell already exists
1533                let cell_ref = self.graph.make_cell_ref_internal(sheet_id, row, col);
1534
1535                if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1536                    // Update existing vertex
1537                    self.graph.update_vertex_value(existing_id, value.clone());
1538                    self.graph.mark_vertex_dirty(existing_id);
1539                    summary.vertices_updated.push(existing_id);
1540                } else {
1541                    // Create new vertex
1542                    let meta = VertexMeta::new(row, col, sheet_id, VertexKind::Cell);
1543                    let id = self.add_vertex(meta);
1544                    self.graph.update_vertex_value(id, value.clone());
1545                    summary.vertices_created.push(id);
1546                }
1547
1548                summary.cells_affected += 1;
1549            }
1550        }
1551
1552        self.commit_batch();
1553
1554        Ok(summary)
1555    }
1556
1557    /// Clear all cells in a rectangular range
1558    pub fn clear_range(
1559        &mut self,
1560        sheet_id: SheetId,
1561        start_row: u32,
1562        start_col: u32,
1563        end_row: u32,
1564        end_col: u32,
1565    ) -> Result<RangeSummary, EditorError> {
1566        let mut summary = RangeSummary::default();
1567
1568        self.begin_batch();
1569
1570        // Collect vertices in range
1571        let vertices_in_range: Vec<_> = self
1572            .graph
1573            .vertices_in_sheet(sheet_id)
1574            .filter(|&id| {
1575                let coord = self.graph.get_coord(id);
1576                let row = coord.row();
1577                let col = coord.col();
1578                row >= start_row && row <= end_row && col >= start_col && col <= end_col
1579            })
1580            .collect();
1581
1582        for id in vertices_in_range {
1583            self.remove_vertex(id)?;
1584            summary.cells_affected += 1;
1585        }
1586
1587        self.commit_batch();
1588
1589        Ok(summary)
1590    }
1591
1592    /// Copy a range to a new location
1593    pub fn copy_range(
1594        &mut self,
1595        sheet_id: SheetId,
1596        from_start_row: u32,
1597        from_start_col: u32,
1598        from_end_row: u32,
1599        from_end_col: u32,
1600        to_sheet_id: SheetId,
1601        to_row: u32,
1602        to_col: u32,
1603    ) -> Result<RangeSummary, EditorError> {
1604        let row_offset = to_row as i32 - from_start_row as i32;
1605        let col_offset = to_col as i32 - from_start_col as i32;
1606
1607        let mut summary = RangeSummary::default();
1608        let mut cell_data = Vec::new();
1609
1610        // Collect source data
1611        let vertices_in_range: Vec<_> = self
1612            .graph
1613            .vertices_in_sheet(sheet_id)
1614            .filter(|&id| {
1615                let coord = self.graph.get_coord(id);
1616                let row = coord.row();
1617                let col = coord.col();
1618                row >= from_start_row
1619                    && row <= from_end_row
1620                    && col >= from_start_col
1621                    && col <= from_end_col
1622            })
1623            .collect();
1624
1625        for id in vertices_in_range {
1626            let coord = self.graph.get_coord(id);
1627            let row = coord.row();
1628            let col = coord.col();
1629
1630            // Get value or formula
1631            if let Some(formula) = self.get_formula_ast(id) {
1632                cell_data.push((
1633                    row - from_start_row,
1634                    col - from_start_col,
1635                    CellData::Formula(formula),
1636                ));
1637            } else if let Some(value) = self.graph.get_value(id) {
1638                cell_data.push((
1639                    row - from_start_row,
1640                    col - from_start_col,
1641                    CellData::Value(value),
1642                ));
1643            }
1644        }
1645
1646        self.begin_batch();
1647
1648        // Apply to destination with relative adjustment
1649        for (row_idx, col_idx, data) in cell_data {
1650            let dest_row = (to_row as i32 + row_idx as i32) as u32;
1651            let dest_col = (to_col as i32 + col_idx as i32) as u32;
1652
1653            match data {
1654                CellData::Value(value) => {
1655                    let cell_ref =
1656                        self.graph
1657                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1658
1659                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1660                        self.graph.update_vertex_value(existing_id, value);
1661                        self.graph.mark_vertex_dirty(existing_id);
1662                        summary.vertices_updated.push(existing_id);
1663                    } else {
1664                        let meta =
1665                            VertexMeta::new(dest_row, dest_col, to_sheet_id, VertexKind::Cell);
1666                        let id = self.add_vertex(meta);
1667                        self.graph.update_vertex_value(id, value);
1668                        summary.vertices_created.push(id);
1669                    }
1670                }
1671                CellData::Formula(formula) => {
1672                    // Adjust relative references in formula
1673                    let adjuster = RelativeReferenceAdjuster::new(row_offset, col_offset);
1674                    let adjusted = adjuster.adjust_formula(&formula);
1675
1676                    let cell_ref =
1677                        self.graph
1678                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1679
1680                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1681                        self.graph.update_vertex_formula(existing_id, adjusted)?;
1682                        summary.vertices_updated.push(existing_id);
1683                    } else {
1684                        let meta = VertexMeta::new(
1685                            dest_row,
1686                            dest_col,
1687                            to_sheet_id,
1688                            VertexKind::FormulaScalar,
1689                        );
1690                        let id = self.add_vertex(meta);
1691                        self.graph.update_vertex_formula(id, adjusted)?;
1692                        summary.vertices_created.push(id);
1693                    }
1694                }
1695            }
1696
1697            summary.cells_affected += 1;
1698        }
1699
1700        self.commit_batch();
1701
1702        Ok(summary)
1703    }
1704
1705    /// Move a range to a new location (copy + clear source)
1706    pub fn move_range(
1707        &mut self,
1708        sheet_id: SheetId,
1709        from_start_row: u32,
1710        from_start_col: u32,
1711        from_end_row: u32,
1712        from_end_col: u32,
1713        to_sheet_id: SheetId,
1714        to_row: u32,
1715        to_col: u32,
1716    ) -> Result<RangeSummary, EditorError> {
1717        // First copy the range
1718        let mut summary = self.copy_range(
1719            sheet_id,
1720            from_start_row,
1721            from_start_col,
1722            from_end_row,
1723            from_end_col,
1724            to_sheet_id,
1725            to_row,
1726            to_col,
1727        )?;
1728
1729        // Then clear the source range
1730        let clear_summary = self.clear_range(
1731            sheet_id,
1732            from_start_row,
1733            from_start_col,
1734            from_end_row,
1735            from_end_col,
1736        )?;
1737
1738        summary.cells_moved = clear_summary.cells_affected;
1739
1740        // Update external references to moved cells
1741        let row_offset = to_row as i32 - from_start_row as i32;
1742        let col_offset = to_col as i32 - from_start_col as i32;
1743
1744        // Find all formulas that reference the moved range
1745        let all_formula_vertices: Vec<_> = self.graph.vertices_with_formulas().collect();
1746
1747        let from_sheet_name = self.graph.sheet_name(sheet_id).to_string();
1748        let to_sheet_name = self.graph.sheet_name(to_sheet_id).to_string();
1749        let adjuster = MoveReferenceAdjuster::new(
1750            sheet_id,
1751            from_sheet_name,
1752            from_start_row,
1753            from_start_col,
1754            from_end_row,
1755            from_end_col,
1756            to_sheet_id,
1757            to_sheet_name,
1758            row_offset,
1759            col_offset,
1760        );
1761
1762        for formula_id in all_formula_vertices {
1763            if let Some(formula) = self.get_formula_ast(formula_id) {
1764                let formula_sheet_id = self.graph.get_vertex_sheet_id(formula_id);
1765                if let Some(adjusted) = adjuster.adjust_if_references(&formula, formula_sheet_id) {
1766                    self.graph.update_vertex_formula(formula_id, adjusted)?;
1767                }
1768            }
1769        }
1770
1771        Ok(summary)
1772    }
1773
1774    /// Define a named range
1775    pub fn define_name(
1776        &mut self,
1777        name: &str,
1778        definition: NamedDefinition,
1779        scope: NameScope,
1780    ) -> Result<(), EditorError> {
1781        self.graph.define_name(name, definition.clone(), scope)?;
1782
1783        self.log_change(ChangeEvent::DefineName {
1784            name: name.to_string(),
1785            scope,
1786            definition,
1787        });
1788
1789        Ok(())
1790    }
1791
1792    /// Helper to create definitions from coordinates for a single cell
1793    pub fn define_name_for_cell(
1794        &mut self,
1795        name: &str,
1796        sheet_name: &str,
1797        row: u32,
1798        col: u32,
1799        scope: NameScope,
1800    ) -> Result<(), EditorError> {
1801        let sheet_id = self
1802            .graph
1803            .sheet_id(sheet_name)
1804            .ok_or_else(|| EditorError::InvalidName {
1805                name: sheet_name.to_string(),
1806                reason: "Sheet not found".to_string(),
1807            })?;
1808        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
1809        self.define_name(name, NamedDefinition::Cell(cell_ref), scope)
1810    }
1811
1812    /// Helper to create definitions from coordinates for a range
1813    pub fn define_name_for_range(
1814        &mut self,
1815        name: &str,
1816        sheet_name: &str,
1817        start_row: u32,
1818        start_col: u32,
1819        end_row: u32,
1820        end_col: u32,
1821        scope: NameScope,
1822    ) -> Result<(), EditorError> {
1823        let sheet_id = self
1824            .graph
1825            .sheet_id(sheet_name)
1826            .ok_or_else(|| EditorError::InvalidName {
1827                name: sheet_name.to_string(),
1828                reason: "Sheet not found".to_string(),
1829            })?;
1830        let start = CellRef::new(
1831            sheet_id,
1832            Coord::from_excel(start_row, start_col, true, true),
1833        );
1834        let end = CellRef::new(sheet_id, Coord::from_excel(end_row, end_col, true, true));
1835        let range_ref = crate::reference::RangeRef::new(start, end);
1836        self.define_name(name, NamedDefinition::Range(range_ref), scope)
1837    }
1838
1839    /// Update an existing named range definition
1840    pub fn update_name(
1841        &mut self,
1842        name: &str,
1843        new_definition: NamedDefinition,
1844        scope: NameScope,
1845    ) -> Result<(), EditorError> {
1846        // Get the old definition for the change log
1847        let old_definition = self
1848            .graph
1849            .resolve_name(
1850                name,
1851                match scope {
1852                    NameScope::Sheet(id) => id,
1853                    NameScope::Workbook => 0,
1854                },
1855            )
1856            .cloned();
1857
1858        self.graph
1859            .update_name(name, new_definition.clone(), scope)?;
1860
1861        if let Some(old_def) = old_definition {
1862            self.log_change(ChangeEvent::UpdateName {
1863                name: name.to_string(),
1864                scope,
1865                old_definition: old_def,
1866                new_definition,
1867            });
1868        }
1869
1870        Ok(())
1871    }
1872
1873    /// Delete a named range
1874    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), EditorError> {
1875        // Capture old definition *before* deletion so undo can restore it.
1876        let old_def = if self.has_logger() {
1877            self.graph
1878                .resolve_name(
1879                    name,
1880                    match scope {
1881                        NameScope::Sheet(id) => id,
1882                        NameScope::Workbook => 0,
1883                    },
1884                )
1885                .cloned()
1886        } else {
1887            None
1888        };
1889
1890        self.graph.delete_name(name, scope)?;
1891        self.log_change(ChangeEvent::DeleteName {
1892            name: name.to_string(),
1893            scope,
1894            old_definition: old_def,
1895        });
1896
1897        Ok(())
1898    }
1899}
1900
1901/// Helper enum for cell data
1902enum CellData {
1903    Value(LiteralValue),
1904    Formula(ASTNode),
1905}
1906
1907impl<'g> Drop for VertexEditor<'g> {
1908    fn drop(&mut self) {
1909        // Ensure batch operations are committed when the editor is dropped
1910        if self.batch_mode {
1911            self.commit_batch();
1912        }
1913    }
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918    use super::*;
1919    use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog};
1920    use crate::reference::Coord;
1921
1922    fn create_test_graph() -> DependencyGraph {
1923        DependencyGraph::new()
1924    }
1925
1926    #[test]
1927    fn test_vertex_editor_creation() {
1928        let mut graph = create_test_graph();
1929        let editor = VertexEditor::new(&mut graph);
1930        assert!(!editor.has_logger());
1931        assert!(!editor.batch_mode);
1932    }
1933
1934    #[test]
1935    fn test_vertex_editor_with_logger() {
1936        let mut graph = create_test_graph();
1937        let mut log = ChangeLog::new();
1938        let editor = VertexEditor::with_logger(&mut graph, &mut log);
1939        assert!(editor.has_logger());
1940        assert!(!editor.batch_mode);
1941    }
1942
1943    #[test]
1944    fn test_add_vertex() {
1945        let mut graph = create_test_graph();
1946        let mut editor = VertexEditor::new(&mut graph);
1947
1948        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell).dirty();
1949        let vertex_id = editor.add_vertex(meta);
1950
1951        // Verify vertex was created (simplified check)
1952        assert!(vertex_id.0 > 0);
1953    }
1954
1955    #[test]
1956    fn test_batch_operations() {
1957        let mut graph = create_test_graph();
1958        let mut editor = VertexEditor::new(&mut graph);
1959
1960        assert!(!editor.batch_mode);
1961        editor.begin_batch();
1962        assert!(editor.batch_mode);
1963
1964        // Add multiple vertices in batch mode
1965        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
1966        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::Cell);
1967
1968        let id1 = editor.add_vertex(meta1);
1969        let id2 = editor.add_vertex(meta2);
1970
1971        // Add edge between them
1972        assert!(editor.add_edge(id1, id2));
1973
1974        editor.commit_batch();
1975        assert!(!editor.batch_mode);
1976    }
1977
1978    #[test]
1979    fn test_remove_vertex() {
1980        let mut graph = create_test_graph();
1981        let mut editor = VertexEditor::new(&mut graph);
1982
1983        let meta = VertexMeta::new(3, 4, 0, VertexKind::Cell).dirty();
1984        let vertex_id = editor.add_vertex(meta);
1985
1986        // Now removal returns Result
1987        assert!(editor.remove_vertex(vertex_id).is_ok());
1988    }
1989
1990    #[test]
1991    fn test_remove_vertex_clears_spill_registry_for_anchor() {
1992        let mut graph = create_test_graph();
1993        let sheet_id = graph.sheet_id_mut("Sheet1");
1994
1995        // Create anchor vertex at A1 (0-based internal coord 0,0).
1996        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
1997        let anchor_vid = {
1998            let mut editor = VertexEditor::new(&mut graph);
1999            editor.set_cell_value(anchor_cell, LiteralValue::Number(0.0))
2000        };
2001
2002        let target_cells = vec![
2003            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
2004            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
2005            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
2006            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
2007        ];
2008        let values = vec![
2009            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
2010            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
2011        ];
2012
2013        graph
2014            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
2015            .unwrap();
2016
2017        assert!(graph.spill_registry_has_anchor(anchor_vid));
2018        for cell in &target_cells {
2019            assert_eq!(
2020                graph.spill_registry_anchor_for_cell(*cell),
2021                Some(anchor_vid)
2022            );
2023        }
2024
2025        {
2026            let mut editor = VertexEditor::new(&mut graph);
2027            editor.remove_vertex(anchor_vid).unwrap();
2028        }
2029
2030        assert!(!graph.spill_registry_has_anchor(anchor_vid));
2031        for cell in &target_cells {
2032            assert_eq!(graph.spill_registry_anchor_for_cell(*cell), None);
2033        }
2034        assert_eq!(graph.spill_registry_counts(), (0, 0));
2035    }
2036
2037    #[test]
2038    fn test_edge_operations() {
2039        let mut graph = create_test_graph();
2040        let mut editor = VertexEditor::new(&mut graph);
2041
2042        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2043        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::FormulaScalar);
2044
2045        let id1 = editor.add_vertex(meta1);
2046        let id2 = editor.add_vertex(meta2);
2047
2048        // Add edge
2049        assert!(editor.add_edge(id1, id2));
2050
2051        // Prevent self-loop
2052        assert!(!editor.add_edge(id1, id1));
2053
2054        // Remove edge
2055        assert!(editor.remove_edge(id1, id2));
2056    }
2057
2058    #[test]
2059    fn test_set_cell_value() {
2060        let mut graph = create_test_graph();
2061        let mut log = ChangeLog::new();
2062
2063        let cell_ref = CellRef {
2064            sheet_id: 0,
2065            coord: Coord::new(2, 3, true, true),
2066        };
2067        let value = LiteralValue::Number(42.0);
2068
2069        let vertex_id = {
2070            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2071            editor.set_cell_value(cell_ref, value.clone())
2072        };
2073
2074        // Verify vertex was created (simplified check)
2075        assert!(vertex_id.0 > 0);
2076
2077        // Verify change log
2078        assert_eq!(log.len(), 1);
2079        match &log.events()[0] {
2080            ChangeEvent::SetValue { addr, new, .. } => {
2081                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2082                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2083                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2084                assert_eq!(new, &value);
2085            }
2086            _ => panic!("Expected SetValue event"),
2087        }
2088    }
2089
2090    #[test]
2091    fn test_set_cell_formula() {
2092        let mut graph = create_test_graph();
2093        let mut log = ChangeLog::new();
2094
2095        let cell_ref = CellRef {
2096            sheet_id: 0,
2097            coord: Coord::new(1, 1, true, true),
2098        };
2099
2100        use formualizer_parse::parser::ASTNodeType;
2101        let formula = formualizer_parse::parser::ASTNode {
2102            node_type: ASTNodeType::Literal(LiteralValue::Number(100.0)),
2103            source_token: None,
2104            contains_volatile: false,
2105        };
2106
2107        let vertex_id = {
2108            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2109            editor.set_cell_formula(cell_ref, formula.clone())
2110        };
2111
2112        // Verify vertex was created (simplified check)
2113        assert!(vertex_id.0 > 0);
2114
2115        // Verify change log
2116        assert_eq!(log.len(), 1);
2117        match &log.events()[0] {
2118            ChangeEvent::SetFormula { addr, .. } => {
2119                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2120                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2121                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2122            }
2123            _ => panic!("Expected SetFormula event"),
2124        }
2125    }
2126
2127    #[test]
2128    fn test_shift_rows() {
2129        let mut graph = create_test_graph();
2130        let mut log = ChangeLog::new();
2131
2132        {
2133            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2134
2135            // Create vertices at different rows
2136            let cell1 = CellRef {
2137                sheet_id: 0,
2138                coord: Coord::new(5, 1, true, true),
2139            };
2140            let cell2 = CellRef {
2141                sheet_id: 0,
2142                coord: Coord::new(10, 1, true, true),
2143            };
2144            let cell3 = CellRef {
2145                sheet_id: 0,
2146                coord: Coord::new(15, 1, true, true),
2147            };
2148
2149            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2150            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2151            editor.set_cell_value(cell3, LiteralValue::Number(3.0));
2152        }
2153
2154        // Clear change log to focus on shift operation
2155        log.clear();
2156
2157        {
2158            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2159            // Shift rows starting at row 10, moving down by 2
2160            editor.shift_rows(0, 10, 2);
2161        }
2162
2163        // Verify change log contains the shift operation
2164        assert_eq!(log.len(), 1);
2165        match &log.events()[0] {
2166            ChangeEvent::SetValue { addr, new, .. } => {
2167                assert_eq!(addr.sheet_id, 0);
2168                assert_eq!(addr.coord.row(), 10);
2169                if let LiteralValue::Text(msg) = new {
2170                    assert!(msg.contains("Row shift"));
2171                    assert!(msg.contains("start=10"));
2172                    assert!(msg.contains("delta=2"));
2173                }
2174            }
2175            _ => panic!("Expected SetValue event for row shift"),
2176        }
2177    }
2178
2179    #[test]
2180    fn test_shift_columns() {
2181        let mut graph = create_test_graph();
2182        let mut log = ChangeLog::new();
2183
2184        {
2185            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2186
2187            // Create vertices at different columns
2188            let cell1 = CellRef {
2189                sheet_id: 0,
2190                coord: Coord::new(1, 5, true, true),
2191            };
2192            let cell2 = CellRef {
2193                sheet_id: 0,
2194                coord: Coord::new(1, 10, 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        }
2200
2201        // Clear change log
2202        log.clear();
2203
2204        {
2205            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2206            // Shift columns starting at col 8, moving right by 3
2207            editor.shift_columns(0, 8, 3);
2208        }
2209
2210        // Verify change log
2211        assert_eq!(log.len(), 1);
2212        match &log.events()[0] {
2213            ChangeEvent::SetValue { addr, new, .. } => {
2214                assert_eq!(addr.sheet_id, 0);
2215                assert_eq!(addr.coord.col(), 8);
2216                if let LiteralValue::Text(msg) = new {
2217                    assert!(msg.contains("Column shift"));
2218                    assert!(msg.contains("start=8"));
2219                    assert!(msg.contains("delta=3"));
2220                }
2221            }
2222            _ => panic!("Expected SetValue event for column shift"),
2223        }
2224    }
2225
2226    #[test]
2227    fn test_move_vertex() {
2228        let mut graph = create_test_graph();
2229        let mut editor = VertexEditor::new(&mut graph);
2230
2231        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell);
2232        let vertex_id = editor.add_vertex(meta);
2233
2234        // Move vertex returns Result
2235        assert!(editor.move_vertex(vertex_id, AbsCoord::new(8, 12)).is_ok());
2236
2237        // Moving to same position should work
2238        assert!(editor.move_vertex(vertex_id, AbsCoord::new(8, 12)).is_ok());
2239    }
2240
2241    #[test]
2242    fn test_vertex_meta_builder() {
2243        let meta = VertexMeta::new(1, 2, 3, VertexKind::FormulaScalar)
2244            .dirty()
2245            .volatile()
2246            .with_flags(0x08);
2247
2248        assert_eq!(meta.coord.row(), 1);
2249        assert_eq!(meta.coord.col(), 2);
2250        assert_eq!(meta.sheet_id, 3);
2251        assert_eq!(meta.kind, VertexKind::FormulaScalar);
2252        assert_eq!(meta.flags, 0x08); // Last with_flags call overwrites previous flags
2253    }
2254
2255    #[test]
2256    fn test_change_log_management() {
2257        let mut graph = create_test_graph();
2258        let mut log = ChangeLog::new();
2259
2260        {
2261            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2262            let cell_ref = CellRef {
2263                sheet_id: 0,
2264                coord: Coord::new(0, 0, true, true),
2265            };
2266            editor.set_cell_value(cell_ref, LiteralValue::Number(1.0));
2267            editor.set_cell_value(cell_ref, LiteralValue::Number(2.0));
2268        }
2269
2270        assert_eq!(log.len(), 2);
2271
2272        log.clear();
2273        assert_eq!(log.len(), 0);
2274    }
2275
2276    #[test]
2277    fn test_editor_drop_commits_batch() {
2278        let mut graph = create_test_graph();
2279        {
2280            let mut editor = VertexEditor::new(&mut graph);
2281            editor.begin_batch();
2282
2283            let meta = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2284            editor.add_vertex(meta);
2285
2286            // Editor will be dropped here, should commit batch
2287        }
2288
2289        // If we reach here without hanging, the batch was properly committed
2290    }
2291}