1use crate::reference::{CellRef, Coord};
2use crate::{SheetId, engine::sheet_registry::SheetRegistry};
3use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
4use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub(crate) enum AbsShiftPolicy {
20 Track,
23 Pin,
25}
26
27impl AbsShiftPolicy {
28 #[inline]
29 fn pins(self) -> bool {
30 matches!(self, Self::Pin)
31 }
32}
33
34pub struct ReferenceAdjuster;
36
37pub(crate) struct ReferenceContext<'a> {
42 formula_sheet_id: SheetId,
43 sheet_registry: &'a SheetRegistry,
44}
45
46impl<'a> ReferenceContext<'a> {
47 pub(crate) fn new(formula_sheet_id: SheetId, sheet_registry: &'a SheetRegistry) -> Self {
48 Self {
49 formula_sheet_id,
50 sheet_registry,
51 }
52 }
53
54 fn reference_sheet_id(&self, sheet: Option<&str>) -> Option<SheetId> {
55 match sheet {
56 Some(name) => self.sheet_registry.get_id(name),
57 None => Some(self.formula_sheet_id),
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
63enum ReferenceAdjustment {
64 Reference(ReferenceType),
65 Invalidated,
66}
67
68#[derive(Debug, Clone)]
69pub enum ShiftOperation {
70 InsertRows {
71 sheet_id: u16,
72 before: u32,
73 count: u32,
74 },
75 DeleteRows {
76 sheet_id: u16,
77 start: u32,
78 count: u32,
79 },
80 InsertColumns {
81 sheet_id: u16,
82 before: u32,
83 count: u32,
84 },
85 DeleteColumns {
86 sheet_id: u16,
87 start: u32,
88 count: u32,
89 },
90}
91
92impl ReferenceAdjuster {
93 pub fn new() -> Self {
94 Self
95 }
96
97 pub fn adjust_ast(&self, ast: &ASTNode, op: &ShiftOperation) -> ASTNode {
103 self.adjust_ast_with_policy(ast, op, AbsShiftPolicy::Track)
104 }
105
106 pub(crate) fn adjust_ast_with_policy(
108 &self,
109 ast: &ASTNode,
110 op: &ShiftOperation,
111 policy: AbsShiftPolicy,
112 ) -> ASTNode {
113 self.adjust_ast_if_changed_inner(ast, op, policy, None)
114 .unwrap_or_else(|| ast.clone())
115 }
116
117 pub fn adjust_ast_if_changed(&self, ast: &ASTNode, op: &ShiftOperation) -> Option<ASTNode> {
119 self.adjust_ast_if_changed_with_policy(ast, op, AbsShiftPolicy::Track)
120 }
121
122 pub(crate) fn adjust_ast_if_changed_with_policy(
124 &self,
125 ast: &ASTNode,
126 op: &ShiftOperation,
127 policy: AbsShiftPolicy,
128 ) -> Option<ASTNode> {
129 self.adjust_ast_if_changed_inner(ast, op, policy, None)
130 }
131
132 pub(crate) fn adjust_ast_in_context(
133 &self,
134 ast: &ASTNode,
135 op: &ShiftOperation,
136 context: &ReferenceContext<'_>,
137 ) -> ASTNode {
138 self.adjust_ast_with_policy_in_context(ast, op, AbsShiftPolicy::Track, context)
139 }
140
141 pub(crate) fn adjust_ast_with_policy_in_context(
142 &self,
143 ast: &ASTNode,
144 op: &ShiftOperation,
145 policy: AbsShiftPolicy,
146 context: &ReferenceContext<'_>,
147 ) -> ASTNode {
148 self.adjust_ast_if_changed_inner(ast, op, policy, Some(context))
149 .unwrap_or_else(|| ast.clone())
150 }
151
152 pub(crate) fn adjust_ast_if_changed_in_context(
153 &self,
154 ast: &ASTNode,
155 op: &ShiftOperation,
156 context: &ReferenceContext<'_>,
157 ) -> Option<ASTNode> {
158 self.adjust_ast_if_changed_with_policy_in_context(ast, op, AbsShiftPolicy::Track, context)
159 }
160
161 pub(crate) fn adjust_ast_if_changed_with_policy_in_context(
162 &self,
163 ast: &ASTNode,
164 op: &ShiftOperation,
165 policy: AbsShiftPolicy,
166 context: &ReferenceContext<'_>,
167 ) -> Option<ASTNode> {
168 self.adjust_ast_if_changed_inner(ast, op, policy, Some(context))
169 }
170
171 fn adjust_ast_if_changed_inner(
172 &self,
173 ast: &ASTNode,
174 op: &ShiftOperation,
175 policy: AbsShiftPolicy,
176 context: Option<&ReferenceContext<'_>>,
177 ) -> Option<ASTNode> {
178 let changed_node_type = match &ast.node_type {
179 ASTNodeType::Reference { reference, .. } => {
180 match self.adjust_reference(reference, op, policy, context) {
181 ReferenceAdjustment::Reference(adjusted) if adjusted == *reference => {
182 return None;
183 }
184 ReferenceAdjustment::Reference(adjusted) => ASTNodeType::Reference {
185 original: adjusted.normalise(),
186 reference: adjusted,
187 },
188 ReferenceAdjustment::Invalidated => ASTNodeType::Literal(LiteralValue::Error(
189 ExcelError::new(ExcelErrorKind::Ref),
190 )),
191 }
192 }
193 ASTNodeType::BinaryOp {
194 op: bin_op,
195 left,
196 right,
197 } => {
198 let adjusted_left = self.adjust_ast_if_changed_inner(left, op, policy, context);
199 let adjusted_right = self.adjust_ast_if_changed_inner(right, op, policy, context);
200 if adjusted_left.is_none() && adjusted_right.is_none() {
201 return None;
202 }
203 ASTNodeType::BinaryOp {
204 op: bin_op.clone(),
205 left: Box::new(adjusted_left.unwrap_or_else(|| (**left).clone())),
206 right: Box::new(adjusted_right.unwrap_or_else(|| (**right).clone())),
207 }
208 }
209 ASTNodeType::UnaryOp { op: un_op, expr } => ASTNodeType::UnaryOp {
210 op: un_op.clone(),
211 expr: Box::new(self.adjust_ast_if_changed_inner(expr, op, policy, context)?),
212 },
213 ASTNodeType::Function { name, args } => {
214 let (args, changed) = self.adjust_children(args, op, policy, context);
215 if !changed {
216 return None;
217 }
218 ASTNodeType::Function {
219 name: name.clone(),
220 args,
221 }
222 }
223 ASTNodeType::Call { callee, args } => {
224 let adjusted_callee = self.adjust_ast_if_changed_inner(callee, op, policy, context);
225 let (args, args_changed) = self.adjust_children(args, op, policy, context);
226 if adjusted_callee.is_none() && !args_changed {
227 return None;
228 }
229 ASTNodeType::Call {
230 callee: Box::new(adjusted_callee.unwrap_or_else(|| (**callee).clone())),
231 args,
232 }
233 }
234 ASTNodeType::Array(rows) => {
235 let mut changed = false;
236 let rows = rows
237 .iter()
238 .map(|row| {
239 let (row, row_changed) = self.adjust_children(row, op, policy, context);
240 changed |= row_changed;
241 row
242 })
243 .collect();
244 if !changed {
245 return None;
246 }
247 ASTNodeType::Array(rows)
248 }
249 _ => return None,
250 };
251
252 Some(ASTNode {
253 node_type: changed_node_type,
254 source_token: None,
255 contains_volatile: ast.contains_volatile,
256 })
257 }
258
259 fn adjust_children(
260 &self,
261 children: &[ASTNode],
262 op: &ShiftOperation,
263 policy: AbsShiftPolicy,
264 context: Option<&ReferenceContext<'_>>,
265 ) -> (Vec<ASTNode>, bool) {
266 let mut changed = false;
267 let children = children
268 .iter()
269 .map(|child| {
270 if let Some(adjusted) = self.adjust_ast_if_changed_inner(child, op, policy, context)
271 {
272 changed = true;
273 adjusted
274 } else {
275 child.clone()
276 }
277 })
278 .collect();
279 (children, changed)
280 }
281
282 pub fn adjust_cell_ref(&self, cell_ref: &CellRef, op: &ShiftOperation) -> Option<CellRef> {
286 self.adjust_cell_ref_with_policy(cell_ref, op, AbsShiftPolicy::Track)
287 }
288
289 pub(crate) fn adjust_cell_ref_with_policy(
291 &self,
292 cell_ref: &CellRef,
293 op: &ShiftOperation,
294 policy: AbsShiftPolicy,
295 ) -> Option<CellRef> {
296 let coord = cell_ref.coord;
297 let adjusted_coord = match op {
298 ShiftOperation::InsertRows {
299 sheet_id,
300 before,
301 count,
302 } if cell_ref.sheet_id == *sheet_id => {
303 if (policy.pins() && coord.row_abs()) || coord.row() < *before {
304 coord
307 } else {
308 Coord::new(
310 coord.row() + count,
311 coord.col(),
312 coord.row_abs(),
313 coord.col_abs(),
314 )
315 }
316 }
317 ShiftOperation::DeleteRows {
318 sheet_id,
319 start,
320 count,
321 } if cell_ref.sheet_id == *sheet_id => {
322 if policy.pins() && coord.row_abs() {
323 coord
325 } else if coord.row() >= *start && coord.row() < start + count {
326 return None;
328 } else if coord.row() >= start + count {
329 Coord::new(
331 coord.row() - count,
332 coord.col(),
333 coord.row_abs(),
334 coord.col_abs(),
335 )
336 } else {
337 coord
339 }
340 }
341 ShiftOperation::InsertColumns {
342 sheet_id,
343 before,
344 count,
345 } if cell_ref.sheet_id == *sheet_id => {
346 if (policy.pins() && coord.col_abs()) || coord.col() < *before {
347 coord
350 } else {
351 Coord::new(
353 coord.row(),
354 coord.col() + count,
355 coord.row_abs(),
356 coord.col_abs(),
357 )
358 }
359 }
360 ShiftOperation::DeleteColumns {
361 sheet_id,
362 start,
363 count,
364 } if cell_ref.sheet_id == *sheet_id => {
365 if policy.pins() && coord.col_abs() {
366 coord
368 } else if coord.col() >= *start && coord.col() < start + count {
369 return None;
371 } else if coord.col() >= start + count {
372 Coord::new(
374 coord.row(),
375 coord.col() - count,
376 coord.row_abs(),
377 coord.col_abs(),
378 )
379 } else {
380 coord
382 }
383 }
384 _ => coord,
385 };
386
387 Some(CellRef::new(cell_ref.sheet_id, adjusted_coord))
388 }
389
390 fn adjust_reference(
392 &self,
393 reference: &ReferenceType,
394 op: &ShiftOperation,
395 policy: AbsShiftPolicy,
396 context: Option<&ReferenceContext<'_>>,
397 ) -> ReferenceAdjustment {
398 let op_sheet_id = match op {
399 ShiftOperation::InsertRows { sheet_id, .. }
400 | ShiftOperation::DeleteRows { sheet_id, .. }
401 | ShiftOperation::InsertColumns { sheet_id, .. }
402 | ShiftOperation::DeleteColumns { sheet_id, .. } => *sheet_id,
403 };
404 match reference {
405 ReferenceType::Cell { sheet, .. } | ReferenceType::Range { sheet, .. } => {
406 if context.is_some_and(|context| {
407 context.reference_sheet_id(sheet.as_deref()) != Some(op_sheet_id)
408 }) {
409 return ReferenceAdjustment::Reference(reference.clone());
410 }
411 }
412 _ => return ReferenceAdjustment::Reference(reference.clone()),
413 }
414
415 let shared = reference.to_sheet_ref_lossy();
416
417 match (reference, shared) {
418 (
419 ReferenceType::Cell {
420 sheet,
421 row_abs,
422 col_abs,
423 ..
424 },
425 Some(crate::reference::SharedRef::Cell(cell)),
426 ) => {
427 let temp_ref = CellRef::new(
428 op_sheet_id,
429 Coord::new(cell.coord.row(), cell.coord.col(), *row_abs, *col_abs),
430 );
431
432 match self.adjust_cell_ref_with_policy(&temp_ref, op, policy) {
433 None => ReferenceAdjustment::Invalidated,
434 Some(adjusted) => ReferenceAdjustment::Reference(ReferenceType::Cell {
435 sheet: sheet.clone(),
436 row: adjusted.coord.row() + 1,
437 col: adjusted.coord.col() + 1,
438 row_abs: *row_abs,
439 col_abs: *col_abs,
440 }),
441 }
442 }
443 (
444 ReferenceType::Range {
445 sheet,
446 start_row_abs,
447 start_col_abs,
448 end_row_abs,
449 end_col_abs,
450 ..
451 },
452 Some(crate::reference::SharedRef::Range(range)),
453 ) => {
454 let sr = range.start_row;
455 let sc = range.start_col;
456 let er = range.end_row;
457 let ec = range.end_col;
458
459 let adjust_insert = |b: formualizer_common::AxisBound, before: u32, count: u32| {
460 if policy.pins() && b.abs {
461 b.index
462 } else if b.index >= before {
463 b.index + count
464 } else {
465 b.index
466 }
467 };
468
469 let adjust_delete = |idx: u32, abs: bool, start: u32, count: u32| {
470 if policy.pins() && abs {
471 idx
472 } else if idx >= start + count {
473 idx - count
474 } else if idx >= start {
475 start
476 } else {
477 idx
478 }
479 };
480
481 let (adj_sr0, adj_er0) = match op {
482 ShiftOperation::InsertRows { before, count, .. } => (
483 sr.map(|b| adjust_insert(b, *before, *count)),
484 er.map(|b| adjust_insert(b, *before, *count)),
485 ),
486 ShiftOperation::DeleteRows { start, count, .. } => match (sr, er) {
487 (Some(range_start), Some(range_end))
488 if !policy.pins() || (!range_start.abs && !range_end.abs) =>
489 {
490 let range_start = range_start.index;
491 let range_end = range_end.index;
492 if range_end < *start || range_start >= start + count {
493 let adj_start = if range_start >= start + count {
494 range_start - count
495 } else {
496 range_start
497 };
498 let adj_end = if range_end >= start + count {
499 range_end - count
500 } else {
501 range_end
502 };
503 (Some(adj_start), Some(adj_end))
504 } else if range_start >= *start && range_end < start + count {
505 return ReferenceAdjustment::Invalidated;
506 } else {
507 let adj_start = if range_start < *start {
508 range_start
509 } else {
510 *start
511 };
512 let adj_end = if range_end >= start + count {
513 range_end - count
514 } else {
515 start.saturating_sub(1)
516 };
517 (Some(adj_start), Some(adj_end))
518 }
519 }
520 (Some(range_start), Some(range_end)) => {
521 let adj_start =
522 adjust_delete(range_start.index, range_start.abs, *start, *count);
523 let adj_end =
524 adjust_delete(range_end.index, range_end.abs, *start, *count);
525 (Some(adj_start), Some(adj_end))
526 }
527 _ => (
528 sr.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
529 er.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
530 ),
531 },
532 _ => (sr.map(|b| b.index), er.map(|b| b.index)),
533 };
534
535 let (adj_sc0, adj_ec0) = match op {
536 ShiftOperation::InsertColumns { before, count, .. } => (
537 sc.map(|b| adjust_insert(b, *before, *count)),
538 ec.map(|b| adjust_insert(b, *before, *count)),
539 ),
540 ShiftOperation::DeleteColumns { start, count, .. } => match (sc, ec) {
541 (Some(range_start), Some(range_end))
542 if !policy.pins() || (!range_start.abs && !range_end.abs) =>
543 {
544 let range_start = range_start.index;
545 let range_end = range_end.index;
546 if range_end < *start || range_start >= start + count {
547 let adj_start = if range_start >= start + count {
548 range_start - count
549 } else {
550 range_start
551 };
552 let adj_end = if range_end >= start + count {
553 range_end - count
554 } else {
555 range_end
556 };
557 (Some(adj_start), Some(adj_end))
558 } else if range_start >= *start && range_end < start + count {
559 return ReferenceAdjustment::Invalidated;
560 } else {
561 let adj_start = if range_start < *start {
562 range_start
563 } else {
564 *start
565 };
566 let adj_end = if range_end >= start + count {
567 range_end - count
568 } else {
569 start.saturating_sub(1)
570 };
571 (Some(adj_start), Some(adj_end))
572 }
573 }
574 (Some(range_start), Some(range_end)) => {
575 let adj_start =
576 adjust_delete(range_start.index, range_start.abs, *start, *count);
577 let adj_end =
578 adjust_delete(range_end.index, range_end.abs, *start, *count);
579 (Some(adj_start), Some(adj_end))
580 }
581 _ => (
582 sc.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
583 ec.map(|b| adjust_delete(b.index, b.abs, *start, *count)),
584 ),
585 },
586 _ => (sc.map(|b| b.index), ec.map(|b| b.index)),
587 };
588
589 ReferenceAdjustment::Reference(ReferenceType::Range {
590 sheet: sheet.clone(),
591 start_row: adj_sr0.map(|i| i + 1),
592 start_col: adj_sc0.map(|i| i + 1),
593 end_row: adj_er0.map(|i| i + 1),
594 end_col: adj_ec0.map(|i| i + 1),
595 start_row_abs: *start_row_abs,
596 start_col_abs: *start_col_abs,
597 end_row_abs: *end_row_abs,
598 end_col_abs: *end_col_abs,
599 })
600 }
601 _ => ReferenceAdjustment::Reference(reference.clone()),
602 }
603 }
604}
605
606impl Default for ReferenceAdjuster {
607 fn default() -> Self {
608 Self::new()
609 }
610}
611
612pub struct RelativeReferenceAdjuster {
614 row_offset: i32,
615 col_offset: i32,
616}
617
618impl RelativeReferenceAdjuster {
619 pub fn new(row_offset: i32, col_offset: i32) -> Self {
620 Self {
621 row_offset,
622 col_offset,
623 }
624 }
625
626 pub fn adjust_formula(&self, ast: &ASTNode) -> ASTNode {
627 match &ast.node_type {
628 ASTNodeType::Reference {
629 original,
630 reference,
631 } => {
632 let adjusted = self.adjust_reference(reference);
633 ASTNode {
634 node_type: ASTNodeType::Reference {
635 original: original.clone(),
636 reference: adjusted,
637 },
638 source_token: ast.source_token.clone(),
639 contains_volatile: ast.contains_volatile,
640 }
641 }
642 ASTNodeType::BinaryOp { op, left, right } => ASTNode {
643 node_type: ASTNodeType::BinaryOp {
644 op: op.clone(),
645 left: Box::new(self.adjust_formula(left)),
646 right: Box::new(self.adjust_formula(right)),
647 },
648 source_token: ast.source_token.clone(),
649 contains_volatile: ast.contains_volatile,
650 },
651 ASTNodeType::UnaryOp { op, expr } => ASTNode {
652 node_type: ASTNodeType::UnaryOp {
653 op: op.clone(),
654 expr: Box::new(self.adjust_formula(expr)),
655 },
656 source_token: ast.source_token.clone(),
657 contains_volatile: ast.contains_volatile,
658 },
659 ASTNodeType::Function { name, args } => ASTNode {
660 node_type: ASTNodeType::Function {
661 name: name.clone(),
662 args: args.iter().map(|arg| self.adjust_formula(arg)).collect(),
663 },
664 source_token: ast.source_token.clone(),
665 contains_volatile: ast.contains_volatile,
666 },
667 _ => ast.clone(),
668 }
669 }
670
671 fn adjust_reference(
672 &self,
673 reference: &formualizer_parse::parser::ReferenceType,
674 ) -> formualizer_parse::parser::ReferenceType {
675 use formualizer_parse::parser::ReferenceType;
676
677 let Some(shared) = reference.to_sheet_ref_lossy() else {
678 return reference.clone();
679 };
680
681 match (reference, shared) {
682 (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
683 let owned = cell.into_owned();
684 let row0 = owned.coord.row();
685 let col0 = owned.coord.col();
686 let row_abs = owned.coord.row_abs();
687 let col_abs = owned.coord.col_abs();
688
689 let new_row0 = if row_abs {
690 row0
691 } else {
692 (row0 as i32 + self.row_offset).max(0) as u32
693 };
694 let new_col0 = if col_abs {
695 col0
696 } else {
697 (col0 as i32 + self.col_offset).max(0) as u32
698 };
699
700 ReferenceType::Cell {
701 sheet: sheet.clone(),
702 row: new_row0 + 1,
703 col: new_col0 + 1,
704 row_abs,
705 col_abs,
706 }
707 }
708 (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
709 let owned = range.into_owned();
710
711 let adj_axis = |b: formualizer_common::AxisBound, off: i32| {
712 if b.abs {
713 b.index
714 } else {
715 (b.index as i32 + off).max(0) as u32
716 }
717 };
718
719 let adj_start_row = owned.start_row.map(|b| adj_axis(b, self.row_offset) + 1);
720 let adj_start_col = owned.start_col.map(|b| adj_axis(b, self.col_offset) + 1);
721 let adj_end_row = owned.end_row.map(|b| adj_axis(b, self.row_offset) + 1);
722 let adj_end_col = owned.end_col.map(|b| adj_axis(b, self.col_offset) + 1);
723
724 let start_row_abs = owned.start_row.map(|b| b.abs).unwrap_or(false);
725 let start_col_abs = owned.start_col.map(|b| b.abs).unwrap_or(false);
726 let end_row_abs = owned.end_row.map(|b| b.abs).unwrap_or(false);
727 let end_col_abs = owned.end_col.map(|b| b.abs).unwrap_or(false);
728
729 ReferenceType::Range {
730 sheet: sheet.clone(),
731 start_row: adj_start_row,
732 start_col: adj_start_col,
733 end_row: adj_end_row,
734 end_col: adj_end_col,
735 start_row_abs,
736 start_col_abs,
737 end_row_abs,
738 end_col_abs,
739 }
740 }
741 _ => reference.clone(),
742 }
743 }
744}
745
746pub struct MoveReferenceAdjuster {
750 from_sheet_id: crate::SheetId,
751 from_sheet_name: String,
752 from_start_row: u32,
753 from_start_col: u32,
754 from_end_row: u32,
755 from_end_col: u32,
756 to_sheet_id: crate::SheetId,
757 to_sheet_name: String,
758 row_offset: i32,
759 col_offset: i32,
760}
761
762impl MoveReferenceAdjuster {
763 pub fn new(
764 from_sheet_id: crate::SheetId,
765 from_sheet_name: String,
766 from_start_row: u32,
767 from_start_col: u32,
768 from_end_row: u32,
769 from_end_col: u32,
770 to_sheet_id: crate::SheetId,
771 to_sheet_name: String,
772 row_offset: i32,
773 col_offset: i32,
774 ) -> Self {
775 Self {
776 from_sheet_id,
777 from_sheet_name,
778 from_start_row,
779 from_start_col,
780 from_end_row,
781 from_end_col,
782 to_sheet_id,
783 to_sheet_name,
784 row_offset,
785 col_offset,
786 }
787 }
788
789 pub fn adjust_if_references(
790 &self,
791 formula: &ASTNode,
792 formula_sheet_id: crate::SheetId,
793 ) -> Option<ASTNode> {
794 let (adjusted, changed) = self.adjust_ast_inner(formula, formula_sheet_id);
795 if changed { Some(adjusted) } else { None }
796 }
797
798 fn adjust_ast_inner(&self, ast: &ASTNode, formula_sheet_id: crate::SheetId) -> (ASTNode, bool) {
799 match &ast.node_type {
800 ASTNodeType::Reference {
801 original,
802 reference,
803 } => {
804 let (adjusted_ref, changed) = self.adjust_reference(reference, formula_sheet_id);
805 if !changed {
806 return (ast.clone(), false);
807 }
808 (
809 ASTNode {
810 node_type: ASTNodeType::Reference {
811 original: original.clone(),
812 reference: adjusted_ref,
813 },
814 source_token: ast.source_token.clone(),
815 contains_volatile: ast.contains_volatile,
816 },
817 true,
818 )
819 }
820 ASTNodeType::BinaryOp { op, left, right } => {
821 let (l_adj, l_ch) = self.adjust_ast_inner(left, formula_sheet_id);
822 let (r_adj, r_ch) = self.adjust_ast_inner(right, formula_sheet_id);
823 if !l_ch && !r_ch {
824 return (ast.clone(), false);
825 }
826 (
827 ASTNode {
828 node_type: ASTNodeType::BinaryOp {
829 op: op.clone(),
830 left: Box::new(l_adj),
831 right: Box::new(r_adj),
832 },
833 source_token: ast.source_token.clone(),
834 contains_volatile: ast.contains_volatile,
835 },
836 true,
837 )
838 }
839 ASTNodeType::UnaryOp { op, expr } => {
840 let (e_adj, e_ch) = self.adjust_ast_inner(expr, formula_sheet_id);
841 if !e_ch {
842 return (ast.clone(), false);
843 }
844 (
845 ASTNode {
846 node_type: ASTNodeType::UnaryOp {
847 op: op.clone(),
848 expr: Box::new(e_adj),
849 },
850 source_token: ast.source_token.clone(),
851 contains_volatile: ast.contains_volatile,
852 },
853 true,
854 )
855 }
856 ASTNodeType::Function { name, args } => {
857 let mut any = false;
858 let new_args: Vec<_> = args
859 .iter()
860 .map(|a| {
861 let (adj, ch) = self.adjust_ast_inner(a, formula_sheet_id);
862 any |= ch;
863 adj
864 })
865 .collect();
866 if !any {
867 return (ast.clone(), false);
868 }
869 (
870 ASTNode {
871 node_type: ASTNodeType::Function {
872 name: name.clone(),
873 args: new_args,
874 },
875 source_token: ast.source_token.clone(),
876 contains_volatile: ast.contains_volatile,
877 },
878 true,
879 )
880 }
881 ASTNodeType::Array(rows) => {
882 let mut any = false;
883 let new_rows: Vec<_> = rows
884 .iter()
885 .map(|row| {
886 row.iter()
887 .map(|c| {
888 let (adj, ch) = self.adjust_ast_inner(c, formula_sheet_id);
889 any |= ch;
890 adj
891 })
892 .collect()
893 })
894 .collect();
895 if !any {
896 return (ast.clone(), false);
897 }
898 (
899 ASTNode {
900 node_type: ASTNodeType::Array(new_rows),
901 source_token: ast.source_token.clone(),
902 contains_volatile: ast.contains_volatile,
903 },
904 true,
905 )
906 }
907 _ => (ast.clone(), false),
908 }
909 }
910
911 fn adjust_reference(
912 &self,
913 reference: &formualizer_parse::parser::ReferenceType,
914 formula_sheet_id: crate::SheetId,
915 ) -> (formualizer_parse::parser::ReferenceType, bool) {
916 use formualizer_parse::parser::ReferenceType;
917
918 let sheet_matches_source = |sheet: &Option<String>| {
919 if let Some(name) = sheet.as_deref() {
920 name == self.from_sheet_name
921 } else {
922 formula_sheet_id == self.from_sheet_id
923 }
924 };
925
926 if !sheet_matches_source(match reference {
927 ReferenceType::Cell { sheet, .. } => sheet,
928 ReferenceType::Range { sheet, .. } => sheet,
929 _ => &None,
930 }) {
931 return (reference.clone(), false);
932 }
933
934 let Some(shared) = reference.to_sheet_ref_lossy() else {
935 return (reference.clone(), false);
936 };
937
938 match (reference, shared) {
939 (ReferenceType::Cell { sheet, .. }, crate::reference::SharedRef::Cell(cell)) => {
940 let owned = cell.into_owned();
941 let row0 = owned.coord.row();
942 let col0 = owned.coord.col();
943 let row_abs = owned.coord.row_abs();
944 let col_abs = owned.coord.col_abs();
945
946 if row0 < self.from_start_row
947 || row0 > self.from_end_row
948 || col0 < self.from_start_col
949 || col0 > self.from_end_col
950 {
951 return (reference.clone(), false);
952 }
953
954 let new_row0 = (row0 as i32 + self.row_offset).max(0) as u32;
955 let new_col0 = (col0 as i32 + self.col_offset).max(0) as u32;
956
957 let new_sheet = if self.to_sheet_id != self.from_sheet_id {
958 Some(self.to_sheet_name.clone())
959 } else {
960 sheet.clone()
961 };
962
963 (
964 ReferenceType::Cell {
965 sheet: new_sheet,
966 row: new_row0 + 1,
967 col: new_col0 + 1,
968 row_abs,
969 col_abs,
970 },
971 true,
972 )
973 }
974 (ReferenceType::Range { sheet, .. }, crate::reference::SharedRef::Range(range)) => {
975 let owned = range.into_owned();
976 let (Some(sr), Some(sc), Some(er), Some(ec)) = (
977 owned.start_row,
978 owned.start_col,
979 owned.end_row,
980 owned.end_col,
981 ) else {
982 return (reference.clone(), false);
983 };
984
985 let sr0 = sr.index;
986 let sc0 = sc.index;
987 let er0 = er.index;
988 let ec0 = ec.index;
989 let start_row_abs = sr.abs;
990 let start_col_abs = sc.abs;
991 let end_row_abs = er.abs;
992 let end_col_abs = ec.abs;
993
994 let fully_contained = sr0 >= self.from_start_row
995 && er0 <= self.from_end_row
996 && sc0 >= self.from_start_col
997 && ec0 <= self.from_end_col;
998 if !fully_contained {
999 return (reference.clone(), false);
1000 }
1001
1002 let new_sr0 = (sr0 as i32 + self.row_offset).max(0) as u32;
1003 let new_er0 = (er0 as i32 + self.row_offset).max(0) as u32;
1004 let new_sc0 = (sc0 as i32 + self.col_offset).max(0) as u32;
1005 let new_ec0 = (ec0 as i32 + self.col_offset).max(0) as u32;
1006
1007 let new_sheet = if self.to_sheet_id != self.from_sheet_id {
1008 Some(self.to_sheet_name.clone())
1009 } else {
1010 sheet.clone()
1011 };
1012
1013 (
1014 ReferenceType::Range {
1015 sheet: new_sheet,
1016 start_row: Some(new_sr0 + 1),
1017 start_col: Some(new_sc0 + 1),
1018 end_row: Some(new_er0 + 1),
1019 end_col: Some(new_ec0 + 1),
1020 start_row_abs,
1021 start_col_abs,
1022 end_row_abs,
1023 end_col_abs,
1024 },
1025 true,
1026 )
1027 }
1028 _ => (reference.clone(), false),
1029 }
1030 }
1031}
1032
1033#[cfg(test)]
1034mod tests {
1035 use super::*;
1036 use formualizer_parse::parser::parse;
1037 use std::sync::OnceLock;
1038
1039 fn context(sheet_id: SheetId) -> ReferenceContext<'static> {
1040 static REGISTRY: OnceLock<SheetRegistry> = OnceLock::new();
1041 let registry = REGISTRY.get_or_init(|| {
1042 let mut registry = SheetRegistry::new();
1043 registry.id_for("Sheet1");
1044 registry.id_for("Other");
1045 registry.id_for("#REF");
1046 registry
1047 });
1048 ReferenceContext::new(sheet_id, registry)
1049 }
1050
1051 fn format_formula(ast: &ASTNode) -> String {
1052 format!("{ast:?}")
1055 }
1056
1057 #[test]
1058 fn context_free_adjuster_compatibility_api_remains_available() {
1059 let adjuster = ReferenceAdjuster::new();
1060 let ast = parse("=A1").unwrap();
1061 let op = ShiftOperation::InsertRows {
1062 sheet_id: 0,
1063 before: 0,
1064 count: 1,
1065 };
1066
1067 let _ = adjuster.adjust_ast(&ast, &op);
1068 let _ = adjuster.adjust_ast_with_policy(&ast, &op, AbsShiftPolicy::Track);
1069 let _ = adjuster.adjust_ast_if_changed(&ast, &op);
1070 let _ = adjuster.adjust_ast_if_changed_with_policy(&ast, &op, AbsShiftPolicy::Track);
1071 }
1072
1073 #[test]
1074 fn adjust_ast_if_changed_returns_none_for_unaffected_column_insert() {
1075 let adjuster = ReferenceAdjuster::new();
1076 let ast = parse("=A1+1").unwrap();
1077
1078 let adjusted = adjuster.adjust_ast_if_changed_in_context(
1079 &ast,
1080 &ShiftOperation::InsertColumns {
1081 sheet_id: 0,
1082 before: 3,
1083 count: 1,
1084 },
1085 &context(0),
1086 );
1087
1088 assert!(adjusted.is_none());
1089 }
1090
1091 #[test]
1092 fn adjust_ast_if_changed_returns_adjusted_for_insert_before_a() {
1093 let adjuster = ReferenceAdjuster::new();
1094 let ast = parse("=A1+1").unwrap();
1095
1096 let adjusted = adjuster
1097 .adjust_ast_if_changed_in_context(
1098 &ast,
1099 &ShiftOperation::InsertColumns {
1100 sheet_id: 0,
1101 before: 0,
1102 count: 1,
1103 },
1104 &context(0),
1105 )
1106 .expect("A1 reference should shift");
1107
1108 if let ASTNodeType::BinaryOp { left, .. } = &adjusted.node_type
1109 && let ASTNodeType::Reference {
1110 reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1111 ..
1112 } = &left.node_type
1113 {
1114 assert_eq!(*row, 1);
1115 assert_eq!(*col, 2);
1116 return;
1117 }
1118
1119 panic!("expected adjusted A1 reference to become B1");
1120 }
1121
1122 #[test]
1123 fn test_reference_adjustment_on_row_insert() {
1124 let adjuster = ReferenceAdjuster::new();
1125
1126 let ast = parse("=A5+B10").unwrap();
1128
1129 let adjusted = adjuster.adjust_ast_in_context(
1131 &ast,
1132 &ShiftOperation::InsertRows {
1133 sheet_id: 0,
1134 before: 7,
1135 count: 2,
1136 },
1137 &context(0),
1138 );
1139
1140 if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
1143 if let ASTNodeType::Reference {
1144 reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1145 ..
1146 } = &left.node_type
1147 {
1148 assert_eq!(*row, 5); assert_eq!(*col, 1);
1150 }
1151 if let ASTNodeType::Reference {
1152 reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1153 ..
1154 } = &right.node_type
1155 {
1156 assert_eq!(*row, 12); assert_eq!(*col, 2);
1158 }
1159 }
1160 }
1161
1162 #[test]
1163 fn test_reference_adjustment_on_column_delete() {
1164 let adjuster = ReferenceAdjuster::new();
1165
1166 let ast = parse("=C1+F1").unwrap();
1168
1169 let adjusted = adjuster.adjust_ast_in_context(
1171 &ast,
1172 &ShiftOperation::DeleteColumns {
1173 sheet_id: 0,
1174 start: 2, count: 2,
1176 },
1177 &context(0),
1178 );
1179
1180 let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type else {
1182 panic!("expected adjusted binary expression, got {adjusted:?}");
1183 };
1184 match &left.node_type {
1185 ASTNodeType::Literal(LiteralValue::Error(error)) => {
1186 assert_eq!(error.kind, ExcelErrorKind::Ref);
1187 assert!(left.source_token.is_none());
1188 }
1189 other => panic!("expected deleted C1 to become a #REF! literal, got {other:?}"),
1190 }
1191 match &right.node_type {
1192 ASTNodeType::Reference {
1193 original,
1194 reference: ReferenceType::Cell { row, col, .. },
1195 } => {
1196 assert_eq!(original, "D1");
1197 assert_eq!(*row, 1); assert_eq!(*col, 4); assert!(right.source_token.is_none());
1200 }
1201 other => panic!("expected surviving F1 reference to shift to D1, got {other:?}"),
1202 }
1203 assert!(adjusted.source_token.is_none());
1204 }
1205
1206 #[test]
1207 fn test_range_reference_adjustment() {
1208 let adjuster = ReferenceAdjuster::new();
1209
1210 let ast = parse("=SUM(A1:A10)").unwrap();
1212
1213 let adjusted = adjuster.adjust_ast_in_context(
1215 &ast,
1216 &ShiftOperation::InsertRows {
1217 sheet_id: 0,
1218 before: 5,
1219 count: 3,
1220 },
1221 &context(0),
1222 );
1223
1224 if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1226 && let Some(ASTNodeType::Reference {
1227 reference:
1228 formualizer_parse::parser::ReferenceType::Range {
1229 start_row, end_row, ..
1230 },
1231 ..
1232 }) = args.first().map(|arg| &arg.node_type)
1233 {
1234 assert_eq!(start_row.unwrap_or(0), 1); assert_eq!(end_row.unwrap_or(0), 13); }
1237 }
1238
1239 #[test]
1240 fn test_relative_reference_copy() {
1241 let adjuster = RelativeReferenceAdjuster::new(2, 3); let ast = parse("=A1+B2").unwrap();
1245 let adjusted = adjuster.adjust_formula(&ast);
1246
1247 if let ASTNodeType::BinaryOp { left, right, .. } = &adjusted.node_type {
1249 if let ASTNodeType::Reference {
1250 reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1251 ..
1252 } = &left.node_type
1253 {
1254 assert_eq!(*row, 3); assert_eq!(*col, 4);
1256 }
1257 if let ASTNodeType::Reference {
1258 reference: formualizer_parse::parser::ReferenceType::Cell { row, col, .. },
1259 ..
1260 } = &right.node_type
1261 {
1262 assert_eq!(*row, 4); assert_eq!(*col, 5);
1264 }
1265 }
1266 }
1267
1268 #[test]
1269 fn test_absolute_row_tracks_row_insert() {
1270 let adjuster = ReferenceAdjuster::new();
1271
1272 let cell_abs_row = CellRef::new(
1274 0,
1275 Coord::new(5, 2, true, false), );
1277
1278 let op = ShiftOperation::InsertRows {
1279 sheet_id: 0,
1280 before: 3,
1281 count: 2,
1282 };
1283
1284 let result = adjuster.adjust_cell_ref(&cell_abs_row, &op);
1287 assert!(result.is_some());
1288 let adjusted = result.unwrap();
1289 assert_eq!(adjusted.coord.row(), 7); assert_eq!(adjusted.coord.col(), 2); assert!(adjusted.coord.row_abs()); assert!(!adjusted.coord.col_abs());
1293
1294 let pinned = adjuster
1296 .adjust_cell_ref_with_policy(&cell_abs_row, &op, AbsShiftPolicy::Pin)
1297 .unwrap();
1298 assert_eq!(pinned.coord.row(), 5);
1299 assert!(pinned.coord.row_abs());
1300 }
1301
1302 #[test]
1303 fn test_absolute_column_tracks_column_delete() {
1304 let adjuster = ReferenceAdjuster::new();
1305
1306 let cell_abs_col = CellRef::new(
1308 0,
1309 Coord::new(5, 2, false, true), );
1311
1312 let op = ShiftOperation::DeleteColumns {
1313 sheet_id: 0,
1314 start: 1,
1315 count: 1,
1316 };
1317
1318 let result = adjuster.adjust_cell_ref(&cell_abs_col, &op);
1320 assert!(result.is_some());
1321 let adjusted = result.unwrap();
1322 assert_eq!(adjusted.coord.row(), 5); assert_eq!(adjusted.coord.col(), 1); assert!(!adjusted.coord.row_abs());
1325 assert!(adjusted.coord.col_abs()); let pinned = adjuster
1329 .adjust_cell_ref_with_policy(&cell_abs_col, &op, AbsShiftPolicy::Pin)
1330 .unwrap();
1331 assert_eq!(pinned.coord.col(), 2);
1332 assert!(pinned.coord.col_abs());
1333 }
1334
1335 #[test]
1336 fn test_absolute_reference_deleted_becomes_ref_error() {
1337 let adjuster = ReferenceAdjuster::new();
1338
1339 let fully_abs = CellRef::new(0, Coord::new(0, 5, true, true));
1342 let result = adjuster.adjust_cell_ref(
1343 &fully_abs,
1344 &ShiftOperation::DeleteRows {
1345 sheet_id: 0,
1346 start: 0,
1347 count: 1,
1348 },
1349 );
1350 assert!(result.is_none(), "deleted absolute target must be #REF!");
1351 }
1352
1353 #[test]
1354 fn test_mixed_absolute_relative_references() {
1355 let adjuster = ReferenceAdjuster::new();
1356
1357 let mixed1 = CellRef::new(
1359 0,
1360 Coord::new(5, 1, false, true), );
1362
1363 let result1 = adjuster.adjust_cell_ref(
1364 &mixed1,
1365 &ShiftOperation::InsertRows {
1366 sheet_id: 0,
1367 before: 3,
1368 count: 2,
1369 },
1370 );
1371
1372 assert!(result1.is_some());
1373 let adj1 = result1.unwrap();
1374 assert_eq!(adj1.coord.row(), 7); assert_eq!(adj1.coord.col(), 1); let mixed2 = CellRef::new(
1379 0,
1380 Coord::new(10, 3, true, false), );
1382
1383 let result2 = adjuster.adjust_cell_ref(
1384 &mixed2,
1385 &ShiftOperation::DeleteColumns {
1386 sheet_id: 0,
1387 start: 1,
1388 count: 1,
1389 },
1390 );
1391
1392 assert!(result2.is_some());
1393 let adj2 = result2.unwrap();
1394 assert_eq!(adj2.coord.row(), 10); assert_eq!(adj2.coord.col(), 2); }
1397
1398 #[test]
1399 fn test_fully_absolute_reference_tracks_structural_ops() {
1400 let adjuster = ReferenceAdjuster::new();
1401
1402 let fully_abs = CellRef::new(
1404 0,
1405 Coord::new(1, 1, true, true), );
1407
1408 let insert = ShiftOperation::InsertRows {
1412 sheet_id: 0,
1413 before: 1,
1414 count: 5,
1415 };
1416 let result1 = adjuster.adjust_cell_ref(&fully_abs, &insert).unwrap();
1417 assert_eq!(result1.coord.row(), 6); assert_eq!(result1.coord.col(), 1);
1419 assert!(result1.coord.row_abs());
1420 assert!(result1.coord.col_abs());
1421
1422 let delete = ShiftOperation::DeleteColumns {
1424 sheet_id: 0,
1425 start: 0,
1426 count: 1,
1427 };
1428 let result2 = adjuster.adjust_cell_ref(&fully_abs, &delete).unwrap();
1429 assert_eq!(result2.coord.row(), 1);
1430 assert_eq!(result2.coord.col(), 0); assert!(result2.coord.row_abs());
1432 assert!(result2.coord.col_abs());
1433
1434 let pinned1 = adjuster
1436 .adjust_cell_ref_with_policy(&fully_abs, &insert, AbsShiftPolicy::Pin)
1437 .unwrap();
1438 assert_eq!(pinned1.coord.row(), 1);
1439 assert_eq!(pinned1.coord.col(), 1);
1440 let pinned2 = adjuster
1441 .adjust_cell_ref_with_policy(&fully_abs, &delete, AbsShiftPolicy::Pin)
1442 .unwrap();
1443 assert_eq!(pinned2.coord.row(), 1);
1444 assert_eq!(pinned2.coord.col(), 1);
1445 }
1446
1447 #[test]
1448 fn test_deleted_reference_becomes_ref_error() {
1449 let adjuster = ReferenceAdjuster::new();
1450
1451 let cell = CellRef::new(
1453 0,
1454 Coord::new(5, 3, false, false), );
1456
1457 let result = adjuster.adjust_cell_ref(
1459 &cell,
1460 &ShiftOperation::DeleteRows {
1461 sheet_id: 0,
1462 start: 5,
1463 count: 1,
1464 },
1465 );
1466
1467 assert!(result.is_none());
1469
1470 let result2 = adjuster.adjust_cell_ref(
1472 &cell,
1473 &ShiftOperation::DeleteColumns {
1474 sheet_id: 0,
1475 start: 3,
1476 count: 1,
1477 },
1478 );
1479
1480 assert!(result2.is_none());
1482 }
1483
1484 #[test]
1485 fn test_range_expansion_on_insert() {
1486 let adjuster = ReferenceAdjuster::new();
1487
1488 let ast = parse("=SUM(B2:D10)").unwrap();
1490
1491 let adjusted = adjuster.adjust_ast_in_context(
1493 &ast,
1494 &ShiftOperation::InsertRows {
1495 sheet_id: 0,
1496 before: 5,
1497 count: 3,
1498 },
1499 &context(0),
1500 );
1501
1502 if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1504 && let Some(ASTNodeType::Reference {
1505 reference:
1506 formualizer_parse::parser::ReferenceType::Range {
1507 start_row,
1508 end_row,
1509 start_col,
1510 end_col,
1511 ..
1512 },
1513 ..
1514 }) = args.first().map(|arg| &arg.node_type)
1515 {
1516 assert_eq!(*start_row, Some(2)); assert_eq!(*end_row, Some(13)); assert_eq!(*start_col, Some(2)); assert_eq!(*end_col, Some(4)); }
1521 }
1522
1523 #[test]
1524 fn test_range_contraction_on_delete() {
1525 let adjuster = ReferenceAdjuster::new();
1526
1527 let ast = parse("=SUM(A5:A20)").unwrap();
1529
1530 let adjusted = adjuster.adjust_ast_in_context(
1532 &ast,
1533 &ShiftOperation::DeleteRows {
1534 sheet_id: 0,
1535 start: 10,
1536 count: 5,
1537 },
1538 &context(0),
1539 );
1540
1541 if let ASTNodeType::Function { args, .. } = &adjusted.node_type
1543 && let Some(ASTNodeType::Reference {
1544 reference:
1545 formualizer_parse::parser::ReferenceType::Range {
1546 start_row, end_row, ..
1547 },
1548 ..
1549 }) = args.first().map(|arg| &arg.node_type)
1550 {
1551 assert_eq!(*start_row, Some(5)); assert_eq!(*end_row, Some(15)); }
1554 }
1555
1556 #[test]
1557 fn fully_deleted_range_becomes_ref_error_literal() {
1558 let ast = parse("=SUM(A2:A4)").unwrap();
1559 let adjusted = ReferenceAdjuster::new().adjust_ast_in_context(
1560 &ast,
1561 &ShiftOperation::DeleteRows {
1562 sheet_id: 0,
1563 start: 1,
1564 count: 3,
1565 },
1566 &context(0),
1567 );
1568
1569 let ASTNodeType::Function { args, .. } = &adjusted.node_type else {
1570 panic!("expected SUM function, got {adjusted:?}");
1571 };
1572 match &args[0].node_type {
1573 ASTNodeType::Literal(LiteralValue::Error(error)) => {
1574 assert_eq!(error.kind, ExcelErrorKind::Ref)
1575 }
1576 other => panic!("expected deleted range to become #REF!, got {other:?}"),
1577 }
1578 }
1579
1580 #[test]
1581 fn structural_adjustment_respects_formula_and_qualified_sheets() {
1582 let adjuster = ReferenceAdjuster::new();
1583 let op = ShiftOperation::DeleteRows {
1584 sheet_id: 0,
1585 start: 0,
1586 count: 1,
1587 };
1588
1589 assert!(
1591 adjuster
1592 .adjust_ast_if_changed_in_context(&parse("=A1").unwrap(), &op, &context(1))
1593 .is_none()
1594 );
1595
1596 assert!(
1598 adjuster
1599 .adjust_ast_if_changed_in_context(&parse("=Other!A1").unwrap(), &op, &context(0))
1600 .is_none()
1601 );
1602 let matching = adjuster
1603 .adjust_ast_if_changed_in_context(&parse("=Sheet1!A1").unwrap(), &op, &context(1))
1604 .expect("qualified reference to edited sheet must change");
1605 assert!(matches!(
1606 matching.node_type,
1607 ASTNodeType::Literal(LiteralValue::Error(ref error))
1608 if error.kind == ExcelErrorKind::Ref
1609 ));
1610
1611 let real_ref_sheet = parse("='#REF'!A1").unwrap();
1613 assert!(
1614 adjuster
1615 .adjust_ast_if_changed_in_context(&real_ref_sheet, &op, &context(0))
1616 .is_none()
1617 );
1618 let shifted = adjuster
1619 .adjust_ast_if_changed_in_context(
1620 &real_ref_sheet,
1621 &ShiftOperation::InsertRows {
1622 sheet_id: 2,
1623 before: 0,
1624 count: 1,
1625 },
1626 &context(0),
1627 )
1628 .expect("a real #REF sheet reference should shift normally");
1629 match shifted.node_type {
1630 ASTNodeType::Reference {
1631 original,
1632 reference:
1633 ReferenceType::Cell {
1634 sheet, row, col, ..
1635 },
1636 } => {
1637 assert_eq!(original, "'#REF'!A2");
1638 assert_eq!(sheet.as_deref(), Some("#REF"));
1639 assert_eq!((row, col), (2, 1));
1640 }
1641 other => panic!("expected a shifted ordinary sheet reference, got {other:?}"),
1642 }
1643 }
1644
1645 #[test]
1646 fn whole_axis_ranges_adjust_only_on_their_bounded_axis() {
1647 let adjuster = ReferenceAdjuster::new();
1648 let whole_col = parse("=SUM(A:A)").unwrap();
1649 assert!(
1650 adjuster
1651 .adjust_ast_if_changed_in_context(
1652 &whole_col,
1653 &ShiftOperation::DeleteRows {
1654 sheet_id: 0,
1655 start: 0,
1656 count: 1,
1657 },
1658 &context(0),
1659 )
1660 .is_none()
1661 );
1662 let deleted_col = adjuster.adjust_ast_in_context(
1663 &whole_col,
1664 &ShiftOperation::DeleteColumns {
1665 sheet_id: 0,
1666 start: 0,
1667 count: 1,
1668 },
1669 &context(0),
1670 );
1671 let ASTNodeType::Function { args, .. } = &deleted_col.node_type else {
1672 panic!("expected SUM function");
1673 };
1674 assert!(matches!(
1675 args[0].node_type,
1676 ASTNodeType::Literal(LiteralValue::Error(ref error))
1677 if error.kind == ExcelErrorKind::Ref
1678 ));
1679
1680 let whole_row = parse("=SUM(1:1)").unwrap();
1681 assert!(
1682 adjuster
1683 .adjust_ast_if_changed_in_context(
1684 &whole_row,
1685 &ShiftOperation::DeleteColumns {
1686 sheet_id: 0,
1687 start: 0,
1688 count: 1,
1689 },
1690 &context(0),
1691 )
1692 .is_none()
1693 );
1694 let deleted_row = adjuster.adjust_ast_in_context(
1695 &whole_row,
1696 &ShiftOperation::DeleteRows {
1697 sheet_id: 0,
1698 start: 0,
1699 count: 1,
1700 },
1701 &context(0),
1702 );
1703 let ASTNodeType::Function { args, .. } = &deleted_row.node_type else {
1704 panic!("expected SUM function");
1705 };
1706 assert!(matches!(
1707 args[0].node_type,
1708 ASTNodeType::Literal(LiteralValue::Error(ref error))
1709 if error.kind == ExcelErrorKind::Ref
1710 ));
1711 }
1712
1713 #[test]
1714 fn call_and_array_children_receive_literal_rewrites() {
1715 let adjuster = ReferenceAdjuster::new();
1716 let op = ShiftOperation::DeleteColumns {
1717 sheet_id: 0,
1718 start: 0,
1719 count: 1,
1720 };
1721
1722 let call =
1723 adjuster.adjust_ast_in_context(&parse("=LAMBDA(x,x)(A1)").unwrap(), &op, &context(0));
1724 let ASTNodeType::Call { args, .. } = &call.node_type else {
1725 panic!("expected immediate call, got {call:?}");
1726 };
1727 assert!(matches!(
1728 args[0].node_type,
1729 ASTNodeType::Literal(LiteralValue::Error(ref error))
1730 if error.kind == ExcelErrorKind::Ref
1731 ));
1732 assert!(call.source_token.is_none());
1733
1734 let array = adjuster.adjust_ast_in_context(&parse("={A1,B1}").unwrap(), &op, &context(0));
1735 let ASTNodeType::Array(rows) = &array.node_type else {
1736 panic!("expected array, got {array:?}");
1737 };
1738 assert!(matches!(
1739 rows[0][0].node_type,
1740 ASTNodeType::Literal(LiteralValue::Error(ref error))
1741 if error.kind == ExcelErrorKind::Ref
1742 ));
1743 match &rows[0][1].node_type {
1744 ASTNodeType::Reference {
1745 original,
1746 reference: ReferenceType::Cell { row, col, .. },
1747 } => {
1748 assert_eq!(original, "A1");
1749 assert_eq!((*row, *col), (1, 1));
1750 }
1751 other => panic!("expected B1 to shift to A1, got {other:?}"),
1752 }
1753 }
1754}