Skip to main content

formualizer_eval/engine/graph/editor/
vertex_editor.rs

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