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