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