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    ///
1553    /// Legacy compatibility API: failures return vertex zero. Prefer
1554    /// [`Self::try_set_cell_formula`] when failure must be observable.
1555    pub fn set_cell_formula(&mut self, cell_ref: CellRef, formula: ASTNode) -> VertexId {
1556        self.set_cell_formula_with_old_state(cell_ref, formula, None, None)
1557    }
1558
1559    /// Set a formula, reporting binding/admission failures without clearing its old spill.
1560    pub fn try_set_cell_formula(
1561        &mut self,
1562        cell_ref: CellRef,
1563        formula: ASTNode,
1564    ) -> Result<VertexId, ExcelError> {
1565        self.try_set_cell_formula_with_old_state(cell_ref, formula, None, None)
1566    }
1567
1568    /// Like [`set_cell_formula`](Self::set_cell_formula), but lets the caller
1569    /// supply old state captured from an external source of truth (e.g. the
1570    /// Arrow store) for the change-log event. Same precedence as
1571    /// [`set_cell_value_with_old_state`](Self::set_cell_value_with_old_state):
1572    /// graph-captured state wins, caller state only fills `None` fields.
1573    pub fn set_cell_formula_with_old_state(
1574        &mut self,
1575        cell_ref: CellRef,
1576        formula: ASTNode,
1577        fallback_old_value: Option<LiteralValue>,
1578        fallback_old_formula: Option<ASTNode>,
1579    ) -> VertexId {
1580        self.try_set_cell_formula_with_old_state(
1581            cell_ref,
1582            formula,
1583            fallback_old_value,
1584            fallback_old_formula,
1585        )
1586        .unwrap_or_else(|_| VertexId::new(0))
1587    }
1588
1589    /// Fallible counterpart to [`Self::set_cell_formula_with_old_state`].
1590    /// Graph-captured old state takes precedence over caller-provided state.
1591    pub fn try_set_cell_formula_with_old_state(
1592        &mut self,
1593        cell_ref: CellRef,
1594        formula: ASTNode,
1595        fallback_old_value: Option<LiteralValue>,
1596        fallback_old_formula: Option<ASTNode>,
1597    ) -> Result<VertexId, ExcelError> {
1598        self.set_cell_formula_with_old_state_and_plan(
1599            cell_ref,
1600            formula,
1601            fallback_old_value,
1602            fallback_old_formula,
1603            None,
1604        )
1605    }
1606
1607    pub(crate) fn set_cell_formula_with_prepared_plan(
1608        &mut self,
1609        cell_ref: CellRef,
1610        formula: ASTNode,
1611        fallback_old_value: Option<LiteralValue>,
1612        fallback_old_formula: Option<ASTNode>,
1613        ast_id: crate::engine::arena::AstNodeId,
1614        plan: crate::engine::ingest_pipeline::DependencyPlanRow,
1615    ) -> VertexId {
1616        self.set_cell_formula_with_old_state_and_plan(
1617            cell_ref,
1618            formula,
1619            fallback_old_value,
1620            fallback_old_formula,
1621            Some((ast_id, plan)),
1622        )
1623        .unwrap_or_else(|_| VertexId::new(0))
1624    }
1625
1626    fn set_cell_formula_with_old_state_and_plan(
1627        &mut self,
1628        cell_ref: CellRef,
1629        formula: ASTNode,
1630        fallback_old_value: Option<LiteralValue>,
1631        fallback_old_formula: Option<ASTNode>,
1632        prepared: Option<(
1633            crate::engine::arena::AstNodeId,
1634            crate::engine::ingest_pipeline::DependencyPlanRow,
1635        )>,
1636    ) -> Result<VertexId, ExcelError> {
1637        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1638
1639        // Capture old state before modification (value + formula); fall back
1640        // to caller-supplied state for anything the graph cannot see.
1641        let old_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1642        let old_value = old_id
1643            .and_then(|id| self.graph.get_value(id))
1644            .or(fallback_old_value);
1645        let old_formula = old_id
1646            .and_then(|id| self.get_formula_ast(id))
1647            .or(fallback_old_formula);
1648
1649        // Snapshot old spill values before updating, but do not clear or log anything
1650        // until the fallible binding/admission path succeeds.
1651        let spill_snapshot =
1652            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1653        let did_spill_clear = spill_snapshot.is_some();
1654
1655        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1656        let result = if let Some((ast_id, plan)) = prepared {
1657            self.graph.set_cell_formula_with_plan(
1658                &sheet_name,
1659                cell_ref.coord.row() + 1,
1660                cell_ref.coord.col() + 1,
1661                ast_id,
1662                &plan,
1663                plan.volatile,
1664                plan.dynamic,
1665            )
1666        } else {
1667            self.graph.set_cell_formula(
1668                &sheet_name,
1669                cell_ref.coord.row() + 1,
1670                cell_ref.coord.col() + 1,
1671                formula.clone(),
1672            )
1673        };
1674        match result {
1675            Ok(summary) => {
1676                if let Some((anchor, old_spill)) = spill_snapshot {
1677                    if let Some(logger) = &mut self.change_logger {
1678                        logger.begin_compound(format!(
1679                            "SetFormulaWithSpillClear sheet={} row={} col={}",
1680                            cell_ref.sheet_id,
1681                            cell_ref.coord.row(),
1682                            cell_ref.coord.col()
1683                        ));
1684                    }
1685                    self.graph.clear_spill_region(anchor);
1686                    self.log_change(ChangeEvent::SpillCleared {
1687                        anchor,
1688                        old: old_spill,
1689                    });
1690                }
1691                // Log change event
1692                let change_event = ChangeEvent::SetFormula {
1693                    addr: cell_ref,
1694                    old_value,
1695                    old_formula,
1696                    new: formula,
1697                };
1698                self.log_change(change_event);
1699
1700                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1701                    logger.end_compound();
1702                }
1703
1704                Ok(summary
1705                    .affected_vertices
1706                    .into_iter()
1707                    .next()
1708                    .unwrap_or(VertexId::new(0)))
1709            }
1710            Err(error) => Err(error),
1711        }
1712    }
1713
1714    // Range operations
1715
1716    /// Set values for a rectangular range of cells
1717    pub fn set_range_values(
1718        &mut self,
1719        sheet_id: SheetId,
1720        start_row: u32,
1721        start_col: u32,
1722        values: &[Vec<LiteralValue>],
1723    ) -> Result<RangeSummary, EditorError> {
1724        let mut summary = RangeSummary::default();
1725
1726        self.begin_batch();
1727        // One multi-source dirty propagation for the whole rectangle instead
1728        // of a full BFS per cell (the loop body cannot error, so the scope
1729        // always closes before returning).
1730        self.graph.begin_deferred_dirty();
1731
1732        for (row_offset, row_values) in values.iter().enumerate() {
1733            for (col_offset, value) in row_values.iter().enumerate() {
1734                let row = start_row + row_offset as u32;
1735                let col = start_col + col_offset as u32;
1736                let cell_ref = self.graph.make_cell_ref_internal(sheet_id, row, col);
1737                let existing_id = self.graph.get_vertex_id_for_address(&cell_ref).copied();
1738
1739                let id = self.set_cell_value(cell_ref, value.clone());
1740                match existing_id {
1741                    Some(existing_id) => summary.vertices_updated.push(existing_id),
1742                    None if id.0 != 0 => summary.vertices_created.push(id),
1743                    None => {}
1744                }
1745                summary.cells_affected += 1;
1746            }
1747        }
1748
1749        let _ = self.graph.end_deferred_dirty();
1750        self.commit_batch();
1751
1752        Ok(summary)
1753    }
1754
1755    /// Clear all cells in a rectangular range
1756    pub fn clear_range(
1757        &mut self,
1758        sheet_id: SheetId,
1759        start_row: u32,
1760        start_col: u32,
1761        end_row: u32,
1762        end_col: u32,
1763    ) -> Result<RangeSummary, EditorError> {
1764        let mut summary = RangeSummary::default();
1765
1766        self.begin_batch();
1767
1768        // Collect vertices in range
1769        let vertices_in_range: Vec<_> = self
1770            .graph
1771            .grid_vertices_in_sheet(sheet_id)
1772            .filter(|(_, coord)| {
1773                let row = coord.row();
1774                let col = coord.col();
1775                row >= start_row && row <= end_row && col >= start_col && col <= end_col
1776            })
1777            .map(|(id, _)| id)
1778            .collect();
1779
1780        for id in vertices_in_range {
1781            self.remove_vertex(id)?;
1782            summary.cells_affected += 1;
1783        }
1784
1785        self.commit_batch();
1786
1787        Ok(summary)
1788    }
1789
1790    /// Copy a range to a new location
1791    pub fn copy_range(
1792        &mut self,
1793        sheet_id: SheetId,
1794        from_start_row: u32,
1795        from_start_col: u32,
1796        from_end_row: u32,
1797        from_end_col: u32,
1798        to_sheet_id: SheetId,
1799        to_row: u32,
1800        to_col: u32,
1801    ) -> Result<RangeSummary, EditorError> {
1802        let row_offset = to_row as i32 - from_start_row as i32;
1803        let col_offset = to_col as i32 - from_start_col as i32;
1804
1805        let mut summary = RangeSummary::default();
1806        let mut cell_data = Vec::new();
1807
1808        // Collect source data
1809        let vertices_in_range: Vec<_> = self
1810            .graph
1811            .grid_vertices_in_sheet(sheet_id)
1812            .filter(|(_, coord)| {
1813                let row = coord.row();
1814                let col = coord.col();
1815                row >= from_start_row
1816                    && row <= from_end_row
1817                    && col >= from_start_col
1818                    && col <= from_end_col
1819            })
1820            .collect();
1821
1822        for (id, coord) in vertices_in_range {
1823            let row = coord.row();
1824            let col = coord.col();
1825
1826            // Get value or formula
1827            if let Some(formula) = self.get_formula_ast(id) {
1828                cell_data.push((
1829                    row - from_start_row,
1830                    col - from_start_col,
1831                    CellData::Formula(formula),
1832                ));
1833            } else if let Some(value) = self.graph.get_value(id) {
1834                cell_data.push((
1835                    row - from_start_row,
1836                    col - from_start_col,
1837                    CellData::Value(value),
1838                ));
1839            }
1840        }
1841
1842        self.begin_batch();
1843
1844        // Apply to destination with relative adjustment
1845        for (row_idx, col_idx, data) in cell_data {
1846            let dest_row = (to_row as i32 + row_idx as i32) as u32;
1847            let dest_col = (to_col as i32 + col_idx as i32) as u32;
1848
1849            match data {
1850                CellData::Value(value) => {
1851                    let cell_ref =
1852                        self.graph
1853                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1854
1855                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1856                        self.graph.update_vertex_value(existing_id, value);
1857                        self.graph.mark_vertex_dirty(existing_id);
1858                        summary.vertices_updated.push(existing_id);
1859                    } else {
1860                        let meta =
1861                            VertexMeta::new(dest_row, dest_col, to_sheet_id, VertexKind::Cell);
1862                        let id = self.try_add_vertex(meta)?;
1863                        self.graph.update_vertex_value(id, value);
1864                        summary.vertices_created.push(id);
1865                    }
1866                }
1867                CellData::Formula(formula) => {
1868                    // Adjust relative references in formula
1869                    let adjuster = RelativeReferenceAdjuster::new(row_offset, col_offset);
1870                    let adjusted = adjuster.adjust_formula(&formula);
1871
1872                    let cell_ref =
1873                        self.graph
1874                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
1875
1876                    if let Some(&existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
1877                        self.graph.update_vertex_formula(existing_id, adjusted)?;
1878                        summary.vertices_updated.push(existing_id);
1879                    } else {
1880                        let meta = VertexMeta::new(
1881                            dest_row,
1882                            dest_col,
1883                            to_sheet_id,
1884                            VertexKind::FormulaScalar,
1885                        );
1886                        let id = self.try_add_vertex(meta)?;
1887                        self.graph.update_vertex_formula(id, adjusted)?;
1888                        summary.vertices_created.push(id);
1889                    }
1890                }
1891            }
1892
1893            summary.cells_affected += 1;
1894        }
1895
1896        self.commit_batch();
1897
1898        Ok(summary)
1899    }
1900
1901    /// Move a range to a new location (copy + clear source)
1902    pub fn move_range(
1903        &mut self,
1904        sheet_id: SheetId,
1905        from_start_row: u32,
1906        from_start_col: u32,
1907        from_end_row: u32,
1908        from_end_col: u32,
1909        to_sheet_id: SheetId,
1910        to_row: u32,
1911        to_col: u32,
1912    ) -> Result<RangeSummary, EditorError> {
1913        // First copy the range
1914        let mut summary = self.copy_range(
1915            sheet_id,
1916            from_start_row,
1917            from_start_col,
1918            from_end_row,
1919            from_end_col,
1920            to_sheet_id,
1921            to_row,
1922            to_col,
1923        )?;
1924
1925        // Then clear the source range
1926        let clear_summary = self.clear_range(
1927            sheet_id,
1928            from_start_row,
1929            from_start_col,
1930            from_end_row,
1931            from_end_col,
1932        )?;
1933
1934        summary.cells_moved = clear_summary.cells_affected;
1935
1936        // Update external references to moved cells
1937        let row_offset = to_row as i32 - from_start_row as i32;
1938        let col_offset = to_col as i32 - from_start_col as i32;
1939
1940        // Find all formulas that reference the moved range
1941        let all_formula_vertices: Vec<_> = self.graph.vertices_with_formulas().collect();
1942
1943        let from_sheet_name = self.graph.sheet_name(sheet_id).to_string();
1944        let to_sheet_name = self.graph.sheet_name(to_sheet_id).to_string();
1945        let adjuster = MoveReferenceAdjuster::new(
1946            sheet_id,
1947            from_sheet_name,
1948            from_start_row,
1949            from_start_col,
1950            from_end_row,
1951            from_end_col,
1952            to_sheet_id,
1953            to_sheet_name,
1954            row_offset,
1955            col_offset,
1956        );
1957
1958        for formula_id in all_formula_vertices {
1959            if let Some(formula) = self.get_formula_ast(formula_id) {
1960                let formula_sheet_id = self.graph.get_vertex_sheet_id(formula_id);
1961                if let Some(adjusted) = adjuster.adjust_if_references(&formula, formula_sheet_id) {
1962                    self.graph.update_vertex_formula(formula_id, adjusted)?;
1963                }
1964            }
1965        }
1966
1967        Ok(summary)
1968    }
1969
1970    /// Define a named range
1971    pub fn define_name(
1972        &mut self,
1973        name: &str,
1974        definition: NamedDefinition,
1975        scope: NameScope,
1976    ) -> Result<(), EditorError> {
1977        self.graph.define_name(name, definition.clone(), scope)?;
1978
1979        self.log_change(ChangeEvent::DefineName {
1980            name: name.to_string(),
1981            scope,
1982            definition,
1983        });
1984
1985        Ok(())
1986    }
1987
1988    /// Helper to create definitions from coordinates for a single cell
1989    pub fn define_name_for_cell(
1990        &mut self,
1991        name: &str,
1992        sheet_name: &str,
1993        row: u32,
1994        col: u32,
1995        scope: NameScope,
1996    ) -> Result<(), EditorError> {
1997        let sheet_id = self
1998            .graph
1999            .sheet_id(sheet_name)
2000            .ok_or_else(|| EditorError::InvalidName {
2001                name: sheet_name.to_string(),
2002                reason: "Sheet not found".to_string(),
2003            })?;
2004        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
2005        self.define_name(name, NamedDefinition::Cell(cell_ref), scope)
2006    }
2007
2008    /// Helper to create definitions from coordinates for a range
2009    pub fn define_name_for_range(
2010        &mut self,
2011        name: &str,
2012        sheet_name: &str,
2013        start_row: u32,
2014        start_col: u32,
2015        end_row: u32,
2016        end_col: u32,
2017        scope: NameScope,
2018    ) -> Result<(), EditorError> {
2019        let sheet_id = self
2020            .graph
2021            .sheet_id(sheet_name)
2022            .ok_or_else(|| EditorError::InvalidName {
2023                name: sheet_name.to_string(),
2024                reason: "Sheet not found".to_string(),
2025            })?;
2026        let start = CellRef::new(
2027            sheet_id,
2028            Coord::from_excel(start_row, start_col, true, true),
2029        );
2030        let end = CellRef::new(sheet_id, Coord::from_excel(end_row, end_col, true, true));
2031        let range_ref = crate::reference::RangeRef::new(start, end);
2032        self.define_name(name, NamedDefinition::Range(range_ref), scope)
2033    }
2034
2035    /// Update an existing named range definition
2036    pub fn update_name(
2037        &mut self,
2038        name: &str,
2039        new_definition: NamedDefinition,
2040        scope: NameScope,
2041    ) -> Result<(), EditorError> {
2042        // Get the old definition for the change log
2043        let old_definition = self
2044            .graph
2045            .resolve_name(
2046                name,
2047                match scope {
2048                    NameScope::Sheet(id) => id,
2049                    NameScope::Workbook => 0,
2050                },
2051            )
2052            .cloned();
2053
2054        self.graph
2055            .update_name(name, new_definition.clone(), scope)?;
2056
2057        if let Some(old_def) = old_definition {
2058            self.log_change(ChangeEvent::UpdateName {
2059                name: name.to_string(),
2060                scope,
2061                old_definition: old_def,
2062                new_definition,
2063            });
2064        }
2065
2066        Ok(())
2067    }
2068
2069    /// Delete a named range
2070    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), EditorError> {
2071        // Capture old definition *before* deletion so undo can restore it.
2072        let old_def = if self.has_logger() {
2073            self.graph
2074                .resolve_name(
2075                    name,
2076                    match scope {
2077                        NameScope::Sheet(id) => id,
2078                        NameScope::Workbook => 0,
2079                    },
2080                )
2081                .cloned()
2082        } else {
2083            None
2084        };
2085
2086        self.graph.delete_name(name, scope)?;
2087        self.log_change(ChangeEvent::DeleteName {
2088            name: name.to_string(),
2089            scope,
2090            old_definition: old_def,
2091        });
2092
2093        Ok(())
2094    }
2095}
2096
2097/// Helper enum for cell data
2098enum CellData {
2099    Value(LiteralValue),
2100    Formula(ASTNode),
2101}
2102
2103impl<'g> Drop for VertexEditor<'g> {
2104    fn drop(&mut self) {
2105        // Ensure batch operations are committed when the editor is dropped
2106        if self.batch_mode {
2107            self.commit_batch();
2108        }
2109    }
2110}
2111
2112#[cfg(test)]
2113mod tests {
2114    use super::*;
2115    use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog};
2116    use crate::reference::Coord;
2117
2118    fn create_test_graph() -> DependencyGraph {
2119        DependencyGraph::new()
2120    }
2121
2122    #[test]
2123    fn test_vertex_editor_creation() {
2124        let mut graph = create_test_graph();
2125        let editor = VertexEditor::new(&mut graph);
2126        assert!(!editor.has_logger());
2127        assert!(!editor.batch_mode);
2128    }
2129
2130    #[test]
2131    fn test_vertex_editor_with_logger() {
2132        let mut graph = create_test_graph();
2133        let mut log = ChangeLog::new();
2134        let editor = VertexEditor::with_logger(&mut graph, &mut log);
2135        assert!(editor.has_logger());
2136        assert!(!editor.batch_mode);
2137    }
2138
2139    #[test]
2140    fn test_add_vertex() {
2141        let mut graph = create_test_graph();
2142        let mut editor = VertexEditor::new(&mut graph);
2143
2144        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell).dirty();
2145        let vertex_id = editor.add_vertex(meta);
2146
2147        // Verify vertex was created (simplified check)
2148        assert!(vertex_id.0 > 0);
2149    }
2150
2151    #[test]
2152    fn test_batch_operations() {
2153        let mut graph = create_test_graph();
2154        let mut editor = VertexEditor::new(&mut graph);
2155
2156        assert!(!editor.batch_mode);
2157        editor.begin_batch();
2158        assert!(editor.batch_mode);
2159
2160        // Add multiple vertices in batch mode
2161        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2162        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::Cell);
2163
2164        let id1 = editor.add_vertex(meta1);
2165        let id2 = editor.add_vertex(meta2);
2166
2167        // Add edge between them
2168        assert!(editor.add_edge(id1, id2));
2169
2170        editor.commit_batch();
2171        assert!(!editor.batch_mode);
2172    }
2173
2174    #[test]
2175    fn test_remove_vertex() {
2176        let mut graph = create_test_graph();
2177        let mut editor = VertexEditor::new(&mut graph);
2178
2179        let meta = VertexMeta::new(3, 4, 0, VertexKind::Cell).dirty();
2180        let vertex_id = editor.add_vertex(meta);
2181
2182        // Now removal returns Result
2183        assert!(editor.remove_vertex(vertex_id).is_ok());
2184    }
2185
2186    #[test]
2187    fn test_remove_vertex_clears_spill_registry_for_anchor() {
2188        let mut graph = create_test_graph();
2189        let sheet_id = graph.sheet_id_mut("Sheet1");
2190
2191        // Create anchor vertex at A1 (0-based internal coord 0,0).
2192        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
2193        let anchor_vid = {
2194            let mut editor = VertexEditor::new(&mut graph);
2195            editor.set_cell_value(anchor_cell, LiteralValue::Number(0.0))
2196        };
2197
2198        let target_cells = vec![
2199            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
2200            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
2201            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
2202            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
2203        ];
2204        let values = vec![
2205            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
2206            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
2207        ];
2208
2209        graph
2210            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
2211            .unwrap();
2212
2213        assert!(graph.spill_registry_has_anchor(anchor_vid));
2214        for cell in &target_cells {
2215            assert_eq!(
2216                graph.spill_registry_anchor_for_cell(*cell),
2217                Some(anchor_vid)
2218            );
2219        }
2220
2221        {
2222            let mut editor = VertexEditor::new(&mut graph);
2223            editor.remove_vertex(anchor_vid).unwrap();
2224        }
2225
2226        assert!(!graph.spill_registry_has_anchor(anchor_vid));
2227        for cell in &target_cells {
2228            assert_eq!(graph.spill_registry_anchor_for_cell(*cell), None);
2229        }
2230        assert_eq!(graph.spill_registry_counts(), (0, 0));
2231    }
2232
2233    #[test]
2234    fn test_edge_operations() {
2235        let mut graph = create_test_graph();
2236        let mut editor = VertexEditor::new(&mut graph);
2237
2238        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2239        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::FormulaScalar);
2240
2241        let id1 = editor.add_vertex(meta1);
2242        let id2 = editor.add_vertex(meta2);
2243
2244        // Add edge
2245        assert!(editor.add_edge(id1, id2));
2246
2247        // Prevent self-loop
2248        assert!(!editor.add_edge(id1, id1));
2249
2250        // Remove edge
2251        assert!(editor.remove_edge(id1, id2));
2252    }
2253
2254    #[test]
2255    fn test_set_cell_value() {
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(2, 3, true, true),
2262        };
2263        let value = LiteralValue::Number(42.0);
2264
2265        let vertex_id = {
2266            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2267            editor.set_cell_value(cell_ref, value.clone())
2268        };
2269
2270        // Verify vertex was created (simplified check)
2271        assert!(vertex_id.0 > 0);
2272
2273        // Verify change log
2274        assert_eq!(log.len(), 1);
2275        match &log.events()[0] {
2276            ChangeEvent::SetValue { addr, new, .. } => {
2277                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2278                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2279                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2280                assert_eq!(new, &value);
2281            }
2282            _ => panic!("Expected SetValue event"),
2283        }
2284    }
2285
2286    #[test]
2287    fn test_set_cell_formula() {
2288        let mut graph = create_test_graph();
2289        let mut log = ChangeLog::new();
2290
2291        let cell_ref = CellRef {
2292            sheet_id: 0,
2293            coord: Coord::new(1, 1, true, true),
2294        };
2295
2296        use formualizer_parse::parser::ASTNodeType;
2297        let formula = formualizer_parse::parser::ASTNode {
2298            node_type: ASTNodeType::Literal(LiteralValue::Number(100.0)),
2299            source_token: None,
2300            contains_volatile: false,
2301        };
2302
2303        let vertex_id = {
2304            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2305            editor.set_cell_formula(cell_ref, formula.clone())
2306        };
2307
2308        // Verify vertex was created (simplified check)
2309        assert!(vertex_id.0 > 0);
2310
2311        // Verify change log
2312        assert_eq!(log.len(), 1);
2313        match &log.events()[0] {
2314            ChangeEvent::SetFormula { addr, .. } => {
2315                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2316                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2317                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2318            }
2319            _ => panic!("Expected SetFormula event"),
2320        }
2321    }
2322
2323    #[test]
2324    fn test_shift_rows() {
2325        let mut graph = create_test_graph();
2326        let mut log = ChangeLog::new();
2327
2328        {
2329            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2330
2331            // Create vertices at different rows
2332            let cell1 = CellRef {
2333                sheet_id: 0,
2334                coord: Coord::new(5, 1, true, true),
2335            };
2336            let cell2 = CellRef {
2337                sheet_id: 0,
2338                coord: Coord::new(10, 1, true, true),
2339            };
2340            let cell3 = CellRef {
2341                sheet_id: 0,
2342                coord: Coord::new(15, 1, true, true),
2343            };
2344
2345            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2346            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2347            editor.set_cell_value(cell3, LiteralValue::Number(3.0));
2348        }
2349
2350        // Clear change log to focus on shift operation
2351        log.clear();
2352
2353        {
2354            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2355            // Shift rows starting at row 10, moving down by 2
2356            editor.shift_rows(0, 10, 2);
2357        }
2358
2359        // Verify change log contains the shift operation
2360        assert_eq!(log.len(), 1);
2361        match &log.events()[0] {
2362            ChangeEvent::SetValue { addr, new, .. } => {
2363                assert_eq!(addr.sheet_id, 0);
2364                assert_eq!(addr.coord.row(), 10);
2365                if let LiteralValue::Text(msg) = new {
2366                    assert!(msg.contains("Row shift"));
2367                    assert!(msg.contains("start=10"));
2368                    assert!(msg.contains("delta=2"));
2369                }
2370            }
2371            _ => panic!("Expected SetValue event for row shift"),
2372        }
2373    }
2374
2375    #[test]
2376    fn test_shift_columns() {
2377        let mut graph = create_test_graph();
2378        let mut log = ChangeLog::new();
2379
2380        {
2381            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2382
2383            // Create vertices at different columns
2384            let cell1 = CellRef {
2385                sheet_id: 0,
2386                coord: Coord::new(1, 5, true, true),
2387            };
2388            let cell2 = CellRef {
2389                sheet_id: 0,
2390                coord: Coord::new(1, 10, true, true),
2391            };
2392
2393            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2394            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2395        }
2396
2397        // Clear change log
2398        log.clear();
2399
2400        {
2401            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2402            // Shift columns starting at col 8, moving right by 3
2403            editor.shift_columns(0, 8, 3);
2404        }
2405
2406        // Verify change log
2407        assert_eq!(log.len(), 1);
2408        match &log.events()[0] {
2409            ChangeEvent::SetValue { addr, new, .. } => {
2410                assert_eq!(addr.sheet_id, 0);
2411                assert_eq!(addr.coord.col(), 8);
2412                if let LiteralValue::Text(msg) = new {
2413                    assert!(msg.contains("Column shift"));
2414                    assert!(msg.contains("start=8"));
2415                    assert!(msg.contains("delta=3"));
2416                }
2417            }
2418            _ => panic!("Expected SetValue event for column shift"),
2419        }
2420    }
2421
2422    #[test]
2423    fn test_move_vertex() {
2424        let mut graph = create_test_graph();
2425        let mut editor = VertexEditor::new(&mut graph);
2426
2427        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell);
2428        let vertex_id = editor.add_vertex(meta);
2429
2430        // Move vertex returns Result
2431        assert!(editor.move_vertex(vertex_id, GridAddr::new(8, 12)).is_ok());
2432
2433        // Moving to same position should work
2434        assert!(editor.move_vertex(vertex_id, GridAddr::new(8, 12)).is_ok());
2435    }
2436
2437    #[test]
2438    fn test_vertex_meta_builder() {
2439        let meta = VertexMeta::new(1, 2, 3, VertexKind::FormulaScalar)
2440            .dirty()
2441            .volatile()
2442            .with_flags(0x08);
2443
2444        assert_eq!(meta.coord.row(), 1);
2445        assert_eq!(meta.coord.col(), 2);
2446        assert_eq!(meta.sheet_id, 3);
2447        assert_eq!(meta.kind, VertexKind::FormulaScalar);
2448        assert_eq!(meta.flags, 0x08); // Last with_flags call overwrites previous flags
2449    }
2450
2451    #[test]
2452    fn test_change_log_management() {
2453        let mut graph = create_test_graph();
2454        let mut log = ChangeLog::new();
2455
2456        {
2457            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2458            let cell_ref = CellRef {
2459                sheet_id: 0,
2460                coord: Coord::new(0, 0, true, true),
2461            };
2462            editor.set_cell_value(cell_ref, LiteralValue::Number(1.0));
2463            editor.set_cell_value(cell_ref, LiteralValue::Number(2.0));
2464        }
2465
2466        assert_eq!(log.len(), 2);
2467
2468        log.clear();
2469        assert_eq!(log.len(), 0);
2470    }
2471
2472    #[test]
2473    fn test_editor_drop_commits_batch() {
2474        let mut graph = create_test_graph();
2475        {
2476            let mut editor = VertexEditor::new(&mut graph);
2477            editor.begin_batch();
2478
2479            let meta = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2480            editor.add_vertex(meta);
2481
2482            // Editor will be dropped here, should commit batch
2483        }
2484
2485        // If we reach here without hanging, the batch was properly committed
2486    }
2487}