Skip to main content

excel_cli/app/
undo_manager.rs

1use crate::actions::{
2    ActionCommand, ActionExecutor, ActionType, CellAction, ColumnAction, MultiColumnAction,
3    MultiRowAction, RowAction, SheetAction, SheetOperation,
4};
5use crate::app::AppState;
6use crate::utils::index_to_col_name;
7use anyhow::Result;
8use std::rc::Rc;
9
10impl AppState<'_> {
11    pub fn undo(&mut self) -> Result<()> {
12        if let Some(action) = self.undo_history.undo() {
13            self.apply_action(&action, true)?;
14
15            self.workbook.recalculate_max_rows();
16            self.workbook.recalculate_max_cols();
17            self.ensure_column_widths();
18
19            self.clamp_selected_cell_to_excel_bounds();
20
21            if self.undo_history.all_undone() {
22                self.workbook.set_modified(false);
23            } else {
24                self.workbook.set_modified(true);
25            }
26        } else {
27            self.add_notification("No operations to undo".to_string());
28        }
29        Ok(())
30    }
31
32    pub fn redo(&mut self) -> Result<()> {
33        if let Some(action) = self.undo_history.redo() {
34            self.apply_action(&action, false)?;
35
36            self.workbook.recalculate_max_rows();
37            self.workbook.recalculate_max_cols();
38            self.ensure_column_widths();
39
40            self.clamp_selected_cell_to_excel_bounds();
41
42            self.workbook.set_modified(true);
43        } else {
44            self.add_notification("No operations to redo".to_string());
45        }
46        Ok(())
47    }
48
49    fn apply_action(&mut self, action: &Rc<ActionCommand>, is_undo: bool) -> Result<()> {
50        match action.as_ref() {
51            ActionCommand::Cell(cell_action) => {
52                let value = if is_undo {
53                    &cell_action.old_value
54                } else {
55                    &cell_action.new_value
56                };
57                self.apply_cell_action(cell_action, value, is_undo, &cell_action.action_type)?;
58            }
59            ActionCommand::Row(row_action) => {
60                self.apply_row_action(row_action, is_undo)?;
61            }
62            ActionCommand::Column(column_action) => {
63                self.apply_column_action(column_action, is_undo)?;
64            }
65            ActionCommand::Sheet(sheet_action) => {
66                self.apply_sheet_action(sheet_action, is_undo)?;
67            }
68            ActionCommand::MultiRow(multi_row_action) => {
69                self.apply_multi_row_action(multi_row_action, is_undo)?;
70            }
71            ActionCommand::MultiColumn(multi_column_action) => {
72                self.apply_multi_column_action(multi_column_action, is_undo)?;
73            }
74        }
75        Ok(())
76    }
77
78    fn apply_cell_action(
79        &mut self,
80        cell_action: &CellAction,
81        value: &crate::excel::Cell,
82        is_undo: bool,
83        action_type: &ActionType,
84    ) -> Result<()> {
85        let current_sheet_index = self.workbook.get_current_sheet_index();
86
87        if current_sheet_index != cell_action.sheet_index {
88            if let Err(e) = self.switch_sheet_by_index(cell_action.sheet_index) {
89                self.add_notification(format!(
90                    "Cannot switch to sheet {}: {}",
91                    cell_action.sheet_name, e
92                ));
93                return Ok(());
94            }
95        }
96
97        self.workbook.get_current_sheet_mut().data[cell_action.row][cell_action.col] =
98            value.clone();
99
100        self.selected_cell = (cell_action.row, cell_action.col);
101        self.handle_scrolling();
102
103        let cell_ref = format!(
104            "{}{}",
105            crate::utils::index_to_col_name(cell_action.col),
106            cell_action.row
107        );
108
109        let operation_text = match action_type {
110            ActionType::Edit => "edit",
111            ActionType::Cut => "cut",
112            ActionType::Paste => "paste",
113            _ => "cell operation",
114        };
115
116        if current_sheet_index != cell_action.sheet_index {
117            let action_word = if is_undo { "Undid" } else { "Redid" };
118            self.add_notification(format!(
119                "{} {} operation on cell {} in sheet {}",
120                action_word, operation_text, cell_ref, cell_action.sheet_name
121            ));
122        } else {
123            let action_word = if is_undo { "Undid" } else { "Redid" };
124            self.add_notification(format!(
125                "{} {} operation on cell {}",
126                action_word, operation_text, cell_ref
127            ));
128        }
129
130        Ok(())
131    }
132
133    fn apply_row_action(&mut self, row_action: &RowAction, is_undo: bool) -> Result<()> {
134        let current_sheet_index = self.workbook.get_current_sheet_index();
135
136        if current_sheet_index != row_action.sheet_index {
137            if let Err(e) = self.switch_sheet_by_index(row_action.sheet_index) {
138                self.add_notification(format!(
139                    "Cannot switch to sheet {}: {}",
140                    row_action.sheet_name, e
141                ));
142                return Ok(());
143            }
144        }
145
146        let sheet = self.workbook.get_current_sheet_mut();
147
148        if is_undo {
149            sheet
150                .data
151                .insert(row_action.row, row_action.row_data.clone());
152
153            sheet.max_rows = sheet.max_rows.saturating_add(1);
154
155            // Recalculate max_cols since restoring a row might affect the maximum column count
156            // This is especially important if the row contained data beyond the current max_cols
157            self.workbook.recalculate_max_cols();
158
159            self.add_notification(format!("Undid row {} deletion", row_action.row));
160        } else if row_action.row < sheet.data.len() {
161            sheet.data.remove(row_action.row);
162            sheet.max_rows = sheet.max_rows.saturating_sub(1);
163
164            self.clamp_selected_cell_to_excel_bounds();
165
166            self.add_notification(format!("Redid row {} deletion", row_action.row));
167        }
168
169        self.handle_scrolling();
170        self.search_results.clear();
171        self.current_search_idx = None;
172
173        Ok(())
174    }
175
176    fn apply_column_action(&mut self, column_action: &ColumnAction, is_undo: bool) -> Result<()> {
177        let current_sheet_index = self.workbook.get_current_sheet_index();
178
179        if current_sheet_index != column_action.sheet_index {
180            if let Err(e) = self.switch_sheet_by_index(column_action.sheet_index) {
181                self.add_notification(format!(
182                    "Cannot switch to sheet {}: {}",
183                    column_action.sheet_name, e
184                ));
185                return Ok(());
186            }
187        }
188
189        let sheet = self.workbook.get_current_sheet_mut();
190        let col = column_action.col;
191
192        if is_undo {
193            let column_data = &column_action.column_data;
194
195            for (i, row) in sheet.data.iter_mut().enumerate() {
196                if i < column_data.len() {
197                    if col <= row.len() {
198                        row.insert(col, column_data[i].clone());
199                    } else {
200                        while row.len() < col {
201                            row.push(crate::excel::Cell::empty());
202                        }
203                        row.push(column_data[i].clone());
204                    }
205                }
206            }
207
208            // Update both max_cols and max_rows when restoring a column
209            sheet.max_cols = sheet.max_cols.saturating_add(1);
210
211            // Recalculate max_rows since restoring a column might affect the maximum row count
212            // This is especially important if the column contained data beyond the current max_rows
213            self.workbook.recalculate_max_rows();
214
215            if col < self.column_widths.len() {
216                self.column_widths.insert(col, column_action.column_width);
217                if !self.column_widths.is_empty() {
218                    self.column_widths.pop();
219                }
220            } else {
221                while self.column_widths.len() < col {
222                    self.column_widths.push(15); // Default width
223                }
224                self.column_widths.push(column_action.column_width);
225            }
226
227            self.ensure_column_visible(col);
228            self.add_notification(format!("Undid column {} deletion", index_to_col_name(col)));
229        } else {
230            for row in sheet.data.iter_mut() {
231                if col < row.len() {
232                    row.remove(col);
233                }
234            }
235
236            sheet.max_cols = sheet.max_cols.saturating_sub(1);
237
238            if self.column_widths.len() > col {
239                self.column_widths.remove(col);
240                self.column_widths.push(15);
241            }
242
243            self.clamp_selected_cell_to_excel_bounds();
244
245            self.add_notification(format!("Redid column {} deletion", index_to_col_name(col)));
246        }
247
248        self.handle_scrolling();
249        self.search_results.clear();
250        self.current_search_idx = None;
251
252        Ok(())
253    }
254
255    fn apply_sheet_action(&mut self, sheet_action: &SheetAction, is_undo: bool) -> Result<()> {
256        match (sheet_action.operation, is_undo) {
257            (SheetOperation::Delete, true) => {
258                self.restore_sheet_from_action(
259                    sheet_action,
260                    format!("Undid sheet {} deletion", sheet_action.sheet_name),
261                );
262            }
263            (SheetOperation::Delete, false) => {
264                self.delete_sheet_from_action(
265                    sheet_action,
266                    format!("Redid deletion of sheet {}", sheet_action.sheet_name),
267                );
268            }
269            (SheetOperation::Create, true) => {
270                self.delete_sheet_from_action(
271                    sheet_action,
272                    format!("Undid creation of sheet {}", sheet_action.sheet_name),
273                );
274            }
275            (SheetOperation::Create, false) => {
276                self.restore_sheet_from_action(
277                    sheet_action,
278                    format!("Redid creation of sheet {}", sheet_action.sheet_name),
279                );
280            }
281        }
282
283        Ok(())
284    }
285
286    fn cleanup_after_sheet_deletion(&mut self, sheet_name: &str) {
287        self.sheet_column_widths.remove(sheet_name);
288        self.sheet_cell_positions.remove(sheet_name);
289
290        let new_sheet_name = self.workbook.get_current_sheet_name();
291
292        // Restore saved cell position for the new current sheet or use default
293        if let Some(saved_position) = self.sheet_cell_positions.get(&new_sheet_name) {
294            self.selected_cell = Self::clamp_cell_to_excel_bounds(saved_position.selected);
295            self.start_row = saved_position.view.0;
296            self.start_col = saved_position.view.1;
297            self.clamp_selected_cell_to_excel_bounds();
298
299            // Make sure the view position is valid relative to the selected cell
300            self.handle_scrolling();
301        } else {
302            // If no saved position exists, use default position
303            self.selected_cell = (1, 1);
304            self.start_row = 1;
305            self.start_col = 1;
306        }
307
308        if let Some(saved_widths) = self.sheet_column_widths.get(&new_sheet_name) {
309            self.column_widths = saved_widths.clone();
310        } else {
311            let max_cols = self.workbook.get_current_sheet().max_cols;
312            let default_width = 15;
313            self.column_widths = vec![default_width; max_cols + 1];
314
315            self.sheet_column_widths
316                .insert(new_sheet_name.clone(), self.column_widths.clone());
317        }
318
319        self.search_results.clear();
320        self.current_search_idx = None;
321        self.update_row_number_width();
322
323        let new_sheet_index = self.workbook.get_current_sheet_index();
324        if self.workbook.is_lazy_loading() && !self.workbook.is_sheet_loaded(new_sheet_index) {
325            self.input_mode = crate::app::InputMode::LazyLoading;
326        } else {
327            self.input_mode = crate::app::InputMode::Normal;
328        }
329    }
330
331    fn restore_sheet_from_action(&mut self, sheet_action: &SheetAction, notification: String) {
332        let sheet_index = sheet_action.sheet_index;
333
334        if let Err(e) = self
335            .workbook
336            .insert_sheet_at_index(sheet_action.sheet_data.clone(), sheet_index)
337        {
338            self.add_notification(format!(
339                "Failed to restore sheet {}: {}",
340                sheet_action.sheet_name, e
341            ));
342            return;
343        }
344
345        self.sheet_column_widths.insert(
346            sheet_action.sheet_name.clone(),
347            sheet_action.column_widths.clone(),
348        );
349
350        self.sheet_cell_positions.insert(
351            sheet_action.sheet_name.clone(),
352            crate::app::CellPosition {
353                selected: (1, 1),
354                view: (1, 1),
355            },
356        );
357
358        if let Err(e) = self.switch_sheet_by_index(sheet_index) {
359            self.add_notification(format!(
360                "Restored sheet {} but couldn't switch to it: {}",
361                sheet_action.sheet_name, e
362            ));
363            return;
364        }
365
366        self.notification_messages.pop();
367        self.add_notification(notification);
368    }
369
370    fn delete_sheet_from_action(&mut self, sheet_action: &SheetAction, notification: String) {
371        if let Err(e) = self.switch_sheet_by_index(sheet_action.sheet_index) {
372            self.add_notification(format!(
373                "Cannot switch to sheet {} to delete it: {}",
374                sheet_action.sheet_name, e
375            ));
376            return;
377        }
378
379        self.notification_messages.pop();
380
381        if let Err(e) = self.workbook.delete_current_sheet() {
382            self.add_notification(format!("Failed to delete sheet: {e}"));
383            return;
384        }
385
386        self.cleanup_after_sheet_deletion(&sheet_action.sheet_name);
387        self.add_notification(notification);
388    }
389
390    fn apply_multi_row_action(
391        &mut self,
392        multi_row_action: &MultiRowAction,
393        is_undo: bool,
394    ) -> Result<()> {
395        let current_sheet_index = self.workbook.get_current_sheet_index();
396
397        if current_sheet_index != multi_row_action.sheet_index {
398            if let Err(e) = self.switch_sheet_by_index(multi_row_action.sheet_index) {
399                self.add_notification(format!(
400                    "Cannot switch to sheet {}: {}",
401                    multi_row_action.sheet_name, e
402                ));
403                return Ok(());
404            }
405        }
406
407        let start_row = multi_row_action.start_row;
408        let end_row = multi_row_action.end_row;
409        let rows_to_restore = end_row - start_row + 1;
410
411        if is_undo {
412            let rows_data = &multi_row_action.rows_data;
413            let sheet = self.workbook.get_current_sheet_mut();
414
415            // Optimized restore function
416            Self::restore_rows(sheet, start_row, rows_data);
417
418            sheet.max_rows = sheet.max_rows.saturating_add(rows_to_restore);
419
420            // Recalculate max_cols since restoring rows might affect the maximum column count
421            self.workbook.recalculate_max_cols();
422
423            self.add_notification(format!("Undid rows {} to {} deletion", start_row, end_row));
424        } else {
425            self.workbook.delete_rows(start_row, end_row)?;
426
427            self.clamp_selected_cell_to_excel_bounds();
428
429            self.add_notification(format!("Redid rows {} to {} deletion", start_row, end_row));
430        }
431
432        self.handle_scrolling();
433        self.search_results.clear();
434        self.current_search_idx = None;
435
436        Ok(())
437    }
438
439    fn apply_multi_column_action(
440        &mut self,
441        multi_column_action: &MultiColumnAction,
442        is_undo: bool,
443    ) -> Result<()> {
444        let current_sheet_index = self.workbook.get_current_sheet_index();
445
446        if current_sheet_index != multi_column_action.sheet_index {
447            if let Err(e) = self.switch_sheet_by_index(multi_column_action.sheet_index) {
448                self.add_notification(format!(
449                    "Cannot switch to sheet {}: {}",
450                    multi_column_action.sheet_name, e
451                ));
452                return Ok(());
453            }
454        }
455
456        let start_col = multi_column_action.start_col;
457        let end_col = multi_column_action.end_col;
458        let cols_to_restore = end_col - start_col + 1;
459
460        if is_undo {
461            let columns_data = &multi_column_action.columns_data;
462            let column_widths = &multi_column_action.column_widths;
463
464            let sheet = self.workbook.get_current_sheet_mut();
465
466            for col_idx in (0..cols_to_restore).rev() {
467                if col_idx < columns_data.len() {
468                    let column_data = &columns_data[col_idx];
469                    Self::restore_column_at_position(sheet, start_col, column_data);
470
471                    Self::restore_column_width(
472                        &mut self.column_widths,
473                        start_col,
474                        col_idx,
475                        column_widths,
476                    );
477                }
478            }
479
480            sheet.max_cols = sheet.max_cols.saturating_add(cols_to_restore);
481
482            // Recalculate max_rows since restoring columns might affect the maximum row count
483            self.workbook.recalculate_max_rows();
484
485            Self::trim_column_widths(&mut self.column_widths, cols_to_restore);
486            self.ensure_column_visible(start_col);
487
488            self.add_notification(format!(
489                "Undid columns {} to {} deletion",
490                index_to_col_name(start_col),
491                index_to_col_name(end_col)
492            ));
493        } else {
494            self.workbook.delete_columns(start_col, end_col)?;
495
496            Self::remove_column_widths(&mut self.column_widths, start_col, end_col);
497
498            self.clamp_selected_cell_to_excel_bounds();
499
500            self.add_notification(format!(
501                "Redid columns {} to {} deletion",
502                index_to_col_name(start_col),
503                index_to_col_name(end_col)
504            ));
505        }
506
507        self.handle_scrolling();
508        self.search_results.clear();
509        self.current_search_idx = None;
510
511        Ok(())
512    }
513
514    fn restore_rows(
515        sheet: &mut crate::excel::Sheet,
516        position: usize,
517        rows_data: &[Vec<crate::excel::Cell>],
518    ) {
519        // Pre-allocate space by extending the vector
520        for row_data in rows_data.iter().rev() {
521            sheet.data.insert(position, row_data.clone());
522        }
523    }
524
525    fn restore_column_at_position(
526        sheet: &mut crate::excel::Sheet,
527        position: usize,
528        column_data: &[crate::excel::Cell],
529    ) {
530        for (i, row) in sheet.data.iter_mut().enumerate() {
531            if i < column_data.len() {
532                if position <= row.len() {
533                    row.insert(position, column_data[i].clone());
534                } else {
535                    let additional = position - row.len();
536                    row.reserve(additional + 1);
537                    while row.len() < position {
538                        row.push(crate::excel::Cell::empty());
539                    }
540                    row.push(column_data[i].clone());
541                }
542            }
543        }
544    }
545
546    fn restore_column_width(
547        column_widths: &mut Vec<usize>,
548        position: usize,
549        col_idx: usize,
550        width_values: &[usize],
551    ) {
552        if position < column_widths.len() {
553            let width = if col_idx < width_values.len() {
554                width_values[col_idx]
555            } else {
556                15 // Default width
557            };
558            column_widths.insert(position, width);
559        }
560    }
561
562    fn trim_column_widths(column_widths: &mut Vec<usize>, count: usize) {
563        if count >= column_widths.len() {
564            return;
565        }
566        column_widths.truncate(column_widths.len() - count);
567    }
568
569    fn remove_column_widths(column_widths: &mut Vec<usize>, start_col: usize, end_col: usize) {
570        let cols_to_remove = end_col - start_col + 1;
571
572        // Pre-allocate space for new entries to avoid multiple resizes
573        column_widths.reserve(cols_to_remove);
574
575        for col in (start_col..=end_col).rev() {
576            if column_widths.len() > col {
577                column_widths.remove(col);
578            }
579        }
580
581        // Add default widths in a single batch to avoid multiple resizes
582        let mut defaults = vec![15; cols_to_remove];
583        column_widths.append(&mut defaults);
584    }
585}
586
587impl ActionExecutor for AppState<'_> {
588    fn execute_action(&mut self, action: &ActionCommand) -> Result<()> {
589        match action {
590            ActionCommand::Cell(action) => self.execute_cell_action(action),
591            ActionCommand::Row(action) => self.execute_row_action(action),
592            ActionCommand::Column(action) => self.execute_column_action(action),
593            ActionCommand::Sheet(action) => self.execute_sheet_action(action),
594            ActionCommand::MultiRow(action) => self.execute_multi_row_action(action),
595            ActionCommand::MultiColumn(action) => self.execute_multi_column_action(action),
596        }
597    }
598
599    fn execute_cell_action(&mut self, action: &CellAction) -> Result<()> {
600        self.workbook
601            .set_cell_value(action.row, action.col, action.new_value.value.clone())
602    }
603
604    fn execute_row_action(&mut self, action: &RowAction) -> Result<()> {
605        self.workbook.delete_row(action.row)
606    }
607
608    fn execute_column_action(&mut self, action: &ColumnAction) -> Result<()> {
609        self.workbook.delete_column(action.col)
610    }
611
612    fn execute_sheet_action(&mut self, action: &SheetAction) -> Result<()> {
613        match action.operation {
614            SheetOperation::Create => {
615                self.workbook
616                    .insert_sheet_at_index(action.sheet_data.clone(), action.sheet_index)?;
617                self.sheet_column_widths
618                    .insert(action.sheet_name.clone(), action.column_widths.clone());
619                self.sheet_cell_positions.insert(
620                    action.sheet_name.clone(),
621                    crate::app::CellPosition {
622                        selected: (1, 1),
623                        view: (1, 1),
624                    },
625                );
626                self.switch_sheet_by_index(action.sheet_index)
627            }
628            SheetOperation::Delete => {
629                self.switch_sheet_by_index(action.sheet_index)?;
630                self.workbook.delete_current_sheet()?;
631                self.cleanup_after_sheet_deletion(&action.sheet_name);
632                Ok(())
633            }
634        }
635    }
636
637    fn execute_multi_row_action(&mut self, action: &MultiRowAction) -> Result<()> {
638        self.workbook.delete_rows(action.start_row, action.end_row)
639    }
640
641    fn execute_multi_column_action(&mut self, action: &MultiColumnAction) -> Result<()> {
642        self.workbook
643            .delete_columns(action.start_col, action.end_col)
644    }
645}