Skip to main content

formualizer_eval/engine/graph/editor/
reference_adjuster.rs

1use crate::reference::{CellRef, Coord};
2use formualizer_parse::parser::{ASTNode, ASTNodeType};
3
4/// Centralized reference adjustment logic for structural changes
5pub struct ReferenceAdjuster;
6
7#[derive(Debug, Clone)]
8pub enum ShiftOperation {
9    InsertRows {
10        sheet_id: u16,
11        before: u32,
12        count: u32,
13    },
14    DeleteRows {
15        sheet_id: u16,
16        start: u32,
17        count: u32,
18    },
19    InsertColumns {
20        sheet_id: u16,
21        before: u32,
22        count: u32,
23    },
24    DeleteColumns {
25        sheet_id: u16,
26        start: u32,
27        count: u32,
28    },
29}
30
31impl ReferenceAdjuster {
32    pub fn new() -> Self {
33        Self
34    }
35
36    /// Adjust an AST for a shift operation, preserving source tokens
37    pub fn adjust_ast(&self, ast: &ASTNode, op: &ShiftOperation) -> ASTNode {
38        match &ast.node_type {
39            ASTNodeType::Reference {
40                original,
41                reference,
42            } => {
43                let adjusted = self.adjust_reference(reference, op);
44                ASTNode {
45                    node_type: ASTNodeType::Reference {
46                        original: original.clone(),
47                        reference: adjusted,
48                    },
49                    source_token: ast.source_token.clone(),
50                    contains_volatile: ast.contains_volatile,
51                }
52            }
53            ASTNodeType::BinaryOp {
54                op: bin_op,
55                left,
56                right,
57            } => ASTNode {
58                node_type: ASTNodeType::BinaryOp {
59                    op: bin_op.clone(),
60                    left: Box::new(self.adjust_ast(left, op)),
61                    right: Box::new(self.adjust_ast(right, op)),
62                },
63                source_token: ast.source_token.clone(),
64                contains_volatile: ast.contains_volatile,
65            },
66            ASTNodeType::UnaryOp { op: un_op, expr } => ASTNode {
67                node_type: ASTNodeType::UnaryOp {
68                    op: un_op.clone(),
69                    expr: Box::new(self.adjust_ast(expr, op)),
70                },
71                source_token: ast.source_token.clone(),
72                contains_volatile: ast.contains_volatile,
73            },
74            ASTNodeType::Function { name, args } => ASTNode {
75                node_type: ASTNodeType::Function {
76                    name: name.clone(),
77                    args: args.iter().map(|arg| self.adjust_ast(arg, op)).collect(),
78                },
79                source_token: ast.source_token.clone(),
80                contains_volatile: ast.contains_volatile,
81            },
82            _ => ast.clone(),
83        }
84    }
85
86    /// Adjust an AST against a structural shift, returning Some(adjusted) only
87    /// if at least one reference actually changed. Avoids cloning when no work
88    /// is needed.
89    pub fn adjust_ast_if_changed(&self, ast: &ASTNode, op: &ShiftOperation) -> Option<ASTNode> {
90        match &ast.node_type {
91            ASTNodeType::Reference {
92                original,
93                reference,
94            } => {
95                let adjusted = self.adjust_reference(reference, op);
96                if adjusted == *reference {
97                    return None;
98                }
99                Some(ASTNode {
100                    node_type: ASTNodeType::Reference {
101                        original: original.clone(),
102                        reference: adjusted,
103                    },
104                    source_token: ast.source_token.clone(),
105                    contains_volatile: ast.contains_volatile,
106                })
107            }
108            ASTNodeType::BinaryOp {
109                op: bin_op,
110                left,
111                right,
112            } => {
113                let adjusted_left = self.adjust_ast_if_changed(left, op);
114                let adjusted_right = self.adjust_ast_if_changed(right, op);
115                if adjusted_left.is_none() && adjusted_right.is_none() {
116                    return None;
117                }
118                Some(ASTNode {
119                    node_type: ASTNodeType::BinaryOp {
120                        op: bin_op.clone(),
121                        left: Box::new(adjusted_left.unwrap_or_else(|| (**left).clone())),
122                        right: Box::new(adjusted_right.unwrap_or_else(|| (**right).clone())),
123                    },
124                    source_token: ast.source_token.clone(),
125                    contains_volatile: ast.contains_volatile,
126                })
127            }
128            ASTNodeType::UnaryOp { op: un_op, expr } => {
129                let adjusted_expr = self.adjust_ast_if_changed(expr, op)?;
130                Some(ASTNode {
131                    node_type: ASTNodeType::UnaryOp {
132                        op: un_op.clone(),
133                        expr: Box::new(adjusted_expr),
134                    },
135                    source_token: ast.source_token.clone(),
136                    contains_volatile: ast.contains_volatile,
137                })
138            }
139            ASTNodeType::Function { name, args } => {
140                let mut changed = false;
141                let adjusted_args = args
142                    .iter()
143                    .map(|arg| {
144                        if let Some(adjusted) = self.adjust_ast_if_changed(arg, op) {
145                            changed = true;
146                            adjusted
147                        } else {
148                            arg.clone()
149                        }
150                    })
151                    .collect();
152                if !changed {
153                    return None;
154                }
155                Some(ASTNode {
156                    node_type: ASTNodeType::Function {
157                        name: name.clone(),
158                        args: adjusted_args,
159                    },
160                    source_token: ast.source_token.clone(),
161                    contains_volatile: ast.contains_volatile,
162                })
163            }
164            _ => None,
165        }
166    }
167
168    /// Adjust a cell reference for a shift operation
169    /// Returns None if the cell is deleted
170    pub fn adjust_cell_ref(&self, cell_ref: &CellRef, op: &ShiftOperation) -> Option<CellRef> {
171        let coord = cell_ref.coord;
172        let adjusted_coord = match op {
173            ShiftOperation::InsertRows {
174                sheet_id,
175                before,
176                count,
177            } if cell_ref.sheet_id == *sheet_id => {
178                if coord.row_abs() || coord.row() < *before {
179                    // Absolute references or cells before insert point don't move
180                    coord
181                } else {
182                    // Shift down
183                    Coord::new(
184                        coord.row() + count,
185                        coord.col(),
186                        coord.row_abs(),
187                        coord.col_abs(),
188                    )
189                }
190            }
191            ShiftOperation::DeleteRows {
192                sheet_id,
193                start,
194                count,
195            } if cell_ref.sheet_id == *sheet_id => {
196                if coord.row_abs() {
197                    // Absolute references don't adjust
198                    coord
199                } else if coord.row() >= *start && coord.row() < start + count {
200                    // Cell deleted
201                    return None;
202                } else if coord.row() >= start + count {
203                    // Shift up
204                    Coord::new(
205                        coord.row() - count,
206                        coord.col(),
207                        coord.row_abs(),
208                        coord.col_abs(),
209                    )
210                } else {
211                    // Before delete range, no change
212                    coord
213                }
214            }
215            ShiftOperation::InsertColumns {
216                sheet_id,
217                before,
218                count,
219            } if cell_ref.sheet_id == *sheet_id => {
220                if coord.col_abs() || coord.col() < *before {
221                    // Absolute references or cells before insert point don't move
222                    coord
223                } else {
224                    // Shift right
225                    Coord::new(
226                        coord.row(),
227                        coord.col() + count,
228                        coord.row_abs(),
229                        coord.col_abs(),
230                    )
231                }
232            }
233            ShiftOperation::DeleteColumns {
234                sheet_id,
235                start,
236                count,
237            } if cell_ref.sheet_id == *sheet_id => {
238                if coord.col_abs() {
239                    // Absolute references don't adjust
240                    coord
241                } else if coord.col() >= *start && coord.col() < start + count {
242                    // Cell deleted
243                    return None;
244                } else if coord.col() >= start + count {
245                    // Shift left
246                    Coord::new(
247                        coord.row(),
248                        coord.col() - count,
249                        coord.row_abs(),
250                        coord.col_abs(),
251                    )
252                } else {
253                    // Before delete range, no change
254                    coord
255                }
256            }
257            _ => coord,
258        };
259
260        Some(CellRef::new(cell_ref.sheet_id, adjusted_coord))
261    }
262
263    /// Adjust a reference type (cell or range) for a shift operation
264    fn adjust_reference(
265        &self,
266        reference: &formualizer_parse::parser::ReferenceType,
267        op: &ShiftOperation,
268    ) -> formualizer_parse::parser::ReferenceType {
269        use formualizer_parse::parser::ReferenceType;
270
271        let shared = reference.to_sheet_ref_lossy();
272
273        match (reference, shared) {
274            (
275                ReferenceType::Cell {
276                    sheet,
277                    row_abs,
278                    col_abs,
279                    ..
280                },
281                Some(crate::reference::SharedRef::Cell(cell)),
282            ) => {
283                let sheet_id = match op {
284                    ShiftOperation::InsertRows { sheet_id, .. }
285                    | ShiftOperation::DeleteRows { sheet_id, .. }
286                    | ShiftOperation::InsertColumns { sheet_id, .. }
287                    | ShiftOperation::DeleteColumns { sheet_id, .. } => *sheet_id,
288                };
289                let temp_ref = CellRef::new(
290                    sheet_id,
291                    Coord::new(cell.coord.row(), cell.coord.col(), *row_abs, *col_abs),
292                );
293
294                match self.adjust_cell_ref(&temp_ref, op) {
295                    None => ReferenceType::Cell {
296                        sheet: Some("#REF".to_string()),
297                        row: 0,
298                        col: 0,
299                        row_abs: *row_abs,
300                        col_abs: *col_abs,
301                    },
302                    Some(adjusted) => ReferenceType::Cell {
303                        sheet: sheet.clone(),
304                        row: adjusted.coord.row() + 1,
305                        col: adjusted.coord.col() + 1,
306                        row_abs: *row_abs,
307                        col_abs: *col_abs,
308                    },
309                }
310            }
311            (
312                ReferenceType::Range {
313                    sheet,
314                    start_row_abs,
315                    start_col_abs,
316                    end_row_abs,
317                    end_col_abs,
318                    ..
319                },
320                Some(crate::reference::SharedRef::Range(range)),
321            ) => {
322                let is_unbounded_column = range.start_row.is_none() && range.end_row.is_none();
323                let is_unbounded_row = range.start_col.is_none() && range.end_col.is_none();
324                if is_unbounded_column || is_unbounded_row {
325                    return reference.clone();
326                }
327
328                let sr = range.start_row;
329                let sc = range.start_col;
330                let er = range.end_row;
331                let ec = range.end_col;
332
333                let adjust_insert = |b: formualizer_common::AxisBound, before: u32, count: u32| {
334                    if b.abs {
335                        b.index
336                    } else if b.index >= before {
337                        b.index + count
338                    } else {
339                        b.index
340                    }
341                };
342
343                let adjust_delete = |idx: u32, abs: bool, start: u32, count: u32| {
344                    if abs {
345                        idx
346                    } else if idx >= start + count {
347                        idx - count
348                    } else if idx >= start {
349                        start
350                    } else {
351                        idx
352                    }
353                };
354
355                let (adj_sr0, adj_er0) = match op {
356                    ShiftOperation::InsertRows { before, count, .. } => (
357                        sr.map(|b| adjust_insert(b, *before, *count)),
358                        er.map(|b| adjust_insert(b, *before, *count)),
359                    ),
360                    ShiftOperation::DeleteRows { start, count, .. } => match (sr, er) {
361                        (Some(range_start), Some(range_end))
362                            if !range_start.abs && !range_end.abs =>
363                        {
364                            let range_start = range_start.index;
365                            let range_end = range_end.index;
366                            if range_end < *start || range_start >= start + count {
367                                let adj_start = if range_start >= start + count {
368                                    range_start - count
369                                } else {
370                                    range_start
371                                };
372                                let adj_end = if range_end >= start + count {
373                                    range_end - count
374                                } else {
375                                    range_end
376                                };
377                                (Some(adj_start), Some(adj_end))
378                            } else if range_start >= *start && range_end < start + count {
379                                return ReferenceType::Range {
380                                    sheet: Some("#REF".to_string()),
381                                    start_row: Some(0),
382                                    start_col: Some(0),
383                                    end_row: Some(0),
384                                    end_col: Some(0),
385                                    start_row_abs: *start_row_abs,
386                                    start_col_abs: *start_col_abs,
387                                    end_row_abs: *end_row_abs,
388                                    end_col_abs: *end_col_abs,
389                                };
390                            } else {
391                                let adj_start = if range_start < *start {
392                                    range_start
393                                } else {
394                                    *start
395                                };
396                                let adj_end = if range_end >= start + count {
397                                    range_end - count
398                                } else {
399                                    start.saturating_sub(1)
400                                };
401                                (Some(adj_start), Some(adj_end))
402                            }
403                        }
404                        (Some(range_start), Some(range_end)) => {
405                            let adj_start =
406                                adjust_delete(range_start.index, range_start.abs, *start, *count);
407                            let adj_end =
408                                adjust_delete(range_end.index, range_end.abs, *start, *count);
409                            (Some(adj_start), Some(adj_end))
410                        }
411                        _ => (
412                            sr.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
413                            er.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
414                        ),
415                    },
416                    _ => (sr.map(|b| b.index), er.map(|b| b.index)),
417                };
418
419                let (adj_sc0, adj_ec0) = match op {
420                    ShiftOperation::InsertColumns { before, count, .. } => (
421                        sc.map(|b| adjust_insert(b, *before, *count)),
422                        ec.map(|b| adjust_insert(b, *before, *count)),
423                    ),
424                    ShiftOperation::DeleteColumns { start, count, .. } => match (sc, ec) {
425                        (Some(range_start), Some(range_end))
426                            if !range_start.abs && !range_end.abs =>
427                        {
428                            let range_start = range_start.index;
429                            let range_end = range_end.index;
430                            if range_end < *start || range_start >= start + count {
431                                let adj_start = if range_start >= start + count {
432                                    range_start - count
433                                } else {
434                                    range_start
435                                };
436                                let adj_end = if range_end >= start + count {
437                                    range_end - count
438                                } else {
439                                    range_end
440                                };
441                                (Some(adj_start), Some(adj_end))
442                            } else if range_start >= *start && range_end < start + count {
443                                return ReferenceType::Range {
444                                    sheet: Some("#REF".to_string()),
445                                    start_row: Some(0),
446                                    start_col: Some(0),
447                                    end_row: Some(0),
448                                    end_col: Some(0),
449                                    start_row_abs: *start_row_abs,
450                                    start_col_abs: *start_col_abs,
451                                    end_row_abs: *end_row_abs,
452                                    end_col_abs: *end_col_abs,
453                                };
454                            } else {
455                                let adj_start = if range_start < *start {
456                                    range_start
457                                } else {
458                                    *start
459                                };
460                                let adj_end = if range_end >= start + count {
461                                    range_end - count
462                                } else {
463                                    start.saturating_sub(1)
464                                };
465                                (Some(adj_start), Some(adj_end))
466                            }
467                        }
468                        (Some(range_start), Some(range_end)) => {
469                            let adj_start =
470                                adjust_delete(range_start.index, range_start.abs, *start, *count);
471                            let adj_end =
472                                adjust_delete(range_end.index, range_end.abs, *start, *count);
473                            (Some(adj_start), Some(adj_end))
474                        }
475                        _ => (
476                            sc.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
477                            ec.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
478                        ),
479                    },
480                    _ => (sc.map(|b| b.index), ec.map(|b| b.index)),
481                };
482
483                ReferenceType::Range {
484                    sheet: sheet.clone(),
485                    start_row: adj_sr0.map(|i| i + 1),
486                    start_col: adj_sc0.map(|i| i + 1),
487                    end_row: adj_er0.map(|i| i + 1),
488                    end_col: adj_ec0.map(|i| i + 1),
489                    start_row_abs: *start_row_abs,
490                    start_col_abs: *start_col_abs,
491                    end_row_abs: *end_row_abs,
492                    end_col_abs: *end_col_abs,
493                }
494            }
495            _ => reference.clone(),
496        }
497    }
498}
499
500impl Default for ReferenceAdjuster {
501    fn default() -> Self {
502        Self::new()
503    }
504}
505
506/// Helper for adjusting references when copying/moving ranges
507pub struct RelativeReferenceAdjuster {
508    row_offset: i32,
509    col_offset: i32,
510}
511
512impl RelativeReferenceAdjuster {
513    pub fn new(row_offset: i32, col_offset: i32) -> Self {
514        Self {
515            row_offset,
516            col_offset,
517        }
518    }
519
520    pub fn adjust_formula(&self, ast: &ASTNode) -> ASTNode {
521        match &ast.node_type {
522            ASTNodeType::Reference {
523                original,
524                reference,
525            } => {
526                let adjusted = self.adjust_reference(reference);
527                ASTNode {
528                    node_type: ASTNodeType::Reference {
529                        original: original.clone(),
530                        reference: adjusted,
531                    },
532                    source_token: ast.source_token.clone(),
533                    contains_volatile: ast.contains_volatile,
534                }
535            }
536            ASTNodeType::BinaryOp { op, left, right } => ASTNode {
537                node_type: ASTNodeType::BinaryOp {
538                    op: op.clone(),
539                    left: Box::new(self.adjust_formula(left)),
540                    right: Box::new(self.adjust_formula(right)),
541                },
542                source_token: ast.source_token.clone(),
543                contains_volatile: ast.contains_volatile,
544            },
545            ASTNodeType::UnaryOp { op, expr } => ASTNode {
546                node_type: ASTNodeType::UnaryOp {
547                    op: op.clone(),
548                    expr: Box::new(self.adjust_formula(expr)),
549                },
550                source_token: ast.source_token.clone(),
551                contains_volatile: ast.contains_volatile,
552            },
553            ASTNodeType::Function { name, args } => ASTNode {
554                node_type: ASTNodeType::Function {
555                    name: name.clone(),
556                    args: args.iter().map(|arg| self.adjust_formula(arg)).collect(),
557                },
558                source_token: ast.source_token.clone(),
559                contains_volatile: ast.contains_volatile,
560            },
561            _ => ast.clone(),
562        }
563    }
564
565    fn adjust_reference(
566        &self,
567        reference: &formualizer_parse::parser::ReferenceType,
568    ) -> formualizer_parse::parser::ReferenceType {
569        use formualizer_parse::parser::ReferenceType;
570
571        let Some(shared) = reference.to_sheet_ref_lossy() else {
572            return reference.clone();
573        };
574
575        match (reference, shared) {
576            (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
577                let owned = cell.into_owned();
578                let row0 = owned.coord.row();
579                let col0 = owned.coord.col();
580                let row_abs = owned.coord.row_abs();
581                let col_abs = owned.coord.col_abs();
582
583                let new_row0 = if row_abs {
584                    row0
585                } else {
586                    (row0 as i32 + self.row_offset).max(0) as u32
587                };
588                let new_col0 = if col_abs {
589                    col0
590                } else {
591                    (col0 as i32 + self.col_offset).max(0) as u32
592                };
593
594                ReferenceType::Cell {
595                    sheet: sheet.clone(),
596                    row: new_row0 + 1,
597                    col: new_col0 + 1,
598                    row_abs,
599                    col_abs,
600                }
601            }
602            (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
603                let owned = range.into_owned();
604
605                let adj_axis = |b: formualizer_common::AxisBound, off: i32| {
606                    if b.abs {
607                        b.index
608                    } else {
609                        (b.index as i32 + off).max(0) as u32
610                    }
611                };
612
613                let adj_start_row = owned.start_row.map(|b| adj_axis(b, self.row_offset) + 1);
614                let adj_start_col = owned.start_col.map(|b| adj_axis(b, self.col_offset) + 1);
615                let adj_end_row = owned.end_row.map(|b| adj_axis(b, self.row_offset) + 1);
616                let adj_end_col = owned.end_col.map(|b| adj_axis(b, self.col_offset) + 1);
617
618                let start_row_abs = owned.start_row.map(|b| b.abs).unwrap_or(false);
619                let start_col_abs = owned.start_col.map(|b| b.abs).unwrap_or(false);
620                let end_row_abs = owned.end_row.map(|b| b.abs).unwrap_or(false);
621                let end_col_abs = owned.end_col.map(|b| b.abs).unwrap_or(false);
622
623                ReferenceType::Range {
624                    sheet: sheet.clone(),
625                    start_row: adj_start_row,
626                    start_col: adj_start_col,
627                    end_row: adj_end_row,
628                    end_col: adj_end_col,
629                    start_row_abs,
630                    start_col_abs,
631                    end_row_abs,
632                    end_col_abs,
633                }
634            }
635            _ => reference.clone(),
636        }
637    }
638}
639
640/// Helper for adjusting references to moved ranges.
641/// This is used when a block of cells is moved; any formula references to cells
642/// fully inside the source rectangle are translated to the destination.
643pub struct MoveReferenceAdjuster {
644    from_sheet_id: crate::SheetId,
645    from_sheet_name: String,
646    from_start_row: u32,
647    from_start_col: u32,
648    from_end_row: u32,
649    from_end_col: u32,
650    to_sheet_id: crate::SheetId,
651    to_sheet_name: String,
652    row_offset: i32,
653    col_offset: i32,
654}
655
656impl MoveReferenceAdjuster {
657    pub fn new(
658        from_sheet_id: crate::SheetId,
659        from_sheet_name: String,
660        from_start_row: u32,
661        from_start_col: u32,
662        from_end_row: u32,
663        from_end_col: u32,
664        to_sheet_id: crate::SheetId,
665        to_sheet_name: String,
666        row_offset: i32,
667        col_offset: i32,
668    ) -> Self {
669        Self {
670            from_sheet_id,
671            from_sheet_name,
672            from_start_row,
673            from_start_col,
674            from_end_row,
675            from_end_col,
676            to_sheet_id,
677            to_sheet_name,
678            row_offset,
679            col_offset,
680        }
681    }
682
683    pub fn adjust_if_references(
684        &self,
685        formula: &ASTNode,
686        formula_sheet_id: crate::SheetId,
687    ) -> Option<ASTNode> {
688        let (adjusted, changed) = self.adjust_ast_inner(formula, formula_sheet_id);
689        if changed { Some(adjusted) } else { None }
690    }
691
692    fn adjust_ast_inner(&self, ast: &ASTNode, formula_sheet_id: crate::SheetId) -> (ASTNode, bool) {
693        match &ast.node_type {
694            ASTNodeType::Reference {
695                original,
696                reference,
697            } => {
698                let (adjusted_ref, changed) = self.adjust_reference(reference, formula_sheet_id);
699                if !changed {
700                    return (ast.clone(), false);
701                }
702                (
703                    ASTNode {
704                        node_type: ASTNodeType::Reference {
705                            original: original.clone(),
706                            reference: adjusted_ref,
707                        },
708                        source_token: ast.source_token.clone(),
709                        contains_volatile: ast.contains_volatile,
710                    },
711                    true,
712                )
713            }
714            ASTNodeType::BinaryOp { op, left, right } => {
715                let (l_adj, l_ch) = self.adjust_ast_inner(left, formula_sheet_id);
716                let (r_adj, r_ch) = self.adjust_ast_inner(right, formula_sheet_id);
717                if !l_ch && !r_ch {
718                    return (ast.clone(), false);
719                }
720                (
721                    ASTNode {
722                        node_type: ASTNodeType::BinaryOp {
723                            op: op.clone(),
724                            left: Box::new(l_adj),
725                            right: Box::new(r_adj),
726                        },
727                        source_token: ast.source_token.clone(),
728                        contains_volatile: ast.contains_volatile,
729                    },
730                    true,
731                )
732            }
733            ASTNodeType::UnaryOp { op, expr } => {
734                let (e_adj, e_ch) = self.adjust_ast_inner(expr, formula_sheet_id);
735                if !e_ch {
736                    return (ast.clone(), false);
737                }
738                (
739                    ASTNode {
740                        node_type: ASTNodeType::UnaryOp {
741                            op: op.clone(),
742                            expr: Box::new(e_adj),
743                        },
744                        source_token: ast.source_token.clone(),
745                        contains_volatile: ast.contains_volatile,
746                    },
747                    true,
748                )
749            }
750            ASTNodeType::Function { name, args } => {
751                let mut any = false;
752                let new_args: Vec<_> = args
753                    .iter()
754                    .map(|a| {
755                        let (adj, ch) = self.adjust_ast_inner(a, formula_sheet_id);
756                        any |= ch;
757                        adj
758                    })
759                    .collect();
760                if !any {
761                    return (ast.clone(), false);
762                }
763                (
764                    ASTNode {
765                        node_type: ASTNodeType::Function {
766                            name: name.clone(),
767                            args: new_args,
768                        },
769                        source_token: ast.source_token.clone(),
770                        contains_volatile: ast.contains_volatile,
771                    },
772                    true,
773                )
774            }
775            ASTNodeType::Array(rows) => {
776                let mut any = false;
777                let new_rows: Vec<_> = rows
778                    .iter()
779                    .map(|row| {
780                        row.iter()
781                            .map(|c| {
782                                let (adj, ch) = self.adjust_ast_inner(c, formula_sheet_id);
783                                any |= ch;
784                                adj
785                            })
786                            .collect()
787                    })
788                    .collect();
789                if !any {
790                    return (ast.clone(), false);
791                }
792                (
793                    ASTNode {
794                        node_type: ASTNodeType::Array(new_rows),
795                        source_token: ast.source_token.clone(),
796                        contains_volatile: ast.contains_volatile,
797                    },
798                    true,
799                )
800            }
801            _ => (ast.clone(), false),
802        }
803    }
804
805    fn adjust_reference(
806        &self,
807        reference: &formualizer_parse::parser::ReferenceType,
808        formula_sheet_id: crate::SheetId,
809    ) -> (formualizer_parse::parser::ReferenceType, bool) {
810        use formualizer_parse::parser::ReferenceType;
811
812        let sheet_matches_source = |sheet: &Option<String>| {
813            if let Some(name) = sheet.as_deref() {
814                name == self.from_sheet_name
815            } else {
816                formula_sheet_id == self.from_sheet_id
817            }
818        };
819
820        if !sheet_matches_source(match reference {
821            ReferenceType::Cell { sheet, .. } => sheet,
822            ReferenceType::Range { sheet, .. } => sheet,
823            _ => &None,
824        }) {
825            return (reference.clone(), false);
826        }
827
828        let Some(shared) = reference.to_sheet_ref_lossy() else {
829            return (reference.clone(), false);
830        };
831
832        match (reference, shared) {
833            (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
834                let owned = cell.into_owned();
835                let row0 = owned.coord.row();
836                let col0 = owned.coord.col();
837                let row_abs = owned.coord.row_abs();
838                let col_abs = owned.coord.col_abs();
839
840                if row0 < self.from_start_row
841                    || row0 > self.from_end_row
842                    || col0 < self.from_start_col
843                    || col0 > self.from_end_col
844                {
845                    return (reference.clone(), false);
846                }
847
848                let new_row0 = (row0 as i32 + self.row_offset).max(0) as u32;
849                let new_col0 = (col0 as i32 + self.col_offset).max(0) as u32;
850
851                let new_sheet = if self.to_sheet_id != self.from_sheet_id {
852                    Some(self.to_sheet_name.clone())
853                } else {
854                    sheet.clone()
855                };
856
857                (
858                    ReferenceType::Cell {
859                        sheet: new_sheet,
860                        row: new_row0 + 1,
861                        col: new_col0 + 1,
862                        row_abs,
863                        col_abs,
864                    },
865                    true,
866                )
867            }
868            (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
869                let owned = range.into_owned();
870                let (Some(sr), Some(sc), Some(er), Some(ec)) = (
871                    owned.start_row,
872                    owned.start_col,
873                    owned.end_row,
874                    owned.end_col,
875                ) else {
876                    return (reference.clone(), false);
877                };
878
879                let sr0 = sr.index;
880                let sc0 = sc.index;
881                let er0 = er.index;
882                let ec0 = ec.index;
883                let start_row_abs = sr.abs;
884                let start_col_abs = sc.abs;
885                let end_row_abs = er.abs;
886                let end_col_abs = ec.abs;
887
888                let fully_contained = sr0 >= self.from_start_row
889                    && er0 <= self.from_end_row
890                    && sc0 >= self.from_start_col
891                    && ec0 <= self.from_end_col;
892                if !fully_contained {
893                    return (reference.clone(), false);
894                }
895
896                let new_sr0 = (sr0 as i32 + self.row_offset).max(0) as u32;
897                let new_er0 = (er0 as i32 + self.row_offset).max(0) as u32;
898                let new_sc0 = (sc0 as i32 + self.col_offset).max(0) as u32;
899                let new_ec0 = (ec0 as i32 + self.col_offset).max(0) as u32;
900
901                let new_sheet = if self.to_sheet_id != self.from_sheet_id {
902                    Some(self.to_sheet_name.clone())
903                } else {
904                    sheet.clone()
905                };
906
907                (
908                    ReferenceType::Range {
909                        sheet: new_sheet,
910                        start_row: Some(new_sr0 + 1),
911                        start_col: Some(new_sc0 + 1),
912                        end_row: Some(new_er0 + 1),
913                        end_col: Some(new_ec0 + 1),
914                        start_row_abs,
915                        start_col_abs,
916                        end_row_abs,
917                        end_col_abs,
918                    },
919                    true,
920                )
921            }
922            _ => (reference.clone(), false),
923        }
924    }
925}
926
927#[cfg(test)]
928mod tests {
929    use super::*;
930    use formualizer_parse::parser::parse;
931
932    fn format_formula(ast: &ASTNode) -> String {
933        // TODO: Use the actual formualizer_parse::parser::to_string when available
934        // For now, a simple representation
935        format!("{ast:?}")
936    }
937
938    #[test]
939    fn adjust_ast_if_changed_returns_none_for_unaffected_column_insert() {
940        let adjuster = ReferenceAdjuster::new();
941        let ast = parse("=A1+1").unwrap();
942
943        let adjusted = adjuster.adjust_ast_if_changed(
944            &ast,
945            &ShiftOperation::InsertColumns {
946                sheet_id: 0,
947                before: 3,
948                count: 1,
949            },
950        );
951
952        assert!(adjusted.is_none());
953    }
954
955    #[test]
956    fn adjust_ast_if_changed_returns_adjusted_for_insert_before_a() {
957        let adjuster = ReferenceAdjuster::new();
958        let ast = parse("=A1+1").unwrap();
959
960        let adjusted = adjuster
961            .adjust_ast_if_changed(
962                &ast,
963                &ShiftOperation::InsertColumns {
964                    sheet_id: 0,
965                    before: 0,
966                    count: 1,
967                },
968            )
969            .expect("A1 reference should shift");
970
971        if let ASTNodeType::BinaryOp { left, .. } = &adjusted.node_type
972            && let ASTNodeType::Reference {
973                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
974                ..
975            } = &left.node_type
976        {
977            assert_eq!(*row, 1);
978            assert_eq!(*col, 2);
979            return;
980        }
981
982        panic!("expected adjusted A1 reference to become B1");
983    }
984
985    #[test]
986    fn test_reference_adjustment_on_row_insert() {
987        let adjuster = ReferenceAdjuster::new();
988
989        // Formula: =A5+B10
990        let ast = parse("=A5+B10").unwrap();
991
992        // Insert 2 rows before row 7
993        let adjusted = adjuster.adjust_ast(
994            &ast,
995            &ShiftOperation::InsertRows {
996                sheet_id: 0,
997                before: 7,
998                count: 2,
999            },
1000        );
1001
1002        // A5 unchanged (before insert point), B10 -> B12
1003        // Verify by checking the AST structure
1004        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
1005            if let ASTNodeType::Reference {
1006                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1007                ..
1008            } = &left.node_type
1009            {
1010                assert_eq!(*row, 5); // A5 unchanged
1011                assert_eq!(*col, 1);
1012            }
1013            if let ASTNodeType::Reference {
1014                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1015                ..
1016            } = &right.node_type
1017            {
1018                assert_eq!(*row, 12); // B10 -> B12
1019                assert_eq!(*col, 2);
1020            }
1021        }
1022    }
1023
1024    #[test]
1025    fn test_reference_adjustment_on_column_delete() {
1026        let adjuster = ReferenceAdjuster::new();
1027
1028        // Formula: =C1+F1
1029        let ast = parse("=C1+F1").unwrap();
1030
1031        // Delete columns B and C (columns 2 and 3)
1032        let adjusted = adjuster.adjust_ast(
1033            &ast,
1034            &ShiftOperation::DeleteColumns {
1035                sheet_id: 0,
1036                start: 2, // Column B
1037                count: 2,
1038            },
1039        );
1040
1041        // C1 -> #REF! (deleted), F1 -> D1 (shifted left by 2)
1042        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
1043            if let ASTNodeType::Reference {
1044                reference:
1045                    formualizer_parse::parser::ReferenceType::Cell {
1046                        sheet, row, col, ..
1047                    },
1048                ..
1049            } = &left.node_type
1050            {
1051                assert_eq!(sheet.as_deref(), Some("#REF"));
1052                assert_eq!(*row, 0);
1053                assert_eq!(*col, 0);
1054            }
1055            if let ASTNodeType::Reference {
1056                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1057                ..
1058            } = &right.node_type
1059            {
1060                assert_eq!(*row, 1); // Row unchanged
1061                assert_eq!(*col, 4); // F1 (col 6) -> D1 (col 4)
1062            }
1063        }
1064    }
1065
1066    #[test]
1067    fn test_range_reference_adjustment() {
1068        let adjuster = ReferenceAdjuster::new();
1069
1070        // Formula: =SUM(A1:A10)
1071        let ast = parse("=SUM(A1:A10)").unwrap();
1072
1073        // Insert 3 rows before row 5
1074        let adjusted = adjuster.adjust_ast(
1075            &ast,
1076            &ShiftOperation::InsertRows {
1077                sheet_id: 0,
1078                before: 5,
1079                count: 3,
1080            },
1081        );
1082
1083        // Range should expand: A1:A10 -> A1:A13
1084        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1085            && let Some(ASTNodeType::Reference {
1086                reference:
1087                    formualizer_parse::parser::ReferenceType::Range {
1088                        start_row, end_row, ..
1089                    },
1090                ..
1091            }) = args.first().map(|arg| &arg.node_type)
1092        {
1093            assert_eq!(start_row.unwrap_or(0), 1); // A1 start unchanged
1094            assert_eq!(end_row.unwrap_or(0), 13); // A10 -> A13
1095        }
1096    }
1097
1098    #[test]
1099    fn test_relative_reference_copy() {
1100        let adjuster = RelativeReferenceAdjuster::new(2, 3); // Move 2 rows down, 3 cols right
1101
1102        // Formula: =A1+B2
1103        let ast = parse("=A1+B2").unwrap();
1104        let adjusted = adjuster.adjust_formula(&ast);
1105
1106        // A1 -> D3, B2 -> E4
1107        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
1108            if let ASTNodeType::Reference {
1109                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1110                ..
1111            } = &left.node_type
1112            {
1113                assert_eq!(*row, 3); // A1 (1,1) -> D3 (3,4)
1114                assert_eq!(*col, 4);
1115            }
1116            if let ASTNodeType::Reference {
1117                reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1118                ..
1119            } = &right.node_type
1120            {
1121                assert_eq!(*row, 4); // B2 (2,2) -> E4 (4,5)
1122                assert_eq!(*col, 5);
1123            }
1124        }
1125    }
1126
1127    #[test]
1128    fn test_absolute_reference_preservation() {
1129        let adjuster = ReferenceAdjuster::new();
1130
1131        // Test with absolute row references ($5)
1132        let cell_abs_row = CellRef::new(
1133            0,
1134            Coord::new(5, 2, true, false), // Row 5 absolute, col 2 relative
1135        );
1136
1137        // Insert rows before the absolute reference
1138        let result = adjuster.adjust_cell_ref(
1139            &cell_abs_row,
1140            &ShiftOperation::InsertRows {
1141                sheet_id: 0,
1142                before: 3,
1143                count: 2,
1144            },
1145        );
1146
1147        // Absolute row should not change
1148        assert!(result.is_some());
1149        let adjusted = result.unwrap();
1150        assert_eq!(adjusted.coord.row(), 5); // Row stays at 5
1151        assert_eq!(adjusted.coord.col(), 2); // Column unchanged
1152        assert!(adjusted.coord.row_abs());
1153        assert!(!adjusted.coord.col_abs());
1154    }
1155
1156    #[test]
1157    fn test_absolute_column_preservation() {
1158        let adjuster = ReferenceAdjuster::new();
1159
1160        // Test with absolute column references ($B)
1161        let cell_abs_col = CellRef::new(
1162            0,
1163            Coord::new(5, 2, false, true), // Row 5 relative, col 2 absolute
1164        );
1165
1166        // Delete columns before the absolute reference
1167        let result = adjuster.adjust_cell_ref(
1168            &cell_abs_col,
1169            &ShiftOperation::DeleteColumns {
1170                sheet_id: 0,
1171                start: 1,
1172                count: 1,
1173            },
1174        );
1175
1176        // Absolute column should not change
1177        assert!(result.is_some());
1178        let adjusted = result.unwrap();
1179        assert_eq!(adjusted.coord.row(), 5); // Row unchanged
1180        assert_eq!(adjusted.coord.col(), 2); // Column stays at 2 despite deletion
1181        assert!(!adjusted.coord.row_abs());
1182        assert!(adjusted.coord.col_abs());
1183    }
1184
1185    #[test]
1186    fn test_mixed_absolute_relative_references() {
1187        let adjuster = ReferenceAdjuster::new();
1188
1189        // Test 1: $A5 (col absolute, row relative) with row insertion
1190        let mixed1 = CellRef::new(
1191            0,
1192            Coord::new(5, 1, false, true), // Row 5 relative, col 1 absolute
1193        );
1194
1195        let result1 = adjuster.adjust_cell_ref(
1196            &mixed1,
1197            &ShiftOperation::InsertRows {
1198                sheet_id: 0,
1199                before: 3,
1200                count: 2,
1201            },
1202        );
1203
1204        assert!(result1.is_some());
1205        let adj1 = result1.unwrap();
1206        assert_eq!(adj1.coord.row(), 7); // Row 5 -> 7 (shifted)
1207        assert_eq!(adj1.coord.col(), 1); // Column stays at 1 (absolute)
1208
1209        // Test 2: B$10 (col relative, row absolute) with column deletion
1210        let mixed2 = CellRef::new(
1211            0,
1212            Coord::new(10, 3, true, false), // Row 10 absolute, col 3 relative
1213        );
1214
1215        let result2 = adjuster.adjust_cell_ref(
1216            &mixed2,
1217            &ShiftOperation::DeleteColumns {
1218                sheet_id: 0,
1219                start: 1,
1220                count: 1,
1221            },
1222        );
1223
1224        assert!(result2.is_some());
1225        let adj2 = result2.unwrap();
1226        assert_eq!(adj2.coord.row(), 10); // Row stays at 10 (absolute)
1227        assert_eq!(adj2.coord.col(), 2); // Column 3 -> 2 (shifted left)
1228    }
1229
1230    #[test]
1231    fn test_fully_absolute_reference() {
1232        let adjuster = ReferenceAdjuster::new();
1233
1234        // Test $A$1 - fully absolute
1235        let fully_abs = CellRef::new(
1236            0,
1237            Coord::new(1, 1, true, true), // Both row and col absolute
1238        );
1239
1240        // Try various operations - nothing should change
1241
1242        // Insert rows
1243        let result1 = adjuster.adjust_cell_ref(
1244            &fully_abs,
1245            &ShiftOperation::InsertRows {
1246                sheet_id: 0,
1247                before: 1,
1248                count: 5,
1249            },
1250        );
1251        assert!(result1.is_some());
1252        assert_eq!(result1.unwrap().coord.row(), 1);
1253        assert_eq!(result1.unwrap().coord.col(), 1);
1254
1255        // Delete columns
1256        let result2 = adjuster.adjust_cell_ref(
1257            &fully_abs,
1258            &ShiftOperation::DeleteColumns {
1259                sheet_id: 0,
1260                start: 0,
1261                count: 1,
1262            },
1263        );
1264        assert!(result2.is_some());
1265        assert_eq!(result2.unwrap().coord.row(), 1);
1266        assert_eq!(result2.unwrap().coord.col(), 1);
1267    }
1268
1269    #[test]
1270    fn test_deleted_reference_becomes_ref_error() {
1271        let adjuster = ReferenceAdjuster::new();
1272
1273        // Test deleting a cell that's referenced
1274        let cell = CellRef::new(
1275            0,
1276            Coord::new(5, 3, false, false), // Row 5, col 3, both relative
1277        );
1278
1279        // Delete the row containing the cell
1280        let result = adjuster.adjust_cell_ref(
1281            &cell,
1282            &ShiftOperation::DeleteRows {
1283                sheet_id: 0,
1284                start: 5,
1285                count: 1,
1286            },
1287        );
1288
1289        // Should return None to indicate deletion
1290        assert!(result.is_none());
1291
1292        // Delete the column containing the cell
1293        let result2 = adjuster.adjust_cell_ref(
1294            &cell,
1295            &ShiftOperation::DeleteColumns {
1296                sheet_id: 0,
1297                start: 3,
1298                count: 1,
1299            },
1300        );
1301
1302        // Should return None to indicate deletion
1303        assert!(result2.is_none());
1304    }
1305
1306    #[test]
1307    fn test_range_expansion_on_insert() {
1308        let adjuster = ReferenceAdjuster::new();
1309
1310        // Test that ranges expand when rows/cols are inserted within them
1311        let ast = parse("=SUM(B2:D10)").unwrap();
1312
1313        // Insert rows in the middle of the range
1314        let adjusted = adjuster.adjust_ast(
1315            &ast,
1316            &ShiftOperation::InsertRows {
1317                sheet_id: 0,
1318                before: 5,
1319                count: 3,
1320            },
1321        );
1322
1323        // Range should expand: B2:D10 -> B2:D13
1324        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1325            && let Some(ASTNodeType::Reference {
1326                reference:
1327                    formualizer_parse::parser::ReferenceType::Range {
1328                        start_row,
1329                        end_row,
1330                        start_col,
1331                        end_col,
1332                        ..
1333                    },
1334                ..
1335            }) = args.first().map(|arg| &arg.node_type)
1336        {
1337            assert_eq!(*start_row, Some(2)); // Start unchanged
1338            assert_eq!(*end_row, Some(13)); // End expanded from 10 to 13
1339            assert_eq!(*start_col, Some(2)); // B column
1340            assert_eq!(*end_col, Some(4)); // D column
1341        }
1342    }
1343
1344    #[test]
1345    fn test_range_contraction_on_delete() {
1346        let adjuster = ReferenceAdjuster::new();
1347
1348        // Test that ranges contract when rows/cols are deleted within them
1349        let ast = parse("=SUM(A5:A20)").unwrap();
1350
1351        // Delete rows in the middle of the range
1352        let adjusted = adjuster.adjust_ast(
1353            &ast,
1354            &ShiftOperation::DeleteRows {
1355                sheet_id: 0,
1356                start: 10,
1357                count: 5,
1358            },
1359        );
1360
1361        // Range should contract: A5:A20 -> A5:A15
1362        if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1363            && let Some(ASTNodeType::Reference {
1364                reference:
1365                    formualizer_parse::parser::ReferenceType::Range {
1366                        start_row, end_row, ..
1367                    },
1368                ..
1369            }) = args.first().map(|arg| &arg.node_type)
1370        {
1371            assert_eq!(*start_row, Some(5)); // Start unchanged
1372            assert_eq!(*end_row, Some(15)); // End contracted from 20 to 15
1373        }
1374    }
1375}