Skip to main content

formualizer_eval/engine/graph/editor/
vertex_editor.rs

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