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.vertex_formulas.remove(&id);
830        self.graph.vertex_values.remove(&id);
831        self.graph.clear_formula_vertex_dirty(id);
832        self.graph.mark_volatile(id, false);
833        self.graph.store.set_kind(id, VertexKind::Empty);
834        self.graph.store.set_dynamic(id, false);
835
836        // Remove all edges
837        self.graph.remove_all_edges(id);
838
839        // Mark all dependents as having #REF! error
840        for dep_id in &dependents {
841            self.graph.mark_as_ref_error(*dep_id);
842        }
843
844        // Mark as deleted in store (tombstone)
845        self.graph.mark_deleted(id, true);
846
847        // Log change event
848        self.log_change(ChangeEvent::RemoveVertex {
849            id,
850            old_value,
851            old_formula,
852            old_dependencies,
853            old_dependents,
854            coord,
855            sheet_id: sheet_id_opt,
856            kind,
857            flags,
858        });
859
860        if did_spill_clear && let Some(logger) = &mut self.change_logger {
861            logger.end_compound();
862        }
863
864        Ok(())
865    }
866
867    /// Convenience: remove vertex at a given cell ref if exists
868    pub fn remove_vertex_at(&mut self, cell: CellRef) -> Result<(), EditorError> {
869        if let Some(id) = self.graph.get_vertex_for_cell(&cell) {
870            self.remove_vertex(id)
871        } else {
872            // A value or referenced cell (legacy's vertex) leaves the used
873            // extent.
874            let (r, c) = (cell.coord.row(), cell.coord.col());
875            self.graph
876                .forget_extent_cells(cell.sheet_id, (r, r), (c, c));
877            Ok(())
878        }
879    }
880
881    /// Move a vertex to a new position
882    ///
883    /// The `GridAddr` argument says where the vertex is going, but the `VertexId` says
884    /// nothing about whether it is somewhere to begin with. A symbol has no position, so
885    /// moving one is meaningless: it is what turned a default-sheet insert into a
886    /// name-hijacked cell (#304). Every in-tree caller iterates `grid_vertices_in_sheet`
887    /// and so cannot reach this, but the method is public, so refuse explicitly.
888    pub fn move_vertex(&mut self, id: VertexId, new_coord: GridAddr) -> Result<(), EditorError> {
889        self.move_vertex_inner(id, new_coord, true)
890    }
891
892    /// [`Self::move_vertex`]; `map_cell` false leaves the cell map alone
893    /// (the destination keeps the vertex a block shift moved there).
894    fn move_vertex_inner(
895        &mut self,
896        id: VertexId,
897        new_coord: GridAddr,
898        map_cell: bool,
899    ) -> Result<(), EditorError> {
900        self.graph.authority_note_structural(false);
901        // A compressed member's formula is relative to its cell: it keeps
902        // its own AST across the move (Program 2).
903        self.graph.own_formula_id(id);
904        // Check if vertex exists
905        if !self.graph.vertex_exists(id) {
906            return Err(EditorError::Excel(
907                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
908            ));
909        }
910        if self.graph.get_grid_addr(id).is_none() {
911            return Err(EditorError::Excel(
912                ExcelError::new(ExcelErrorKind::Ref).with_message(
913                    "Symbol vertices have no position and cannot be moved onto the grid",
914                ),
915            ));
916        }
917
918        // Get old cell reference
919        let old_cell_ref = self.graph.get_cell_ref_for_vertex(id);
920
921        // Create new cell reference
922        let sheet_id = self.graph.get_sheet_id(id);
923        let new_cell_ref = CellRef::new(
924            sheet_id,
925            Coord::new(new_coord.row(), new_coord.col(), true, true),
926        );
927
928        // Update coordinate in store
929        self.graph.set_grid_addr(id, new_coord);
930
931        // Update edge cache coordinate if needed
932        self.graph.update_edge_grid_addr(id, new_coord);
933
934        // Update cell mapping
935        if map_cell {
936            self.graph
937                .update_cell_mapping(id, old_cell_ref, new_cell_ref);
938        }
939
940        // Mark dependents as dirty
941        self.graph.mark_dependents_dirty(id);
942
943        Ok(())
944    }
945
946    /// Update vertex metadata
947    pub fn patch_vertex_meta(
948        &mut self,
949        id: VertexId,
950        patch: VertexMetaPatch,
951    ) -> Result<MetaUpdateSummary, EditorError> {
952        if !self.graph.vertex_exists(id) {
953            return Err(EditorError::Excel(
954                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
955            ));
956        }
957
958        let mut summary = MetaUpdateSummary::default();
959
960        if let Some(coord) = patch.coord {
961            // Same reasoning as `move_vertex`: a symbol has no position to patch.
962            if self.graph.get_grid_addr(id).is_none() {
963                return Err(EditorError::Excel(
964                    ExcelError::new(ExcelErrorKind::Ref).with_message(
965                        "Symbol vertices have no position and cannot be moved onto the grid",
966                    ),
967                ));
968            }
969            // A compressed member's formula is relative to its cell.
970            let _ = self.graph.own_formula_id(id);
971            self.graph.set_grid_addr(id, coord);
972            self.graph.update_edge_grid_addr(id, coord);
973            summary.coord_changed = true;
974        }
975
976        if let Some(kind) = patch.kind {
977            self.graph.set_kind(id, kind);
978            summary.kind_changed = true;
979        }
980
981        if let Some(dirty) = patch.dirty {
982            self.graph.set_dirty(id, dirty);
983            summary.flags_changed = true;
984        }
985
986        if let Some(volatile) = patch.volatile {
987            self.graph.mark_volatile(id, volatile);
988            summary.flags_changed = true;
989        }
990
991        Ok(summary)
992    }
993
994    /// Update vertex data (value or formula)
995    pub fn patch_vertex_data(
996        &mut self,
997        id: VertexId,
998        patch: VertexDataPatch,
999    ) -> Result<DataUpdateSummary, EditorError> {
1000        if !self.graph.vertex_exists(id) {
1001            return Err(EditorError::Excel(
1002                ExcelError::new(ExcelErrorKind::Ref).with_message("Vertex does not exist"),
1003            ));
1004        }
1005
1006        let mut summary = DataUpdateSummary::default();
1007
1008        if let Some(value) = patch.value {
1009            self.graph.update_vertex_value(id, value);
1010            summary.value_changed = true;
1011
1012            // Mark dependents as dirty. get_dependents is delta-aware, so no
1013            // CSR rebuild is required even when edits are pending (#125).
1014            let dependents = self.graph.authority_in_edge_readers(id);
1015            for dep in &dependents {
1016                self.graph.set_dirty(*dep, true);
1017            }
1018            summary.dependents_marked_dirty = dependents;
1019        }
1020
1021        if let Some(_formula) = patch.formula {
1022            // This would need proper formula update implementation
1023            // For now, we'll mark as changed
1024            summary.formula_changed = true;
1025        }
1026
1027        Ok(summary)
1028    }
1029
1030    /// No-op kept for journal replay of old `EdgeAdded` events: the
1031    /// dependency authority derives every edge from formulas.
1032    pub fn add_edge(&mut self, from: VertexId, to: VertexId) -> bool {
1033        if from == to {
1034            return false; // Prevent self-loops
1035        }
1036
1037        // TODO: Add edge through proper API when available
1038        // For now, return true to indicate intent
1039        true
1040    }
1041
1042    /// No-op kept for journal replay of old `EdgeRemoved` events.
1043    pub fn remove_edge(&mut self, _from: VertexId, _to: VertexId) -> bool {
1044        // TODO: Remove edge through proper API when available
1045        true
1046    }
1047
1048    /// Insert rows at the specified position, shifting existing rows down
1049    pub fn insert_rows(
1050        &mut self,
1051        sheet_id: SheetId,
1052        before: u32,
1053        count: u32,
1054    ) -> Result<ShiftSummary, EditorError> {
1055        let result = self.insert_rows_impl(sheet_id, before, count);
1056        self.graph.authority_end_structural();
1057        result
1058    }
1059
1060    fn insert_rows_impl(
1061        &mut self,
1062        sheet_id: SheetId,
1063        before: u32,
1064        count: u32,
1065    ) -> Result<ShiftSummary, EditorError> {
1066        self.graph.authority_note_structural_shift();
1067        if count == 0 {
1068            return Ok(ShiftSummary::default());
1069        }
1070
1071        let mut summary = ShiftSummary::default();
1072
1073        // Begin batch for efficiency
1074        self.begin_batch();
1075        let conservative = crate::engine::graph::StructuralOccupancy::conservative();
1076        let occupancy = self.structural_occupancy.as_ref().unwrap_or(&conservative);
1077        let range_dependents = self.graph.authority_structural_band_readers(
1078            sheet_id,
1079            crate::engine::graph::StructuralEdit::InsertRows { before },
1080            occupancy,
1081        );
1082        #[cfg(test)]
1083        {
1084            summary.structural_dependents_dirtied = range_dependents.clone();
1085        }
1086        self.graph.mark_dirty_many(&range_dependents);
1087
1088        // 1. Collect vertices to shift (those at or after the insert point)
1089        let vertices_to_shift: Vec<(VertexId, GridAddr)> = self
1090            .graph
1091            .grid_vertices_in_sheet(sheet_id)
1092            .filter(|(_, coord)| coord.row() >= before)
1093            .collect();
1094
1095        if let Some(logger) = &mut self.change_logger {
1096            logger.begin_compound(format!(
1097                "InsertRows sheet={sheet_id} before={before} count={count}"
1098            ));
1099        }
1100        // 2-3. Shift vertices (emit VertexMoved) and adjust formulas
1101        // (emit FormulaAdjusted).
1102        let op = ShiftOperation::InsertRows {
1103            sheet_id,
1104            before,
1105            count,
1106        };
1107        self.shift_and_adjust(
1108            sheet_id,
1109            &op,
1110            vertices_to_shift,
1111            |old_coord| GridAddr::new(old_coord.row() + count, old_coord.col()),
1112            &mut summary,
1113        )?;
1114
1115        // 4. Adjust named ranges
1116        let old_names = if self.has_logger() {
1117            Some(self.snapshot_named_definitions())
1118        } else {
1119            None
1120        };
1121        self.graph.adjust_named_ranges(&op)?;
1122        if let Some(old_names) = old_names {
1123            let new_names = self.snapshot_named_definitions();
1124            for ((scope, name), old_definition) in old_names {
1125                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1126                    && *new_definition != old_definition
1127                {
1128                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1129                        name,
1130                        scope,
1131                        old_definition,
1132                        new_definition: new_definition.clone(),
1133                    });
1134                }
1135            }
1136        }
1137
1138        // 5. Log change event
1139        if let Some(logger) = &mut self.change_logger {
1140            logger.end_compound();
1141        }
1142
1143        self.commit_batch();
1144
1145        Ok(summary)
1146    }
1147
1148    /// Delete rows at the specified position, shifting remaining rows up
1149    pub fn delete_rows(
1150        &mut self,
1151        sheet_id: SheetId,
1152        start: u32,
1153        count: u32,
1154    ) -> Result<ShiftSummary, EditorError> {
1155        let result = self.delete_rows_impl(sheet_id, start, count);
1156        self.graph.authority_end_structural();
1157        result
1158    }
1159
1160    fn delete_rows_impl(
1161        &mut self,
1162        sheet_id: SheetId,
1163        start: u32,
1164        count: u32,
1165    ) -> Result<ShiftSummary, EditorError> {
1166        if count == 0 {
1167            self.graph.authority_note_structural_shift();
1168            return Ok(ShiftSummary::default());
1169        }
1170        // The deleted band's readers (and theirs) are dirty, in the pre-edit
1171        // frame: value cells have no vertex whose removal would dirty them
1172        // (decision 27). Only occupied columns when the caller knows them
1173        // (an empty column's readers see no change).
1174        let end = start.saturating_add(count - 1);
1175        let rects: Vec<(SheetId, u32, u32, u32, u32)> = match self
1176            .structural_occupancy
1177            .as_ref()
1178            .and_then(|o| o.occupied_column_runs())
1179        {
1180            Some(runs) => runs
1181                .into_iter()
1182                .map(|(c0, c1)| (sheet_id, start, end, c0, c1))
1183                .collect(),
1184            None => vec![(sheet_id, start, end, 0, 16_383)],
1185        };
1186        let _ = self.graph.mark_dirty_rects(&rects);
1187        self.graph.authority_note_structural_shift();
1188
1189        let mut summary = ShiftSummary::default();
1190
1191        self.begin_batch();
1192
1193        if let Some(logger) = &mut self.change_logger {
1194            logger.begin_compound(format!(
1195                "DeleteRows sheet={sheet_id} start={start} count={count}"
1196            ));
1197        }
1198
1199        // 1. Delete vertices in the range
1200        let vertices_to_delete: Vec<VertexId> = self
1201            .graph
1202            .grid_vertices_in_sheet(sheet_id)
1203            .filter(|(_, coord)| coord.row() >= start && coord.row() < start + count)
1204            .map(|(id, _)| id)
1205            .collect();
1206        let conservative = crate::engine::graph::StructuralOccupancy::conservative();
1207        let occupancy = self.structural_occupancy.as_ref().unwrap_or(&conservative);
1208        let range_dependents = self.graph.authority_structural_band_readers(
1209            sheet_id,
1210            crate::engine::graph::StructuralEdit::DeleteRows {
1211                start,
1212                end: start.saturating_add(count).saturating_sub(1).max(start),
1213            },
1214            occupancy,
1215        );
1216        #[cfg(test)]
1217        {
1218            summary.structural_dependents_dirtied = range_dependents.clone();
1219        }
1220        self.graph.mark_dirty_many(&range_dependents);
1221
1222        for id in vertices_to_delete {
1223            self.remove_vertex(id)?;
1224            summary.vertices_deleted.push(id);
1225        }
1226        // 2-3. Shift the remaining vertices (emit VertexMoved) and adjust
1227        // formulas (emit FormulaAdjusted).
1228        let vertices_to_shift: Vec<(VertexId, GridAddr)> = self
1229            .graph
1230            .grid_vertices_in_sheet(sheet_id)
1231            .filter(|(_, coord)| coord.row() >= start + count)
1232            .collect();
1233        let op = ShiftOperation::DeleteRows {
1234            sheet_id,
1235            start,
1236            count,
1237        };
1238        self.shift_and_adjust(
1239            sheet_id,
1240            &op,
1241            vertices_to_shift,
1242            |old_coord| GridAddr::new(old_coord.row() - count, old_coord.col()),
1243            &mut summary,
1244        )?;
1245
1246        // 4. Adjust named ranges
1247        let old_names = if self.has_logger() {
1248            Some(self.snapshot_named_definitions())
1249        } else {
1250            None
1251        };
1252        self.graph.adjust_named_ranges(&op)?;
1253        if let Some(old_names) = old_names {
1254            let new_names = self.snapshot_named_definitions();
1255            for ((scope, name), old_definition) in old_names {
1256                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1257                    && *new_definition != old_definition
1258                {
1259                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1260                        name,
1261                        scope,
1262                        old_definition,
1263                        new_definition: new_definition.clone(),
1264                    });
1265                }
1266            }
1267        }
1268
1269        // 5. Log change event
1270        if let Some(logger) = &mut self.change_logger {
1271            logger.end_compound();
1272        }
1273
1274        self.commit_batch();
1275
1276        Ok(summary)
1277    }
1278
1279    /// Insert columns at the specified position, shifting existing columns right
1280    pub fn insert_columns(
1281        &mut self,
1282        sheet_id: SheetId,
1283        before: u32,
1284        count: u32,
1285    ) -> Result<ShiftSummary, EditorError> {
1286        let result = self.insert_columns_impl(sheet_id, before, count);
1287        self.graph.authority_end_structural();
1288        result
1289    }
1290
1291    fn insert_columns_impl(
1292        &mut self,
1293        sheet_id: SheetId,
1294        before: u32,
1295        count: u32,
1296    ) -> Result<ShiftSummary, EditorError> {
1297        self.graph.authority_note_structural_shift();
1298        if count == 0 {
1299            return Ok(ShiftSummary::default());
1300        }
1301
1302        let mut summary = ShiftSummary::default();
1303
1304        // Begin batch for efficiency
1305        self.begin_batch();
1306        let conservative = crate::engine::graph::StructuralOccupancy::conservative();
1307        let occupancy = self.structural_occupancy.as_ref().unwrap_or(&conservative);
1308        let range_dependents = self.graph.authority_structural_band_readers(
1309            sheet_id,
1310            crate::engine::graph::StructuralEdit::InsertColumns { before },
1311            occupancy,
1312        );
1313        #[cfg(test)]
1314        {
1315            summary.structural_dependents_dirtied = range_dependents.clone();
1316        }
1317        self.graph.mark_dirty_many(&range_dependents);
1318
1319        // 1. Collect vertices to shift (those at or after the insert point)
1320        let vertices_to_shift: Vec<(VertexId, GridAddr)> = self
1321            .graph
1322            .grid_vertices_in_sheet(sheet_id)
1323            .filter(|(_, coord)| coord.col() >= before)
1324            .collect();
1325
1326        if let Some(logger) = &mut self.change_logger {
1327            logger.begin_compound(format!(
1328                "InsertColumns sheet={sheet_id} before={before} count={count}"
1329            ));
1330        }
1331        // 2-3. Shift vertices (emit VertexMoved) and adjust formulas
1332        // (emit FormulaAdjusted).
1333        let op = ShiftOperation::InsertColumns {
1334            sheet_id,
1335            before,
1336            count,
1337        };
1338        self.shift_and_adjust(
1339            sheet_id,
1340            &op,
1341            vertices_to_shift,
1342            |old_coord| GridAddr::new(old_coord.row(), old_coord.col() + count),
1343            &mut summary,
1344        )?;
1345
1346        // 4. Adjust named ranges
1347        let old_names = if self.has_logger() {
1348            Some(self.snapshot_named_definitions())
1349        } else {
1350            None
1351        };
1352        self.graph.adjust_named_ranges(&op)?;
1353        if let Some(old_names) = old_names {
1354            let new_names = self.snapshot_named_definitions();
1355            for ((scope, name), old_definition) in old_names {
1356                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1357                    && *new_definition != old_definition
1358                {
1359                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1360                        name,
1361                        scope,
1362                        old_definition,
1363                        new_definition: new_definition.clone(),
1364                    });
1365                }
1366            }
1367        }
1368
1369        // 5. Log change event
1370        if let Some(logger) = &mut self.change_logger {
1371            logger.end_compound();
1372        }
1373
1374        self.commit_batch();
1375
1376        Ok(summary)
1377    }
1378
1379    /// Steps 2 and 3 of a row/column insert or delete: move the vertices
1380    /// at or past the edit (logging `VertexMoved`), then adjust every
1381    /// formula (logging `FormulaAdjusted`).
1382    ///
1383    /// Program 2 (contract decision 20.5): virtual family member runs are
1384    /// shifted and adjusted as one first (`shift_virtual_runs`); their
1385    /// members are not moved or rewritten one by one, and their
1386    /// `FormulaAdjusted` events go to the log as one run record, expanded
1387    /// only when read. Runs the block transform cannot express come back
1388    /// to the per-cell maps and take the per-vertex path below. Events are
1389    /// logged in vertex-id order (moves, then adjustments), the per-vertex
1390    /// order legacy logged moves in.
1391    fn shift_and_adjust(
1392        &mut self,
1393        sheet_id: SheetId,
1394        op: &ShiftOperation,
1395        vertices_to_shift: Vec<(VertexId, GridAddr)>,
1396        shift: impl Fn(GridAddr) -> GridAddr,
1397        summary: &mut ShiftSummary,
1398    ) -> Result<(), EditorError> {
1399        let journal = self.has_logger();
1400        self.graph.shift_retired_ids(op, journal);
1401        let runs = self.graph.shift_virtual_runs(op);
1402
1403        for (id, old_coord) in vertices_to_shift {
1404            let new_coord = shift(old_coord);
1405            if self.has_logger() {
1406                self.log_change(ChangeEvent::VertexMoved {
1407                    id,
1408                    sheet_id,
1409                    old_coord,
1410                    new_coord,
1411                });
1412            }
1413            if self.graph.is_virtual_member(id) {
1414                // Moved with its run.
1415                debug_assert_eq!(self.graph.get_grid_addr(id), Some(new_coord));
1416            } else {
1417                // Legacy moved vertices one at a time in id order: of two
1418                // vertices shifted onto one cell (a tombstone or stale
1419                // vertex, and a live one) the later id kept the mapping. A
1420                // member its run moved there already keeps the cell when
1421                // its id is later.
1422                let cell = CellRef::new(
1423                    sheet_id,
1424                    Coord::new(new_coord.row(), new_coord.col(), true, true),
1425                );
1426                let member_moves_later = self
1427                    .graph
1428                    .virtual_member_at(&cell)
1429                    .is_some_and(|m| m.0 > id.0 && moved_run_holds(&runs, m));
1430                self.move_vertex_inner(id, new_coord, !member_moves_later)?;
1431            }
1432            summary.vertices_moved.push(id);
1433        }
1434        self.graph.after_shift_moves(&runs);
1435
1436        let adjuster = ReferenceAdjuster::new();
1437        let mut adjusted_runs: Vec<&crate::engine::graph::ShiftedRun> =
1438            runs.iter().filter(|r| r.adjusted.is_some()).collect();
1439        adjusted_runs.sort_unstable_by_key(|r| r.first.0);
1440        let mut next_run = 0;
1441        let formula_vertices = self.graph.materialized_formula_vertices_sorted();
1442        for id in formula_vertices {
1443            while next_run < adjusted_runs.len() && adjusted_runs[next_run].first.0 < id.0 {
1444                self.log_adjusted_run(adjusted_runs[next_run], summary);
1445                next_run += 1;
1446            }
1447            if let Some(ast) = self.get_formula_ast(id)
1448                && let Some(adjusted) = adjuster.adjust_ast_if_changed_in_context(
1449                    &ast,
1450                    op,
1451                    &ReferenceContext::new(self.graph.get_sheet_id(id), self.graph.sheet_reg()),
1452                )
1453            {
1454                if self.has_logger() {
1455                    self.log_change(ChangeEvent::FormulaAdjusted {
1456                        id,
1457                        addr: self.graph.get_cell_ref_for_vertex(id),
1458                        old_ast: ast.clone(),
1459                        new_ast: adjusted.clone(),
1460                    });
1461                }
1462                self.graph.update_vertex_formula(id, adjusted)?;
1463                self.graph.mark_vertex_dirty(id);
1464                summary.formulas_updated += 1;
1465            }
1466        }
1467        for run in &adjusted_runs[next_run..] {
1468            self.log_adjusted_run(run, summary);
1469        }
1470        Ok(())
1471    }
1472
1473    /// A run the block transform adjusted: its members' `FormulaAdjusted`
1474    /// events as one record (the transform already rewrote and dirtied
1475    /// them).
1476    fn log_adjusted_run(
1477        &mut self,
1478        run: &crate::engine::graph::ShiftedRun,
1479        summary: &mut ShiftSummary,
1480    ) {
1481        summary.formulas_updated += run.len as usize;
1482        let Some((old_first, new_first)) = &run.adjusted else {
1483            return;
1484        };
1485        if let Some(logger) = &mut self.change_logger {
1486            logger.record_formula_run(FormulaRunAdjusted {
1487                first: run.first,
1488                len: run.len,
1489                sheet_id: run.sheet,
1490                col: run.col,
1491                row0: run.row0,
1492                old_first: old_first.clone(),
1493                new_first: new_first.clone(),
1494            });
1495        }
1496    }
1497
1498    /// Delete columns at the specified position, shifting remaining columns left
1499    pub fn delete_columns(
1500        &mut self,
1501        sheet_id: SheetId,
1502        start: u32,
1503        count: u32,
1504    ) -> Result<ShiftSummary, EditorError> {
1505        let result = self.delete_columns_impl(sheet_id, start, count);
1506        self.graph.authority_end_structural();
1507        result
1508    }
1509
1510    fn delete_columns_impl(
1511        &mut self,
1512        sheet_id: SheetId,
1513        start: u32,
1514        count: u32,
1515    ) -> Result<ShiftSummary, EditorError> {
1516        if count == 0 {
1517            self.graph.authority_note_structural_shift();
1518            return Ok(ShiftSummary::default());
1519        }
1520        // The deleted band's readers (and theirs) are dirty, in the pre-edit
1521        // frame: value cells have no vertex whose removal would dirty them
1522        // (decision 27).
1523        let _ = self.graph.mark_dirty_rects(&[(
1524            sheet_id,
1525            0,
1526            1_048_575,
1527            start,
1528            start.saturating_add(count - 1),
1529        )]);
1530        self.graph.authority_note_structural_shift();
1531
1532        let mut summary = ShiftSummary::default();
1533
1534        self.begin_batch();
1535
1536        if let Some(logger) = &mut self.change_logger {
1537            logger.begin_compound(format!(
1538                "DeleteColumns sheet={sheet_id} start={start} count={count}"
1539            ));
1540        }
1541
1542        // 1. Delete vertices in the range
1543        let vertices_to_delete: Vec<VertexId> = self
1544            .graph
1545            .grid_vertices_in_sheet(sheet_id)
1546            .filter(|(_, coord)| coord.col() >= start && coord.col() < start + count)
1547            .map(|(id, _)| id)
1548            .collect();
1549        let conservative = crate::engine::graph::StructuralOccupancy::conservative();
1550        let occupancy = self.structural_occupancy.as_ref().unwrap_or(&conservative);
1551        let range_dependents = self.graph.authority_structural_band_readers(
1552            sheet_id,
1553            crate::engine::graph::StructuralEdit::DeleteColumns {
1554                start,
1555                end: start.saturating_add(count).saturating_sub(1).max(start),
1556            },
1557            occupancy,
1558        );
1559        #[cfg(test)]
1560        {
1561            summary.structural_dependents_dirtied = range_dependents.clone();
1562        }
1563        self.graph.mark_dirty_many(&range_dependents);
1564
1565        for id in vertices_to_delete {
1566            self.remove_vertex(id)?;
1567            summary.vertices_deleted.push(id);
1568        }
1569        // 2-3. Shift the remaining vertices (emit VertexMoved) and adjust
1570        // formulas (emit FormulaAdjusted).
1571        let vertices_to_shift: Vec<(VertexId, GridAddr)> = self
1572            .graph
1573            .grid_vertices_in_sheet(sheet_id)
1574            .filter(|(_, coord)| coord.col() >= start + count)
1575            .collect();
1576        let op = ShiftOperation::DeleteColumns {
1577            sheet_id,
1578            start,
1579            count,
1580        };
1581        self.shift_and_adjust(
1582            sheet_id,
1583            &op,
1584            vertices_to_shift,
1585            |old_coord| GridAddr::new(old_coord.row(), old_coord.col() - count),
1586            &mut summary,
1587        )?;
1588
1589        // 4. Adjust named ranges
1590        let old_names = if self.has_logger() {
1591            Some(self.snapshot_named_definitions())
1592        } else {
1593            None
1594        };
1595        self.graph.adjust_named_ranges(&op)?;
1596        if let Some(old_names) = old_names {
1597            let new_names = self.snapshot_named_definitions();
1598            for ((scope, name), old_definition) in old_names {
1599                if let Some(new_definition) = new_names.get(&(scope, name.clone()))
1600                    && *new_definition != old_definition
1601                {
1602                    self.log_change(ChangeEvent::NamedRangeAdjusted {
1603                        name,
1604                        scope,
1605                        old_definition,
1606                        new_definition: new_definition.clone(),
1607                    });
1608                }
1609            }
1610        }
1611
1612        // 5. Log change event
1613        if let Some(logger) = &mut self.change_logger {
1614            logger.end_compound();
1615        }
1616
1617        self.commit_batch();
1618
1619        Ok(summary)
1620    }
1621
1622    /// Shift rows down/up within a sheet (Excel's insert/delete rows)
1623    pub fn shift_rows(&mut self, sheet_id: SheetId, start_row: u32, delta: i32) {
1624        if delta == 0 {
1625            return;
1626        }
1627
1628        // Log change event for undo/redo
1629        let change_event = ChangeEvent::SetValue {
1630            addr: CellRef {
1631                sheet_id,
1632                coord: Coord::new(start_row, 0, true, true),
1633            },
1634            old_value: None,
1635            old_formula: None,
1636            new: LiteralValue::Text(format!("Row shift: start={start_row}, delta={delta}")),
1637        };
1638        self.log_change(change_event);
1639
1640        // TODO: Implement actual row shifting logic
1641        // This would require coordination with the vertex store and dependency tracking
1642    }
1643
1644    /// Shift columns left/right within a sheet (Excel's insert/delete columns)
1645    pub fn shift_columns(&mut self, sheet_id: SheetId, start_col: u32, delta: i32) {
1646        if delta == 0 {
1647            return;
1648        }
1649
1650        // Log change event
1651        let change_event = ChangeEvent::SetValue {
1652            addr: CellRef {
1653                sheet_id,
1654                coord: Coord::new(0, start_col, true, true),
1655            },
1656            old_value: None,
1657            old_formula: None,
1658            new: LiteralValue::Text(format!("Column shift: start={start_col}, delta={delta}")),
1659        };
1660        self.log_change(change_event);
1661
1662        // TODO: Implement actual column shifting logic
1663        // This would require coordination with the vertex store and dependency tracking
1664    }
1665
1666    /// Set a cell value. A value cell has no vertex (decision 27): returns
1667    /// `VertexId(0)`; a formula it replaces retires its id.
1668    pub fn set_cell_value(&mut self, cell_ref: CellRef, value: LiteralValue) -> VertexId {
1669        self.set_cell_value_with_old_state(cell_ref, value, None, None)
1670    }
1671
1672    /// Like [`set_cell_value`](Self::set_cell_value), but lets the caller
1673    /// supply old state captured from an external source of truth (e.g. the
1674    /// Arrow store, whose values are invisible here when the graph value cache
1675    /// is disabled) for the change-log event.
1676    ///
1677    /// Precedence matches the historical append-then-patch flow
1678    /// (`ChangeLog::patch_last_cell_event_old_state`): state the editor
1679    /// captures from the graph wins; caller-supplied state only fills fields
1680    /// the graph left `None`.
1681    pub fn set_cell_value_with_old_state(
1682        &mut self,
1683        cell_ref: CellRef,
1684        value: LiteralValue,
1685        fallback_old_value: Option<LiteralValue>,
1686        fallback_old_formula: Option<ASTNode>,
1687    ) -> VertexId {
1688        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1689
1690        // Capture old state before modification (value + formula); fall back
1691        // to caller-supplied state for anything the graph cannot see.
1692        let old_id = self.graph.get_vertex_id_for_address(&cell_ref);
1693        let old_value = old_id
1694            .and_then(|id| self.graph.get_value(id))
1695            .or(fallback_old_value);
1696        let old_formula = old_id
1697            .and_then(|id| self.get_formula_ast(id))
1698            .or(fallback_old_formula);
1699
1700        // If this cell currently anchors a spill, clear the spill first and log it.
1701        // This keeps spill ownership maps and children consistent under undo/redo.
1702        let spill_snapshot =
1703            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1704        let did_spill_clear = spill_snapshot.is_some();
1705        if let Some((anchor, old_spill)) = spill_snapshot {
1706            if let Some(logger) = &mut self.change_logger {
1707                logger.begin_compound(format!(
1708                    "SetValueWithSpillClear sheet={} row={} col={}",
1709                    cell_ref.sheet_id,
1710                    cell_ref.coord.row(),
1711                    cell_ref.coord.col()
1712                ));
1713            }
1714            self.graph.clear_spill_region(anchor);
1715            self.log_change(ChangeEvent::SpillCleared {
1716                anchor,
1717                old: old_spill,
1718            });
1719        }
1720
1721        // Use the existing DependencyGraph API
1722        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1723        match self.graph.set_cell_value(
1724            &sheet_name,
1725            cell_ref.coord.row() + 1,
1726            cell_ref.coord.col() + 1,
1727            value.clone(),
1728        ) {
1729            Ok(_) => {
1730                // Log change event
1731                let change_event = ChangeEvent::SetValue {
1732                    addr: cell_ref,
1733                    old_value,
1734                    old_formula,
1735                    new: value,
1736                };
1737                self.log_change(change_event);
1738
1739                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1740                    logger.end_compound();
1741                }
1742
1743                // A value cell has no vertex (decision 27).
1744                VertexId::new(0)
1745            }
1746            Err(_) => VertexId::new(0),
1747        }
1748    }
1749
1750    /// Set a cell formula, creating the vertex if it doesn't exist.
1751    ///
1752    /// Legacy compatibility API: failures return vertex zero. Prefer
1753    /// [`Self::try_set_cell_formula`] when failure must be observable.
1754    pub fn set_cell_formula(&mut self, cell_ref: CellRef, formula: ASTNode) -> VertexId {
1755        self.set_cell_formula_with_old_state(cell_ref, formula, None, None)
1756    }
1757
1758    /// Set a formula, reporting binding/admission failures without clearing its old spill.
1759    pub fn try_set_cell_formula(
1760        &mut self,
1761        cell_ref: CellRef,
1762        formula: ASTNode,
1763    ) -> Result<VertexId, ExcelError> {
1764        self.try_set_cell_formula_with_old_state(cell_ref, formula, None, None)
1765    }
1766
1767    /// Like [`set_cell_formula`](Self::set_cell_formula), but lets the caller
1768    /// supply old state captured from an external source of truth (e.g. the
1769    /// Arrow store) for the change-log event. Same precedence as
1770    /// [`set_cell_value_with_old_state`](Self::set_cell_value_with_old_state):
1771    /// graph-captured state wins, caller state only fills `None` fields.
1772    pub fn set_cell_formula_with_old_state(
1773        &mut self,
1774        cell_ref: CellRef,
1775        formula: ASTNode,
1776        fallback_old_value: Option<LiteralValue>,
1777        fallback_old_formula: Option<ASTNode>,
1778    ) -> VertexId {
1779        self.try_set_cell_formula_with_old_state(
1780            cell_ref,
1781            formula,
1782            fallback_old_value,
1783            fallback_old_formula,
1784        )
1785        .unwrap_or_else(|_| VertexId::new(0))
1786    }
1787
1788    /// Fallible counterpart to [`Self::set_cell_formula_with_old_state`].
1789    /// Graph-captured old state takes precedence over caller-provided state.
1790    pub fn try_set_cell_formula_with_old_state(
1791        &mut self,
1792        cell_ref: CellRef,
1793        formula: ASTNode,
1794        fallback_old_value: Option<LiteralValue>,
1795        fallback_old_formula: Option<ASTNode>,
1796    ) -> Result<VertexId, ExcelError> {
1797        self.set_cell_formula_with_old_state_and_plan(
1798            cell_ref,
1799            formula,
1800            fallback_old_value,
1801            fallback_old_formula,
1802            None,
1803        )
1804    }
1805
1806    pub(crate) fn set_cell_formula_with_prepared_plan(
1807        &mut self,
1808        cell_ref: CellRef,
1809        formula: ASTNode,
1810        fallback_old_value: Option<LiteralValue>,
1811        fallback_old_formula: Option<ASTNode>,
1812        ast_id: crate::engine::arena::AstNodeId,
1813        plan: crate::engine::ingest_pipeline::DependencyPlanRow,
1814    ) -> VertexId {
1815        self.set_cell_formula_with_old_state_and_plan(
1816            cell_ref,
1817            formula,
1818            fallback_old_value,
1819            fallback_old_formula,
1820            Some((ast_id, plan)),
1821        )
1822        .unwrap_or_else(|_| VertexId::new(0))
1823    }
1824
1825    fn set_cell_formula_with_old_state_and_plan(
1826        &mut self,
1827        cell_ref: CellRef,
1828        formula: ASTNode,
1829        fallback_old_value: Option<LiteralValue>,
1830        fallback_old_formula: Option<ASTNode>,
1831        prepared: Option<(
1832            crate::engine::arena::AstNodeId,
1833            crate::engine::ingest_pipeline::DependencyPlanRow,
1834        )>,
1835    ) -> Result<VertexId, ExcelError> {
1836        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id).to_string();
1837
1838        // Capture old state before modification (value + formula); fall back
1839        // to caller-supplied state for anything the graph cannot see.
1840        let old_id = self.graph.get_vertex_id_for_address(&cell_ref);
1841        let old_value = old_id
1842            .and_then(|id| self.graph.get_value(id))
1843            .or(fallback_old_value);
1844        let old_formula = old_id
1845            .and_then(|id| self.get_formula_ast(id))
1846            .or(fallback_old_formula);
1847
1848        // Snapshot old spill values before updating, but do not clear or log anything
1849        // until the fallible binding/admission path succeeds.
1850        let spill_snapshot =
1851            old_id.and_then(|id| self.snapshot_spill_for_anchor(id).map(|s| (id, s)));
1852        let did_spill_clear = spill_snapshot.is_some();
1853
1854        // VertexEditor operates on internal 0-based coords; graph APIs are 1-based.
1855        let result = if let Some((ast_id, plan)) = prepared {
1856            self.graph.set_cell_formula_with_plan(
1857                &sheet_name,
1858                cell_ref.coord.row() + 1,
1859                cell_ref.coord.col() + 1,
1860                ast_id,
1861                &plan,
1862                plan.volatile,
1863                plan.dynamic,
1864            )
1865        } else {
1866            self.graph.set_cell_formula(
1867                &sheet_name,
1868                cell_ref.coord.row() + 1,
1869                cell_ref.coord.col() + 1,
1870                formula.clone(),
1871            )
1872        };
1873        match result {
1874            Ok(summary) => {
1875                if let Some((anchor, old_spill)) = spill_snapshot {
1876                    if let Some(logger) = &mut self.change_logger {
1877                        logger.begin_compound(format!(
1878                            "SetFormulaWithSpillClear sheet={} row={} col={}",
1879                            cell_ref.sheet_id,
1880                            cell_ref.coord.row(),
1881                            cell_ref.coord.col()
1882                        ));
1883                    }
1884                    self.graph.clear_spill_region(anchor);
1885                    self.log_change(ChangeEvent::SpillCleared {
1886                        anchor,
1887                        old: old_spill,
1888                    });
1889                }
1890                // Log change event
1891                let change_event = ChangeEvent::SetFormula {
1892                    addr: cell_ref,
1893                    old_value,
1894                    old_formula,
1895                    new: formula,
1896                };
1897                self.log_change(change_event);
1898
1899                if did_spill_clear && let Some(logger) = &mut self.change_logger {
1900                    logger.end_compound();
1901                }
1902
1903                Ok(summary
1904                    .affected_vertices
1905                    .into_iter()
1906                    .next()
1907                    .unwrap_or(VertexId::new(0)))
1908            }
1909            Err(error) => Err(error),
1910        }
1911    }
1912
1913    // Range operations
1914
1915    /// Set values for a rectangular range of cells
1916    pub fn set_range_values(
1917        &mut self,
1918        sheet_id: SheetId,
1919        start_row: u32,
1920        start_col: u32,
1921        values: &[Vec<LiteralValue>],
1922    ) -> Result<RangeSummary, EditorError> {
1923        let mut summary = RangeSummary::default();
1924
1925        self.begin_batch();
1926        // One multi-source dirty propagation for the whole rectangle instead
1927        // of a full BFS per cell (the loop body cannot error, so the scope
1928        // always closes before returning).
1929        self.graph.begin_deferred_dirty();
1930
1931        for (row_offset, row_values) in values.iter().enumerate() {
1932            for (col_offset, value) in row_values.iter().enumerate() {
1933                let row = start_row + row_offset as u32;
1934                let col = start_col + col_offset as u32;
1935                let cell_ref = self.graph.make_cell_ref_internal(sheet_id, row, col);
1936                let existing_id = self.graph.get_vertex_id_for_address(&cell_ref);
1937
1938                let id = self.set_cell_value(cell_ref, value.clone());
1939                match existing_id {
1940                    Some(existing_id) => summary.vertices_updated.push(existing_id),
1941                    None if id.0 != 0 => summary.vertices_created.push(id),
1942                    None => {}
1943                }
1944                summary.cells_affected += 1;
1945            }
1946        }
1947
1948        let _ = self.graph.end_deferred_dirty();
1949        self.commit_batch();
1950
1951        Ok(summary)
1952    }
1953
1954    /// Clear all cells in a rectangular range
1955    pub fn clear_range(
1956        &mut self,
1957        sheet_id: SheetId,
1958        start_row: u32,
1959        start_col: u32,
1960        end_row: u32,
1961        end_col: u32,
1962    ) -> Result<RangeSummary, EditorError> {
1963        let mut summary = RangeSummary::default();
1964
1965        self.begin_batch();
1966
1967        // Collect vertices in range
1968        let vertices_in_range: Vec<_> = self
1969            .graph
1970            .grid_vertices_in_sheet(sheet_id)
1971            .filter(|(_, coord)| {
1972                let row = coord.row();
1973                let col = coord.col();
1974                row >= start_row && row <= end_row && col >= start_col && col <= end_col
1975            })
1976            .collect();
1977
1978        let mut vertex_cells = rustc_hash::FxHashSet::default();
1979        for (id, coord) in vertices_in_range {
1980            self.remove_vertex(id)?;
1981            vertex_cells.insert((coord.row(), coord.col()));
1982            summary.cells_affected += 1;
1983        }
1984        // Value and referenced cells (legacy's vertices) leave the used
1985        // extent too, and count as cleared cells, as their vertices did.
1986        for (col, r0, r1) in
1987            self.graph
1988                .forget_extent_cells(sheet_id, (start_row, end_row), (start_col, end_col))
1989        {
1990            summary.cells_affected += (r0..=r1)
1991                .filter(|&row| !vertex_cells.contains(&(row, col)))
1992                .count();
1993        }
1994
1995        self.commit_batch();
1996
1997        Ok(summary)
1998    }
1999
2000    /// Copy a range to a new location
2001    pub fn copy_range(
2002        &mut self,
2003        sheet_id: SheetId,
2004        from_start_row: u32,
2005        from_start_col: u32,
2006        from_end_row: u32,
2007        from_end_col: u32,
2008        to_sheet_id: SheetId,
2009        to_row: u32,
2010        to_col: u32,
2011    ) -> Result<RangeSummary, EditorError> {
2012        let row_offset = to_row as i32 - from_start_row as i32;
2013        let col_offset = to_col as i32 - from_start_col as i32;
2014
2015        let mut summary = RangeSummary::default();
2016        let mut cell_data = Vec::new();
2017
2018        // Collect source data
2019        let vertices_in_range: Vec<_> = self
2020            .graph
2021            .grid_vertices_in_sheet(sheet_id)
2022            .filter(|(_, coord)| {
2023                let row = coord.row();
2024                let col = coord.col();
2025                row >= from_start_row
2026                    && row <= from_end_row
2027                    && col >= from_start_col
2028                    && col <= from_end_col
2029            })
2030            .collect();
2031
2032        for (id, coord) in vertices_in_range {
2033            let row = coord.row();
2034            let col = coord.col();
2035
2036            // Get value or formula
2037            if let Some(formula) = self.get_formula_ast(id) {
2038                cell_data.push((
2039                    row - from_start_row,
2040                    col - from_start_col,
2041                    CellData::Formula(formula),
2042                ));
2043            } else if let Some(value) = self.graph.get_value(id) {
2044                cell_data.push((
2045                    row - from_start_row,
2046                    col - from_start_col,
2047                    CellData::Value(value),
2048                ));
2049            }
2050        }
2051
2052        self.begin_batch();
2053
2054        // Apply to destination with relative adjustment
2055        for (row_idx, col_idx, data) in cell_data {
2056            let dest_row = (to_row as i32 + row_idx as i32) as u32;
2057            let dest_col = (to_col as i32 + col_idx as i32) as u32;
2058
2059            match data {
2060                CellData::Value(value) => {
2061                    let cell_ref =
2062                        self.graph
2063                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
2064
2065                    if let Some(existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
2066                        self.graph.update_vertex_value(existing_id, value);
2067                        self.graph.mark_vertex_dirty(existing_id);
2068                        summary.vertices_updated.push(existing_id);
2069                    } else {
2070                        let meta =
2071                            VertexMeta::new(dest_row, dest_col, to_sheet_id, VertexKind::Cell);
2072                        let id = self.try_add_vertex(meta)?;
2073                        self.graph.update_vertex_value(id, value);
2074                        summary.vertices_created.push(id);
2075                    }
2076                }
2077                CellData::Formula(formula) => {
2078                    // Adjust relative references in formula
2079                    let adjuster = RelativeReferenceAdjuster::new(row_offset, col_offset);
2080                    let adjusted = adjuster.adjust_formula(&formula);
2081
2082                    let cell_ref =
2083                        self.graph
2084                            .make_cell_ref_internal(to_sheet_id, dest_row, dest_col);
2085
2086                    if let Some(existing_id) = self.graph.get_vertex_id_for_address(&cell_ref) {
2087                        self.graph.update_vertex_formula(existing_id, adjusted)?;
2088                        summary.vertices_updated.push(existing_id);
2089                    } else {
2090                        let meta = VertexMeta::new(
2091                            dest_row,
2092                            dest_col,
2093                            to_sheet_id,
2094                            VertexKind::FormulaScalar,
2095                        );
2096                        let id = self.try_add_vertex(meta)?;
2097                        self.graph.update_vertex_formula(id, adjusted)?;
2098                        summary.vertices_created.push(id);
2099                    }
2100                }
2101            }
2102
2103            summary.cells_affected += 1;
2104        }
2105
2106        self.commit_batch();
2107
2108        Ok(summary)
2109    }
2110
2111    /// Move a range to a new location (copy + clear source)
2112    pub fn move_range(
2113        &mut self,
2114        sheet_id: SheetId,
2115        from_start_row: u32,
2116        from_start_col: u32,
2117        from_end_row: u32,
2118        from_end_col: u32,
2119        to_sheet_id: SheetId,
2120        to_row: u32,
2121        to_col: u32,
2122    ) -> Result<RangeSummary, EditorError> {
2123        let result = self.move_range_impl(
2124            sheet_id,
2125            from_start_row,
2126            from_start_col,
2127            from_end_row,
2128            from_end_col,
2129            to_sheet_id,
2130            to_row,
2131            to_col,
2132        );
2133        self.graph.authority_end_structural();
2134        result
2135    }
2136
2137    fn move_range_impl(
2138        &mut self,
2139        sheet_id: SheetId,
2140        from_start_row: u32,
2141        from_start_col: u32,
2142        from_end_row: u32,
2143        from_end_col: u32,
2144        to_sheet_id: SheetId,
2145        to_row: u32,
2146        to_col: u32,
2147    ) -> Result<RangeSummary, EditorError> {
2148        self.graph.authority_note_structural(true);
2149        // First copy the range
2150        let mut summary = self.copy_range(
2151            sheet_id,
2152            from_start_row,
2153            from_start_col,
2154            from_end_row,
2155            from_end_col,
2156            to_sheet_id,
2157            to_row,
2158            to_col,
2159        )?;
2160
2161        // Then clear the source range
2162        let clear_summary = self.clear_range(
2163            sheet_id,
2164            from_start_row,
2165            from_start_col,
2166            from_end_row,
2167            from_end_col,
2168        )?;
2169
2170        summary.cells_moved = clear_summary.cells_affected;
2171
2172        // Update external references to moved cells
2173        let row_offset = to_row as i32 - from_start_row as i32;
2174        let col_offset = to_col as i32 - from_start_col as i32;
2175
2176        // Find all formulas that reference the moved range
2177        let all_formula_vertices: Vec<_> = self.graph.vertices_with_formulas().collect();
2178
2179        let from_sheet_name = self.graph.sheet_name(sheet_id).to_string();
2180        let to_sheet_name = self.graph.sheet_name(to_sheet_id).to_string();
2181        let adjuster = MoveReferenceAdjuster::new(
2182            sheet_id,
2183            from_sheet_name,
2184            from_start_row,
2185            from_start_col,
2186            from_end_row,
2187            from_end_col,
2188            to_sheet_id,
2189            to_sheet_name,
2190            row_offset,
2191            col_offset,
2192        );
2193
2194        for formula_id in all_formula_vertices {
2195            if let Some(formula) = self.get_formula_ast(formula_id) {
2196                let formula_sheet_id = self.graph.get_vertex_sheet_id(formula_id);
2197                if let Some(adjusted) = adjuster.adjust_if_references(&formula, formula_sheet_id) {
2198                    self.graph.update_vertex_formula(formula_id, adjusted)?;
2199                }
2200            }
2201        }
2202
2203        Ok(summary)
2204    }
2205
2206    /// Define a named range
2207    pub fn define_name(
2208        &mut self,
2209        name: &str,
2210        definition: NamedDefinition,
2211        scope: NameScope,
2212    ) -> Result<(), EditorError> {
2213        self.graph.define_name(name, definition.clone(), scope)?;
2214
2215        self.log_change(ChangeEvent::DefineName {
2216            name: name.to_string(),
2217            scope,
2218            definition,
2219        });
2220
2221        Ok(())
2222    }
2223
2224    /// Helper to create definitions from coordinates for a single cell
2225    pub fn define_name_for_cell(
2226        &mut self,
2227        name: &str,
2228        sheet_name: &str,
2229        row: u32,
2230        col: u32,
2231        scope: NameScope,
2232    ) -> Result<(), EditorError> {
2233        let sheet_id = self
2234            .graph
2235            .sheet_id(sheet_name)
2236            .ok_or_else(|| EditorError::InvalidName {
2237                name: sheet_name.to_string(),
2238                reason: "Sheet not found".to_string(),
2239            })?;
2240        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
2241        self.define_name(name, NamedDefinition::Cell(cell_ref), scope)
2242    }
2243
2244    /// Helper to create definitions from coordinates for a range
2245    pub fn define_name_for_range(
2246        &mut self,
2247        name: &str,
2248        sheet_name: &str,
2249        start_row: u32,
2250        start_col: u32,
2251        end_row: u32,
2252        end_col: u32,
2253        scope: NameScope,
2254    ) -> Result<(), EditorError> {
2255        let sheet_id = self
2256            .graph
2257            .sheet_id(sheet_name)
2258            .ok_or_else(|| EditorError::InvalidName {
2259                name: sheet_name.to_string(),
2260                reason: "Sheet not found".to_string(),
2261            })?;
2262        let start = CellRef::new(
2263            sheet_id,
2264            Coord::from_excel(start_row, start_col, true, true),
2265        );
2266        let end = CellRef::new(sheet_id, Coord::from_excel(end_row, end_col, true, true));
2267        let range_ref = crate::reference::RangeRef::new(start, end);
2268        self.define_name(name, NamedDefinition::Range(range_ref), scope)
2269    }
2270
2271    /// Update an existing named range definition
2272    pub fn update_name(
2273        &mut self,
2274        name: &str,
2275        new_definition: NamedDefinition,
2276        scope: NameScope,
2277    ) -> Result<(), EditorError> {
2278        // Get the old definition for the change log
2279        let old_definition = self
2280            .graph
2281            .resolve_name(
2282                name,
2283                match scope {
2284                    NameScope::Sheet(id) => id,
2285                    NameScope::Workbook => 0,
2286                },
2287            )
2288            .cloned();
2289
2290        self.graph
2291            .update_name(name, new_definition.clone(), scope)?;
2292
2293        if let Some(old_def) = old_definition {
2294            self.log_change(ChangeEvent::UpdateName {
2295                name: name.to_string(),
2296                scope,
2297                old_definition: old_def,
2298                new_definition,
2299            });
2300        }
2301
2302        Ok(())
2303    }
2304
2305    /// Delete a named range
2306    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), EditorError> {
2307        // Capture old definition *before* deletion so undo can restore it.
2308        let old_def = if self.has_logger() {
2309            self.graph
2310                .resolve_name(
2311                    name,
2312                    match scope {
2313                        NameScope::Sheet(id) => id,
2314                        NameScope::Workbook => 0,
2315                    },
2316                )
2317                .cloned()
2318        } else {
2319            None
2320        };
2321
2322        self.graph.delete_name(name, scope)?;
2323        self.log_change(ChangeEvent::DeleteName {
2324            name: name.to_string(),
2325            scope,
2326            old_definition: old_def,
2327        });
2328
2329        Ok(())
2330    }
2331}
2332
2333/// Helper enum for cell data
2334enum CellData {
2335    Value(LiteralValue),
2336    Formula(ASTNode),
2337}
2338
2339impl<'g> Drop for VertexEditor<'g> {
2340    fn drop(&mut self) {
2341        // Ensure batch operations are committed when the editor is dropped
2342        if self.batch_mode {
2343            self.commit_batch();
2344        }
2345    }
2346}
2347
2348#[cfg(test)]
2349mod tests {
2350    use super::*;
2351    use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog};
2352    use crate::reference::Coord;
2353
2354    fn create_test_graph() -> DependencyGraph {
2355        DependencyGraph::new()
2356    }
2357
2358    #[test]
2359    fn test_vertex_editor_creation() {
2360        let mut graph = create_test_graph();
2361        let editor = VertexEditor::new(&mut graph);
2362        assert!(!editor.has_logger());
2363        assert!(!editor.batch_mode);
2364    }
2365
2366    #[test]
2367    fn test_vertex_editor_with_logger() {
2368        let mut graph = create_test_graph();
2369        let mut log = ChangeLog::new();
2370        let editor = VertexEditor::with_logger(&mut graph, &mut log);
2371        assert!(editor.has_logger());
2372        assert!(!editor.batch_mode);
2373    }
2374
2375    #[test]
2376    fn test_add_vertex() {
2377        let mut graph = create_test_graph();
2378        let mut editor = VertexEditor::new(&mut graph);
2379
2380        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell).dirty();
2381        let vertex_id = editor.add_vertex(meta);
2382
2383        // Verify vertex was created (simplified check)
2384        assert!(vertex_id.0 > 0);
2385    }
2386
2387    #[test]
2388    fn test_batch_operations() {
2389        let mut graph = create_test_graph();
2390        let mut editor = VertexEditor::new(&mut graph);
2391
2392        assert!(!editor.batch_mode);
2393        editor.begin_batch();
2394        assert!(editor.batch_mode);
2395
2396        // Add multiple vertices in batch mode
2397        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2398        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::Cell);
2399
2400        let id1 = editor.add_vertex(meta1);
2401        let id2 = editor.add_vertex(meta2);
2402
2403        // Add edge between them
2404        assert!(editor.add_edge(id1, id2));
2405
2406        editor.commit_batch();
2407        assert!(!editor.batch_mode);
2408    }
2409
2410    #[test]
2411    fn test_remove_vertex() {
2412        let mut graph = create_test_graph();
2413        let mut editor = VertexEditor::new(&mut graph);
2414
2415        let meta = VertexMeta::new(3, 4, 0, VertexKind::Cell).dirty();
2416        let vertex_id = editor.add_vertex(meta);
2417
2418        // Now removal returns Result
2419        assert!(editor.remove_vertex(vertex_id).is_ok());
2420    }
2421
2422    #[test]
2423    fn test_remove_vertex_clears_spill_registry_for_anchor() {
2424        let mut graph = create_test_graph();
2425        let sheet_id = graph.sheet_id_mut("Sheet1");
2426
2427        // Create anchor vertex at A1 (0-based internal coord 0,0).
2428        let anchor_cell = CellRef::new(sheet_id, Coord::new(0, 0, true, true));
2429        let anchor_vid = {
2430            let mut editor = VertexEditor::new(&mut graph);
2431            // A formula anchors a spill (value cells have no vertex,
2432            // decision 27; this used a value cell's vertex).
2433            editor.set_cell_formula(anchor_cell, formualizer_parse::parser::parse("=0").unwrap())
2434        };
2435
2436        let target_cells = vec![
2437            CellRef::new(sheet_id, Coord::new(0, 0, true, true)),
2438            CellRef::new(sheet_id, Coord::new(0, 1, true, true)),
2439            CellRef::new(sheet_id, Coord::new(1, 0, true, true)),
2440            CellRef::new(sheet_id, Coord::new(1, 1, true, true)),
2441        ];
2442        let values = vec![
2443            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
2444            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
2445        ];
2446
2447        graph
2448            .commit_spill_region_atomic_with_fault(anchor_vid, target_cells.clone(), values, None)
2449            .unwrap();
2450
2451        assert!(graph.spill_registry_has_anchor(anchor_vid));
2452        for cell in &target_cells {
2453            assert_eq!(
2454                graph.spill_registry_anchor_for_cell(*cell),
2455                Some(anchor_vid)
2456            );
2457        }
2458
2459        {
2460            let mut editor = VertexEditor::new(&mut graph);
2461            editor.remove_vertex(anchor_vid).unwrap();
2462        }
2463
2464        assert!(!graph.spill_registry_has_anchor(anchor_vid));
2465        for cell in &target_cells {
2466            assert_eq!(graph.spill_registry_anchor_for_cell(*cell), None);
2467        }
2468        assert_eq!(graph.spill_registry_counts(), (0, 0));
2469    }
2470
2471    #[test]
2472    fn test_edge_operations() {
2473        let mut graph = create_test_graph();
2474        let mut editor = VertexEditor::new(&mut graph);
2475
2476        let meta1 = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2477        let meta2 = VertexMeta::new(2, 2, 0, VertexKind::FormulaScalar);
2478
2479        let id1 = editor.add_vertex(meta1);
2480        let id2 = editor.add_vertex(meta2);
2481
2482        // Add edge
2483        assert!(editor.add_edge(id1, id2));
2484
2485        // Prevent self-loop
2486        assert!(!editor.add_edge(id1, id1));
2487
2488        // Remove edge
2489        assert!(editor.remove_edge(id1, id2));
2490    }
2491
2492    #[test]
2493    fn test_set_cell_value() {
2494        let mut graph = create_test_graph();
2495        let mut log = ChangeLog::new();
2496
2497        let cell_ref = CellRef {
2498            sheet_id: 0,
2499            coord: Coord::new(2, 3, true, true),
2500        };
2501        let value = LiteralValue::Number(42.0);
2502
2503        let vertex_id = {
2504            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2505            editor.set_cell_value(cell_ref, value.clone())
2506        };
2507
2508        // A value cell has no vertex (decision 27): the sentinel id.
2509        assert_eq!(vertex_id.0, 0);
2510
2511        // Verify change log
2512        assert_eq!(log.len(), 1);
2513        match &log.events()[0] {
2514            ChangeEvent::SetValue { addr, new, .. } => {
2515                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2516                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2517                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2518                assert_eq!(new, &value);
2519            }
2520            _ => panic!("Expected SetValue event"),
2521        }
2522    }
2523
2524    #[test]
2525    fn test_set_cell_formula() {
2526        let mut graph = create_test_graph();
2527        let mut log = ChangeLog::new();
2528
2529        let cell_ref = CellRef {
2530            sheet_id: 0,
2531            coord: Coord::new(1, 1, true, true),
2532        };
2533
2534        use formualizer_parse::parser::ASTNodeType;
2535        let formula = formualizer_parse::parser::ASTNode {
2536            node_type: ASTNodeType::Literal(LiteralValue::Number(100.0)),
2537            source_token: None,
2538            contains_volatile: false,
2539        };
2540
2541        let vertex_id = {
2542            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2543            editor.set_cell_formula(cell_ref, formula.clone())
2544        };
2545
2546        // Verify vertex was created (simplified check)
2547        assert!(vertex_id.0 > 0);
2548
2549        // Verify change log
2550        assert_eq!(log.len(), 1);
2551        match &log.events()[0] {
2552            ChangeEvent::SetFormula { addr, .. } => {
2553                assert_eq!(addr.sheet_id, cell_ref.sheet_id);
2554                assert_eq!(addr.coord.row(), cell_ref.coord.row());
2555                assert_eq!(addr.coord.col(), cell_ref.coord.col());
2556            }
2557            _ => panic!("Expected SetFormula event"),
2558        }
2559    }
2560
2561    #[test]
2562    fn test_shift_rows() {
2563        let mut graph = create_test_graph();
2564        let mut log = ChangeLog::new();
2565
2566        {
2567            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2568
2569            // Create vertices at different rows
2570            let cell1 = CellRef {
2571                sheet_id: 0,
2572                coord: Coord::new(5, 1, true, true),
2573            };
2574            let cell2 = CellRef {
2575                sheet_id: 0,
2576                coord: Coord::new(10, 1, true, true),
2577            };
2578            let cell3 = CellRef {
2579                sheet_id: 0,
2580                coord: Coord::new(15, 1, true, true),
2581            };
2582
2583            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2584            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2585            editor.set_cell_value(cell3, LiteralValue::Number(3.0));
2586        }
2587
2588        // Clear change log to focus on shift operation
2589        log.clear();
2590
2591        {
2592            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2593            // Shift rows starting at row 10, moving down by 2
2594            editor.shift_rows(0, 10, 2);
2595        }
2596
2597        // Verify change log contains the shift operation
2598        assert_eq!(log.len(), 1);
2599        match &log.events()[0] {
2600            ChangeEvent::SetValue { addr, new, .. } => {
2601                assert_eq!(addr.sheet_id, 0);
2602                assert_eq!(addr.coord.row(), 10);
2603                if let LiteralValue::Text(msg) = new {
2604                    assert!(msg.contains("Row shift"));
2605                    assert!(msg.contains("start=10"));
2606                    assert!(msg.contains("delta=2"));
2607                }
2608            }
2609            _ => panic!("Expected SetValue event for row shift"),
2610        }
2611    }
2612
2613    #[test]
2614    fn test_shift_columns() {
2615        let mut graph = create_test_graph();
2616        let mut log = ChangeLog::new();
2617
2618        {
2619            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2620
2621            // Create vertices at different columns
2622            let cell1 = CellRef {
2623                sheet_id: 0,
2624                coord: Coord::new(1, 5, true, true),
2625            };
2626            let cell2 = CellRef {
2627                sheet_id: 0,
2628                coord: Coord::new(1, 10, true, true),
2629            };
2630
2631            editor.set_cell_value(cell1, LiteralValue::Number(1.0));
2632            editor.set_cell_value(cell2, LiteralValue::Number(2.0));
2633        }
2634
2635        // Clear change log
2636        log.clear();
2637
2638        {
2639            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2640            // Shift columns starting at col 8, moving right by 3
2641            editor.shift_columns(0, 8, 3);
2642        }
2643
2644        // Verify change log
2645        assert_eq!(log.len(), 1);
2646        match &log.events()[0] {
2647            ChangeEvent::SetValue { addr, new, .. } => {
2648                assert_eq!(addr.sheet_id, 0);
2649                assert_eq!(addr.coord.col(), 8);
2650                if let LiteralValue::Text(msg) = new {
2651                    assert!(msg.contains("Column shift"));
2652                    assert!(msg.contains("start=8"));
2653                    assert!(msg.contains("delta=3"));
2654                }
2655            }
2656            _ => panic!("Expected SetValue event for column shift"),
2657        }
2658    }
2659
2660    #[test]
2661    fn test_move_vertex() {
2662        let mut graph = create_test_graph();
2663        let mut editor = VertexEditor::new(&mut graph);
2664
2665        let meta = VertexMeta::new(5, 10, 0, VertexKind::Cell);
2666        let vertex_id = editor.add_vertex(meta);
2667
2668        // Move vertex returns Result
2669        assert!(editor.move_vertex(vertex_id, GridAddr::new(8, 12)).is_ok());
2670
2671        // Moving to same position should work
2672        assert!(editor.move_vertex(vertex_id, GridAddr::new(8, 12)).is_ok());
2673    }
2674
2675    #[test]
2676    fn test_vertex_meta_builder() {
2677        let meta = VertexMeta::new(1, 2, 3, VertexKind::FormulaScalar)
2678            .dirty()
2679            .volatile()
2680            .with_flags(0x08);
2681
2682        assert_eq!(meta.coord.row(), 1);
2683        assert_eq!(meta.coord.col(), 2);
2684        assert_eq!(meta.sheet_id, 3);
2685        assert_eq!(meta.kind, VertexKind::FormulaScalar);
2686        assert_eq!(meta.flags, 0x08); // Last with_flags call overwrites previous flags
2687    }
2688
2689    #[test]
2690    fn test_change_log_management() {
2691        let mut graph = create_test_graph();
2692        let mut log = ChangeLog::new();
2693
2694        {
2695            let mut editor = VertexEditor::with_logger(&mut graph, &mut log);
2696            let cell_ref = CellRef {
2697                sheet_id: 0,
2698                coord: Coord::new(0, 0, true, true),
2699            };
2700            editor.set_cell_value(cell_ref, LiteralValue::Number(1.0));
2701            editor.set_cell_value(cell_ref, LiteralValue::Number(2.0));
2702        }
2703
2704        assert_eq!(log.len(), 2);
2705
2706        log.clear();
2707        assert_eq!(log.len(), 0);
2708    }
2709
2710    #[test]
2711    fn test_editor_drop_commits_batch() {
2712        let mut graph = create_test_graph();
2713        {
2714            let mut editor = VertexEditor::new(&mut graph);
2715            editor.begin_batch();
2716
2717            let meta = VertexMeta::new(1, 1, 0, VertexKind::Cell);
2718            editor.add_vertex(meta);
2719
2720            // Editor will be dropped here, should commit batch
2721        }
2722
2723        // If we reach here without hanging, the batch was properly committed
2724    }
2725}