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