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        match reference {
190            ReferenceType::Cell { sheet, row, col } => {
191                // Create a temporary CellRef to reuse adjustment logic
192                // For now, assume same sheet if no sheet specified
193                let temp_ref = CellRef::new(
194                    match op {
195                        ShiftOperation::InsertRows { sheet_id, .. }
196                        | ShiftOperation::DeleteRows { sheet_id, .. }
197                        | ShiftOperation::InsertColumns { sheet_id, .. }
198                        | ShiftOperation::DeleteColumns { sheet_id, .. } => *sheet_id,
199                    },
200                    Coord::new(*row, *col, false, false), // Assume relative for now
201                );
202
203                match self.adjust_cell_ref(&temp_ref, op) {
204                    None => {
205                        // Cell was deleted, create a special marker
206                        // We'll need to handle this at a higher level
207                        ReferenceType::Cell {
208                            sheet: Some("#REF".to_string()),
209                            row: 0,
210                            col: 0,
211                        }
212                    }
213                    Some(adjusted) => ReferenceType::Cell {
214                        sheet: sheet.clone(),
215                        row: adjusted.coord.row,
216                        col: adjusted.coord.col,
217                    },
218                }
219            }
220            ReferenceType::Range {
221                sheet,
222                start_row,
223                start_col,
224                end_row,
225                end_col,
226            } => {
227                // Check if this is an unbounded (infinite) range
228                // Unbounded column: A:A has no row bounds (both None)
229                // Unbounded row: 1:1 has no column bounds (both None)
230                let is_unbounded_column = start_row.is_none() && end_row.is_none();
231                let is_unbounded_row = start_col.is_none() && end_col.is_none();
232
233                // Don't adjust unbounded ranges - they conceptually represent "all rows/columns"
234                // and should remain unchanged during structural operations
235                if is_unbounded_column || is_unbounded_row {
236                    return reference.clone();
237                }
238
239                // Adjust range boundaries based on operation
240                let (adj_start_row, adj_end_row) = match op {
241                    ShiftOperation::InsertRows { before, count, .. } => {
242                        // Only adjust if both bounds are present (bounded range)
243                        match (start_row, end_row) {
244                            (Some(start), Some(end)) => {
245                                let adj_start = if *start >= *before {
246                                    start + count
247                                } else {
248                                    *start
249                                };
250                                let adj_end = if *end >= *before { end + count } else { *end };
251                                (Some(adj_start), Some(adj_end))
252                            }
253                            // Preserve None values for partially bounded ranges
254                            _ => (*start_row, *end_row),
255                        }
256                    }
257                    ShiftOperation::DeleteRows { start, count, .. } => {
258                        // Only adjust if both bounds are present
259                        match (start_row, end_row) {
260                            (Some(range_start), Some(range_end)) => {
261                                if *range_end < *start || *range_start >= start + count {
262                                    // Range outside delete area
263                                    let adj_start = if *range_start >= start + count {
264                                        range_start - count
265                                    } else {
266                                        *range_start
267                                    };
268                                    let adj_end = if *range_end >= start + count {
269                                        range_end - count
270                                    } else {
271                                        *range_end
272                                    };
273                                    (Some(adj_start), Some(adj_end))
274                                } else if *range_start >= *start && *range_end < start + count {
275                                    // Entire range deleted - mark with special sheet name
276                                    return ReferenceType::Range {
277                                        sheet: Some("#REF".to_string()),
278                                        start_row: Some(0),
279                                        start_col: Some(0),
280                                        end_row: Some(0),
281                                        end_col: Some(0),
282                                    };
283                                } else {
284                                    // Range partially overlaps delete area
285                                    let adj_start = if *range_start < *start {
286                                        *range_start
287                                    } else {
288                                        *start
289                                    };
290                                    let adj_end = if *range_end >= start + count {
291                                        range_end - count
292                                    } else {
293                                        start - 1
294                                    };
295                                    (Some(adj_start), Some(adj_end))
296                                }
297                            }
298                            // Preserve None values for partially bounded ranges
299                            _ => (*start_row, *end_row),
300                        }
301                    }
302                    _ => (*start_row, *end_row),
303                };
304
305                // Similar logic for columns
306                let (adj_start_col, adj_end_col) = match op {
307                    ShiftOperation::InsertColumns { before, count, .. } => {
308                        // Only adjust if both bounds are present
309                        match (start_col, end_col) {
310                            (Some(start), Some(end)) => {
311                                let adj_start = if *start >= *before {
312                                    start + count
313                                } else {
314                                    *start
315                                };
316                                let adj_end = if *end >= *before { end + count } else { *end };
317                                (Some(adj_start), Some(adj_end))
318                            }
319                            // Preserve None values
320                            _ => (*start_col, *end_col),
321                        }
322                    }
323                    ShiftOperation::DeleteColumns { start, count, .. } => {
324                        // Only adjust if both bounds are present
325                        match (start_col, end_col) {
326                            (Some(range_start), Some(range_end)) => {
327                                if *range_end < *start || *range_start >= start + count {
328                                    // Range outside delete area
329                                    let adj_start = if *range_start >= start + count {
330                                        range_start - count
331                                    } else {
332                                        *range_start
333                                    };
334                                    let adj_end = if *range_end >= start + count {
335                                        range_end - count
336                                    } else {
337                                        *range_end
338                                    };
339                                    (Some(adj_start), Some(adj_end))
340                                } else if *range_start >= *start && *range_end < start + count {
341                                    // Entire range deleted - mark with special sheet name
342                                    return ReferenceType::Range {
343                                        sheet: Some("#REF".to_string()),
344                                        start_row: Some(0),
345                                        start_col: Some(0),
346                                        end_row: Some(0),
347                                        end_col: Some(0),
348                                    };
349                                } else {
350                                    // Range partially overlaps delete area
351                                    let adj_start = if *range_start < *start {
352                                        *range_start
353                                    } else {
354                                        *start
355                                    };
356                                    let adj_end = if *range_end >= start + count {
357                                        range_end - count
358                                    } else {
359                                        start - 1
360                                    };
361                                    (Some(adj_start), Some(adj_end))
362                                }
363                            }
364                            // Preserve None values
365                            _ => (*start_col, *end_col),
366                        }
367                    }
368                    _ => (*start_col, *end_col),
369                };
370
371                ReferenceType::Range {
372                    sheet: sheet.clone(),
373                    start_row: adj_start_row,
374                    start_col: adj_start_col,
375                    end_row: adj_end_row,
376                    end_col: adj_end_col,
377                }
378            }
379            _ => reference.clone(),
380        }
381    }
382}
383
384impl Default for ReferenceAdjuster {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390/// Helper for adjusting references when copying/moving ranges
391pub struct RelativeReferenceAdjuster {
392    row_offset: i32,
393    col_offset: i32,
394}
395
396impl RelativeReferenceAdjuster {
397    pub fn new(row_offset: i32, col_offset: i32) -> Self {
398        Self {
399            row_offset,
400            col_offset,
401        }
402    }
403
404    pub fn adjust_formula(&self, ast: &ASTNode) -> ASTNode {
405        match &ast.node_type {
406            ASTNodeType::Reference {
407                original,
408                reference,
409            } => {
410                let adjusted = self.adjust_reference(reference);
411                ASTNode {
412                    node_type: ASTNodeType::Reference {
413                        original: original.clone(),
414                        reference: adjusted,
415                    },
416                    source_token: ast.source_token.clone(),
417                    contains_volatile: ast.contains_volatile,
418                }
419            }
420            ASTNodeType::BinaryOp { op, left, right } => ASTNode {
421                node_type: ASTNodeType::BinaryOp {
422                    op: op.clone(),
423                    left: Box::new(self.adjust_formula(left)),
424                    right: Box::new(self.adjust_formula(right)),
425                },
426                source_token: ast.source_token.clone(),
427                contains_volatile: ast.contains_volatile,
428            },
429            ASTNodeType::UnaryOp { op, expr } => ASTNode {
430                node_type: ASTNodeType::UnaryOp {
431                    op: op.clone(),
432                    expr: Box::new(self.adjust_formula(expr)),
433                },
434                source_token: ast.source_token.clone(),
435                contains_volatile: ast.contains_volatile,
436            },
437            ASTNodeType::Function { name, args } => ASTNode {
438                node_type: ASTNodeType::Function {
439                    name: name.clone(),
440                    args: args.iter().map(|arg| self.adjust_formula(arg)).collect(),
441                },
442                source_token: ast.source_token.clone(),
443                contains_volatile: ast.contains_volatile,
444            },
445            _ => ast.clone(),
446        }
447    }
448
449    fn adjust_reference(
450        &self,
451        reference: &formualizer_parse::parser::ReferenceType,
452    ) -> formualizer_parse::parser::ReferenceType {
453        use formualizer_parse::parser::ReferenceType;
454
455        match reference {
456            ReferenceType::Cell { sheet, row, col } => {
457                // Only adjust relative references
458                // TODO: Check for absolute references when we have that info
459                let new_row = (*row as i32 + self.row_offset).max(1) as u32;
460                let new_col = (*col as i32 + self.col_offset).max(1) as u32;
461
462                ReferenceType::Cell {
463                    sheet: sheet.clone(),
464                    row: new_row,
465                    col: new_col,
466                }
467            }
468            ReferenceType::Range {
469                sheet,
470                start_row,
471                start_col,
472                end_row,
473                end_col,
474            } => {
475                // Adjust range boundaries
476                let adj_start_row = start_row.map(|r| (r as i32 + self.row_offset).max(1) as u32);
477                let adj_start_col = start_col.map(|c| (c as i32 + self.col_offset).max(1) as u32);
478                let adj_end_row = end_row.map(|r| (r as i32 + self.row_offset).max(1) as u32);
479                let adj_end_col = end_col.map(|c| (c as i32 + self.col_offset).max(1) as u32);
480
481                ReferenceType::Range {
482                    sheet: sheet.clone(),
483                    start_row: adj_start_row,
484                    start_col: adj_start_col,
485                    end_row: adj_end_row,
486                    end_col: adj_end_col,
487                }
488            }
489            _ => reference.clone(),
490        }
491    }
492}
493
494#[cfg(test)]
495mod tests {
496    use super::*;
497    use formualizer_parse::parser::parse;
498
499    fn format_formula(ast: &ASTNode) -> String {
500        // TODO: Use the actual formualizer_parse::parser::to_string when available
501        // For now, a simple representation
502        format!("{ast:?}")
503    }
504
505    #[test]
506    fn test_reference_adjustment_on_row_insert() {
507        let adjuster = ReferenceAdjuster::new();
508
509        // Formula: =A5+B10
510        let ast = parse("=A5+B10").unwrap();
511
512        // Insert 2 rows before row 7
513        let adjusted = adjuster.adjust_ast(
514            &ast,
515            &ShiftOperation::InsertRows {
516                sheet_id: 0,
517                before: 7,
518                count: 2,
519            },
520        );
521
522        // A5 unchanged (before insert point), B10 -> B12
523        // Verify by checking the AST structure
524        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
525            if let ASTNodeType::Reference {
526                reference: left_ref,
527                ..
528            } = &left.node_type
529            {
530                if let formualizer_parse::parser::ReferenceType::Cell { row, col, .. } = left_ref {
531                    assert_eq!(*row, 5); // A5 unchanged
532                    assert_eq!(*col, 1);
533                }
534            }
535            if let ASTNodeType::Reference {
536                reference: right_ref,
537                ..
538            } = &right.node_type
539            {
540                if let formualizer_parse::parser::ReferenceType::Cell { row, col, .. } = right_ref {
541                    assert_eq!(*row, 12); // B10 -> B12
542                    assert_eq!(*col, 2);
543                }
544            }
545        }
546    }
547
548    #[test]
549    fn test_reference_adjustment_on_column_delete() {
550        let adjuster = ReferenceAdjuster::new();
551
552        // Formula: =C1+F1
553        let ast = parse("=C1+F1").unwrap();
554
555        // Delete columns B and C (columns 2 and 3)
556        let adjusted = adjuster.adjust_ast(
557            &ast,
558            &ShiftOperation::DeleteColumns {
559                sheet_id: 0,
560                start: 2, // Column B
561                count: 2,
562            },
563        );
564
565        // C1 -> #REF! (deleted), F1 -> D1 (shifted left by 2)
566        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
567            if let ASTNodeType::Reference {
568                reference: left_ref,
569                ..
570            } = &left.node_type
571            {
572                // C1 should become #REF! (marked with special sheet name)
573                if let formualizer_parse::parser::ReferenceType::Cell { sheet, row, col } = left_ref
574                {
575                    assert_eq!(sheet.as_deref(), Some("#REF"));
576                    assert_eq!(*row, 0);
577                    assert_eq!(*col, 0);
578                }
579            }
580            if let ASTNodeType::Reference {
581                reference: right_ref,
582                ..
583            } = &right.node_type
584            {
585                if let formualizer_parse::parser::ReferenceType::Cell { row, col, .. } = right_ref {
586                    assert_eq!(*row, 1); // Row unchanged
587                    assert_eq!(*col, 4); // F1 (col 6) -> D1 (col 4)
588                }
589            }
590        }
591    }
592
593    #[test]
594    fn test_range_reference_adjustment() {
595        let adjuster = ReferenceAdjuster::new();
596
597        // Formula: =SUM(A1:A10)
598        let ast = parse("=SUM(A1:A10)").unwrap();
599
600        // Insert 3 rows before row 5
601        let adjusted = adjuster.adjust_ast(
602            &ast,
603            &ShiftOperation::InsertRows {
604                sheet_id: 0,
605                before: 5,
606                count: 3,
607            },
608        );
609
610        // Range should expand: A1:A10 -> A1:A13
611        if let ASTNodeType::Function { args, .. } = &adjusted.node_type {
612            if let Some(first_arg) = args.first() {
613                if let ASTNodeType::Reference { reference, .. } = &first_arg.node_type {
614                    if let formualizer_parse::parser::ReferenceType::Range {
615                        start_row,
616                        end_row,
617                        ..
618                    } = reference
619                    {
620                        assert_eq!(start_row.unwrap_or(0), 1); // A1 start unchanged
621                        assert_eq!(end_row.unwrap_or(0), 13); // A10 -> A13
622                    }
623                }
624            }
625        }
626    }
627
628    #[test]
629    fn test_relative_reference_copy() {
630        let adjuster = RelativeReferenceAdjuster::new(2, 3); // Move 2 rows down, 3 cols right
631
632        // Formula: =A1+B2
633        let ast = parse("=A1+B2").unwrap();
634        let adjusted = adjuster.adjust_formula(&ast);
635
636        // A1 -> D3, B2 -> E4
637        if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
638            if let ASTNodeType::Reference {
639                reference: left_ref,
640                ..
641            } = &left.node_type
642            {
643                if let formualizer_parse::parser::ReferenceType::Cell { row, col, .. } = left_ref {
644                    assert_eq!(*row, 3); // A1 (1,1) -> D3 (3,4)
645                    assert_eq!(*col, 4);
646                }
647            }
648            if let ASTNodeType::Reference {
649                reference: right_ref,
650                ..
651            } = &right.node_type
652            {
653                if let formualizer_parse::parser::ReferenceType::Cell { row, col, .. } = right_ref {
654                    assert_eq!(*row, 4); // B2 (2,2) -> E4 (4,5)
655                    assert_eq!(*col, 5);
656                }
657            }
658        }
659    }
660
661    #[test]
662    fn test_absolute_reference_preservation() {
663        let adjuster = ReferenceAdjuster::new();
664
665        // Test with absolute row references ($5)
666        let cell_abs_row = CellRef::new(
667            0,
668            Coord::new(5, 2, true, false), // Row 5 absolute, col 2 relative
669        );
670
671        // Insert rows before the absolute reference
672        let result = adjuster.adjust_cell_ref(
673            &cell_abs_row,
674            &ShiftOperation::InsertRows {
675                sheet_id: 0,
676                before: 3,
677                count: 2,
678            },
679        );
680
681        // Absolute row should not change
682        assert!(result.is_some());
683        let adjusted = result.unwrap();
684        assert_eq!(adjusted.coord.row, 5); // Row stays at 5
685        assert_eq!(adjusted.coord.col, 2); // Column unchanged
686        assert!(adjusted.coord.row_abs());
687        assert!(!adjusted.coord.col_abs());
688    }
689
690    #[test]
691    fn test_absolute_column_preservation() {
692        let adjuster = ReferenceAdjuster::new();
693
694        // Test with absolute column references ($B)
695        let cell_abs_col = CellRef::new(
696            0,
697            Coord::new(5, 2, false, true), // Row 5 relative, col 2 absolute
698        );
699
700        // Delete columns before the absolute reference
701        let result = adjuster.adjust_cell_ref(
702            &cell_abs_col,
703            &ShiftOperation::DeleteColumns {
704                sheet_id: 0,
705                start: 1,
706                count: 1,
707            },
708        );
709
710        // Absolute column should not change
711        assert!(result.is_some());
712        let adjusted = result.unwrap();
713        assert_eq!(adjusted.coord.row, 5); // Row unchanged
714        assert_eq!(adjusted.coord.col, 2); // Column stays at 2 despite deletion
715        assert!(!adjusted.coord.row_abs());
716        assert!(adjusted.coord.col_abs());
717    }
718
719    #[test]
720    fn test_mixed_absolute_relative_references() {
721        let adjuster = ReferenceAdjuster::new();
722
723        // Test 1: $A5 (col absolute, row relative) with row insertion
724        let mixed1 = CellRef::new(
725            0,
726            Coord::new(5, 1, false, true), // Row 5 relative, col 1 absolute
727        );
728
729        let result1 = adjuster.adjust_cell_ref(
730            &mixed1,
731            &ShiftOperation::InsertRows {
732                sheet_id: 0,
733                before: 3,
734                count: 2,
735            },
736        );
737
738        assert!(result1.is_some());
739        let adj1 = result1.unwrap();
740        assert_eq!(adj1.coord.row, 7); // Row 5 -> 7 (shifted)
741        assert_eq!(adj1.coord.col, 1); // Column stays at 1 (absolute)
742
743        // Test 2: B$10 (col relative, row absolute) with column deletion
744        let mixed2 = CellRef::new(
745            0,
746            Coord::new(10, 3, true, false), // Row 10 absolute, col 3 relative
747        );
748
749        let result2 = adjuster.adjust_cell_ref(
750            &mixed2,
751            &ShiftOperation::DeleteColumns {
752                sheet_id: 0,
753                start: 1,
754                count: 1,
755            },
756        );
757
758        assert!(result2.is_some());
759        let adj2 = result2.unwrap();
760        assert_eq!(adj2.coord.row, 10); // Row stays at 10 (absolute)
761        assert_eq!(adj2.coord.col, 2); // Column 3 -> 2 (shifted left)
762    }
763
764    #[test]
765    fn test_fully_absolute_reference() {
766        let adjuster = ReferenceAdjuster::new();
767
768        // Test $A$1 - fully absolute
769        let fully_abs = CellRef::new(
770            0,
771            Coord::new(1, 1, true, true), // Both row and col absolute
772        );
773
774        // Try various operations - nothing should change
775
776        // Insert rows
777        let result1 = adjuster.adjust_cell_ref(
778            &fully_abs,
779            &ShiftOperation::InsertRows {
780                sheet_id: 0,
781                before: 1,
782                count: 5,
783            },
784        );
785        assert!(result1.is_some());
786        assert_eq!(result1.unwrap().coord.row, 1);
787        assert_eq!(result1.unwrap().coord.col, 1);
788
789        // Delete columns
790        let result2 = adjuster.adjust_cell_ref(
791            &fully_abs,
792            &ShiftOperation::DeleteColumns {
793                sheet_id: 0,
794                start: 0,
795                count: 1,
796            },
797        );
798        assert!(result2.is_some());
799        assert_eq!(result2.unwrap().coord.row, 1);
800        assert_eq!(result2.unwrap().coord.col, 1);
801    }
802
803    #[test]
804    fn test_deleted_reference_becomes_ref_error() {
805        let adjuster = ReferenceAdjuster::new();
806
807        // Test deleting a cell that's referenced
808        let cell = CellRef::new(
809            0,
810            Coord::new(5, 3, false, false), // Row 5, col 3, both relative
811        );
812
813        // Delete the row containing the cell
814        let result = adjuster.adjust_cell_ref(
815            &cell,
816            &ShiftOperation::DeleteRows {
817                sheet_id: 0,
818                start: 5,
819                count: 1,
820            },
821        );
822
823        // Should return None to indicate deletion
824        assert!(result.is_none());
825
826        // Delete the column containing the cell
827        let result2 = adjuster.adjust_cell_ref(
828            &cell,
829            &ShiftOperation::DeleteColumns {
830                sheet_id: 0,
831                start: 3,
832                count: 1,
833            },
834        );
835
836        // Should return None to indicate deletion
837        assert!(result2.is_none());
838    }
839
840    #[test]
841    fn test_range_expansion_on_insert() {
842        let adjuster = ReferenceAdjuster::new();
843
844        // Test that ranges expand when rows/cols are inserted within them
845        let ast = parse("=SUM(B2:D10)").unwrap();
846
847        // Insert rows in the middle of the range
848        let adjusted = adjuster.adjust_ast(
849            &ast,
850            &ShiftOperation::InsertRows {
851                sheet_id: 0,
852                before: 5,
853                count: 3,
854            },
855        );
856
857        // Range should expand: B2:D10 -> B2:D13
858        if let ASTNodeType::Function { args, .. } = &adjusted.node_type {
859            if let Some(first_arg) = args.first() {
860                if let ASTNodeType::Reference { reference, .. } = &first_arg.node_type {
861                    if let formualizer_parse::parser::ReferenceType::Range {
862                        start_row,
863                        end_row,
864                        start_col,
865                        end_col,
866                        ..
867                    } = reference
868                    {
869                        assert_eq!(*start_row, Some(2)); // Start unchanged
870                        assert_eq!(*end_row, Some(13)); // End expanded from 10 to 13
871                        assert_eq!(*start_col, Some(2)); // B column
872                        assert_eq!(*end_col, Some(4)); // D column
873                    }
874                }
875            }
876        }
877    }
878
879    #[test]
880    fn test_range_contraction_on_delete() {
881        let adjuster = ReferenceAdjuster::new();
882
883        // Test that ranges contract when rows/cols are deleted within them
884        let ast = parse("=SUM(A5:A20)").unwrap();
885
886        // Delete rows in the middle of the range
887        let adjusted = adjuster.adjust_ast(
888            &ast,
889            &ShiftOperation::DeleteRows {
890                sheet_id: 0,
891                start: 10,
892                count: 5,
893            },
894        );
895
896        // Range should contract: A5:A20 -> A5:A15
897        if let ASTNodeType::Function { args, .. } = &adjusted.node_type {
898            if let Some(first_arg) = args.first() {
899                if let ASTNodeType::Reference { reference, .. } = &first_arg.node_type {
900                    if let formualizer_parse::parser::ReferenceType::Range {
901                        start_row,
902                        end_row,
903                        ..
904                    } = reference
905                    {
906                        assert_eq!(*start_row, Some(5)); // Start unchanged
907                        assert_eq!(*end_row, Some(15)); // End contracted from 20 to 15
908                    }
909                }
910            }
911        }
912    }
913}