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