Skip to main content

ironcalc_base/user_model/
autofill.rs

1#![deny(missing_docs)]
2
3use std::borrow::Cow;
4use std::collections::{HashMap, HashSet};
5
6use crate::{
7    expressions::{
8        types::Area,
9        utils::{is_valid_column_number, is_valid_row},
10    },
11    model::CellStructure,
12    types::MergedCell,
13    user_model::sequence_detector::detect_progression,
14    UserModel,
15};
16
17use crate::user_model::history::Diff;
18
19const PARTIAL_MERGE_ERROR: &str =
20    "Cannot auto-fill: a merged cell partially overlaps the fill area";
21const CUT_MERGE_ERROR: &str = "Cannot auto-fill: the fill size must fit whole merged cells";
22
23// How the merged cells of the sheet interact with a fill:
24// the merges of the source are tiled into the fill target, and the merges
25// already in the target are replaced by that pattern.
26struct MergedFillPlan {
27    // source cells covered by a merge (every cell of a source merge except
28    // its anchor): they hold no content, and neither do their tiled copies
29    covered_source: HashSet<(i32, i32)>,
30    // the tiled copies of the source merges, to create in the fill target
31    new_merges: Vec<MergedCell>,
32    // the merged cells of the sheet before the fill
33    old_merged_cells: Vec<MergedCell>,
34    // the merged cells after removing the ones replaced by the fill, before
35    // adding `new_merges`
36    merged_cells_after_removal: Vec<MergedCell>,
37}
38
39fn merge_is_contained(m: &MergedCell, row1: i32, column1: i32, row2: i32, column2: i32) -> bool {
40    m.row >= row1
41        && m.column >= column1
42        && m.row + m.height - 1 <= row2
43        && m.column + m.width - 1 <= column2
44}
45
46impl<'a> UserModel<'a> {
47    // Validates the fill of `target` from `source` (both closed rectangles
48    // `(first_row, first_column, last_row, last_column)` on `sheet`) against
49    // the merged cells of the sheet and returns the resulting plan:
50    //
51    // * A merge intersecting the source or the target but not fully contained
52    //   in it fails with [PARTIAL_MERGE_ERROR].
53    // * The source merges are tiled into the target with period the source
54    //   height (`by_rows`) or width. A tiled merge that would stick out of
55    //   the target fails with [CUT_MERGE_ERROR]: the fill can stop mid-tile,
56    //   but only where it cuts no merge.
57    // * Merges fully contained in the target are replaced by the tiled
58    //   pattern (they are dropped from `merged_cells_after_removal`).
59    fn plan_fill_merges(
60        &self,
61        sheet: u32,
62        source: (i32, i32, i32, i32),
63        target: (i32, i32, i32, i32),
64        by_rows: bool,
65    ) -> Result<MergedFillPlan, String> {
66        let (s_row1, s_column1, s_row2, s_column2) = source;
67        let (t_row1, t_column1, t_row2, t_column2) = target;
68        let merged_cells = &self.model.workbook.worksheet(sheet)?.merged_cells;
69
70        let mut source_merges = Vec::new();
71        let mut merged_cells_after_removal = Vec::new();
72        for m in merged_cells {
73            if m.intersects(
74                s_row1,
75                s_column1,
76                s_column2 - s_column1 + 1,
77                s_row2 - s_row1 + 1,
78            ) {
79                if !merge_is_contained(m, s_row1, s_column1, s_row2, s_column2) {
80                    return Err(PARTIAL_MERGE_ERROR.to_string());
81                }
82                source_merges.push(*m);
83                merged_cells_after_removal.push(*m);
84            } else if m.intersects(
85                t_row1,
86                t_column1,
87                t_column2 - t_column1 + 1,
88                t_row2 - t_row1 + 1,
89            ) {
90                if !merge_is_contained(m, t_row1, t_column1, t_row2, t_column2) {
91                    return Err(PARTIAL_MERGE_ERROR.to_string());
92                }
93                // fully contained in the fill target: replaced by the fill
94            } else {
95                merged_cells_after_removal.push(*m);
96            }
97        }
98
99        let mut covered_source = HashSet::new();
100        for m in &source_merges {
101            for row in m.row..m.row + m.height {
102                for column in m.column..m.column + m.width {
103                    if (row, column) != (m.row, m.column) {
104                        covered_source.insert((row, column));
105                    }
106                }
107            }
108        }
109
110        // Tile the source merges into the target. Tiles start at the target
111        // edge next to the source and repeat away from it.
112        let mut new_merges = Vec::new();
113        if !source_merges.is_empty() {
114            let mut tile_starts = Vec::new();
115            if by_rows {
116                let period = s_row2 - s_row1 + 1;
117                if t_row1 > s_row2 {
118                    let mut start = t_row1;
119                    while start <= t_row2 {
120                        tile_starts.push(start);
121                        start += period;
122                    }
123                } else {
124                    let mut start = s_row1 - period;
125                    while start + period > t_row1 {
126                        tile_starts.push(start);
127                        start -= period;
128                    }
129                }
130                for tile_start in tile_starts {
131                    for m in &source_merges {
132                        let first_row = tile_start + (m.row - s_row1);
133                        let last_row = first_row + m.height - 1;
134                        if last_row < t_row1 || first_row > t_row2 {
135                            continue;
136                        }
137                        if first_row < t_row1 || last_row > t_row2 {
138                            return Err(CUT_MERGE_ERROR.to_string());
139                        }
140                        new_merges.push(MergedCell {
141                            row: first_row,
142                            column: m.column,
143                            width: m.width,
144                            height: m.height,
145                        });
146                    }
147                }
148            } else {
149                let period = s_column2 - s_column1 + 1;
150                if t_column1 > s_column2 {
151                    let mut start = t_column1;
152                    while start <= t_column2 {
153                        tile_starts.push(start);
154                        start += period;
155                    }
156                } else {
157                    let mut start = s_column1 - period;
158                    while start + period > t_column1 {
159                        tile_starts.push(start);
160                        start -= period;
161                    }
162                }
163                for tile_start in tile_starts {
164                    for m in &source_merges {
165                        let first_column = tile_start + (m.column - s_column1);
166                        let last_column = first_column + m.width - 1;
167                        if last_column < t_column1 || first_column > t_column2 {
168                            continue;
169                        }
170                        if first_column < t_column1 || last_column > t_column2 {
171                            return Err(CUT_MERGE_ERROR.to_string());
172                        }
173                        new_merges.push(MergedCell {
174                            row: m.row,
175                            column: first_column,
176                            width: m.width,
177                            height: m.height,
178                        });
179                    }
180                }
181            }
182        }
183
184        Ok(MergedFillPlan {
185            covered_source,
186            new_merges,
187            old_merged_cells: merged_cells.clone(),
188            merged_cells_after_removal,
189        })
190    }
191
192    // Removes the merges the fill target replaces, with its diff. Pushed
193    // before the cell diffs so a forward replay writes on unmerged cells.
194    fn apply_fill_merge_removal(
195        &mut self,
196        sheet: u32,
197        plan: &MergedFillPlan,
198        diff_list: &mut Vec<Diff>,
199    ) -> Result<(), String> {
200        if plan.merged_cells_after_removal != plan.old_merged_cells {
201            self.model.workbook.worksheet_mut(sheet)?.merged_cells =
202                plan.merged_cells_after_removal.clone();
203            diff_list.push(Diff::SetMergedCells {
204                sheet,
205                old_value: plan.old_merged_cells.clone(),
206                new_value: plan.merged_cells_after_removal.clone(),
207            });
208        }
209        Ok(())
210    }
211
212    // Creates the tiled merges in the fill target, with its diff. Pushed
213    // after the cell diffs so an undo unmerges first and can then restore the
214    // old content of the covered cells.
215    fn apply_fill_merge_creation(
216        &mut self,
217        sheet: u32,
218        plan: &MergedFillPlan,
219        diff_list: &mut Vec<Diff>,
220    ) -> Result<(), String> {
221        if !plan.new_merges.is_empty() {
222            let worksheet = self.model.workbook.worksheet_mut(sheet)?;
223            worksheet
224                .merged_cells
225                .extend(plan.new_merges.iter().copied());
226            diff_list.push(Diff::SetMergedCells {
227                sheet,
228                old_value: plan.merged_cells_after_removal.clone(),
229                new_value: worksheet.merged_cells.clone(),
230            });
231        }
232        Ok(())
233    }
234
235    // The fill target cell is covered by a tiled merge: it gets no value,
236    // only the style and (lack of) link of its source cell, and its old
237    // content is cleared. `source` and `target` are `(row, column)` pairs.
238    fn fill_covered_cell(
239        &mut self,
240        sheet: u32,
241        source: (i32, i32),
242        target: (i32, i32),
243        old_value: Option<crate::types::Cell>,
244        diff_list: &mut Vec<Diff>,
245    ) -> Result<(), String> {
246        let (source_row, source_column) = source;
247        let (row, column) = target;
248        let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
249        // Going through prepare_cell_for_user_input keeps dynamic array
250        // formulas consistent (an anchor spilling into the cell is reset)
251        self.model.prepare_cell_for_user_input(sheet, row, column)?;
252        let worksheet = self.model.workbook.worksheet_mut(sheet)?;
253        if worksheet.cell(row, column).is_some() {
254            worksheet.cell_clear_contents(row, column)?;
255        }
256        if old_value.is_some() {
257            diff_list.push(Diff::SetCellValue {
258                sheet,
259                row,
260                column,
261                new_value: "".to_string(),
262                old_value: Box::new(old_value),
263            });
264        }
265
266        let new_style = self
267            .model
268            .get_style_for_cell(sheet, source_row, source_column)?;
269        self.model.set_cell_style(sheet, row, column, &new_style)?;
270        diff_list.push(Diff::SetCellStyle {
271            sheet,
272            row,
273            column,
274            old_value: Box::new(old_style),
275            new_value: Box::new(new_style),
276        });
277
278        self.fill_cell_link(sheet, source_row, source_column, row, column, diff_list)
279    }
280    /// Scans the fill target rectangle (`row_start..=row_end`, `col_start..=col_end`) for
281    /// CSE array formulas and prepares them for overwriting:
282    ///
283    /// * A CSE formula **completely** inside the rectangle is cleared; its original cell
284    ///   values are returned keyed by `(row, col)` so the caller can emit correct undo diffs.
285    /// * A CSE formula only **partially** overlapping the rectangle causes an immediate error.
286    fn collect_and_clear_cse_in_fill_target(
287        &mut self,
288        sheet: u32,
289        row_start: i32,
290        row_end: i32,
291        col_start: i32,
292        col_end: i32,
293    ) -> Result<HashMap<(i32, i32), Option<crate::types::Cell>>, String> {
294        // First pass: validate all affected CSE anchors without mutating the worksheet.
295        // An error here leaves the sheet untouched.
296        let mut anchors: Vec<(i32, i32, i32, i32)> = Vec::new();
297        let mut handled: Vec<(i32, i32)> = Vec::new();
298        for row in row_start..=row_end {
299            for col in col_start..=col_end {
300                let (ar, ac, w, h) = match self.model.get_cell_structure(sheet, row, col)? {
301                    CellStructure::ArrayFormula { range: (w, h) } if w > 1 || h > 1 => {
302                        (row, col, w, h)
303                    }
304                    CellStructure::SpillArray {
305                        anchor: (ar, ac),
306                        range: (w, h),
307                    } => (ar, ac, w, h),
308                    _ => continue,
309                };
310                if handled.contains(&(ar, ac)) {
311                    continue;
312                }
313                handled.push((ar, ac));
314                let completely_covered = ar >= row_start
315                    && ar + h - 1 <= row_end
316                    && ac >= col_start
317                    && ac + w - 1 <= col_end;
318                if !completely_covered {
319                    return Err(
320                        "Cannot autofill: selection partially overlaps an array formula"
321                            .to_string(),
322                    );
323                }
324                anchors.push((ar, ac, w, h));
325            }
326        }
327
328        // Second pass: all anchors are completely covered — safe to save and clear.
329        let mut saved: HashMap<(i32, i32), Option<crate::types::Cell>> = HashMap::new();
330        for (ar, ac, w, h) in anchors {
331            for r in ar..ar + h {
332                for c in ac..ac + w {
333                    let cell = self
334                        .model
335                        .workbook
336                        .worksheet(sheet)?
337                        .cell(r, c)
338                        .map(Cow::into_owned);
339                    saved.insert((r, c), cell);
340                }
341            }
342            let ws = self.model.workbook.worksheet_mut(sheet)?;
343            for r in ar..ar + h {
344                for c in ac..ac + w {
345                    let _ = ws.cell_clear_contents(r, c);
346                }
347            }
348        }
349        Ok(saved)
350    }
351
352    /// Fills the cells from `source_area` until `to_row`.
353    /// This simulates the user clicking on the cell outline handle and dragging it downwards (or upwards)
354    pub fn auto_fill_rows(&mut self, source_area: &Area, to_row: i32) -> Result<(), String> {
355        let mut diff_list = Vec::new();
356        let sheet = source_area.sheet;
357        let row1 = source_area.row;
358        let column1 = source_area.column;
359        let width = source_area.width;
360        let height = source_area.height;
361
362        // Check first all parameters are valid
363        if self.model.workbook.worksheet(sheet).is_err() {
364            return Err(format!("Invalid worksheet index: '{sheet}'"));
365        }
366
367        if !is_valid_column_number(column1) {
368            return Err(format!("Invalid column: '{column1}'"));
369        }
370        if !is_valid_row(row1) {
371            return Err(format!("Invalid row: '{row1}'"));
372        }
373        if width <= 0 || height <= 0 {
374            return Err(format!("Invalid width='{}' or height='{}'", width, height));
375        }
376
377        let last_column = column1 + width - 1;
378        let last_row = row1 + height - 1;
379
380        if !is_valid_column_number(last_column) {
381            return Err(format!("Invalid column: '{last_column}'"));
382        }
383        if !is_valid_row(last_row) {
384            return Err(format!("Invalid row: '{last_row}'"));
385        }
386
387        if !is_valid_row(to_row) {
388            return Err(format!("Invalid row: '{to_row}'"));
389        }
390
391        // anchor_row is the first row that repeats in each case.
392        let anchor_row;
393        let sign;
394        // this is the range of rows we are going to fill
395        let row_range: Vec<i32>;
396
397        if to_row > last_row {
398            // we go downwards, we start from `row1 + height1` to `to_row`,
399            anchor_row = row1;
400            sign = 1;
401            row_range = (last_row + 1..=to_row).collect();
402        } else if to_row < row1 {
403            // we go upwards, starting from `row1 - 1` all the way to `to_row`
404            anchor_row = last_row;
405            sign = -1;
406            row_range = (to_row..row1).rev().collect();
407        } else {
408            return Err("Invalid parameters for autofill".to_string());
409        }
410
411        // Fill target: rows in row_range, all source columns.
412        let fill_row_start = if sign < 0 { to_row } else { last_row + 1 };
413        let fill_row_end = if sign < 0 { row1 - 1 } else { to_row };
414
415        // The merged cells of the source are tiled into the fill target and
416        // replace the merges already there; a merge partially overlapping the
417        // source or the target, or cut by the fill boundary, is an error.
418        let plan = self.plan_fill_merges(
419            sheet,
420            (row1, column1, last_row, last_column),
421            (fill_row_start, column1, fill_row_end, last_column),
422            true,
423        )?;
424        self.apply_fill_merge_removal(sheet, &plan, &mut diff_list)?;
425
426        let saved_cse = self.collect_and_clear_cse_in_fill_target(
427            sheet,
428            fill_row_start,
429            fill_row_end,
430            column1,
431            last_column,
432        )?;
433
434        for column in column1..=last_column {
435            let mut index = 0;
436            let locale = &self.model.locale;
437            // covered cells hold no content: the progression is detected over
438            // the rest and lands on the non-covered cells of the target
439            let values = if sign < 0 {
440                (row1..=last_row)
441                    .rev()
442                    .filter(|row| !plan.covered_source.contains(&(*row, column)))
443                    .map(|row| self.get_cell_content(sheet, row, column))
444                    .collect::<Result<Vec<_>, _>>()?
445            } else {
446                (row1..=last_row)
447                    .filter(|row| !plan.covered_source.contains(&(*row, column)))
448                    .map(|row| self.get_cell_content(sheet, row, column))
449                    .collect::<Result<Vec<_>, _>>()?
450            };
451            let case_seed = self.get_cell_content(sheet, row1, column)?;
452            let possible_progression = if values.is_empty() {
453                None
454            } else {
455                detect_progression(&values, locale, &case_seed)
456            };
457            let mut progression_idx = 0;
458            for row_ref in row_range.iter() {
459                let row = *row_ref;
460
461                let old_value = saved_cse.get(&(row, column)).cloned().unwrap_or_else(|| {
462                    self.model
463                        .workbook
464                        .worksheet(sheet)
465                        .ok()
466                        .and_then(|ws| ws.cell(row, column).map(Cow::into_owned))
467                });
468
469                let source_row = anchor_row + index;
470
471                if plan.covered_source.contains(&(source_row, column)) {
472                    // the target cell is covered by a tiled merge
473                    self.fill_covered_cell(
474                        sheet,
475                        (source_row, column),
476                        (row, column),
477                        old_value,
478                        &mut diff_list,
479                    )?;
480                    index = (index + sign) % source_area.height;
481                    continue;
482                }
483
484                let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
485                let target_value;
486
487                // compute the new value and set it
488                if let Some(ref detected_progression) = possible_progression {
489                    target_value = detected_progression.next(progression_idx);
490                    progression_idx += 1;
491                } else {
492                    target_value = self
493                        .model
494                        .extend_to(sheet, source_row, column, row, column)?;
495                }
496
497                self.model
498                    .set_user_input(sheet, row, column, target_value.to_string())?;
499
500                // Compute the new style and set it
501                let new_style = self.model.get_style_for_cell(sheet, source_row, column)?;
502                self.model.set_cell_style(sheet, row, column, &new_style)?;
503
504                // Add the diffs
505                diff_list.push(Diff::SetCellStyle {
506                    sheet,
507                    row,
508                    column,
509                    old_value: Box::new(old_style),
510                    new_value: Box::new(new_style),
511                });
512                diff_list.push(Diff::SetCellValue {
513                    sheet,
514                    row,
515                    column,
516                    new_value: target_value.to_string(),
517                    old_value: Box::new(old_value),
518                });
519
520                self.fill_cell_link(sheet, source_row, column, row, column, &mut diff_list)?;
521
522                index = (index + sign) % source_area.height;
523            }
524        }
525        self.apply_fill_merge_creation(sheet, &plan, &mut diff_list)?;
526        self.push_diff_list(diff_list);
527        self.evaluate();
528        Ok(())
529    }
530
531    /// Fills the cells from `source_area` until `to_column`.
532    /// This simulates the user clicking on the cell outline handle and dragging it to the right (or to the left)
533    pub fn auto_fill_columns(&mut self, source_area: &Area, to_column: i32) -> Result<(), String> {
534        let mut diff_list = Vec::new();
535        let sheet = source_area.sheet;
536        let row1 = source_area.row;
537        let column1 = source_area.column;
538        let width = source_area.width;
539        let height = source_area.height;
540
541        // Check first all parameters are valid
542        if self.model.workbook.worksheet(sheet).is_err() {
543            return Err(format!("Invalid worksheet index: '{sheet}'"));
544        }
545
546        if !is_valid_column_number(column1) {
547            return Err(format!("Invalid column: '{column1}'"));
548        }
549        if !is_valid_row(row1) {
550            return Err(format!("Invalid row: '{row1}'"));
551        }
552        if width <= 0 || height <= 0 {
553            return Err(format!("Invalid width='{}' or height='{}'", width, height));
554        }
555
556        let last_column = column1 + width - 1;
557        let last_row = row1 + height - 1;
558
559        if !is_valid_column_number(last_column) {
560            return Err(format!("Invalid column: '{last_column}'"));
561        }
562        if !is_valid_row(last_row) {
563            return Err(format!("Invalid row: '{last_row}'"));
564        }
565
566        if !is_valid_column_number(to_column) {
567            return Err(format!("Invalid column: '{to_column}'"));
568        }
569
570        // anchor_column is the first column that repeats in each case.
571        let anchor_column;
572        let sign;
573        // this is the range of columns we are going to fill
574        let column_range: Vec<i32>;
575
576        if to_column > last_column {
577            // we go right, we start from `last_column + 1` to `to_column`,
578            anchor_column = column1;
579            sign = 1;
580            column_range = (last_column + 1..to_column + 1).collect();
581        } else if to_column < column1 {
582            // we go left, starting from `column1 - 1` all the way to `to_column`
583            anchor_column = last_column;
584            sign = -1;
585            column_range = (to_column..column1).rev().collect();
586        } else {
587            return Err("Invalid parameters for autofill".to_string());
588        }
589
590        // Fill target: all source rows, columns in column_range.
591        let fill_col_start = if sign < 0 { to_column } else { last_column + 1 };
592        let fill_col_end = if sign < 0 { column1 - 1 } else { to_column };
593
594        // The merged cells of the source are tiled into the fill target and
595        // replace the merges already there; a merge partially overlapping the
596        // source or the target, or cut by the fill boundary, is an error.
597        let plan = self.plan_fill_merges(
598            sheet,
599            (row1, column1, last_row, last_column),
600            (row1, fill_col_start, last_row, fill_col_end),
601            false,
602        )?;
603        self.apply_fill_merge_removal(sheet, &plan, &mut diff_list)?;
604
605        let saved_cse = self.collect_and_clear_cse_in_fill_target(
606            sheet,
607            row1,
608            last_row,
609            fill_col_start,
610            fill_col_end,
611        )?;
612
613        for row in row1..=last_row {
614            let mut index = 0;
615            let locale = &self.model.locale;
616            // covered cells hold no content: the progression is detected over
617            // the rest and lands on the non-covered cells of the target
618            let values = if sign < 0 {
619                (column1..=last_column)
620                    .rev()
621                    .filter(|column| !plan.covered_source.contains(&(row, *column)))
622                    .map(|column| self.get_cell_content(sheet, row, column))
623                    .collect::<Result<Vec<_>, _>>()?
624            } else {
625                (column1..=last_column)
626                    .filter(|column| !plan.covered_source.contains(&(row, *column)))
627                    .map(|column| self.get_cell_content(sheet, row, column))
628                    .collect::<Result<Vec<_>, _>>()?
629            };
630            let case_seed = self.get_cell_content(sheet, row, column1)?;
631            let possible_progression = if values.is_empty() {
632                None
633            } else {
634                detect_progression(&values, locale, &case_seed)
635            };
636            let mut progression_idx = 0;
637            for column_ref in column_range.iter() {
638                let column = *column_ref;
639
640                // Save value and style first
641                let old_value = saved_cse.get(&(row, column)).cloned().unwrap_or_else(|| {
642                    self.model
643                        .workbook
644                        .worksheet(sheet)
645                        .ok()
646                        .and_then(|ws| ws.cell(row, column).map(Cow::into_owned))
647                });
648
649                let source_column = anchor_column + index;
650
651                if plan.covered_source.contains(&(row, source_column)) {
652                    // the target cell is covered by a tiled merge
653                    self.fill_covered_cell(
654                        sheet,
655                        (row, source_column),
656                        (row, column),
657                        old_value,
658                        &mut diff_list,
659                    )?;
660                    index = (index + sign) % source_area.width;
661                    continue;
662                }
663
664                let old_style = self.model.get_cell_style_or_none(sheet, row, column)?;
665                let target_value;
666
667                // compute the new value and set it
668                if let Some(ref detected_progression) = possible_progression {
669                    target_value = detected_progression.next(progression_idx);
670                    progression_idx += 1;
671                } else {
672                    target_value = self
673                        .model
674                        .extend_to(sheet, row, source_column, row, column)?;
675                }
676
677                self.model
678                    .set_user_input(sheet, row, column, target_value.to_string())?;
679
680                let new_style = self.model.get_style_for_cell(sheet, row, source_column)?;
681                // Compute the new style and set it
682
683                self.model.set_cell_style(sheet, row, column, &new_style)?;
684
685                // Add the diffs
686                diff_list.push(Diff::SetCellStyle {
687                    sheet,
688                    row,
689                    column,
690                    old_value: Box::new(old_style),
691                    new_value: Box::new(new_style),
692                });
693
694                diff_list.push(Diff::SetCellValue {
695                    sheet,
696                    row,
697                    column,
698                    new_value: target_value.to_string(),
699                    old_value: Box::new(old_value),
700                });
701
702                self.fill_cell_link(sheet, row, source_column, row, column, &mut diff_list)?;
703
704                index = (index + sign) % source_area.width;
705            }
706        }
707        self.apply_fill_merge_creation(sheet, &plan, &mut diff_list)?;
708        self.push_diff_list(diff_list);
709        self.evaluate();
710        Ok(())
711    }
712
713    /// Makes the link of the fill target cell match the one of its source cell,
714    /// adding the corresponding diff. This runs after the value is set so that
715    /// it also overrides any link auto-created by an URL value.
716    fn fill_cell_link(
717        &mut self,
718        sheet: u32,
719        source_row: i32,
720        source_column: i32,
721        row: i32,
722        column: i32,
723        diff_list: &mut Vec<Diff>,
724    ) -> Result<(), String> {
725        let new_link = self.model.get_cell_link(sheet, source_row, source_column)?;
726        let old_link = self.model.get_cell_link(sheet, row, column)?;
727        if old_link == new_link {
728            return Ok(());
729        }
730        match &new_link {
731            Some(link) => self.model.set_cell_link(sheet, row, column, link.clone())?,
732            None => self.model.delete_cell_link(sheet, row, column)?,
733        }
734        diff_list.push(Diff::SetCellLink {
735            sheet,
736            row,
737            column,
738            old_value: Box::new(old_link),
739            new_value: Box::new(new_link),
740        });
741        Ok(())
742    }
743}