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