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