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