excel_cli/app/
sheet.rs

1use crate::actions::{
2    ActionCommand, ColumnAction, MultiColumnAction, MultiRowAction, RowAction, SheetAction,
3};
4use crate::app::AppState;
5use crate::utils::index_to_col_name;
6use anyhow::Result;
7
8impl AppState<'_> {
9    pub fn next_sheet(&mut self) -> Result<()> {
10        let sheet_count = self.workbook.get_sheet_names().len();
11        let current_index = self.workbook.get_current_sheet_index();
12
13        if current_index >= sheet_count - 1 {
14            self.add_notification("Already at the last sheet".to_string());
15            return Ok(());
16        }
17
18        self.switch_sheet_by_index(current_index + 1)
19    }
20
21    pub fn prev_sheet(&mut self) -> Result<()> {
22        let current_index = self.workbook.get_current_sheet_index();
23
24        if current_index == 0 {
25            self.add_notification("Already at the first sheet".to_string());
26            return Ok(());
27        }
28
29        self.switch_sheet_by_index(current_index - 1)
30    }
31
32    pub fn switch_sheet_by_index(&mut self, index: usize) -> Result<()> {
33        let current_sheet_name = self.workbook.get_current_sheet_name();
34
35        // Save current column widths if they've changed
36        if !self.sheet_column_widths.contains_key(&current_sheet_name)
37            || self.sheet_column_widths[&current_sheet_name] != self.column_widths
38        {
39            self.sheet_column_widths
40                .insert(current_sheet_name.clone(), self.column_widths.clone());
41        }
42
43        // Save current cell position and view position
44        let current_position = crate::app::CellPosition {
45            selected: self.selected_cell,
46            view: (self.start_row, self.start_col),
47        };
48        self.sheet_cell_positions
49            .insert(current_sheet_name, current_position);
50
51        self.workbook.switch_sheet(index)?;
52
53        let new_sheet_name = self.workbook.get_current_sheet_name();
54
55        // Restore column widths for the new sheet
56        if let Some(saved_widths) = self.sheet_column_widths.get(&new_sheet_name) {
57            if &self.column_widths != saved_widths {
58                self.column_widths = saved_widths.clone();
59            }
60        } else {
61            let max_cols = self.workbook.get_current_sheet().max_cols;
62            let default_width = 15;
63            self.column_widths = vec![default_width; max_cols + 1];
64
65            self.sheet_column_widths
66                .insert(new_sheet_name.clone(), self.column_widths.clone());
67        }
68
69        // Restore cell position and view position for the new sheet
70        if let Some(saved_position) = self.sheet_cell_positions.get(&new_sheet_name) {
71            // Ensure the saved position is valid for the current sheet
72            let sheet = self.workbook.get_current_sheet();
73            let valid_row = saved_position.selected.0.min(sheet.max_rows.max(1));
74            let valid_col = saved_position.selected.1.min(sheet.max_cols.max(1));
75
76            self.selected_cell = (valid_row, valid_col);
77            self.start_row = saved_position.view.0;
78            self.start_col = saved_position.view.1;
79
80            // Make sure the view position is valid relative to the selected cell
81            self.handle_scrolling();
82        } else {
83            // If no saved position exists, use default position
84            self.selected_cell = (1, 1);
85            self.start_row = 1;
86            self.start_col = 1;
87        }
88
89        // Clear search results as they're specific to the previous sheet
90        if !self.search_results.is_empty() {
91            self.search_results.clear();
92            self.current_search_idx = None;
93        }
94
95        self.update_row_number_width();
96
97        // Check if the new sheet is loaded when using lazy loading
98        let is_lazy_loading = self.workbook.is_lazy_loading();
99        let is_sheet_loaded = self.workbook.is_sheet_loaded(index);
100
101        if is_lazy_loading && !is_sheet_loaded {
102            // If the sheet is not loaded, switch to LazyLoading mode
103            self.input_mode = crate::app::InputMode::LazyLoading;
104            self.add_notification(format!(
105                "Switched to sheet: {new_sheet_name} (press Enter to load)"
106            ));
107        } else {
108            self.add_notification(format!("Switched to sheet: {new_sheet_name}"));
109        }
110
111        Ok(())
112    }
113
114    pub fn switch_to_sheet(&mut self, name_or_index: &str) {
115        // Get all sheet names
116        let sheet_names = self.workbook.get_sheet_names();
117
118        // Try to parse as index first
119        if let Ok(index) = name_or_index.parse::<usize>() {
120            // Convert to 0-based index
121            let zero_based_index = index.saturating_sub(1);
122
123            if zero_based_index < sheet_names.len() {
124                match self.switch_sheet_by_index(zero_based_index) {
125                    Ok(()) => return,
126                    Err(e) => {
127                        self.add_notification(format!("Failed to switch to sheet {index}: {e}"));
128                        return;
129                    }
130                }
131            }
132        }
133
134        // Try to find by name
135        for (i, name) in sheet_names.iter().enumerate() {
136            if name.eq_ignore_ascii_case(name_or_index) {
137                match self.switch_sheet_by_index(i) {
138                    Ok(()) => return,
139                    Err(e) => {
140                        self.add_notification(format!(
141                            "Failed to switch to sheet '{name_or_index}': {e}"
142                        ));
143                        return;
144                    }
145                }
146            }
147        }
148
149        // If we get here, no matching sheet was found
150        self.add_notification(format!("Sheet '{name_or_index}' not found"));
151    }
152
153    pub fn delete_current_sheet(&mut self) {
154        let current_sheet_name = self.workbook.get_current_sheet_name();
155        let sheet_index = self.workbook.get_current_sheet_index();
156
157        // Save the sheet data for undo
158        let sheet_data = self.workbook.get_current_sheet().clone();
159        let column_widths = self.column_widths.clone();
160
161        match self.workbook.delete_current_sheet() {
162            Ok(()) => {
163                // Create the undo action
164                let sheet_action = SheetAction {
165                    sheet_index,
166                    sheet_name: current_sheet_name.clone(),
167                    sheet_data,
168                    column_widths,
169                };
170
171                self.undo_history.push(ActionCommand::Sheet(sheet_action));
172                self.sheet_column_widths.remove(&current_sheet_name);
173                self.sheet_cell_positions.remove(&current_sheet_name);
174
175                let new_sheet_name = self.workbook.get_current_sheet_name();
176                let new_sheet_index = self.workbook.get_current_sheet_index();
177                let is_new_sheet_loaded = self.workbook.is_sheet_loaded(new_sheet_index);
178
179                // Restore saved cell position for the new current sheet or use default
180                if let Some(saved_position) = self.sheet_cell_positions.get(&new_sheet_name) {
181                    // Ensure the saved position is valid for the current sheet
182                    let sheet = self.workbook.get_current_sheet();
183                    let valid_row = saved_position.selected.0.min(sheet.max_rows.max(1));
184                    let valid_col = saved_position.selected.1.min(sheet.max_cols.max(1));
185
186                    self.selected_cell = (valid_row, valid_col);
187                    self.start_row = saved_position.view.0;
188                    self.start_col = saved_position.view.1;
189
190                    // Make sure the view position is valid relative to the selected cell
191                    self.handle_scrolling();
192                } else {
193                    // If no saved position exists, use default position
194                    self.selected_cell = (1, 1);
195                    self.start_row = 1;
196                    self.start_col = 1;
197                }
198
199                if let Some(saved_widths) = self.sheet_column_widths.get(&new_sheet_name) {
200                    self.column_widths = saved_widths.clone();
201                } else {
202                    let max_cols = self.workbook.get_current_sheet().max_cols;
203                    let default_width = 15;
204                    self.column_widths = vec![default_width; max_cols + 1];
205
206                    self.sheet_column_widths
207                        .insert(new_sheet_name.clone(), self.column_widths.clone());
208                }
209
210                // Clear search results as they're specific to the previous sheet
211                self.search_results.clear();
212                self.current_search_idx = None;
213
214                // Check if the new current sheet is loaded when using lazy loading
215                if self.workbook.is_lazy_loading() && !is_new_sheet_loaded {
216                    // If the sheet is not loaded, switch to LazyLoading mode
217                    self.input_mode = crate::app::InputMode::LazyLoading;
218                    self.add_notification(format!(
219                        "Deleted sheet: {current_sheet_name}. Switched to sheet: {new_sheet_name} (press Enter to load)"
220                    ));
221                } else {
222                    self.add_notification(format!("Deleted sheet: {current_sheet_name}"));
223                }
224            }
225            Err(e) => {
226                self.add_notification(format!("Failed to delete sheet: {e}"));
227            }
228        }
229    }
230
231    pub fn delete_current_row(&mut self) -> Result<()> {
232        let row = self.selected_cell.0;
233        let sheet = self.workbook.get_current_sheet();
234
235        // If row is outside the valid range, return success
236        if row < 1 || row > sheet.max_rows {
237            return Ok(());
238        }
239
240        // Save row data for undo
241        let sheet_index = self.workbook.get_current_sheet_index();
242        let sheet_name = self.workbook.get_current_sheet_name();
243
244        // Create a copy of the row data before deletion
245        let row_data = if row < sheet.data.len() {
246            sheet.data[row].clone()
247        } else {
248            Vec::new()
249        };
250
251        // Create and add undo action
252        let row_action = RowAction {
253            sheet_index,
254            sheet_name,
255            row,
256            row_data,
257        };
258
259        self.undo_history.push(ActionCommand::Row(row_action));
260        self.workbook.delete_row(row)?;
261
262        self.workbook.recalculate_max_rows();
263        self.workbook.recalculate_max_cols();
264        let sheet = self.workbook.get_current_sheet();
265
266        if self.selected_cell.0 > sheet.max_rows {
267            self.selected_cell.0 = sheet.max_rows.max(1);
268        }
269
270        self.handle_scrolling();
271        self.search_results.clear();
272        self.current_search_idx = None;
273
274        self.add_notification(format!("Deleted row {row}"));
275        Ok(())
276    }
277
278    pub fn delete_row(&mut self, row: usize) -> Result<()> {
279        let sheet = self.workbook.get_current_sheet();
280
281        // If row is outside the valid range, return success
282        if row < 1 || row > sheet.max_rows {
283            return Ok(());
284        }
285
286        // Save row data for undo
287        let sheet_index = self.workbook.get_current_sheet_index();
288        let sheet_name = self.workbook.get_current_sheet_name();
289
290        // Create a copy of the row data before deletion
291        let row_data = if row < sheet.data.len() {
292            sheet.data[row].clone()
293        } else {
294            Vec::new()
295        };
296
297        // Create and add undo action
298        let row_action = RowAction {
299            sheet_index,
300            sheet_name,
301            row,
302            row_data,
303        };
304
305        self.undo_history.push(ActionCommand::Row(row_action));
306        self.workbook.delete_row(row)?;
307
308        self.workbook.recalculate_max_rows();
309        self.workbook.recalculate_max_cols();
310        let sheet = self.workbook.get_current_sheet();
311
312        if self.selected_cell.0 > sheet.max_rows {
313            self.selected_cell.0 = sheet.max_rows.max(1);
314        }
315
316        self.handle_scrolling();
317        self.search_results.clear();
318        self.current_search_idx = None;
319
320        self.add_notification(format!("Deleted row {row}"));
321        Ok(())
322    }
323
324    pub fn delete_rows(&mut self, start_row: usize, end_row: usize) -> Result<()> {
325        if start_row == end_row {
326            return self.delete_row(start_row);
327        }
328
329        let sheet = self.workbook.get_current_sheet();
330
331        // If the entire range is outside the valid range, return success
332        if start_row < 1 || start_row > sheet.max_rows || start_row > end_row {
333            return Ok(());
334        }
335
336        // If start_row is valid but end_row exceeds max_rows, adjust end_row to max_rows
337        let effective_end_row = end_row.min(sheet.max_rows);
338
339        // Save all row data for batch undo
340        let sheet_index = self.workbook.get_current_sheet_index();
341        let sheet_name = self.workbook.get_current_sheet_name();
342
343        // Save row data in the original order from top to bottom
344        let rows_to_save = effective_end_row - start_row + 1;
345        let mut rows_data = Vec::with_capacity(rows_to_save);
346
347        for row in start_row..=effective_end_row {
348            if row < sheet.data.len() {
349                rows_data.push(sheet.data[row].clone());
350            } else {
351                rows_data.push(Vec::new());
352            }
353        }
354
355        // Create and add batch undo action
356        let multi_row_action = MultiRowAction {
357            sheet_index,
358            sheet_name,
359            start_row,
360            end_row: effective_end_row,
361            rows_data,
362        };
363
364        self.undo_history
365            .push(ActionCommand::MultiRow(multi_row_action));
366        self.workbook.delete_rows(start_row, effective_end_row)?;
367
368        self.workbook.recalculate_max_rows();
369        self.workbook.recalculate_max_cols();
370        let sheet = self.workbook.get_current_sheet();
371
372        if self.selected_cell.0 > sheet.max_rows {
373            self.selected_cell.0 = sheet.max_rows.max(1);
374        }
375
376        self.handle_scrolling();
377        self.search_results.clear();
378        self.current_search_idx = None;
379
380        self.add_notification(format!("Deleted rows {start_row} to {effective_end_row}"));
381        Ok(())
382    }
383
384    pub fn delete_current_column(&mut self) -> Result<()> {
385        let col = self.selected_cell.1;
386        let sheet = self.workbook.get_current_sheet();
387
388        // If column is outside the valid range, return success
389        if col < 1 || col > sheet.max_cols {
390            return Ok(());
391        }
392
393        // Save column data for undo
394        let sheet_index = self.workbook.get_current_sheet_index();
395        let sheet_name = self.workbook.get_current_sheet_name();
396
397        // Extract the column data from each row
398        let mut column_data = Vec::with_capacity(sheet.data.len());
399        for row in &sheet.data {
400            if col < row.len() {
401                column_data.push(row[col].clone());
402            } else {
403                column_data.push(crate::excel::Cell::empty());
404            }
405        }
406
407        // Save the column width
408        let column_width = if col < self.column_widths.len() {
409            self.column_widths[col]
410        } else {
411            15 // Default width
412        };
413
414        let column_action = ColumnAction {
415            sheet_index,
416            sheet_name,
417            col,
418            column_data,
419            column_width,
420        };
421
422        self.undo_history.push(ActionCommand::Column(column_action));
423        self.workbook.delete_column(col)?;
424
425        self.workbook.recalculate_max_rows();
426        self.workbook.recalculate_max_cols();
427        let sheet = self.workbook.get_current_sheet();
428
429        if col > sheet.max_cols {
430            self.selected_cell.1 = sheet.max_cols.max(1);
431        }
432
433        if self.selected_cell.0 > sheet.max_rows {
434            self.selected_cell.0 = sheet.max_rows.max(1);
435        }
436
437        if self.column_widths.len() > col {
438            self.column_widths.remove(col);
439        }
440
441        self.adjust_column_widths(sheet.max_cols);
442
443        self.handle_scrolling();
444        self.search_results.clear();
445        self.current_search_idx = None;
446
447        let col_name = index_to_col_name(col);
448        self.add_notification(format!("Deleted column {col_name}"));
449        Ok(())
450    }
451
452    pub fn delete_column(&mut self, col: usize) -> Result<()> {
453        let sheet = self.workbook.get_current_sheet();
454
455        // If column is outside the valid range, return success
456        if col < 1 || col > sheet.max_cols {
457            return Ok(());
458        }
459
460        // Save column data for undo
461        let sheet_index = self.workbook.get_current_sheet_index();
462        let sheet_name = self.workbook.get_current_sheet_name();
463
464        // Extract the column data from each row
465        let mut column_data = Vec::with_capacity(sheet.data.len());
466        for row in &sheet.data {
467            if col < row.len() {
468                column_data.push(row[col].clone());
469            } else {
470                column_data.push(crate::excel::Cell::empty());
471            }
472        }
473
474        // Save the column width
475        let column_width = if col < self.column_widths.len() {
476            self.column_widths[col]
477        } else {
478            15 // Default width
479        };
480
481        let column_action = ColumnAction {
482            sheet_index,
483            sheet_name,
484            col,
485            column_data,
486            column_width,
487        };
488
489        self.undo_history.push(ActionCommand::Column(column_action));
490        self.workbook.delete_column(col)?;
491
492        self.workbook.recalculate_max_rows();
493        self.workbook.recalculate_max_cols();
494        let sheet = self.workbook.get_current_sheet();
495
496        if self.selected_cell.1 > sheet.max_cols {
497            self.selected_cell.1 = sheet.max_cols.max(1);
498        }
499
500        if self.selected_cell.0 > sheet.max_rows {
501            self.selected_cell.0 = sheet.max_rows.max(1);
502        }
503
504        if self.column_widths.len() > col {
505            self.column_widths.remove(col);
506        }
507
508        self.adjust_column_widths(sheet.max_cols);
509
510        self.handle_scrolling();
511        self.search_results.clear();
512        self.current_search_idx = None;
513
514        let col_name = index_to_col_name(col);
515        self.add_notification(format!("Deleted column {col_name}"));
516        Ok(())
517    }
518
519    pub fn delete_columns(&mut self, start_col: usize, end_col: usize) -> Result<()> {
520        if start_col == end_col {
521            return self.delete_column(start_col);
522        }
523
524        let sheet = self.workbook.get_current_sheet();
525
526        // If the entire range is outside the valid range, return success
527        if start_col < 1 || start_col > sheet.max_cols || start_col > end_col {
528            return Ok(());
529        }
530
531        // If start_col is valid but end_col exceeds max_cols, adjust end_col to max_cols
532        let effective_end_col = end_col.min(sheet.max_cols);
533
534        // For multiple columns, save all column data for batch undo
535        let sheet_index = self.workbook.get_current_sheet_index();
536        let sheet_name = self.workbook.get_current_sheet_name();
537
538        // Save column data and widths for batch undo
539        let cols_to_save = effective_end_col - start_col + 1;
540        let mut columns_data = Vec::with_capacity(cols_to_save);
541        let mut column_widths = Vec::with_capacity(cols_to_save);
542
543        for col in start_col..=effective_end_col {
544            // Extract the column data from each row
545            let mut column_data = Vec::with_capacity(sheet.data.len());
546            for row in &sheet.data {
547                if col < row.len() {
548                    column_data.push(row[col].clone());
549                } else {
550                    column_data.push(crate::excel::Cell::empty());
551                }
552            }
553            columns_data.push(column_data);
554
555            // Save the column width
556            let column_width = if col < self.column_widths.len() {
557                self.column_widths[col]
558            } else {
559                15 // Default width
560            };
561            column_widths.push(column_width);
562        }
563
564        // Create and add batch undo action
565        let multi_column_action = MultiColumnAction {
566            sheet_index,
567            sheet_name,
568            start_col,
569            end_col: effective_end_col,
570            columns_data,
571            column_widths,
572        };
573
574        self.undo_history
575            .push(ActionCommand::MultiColumn(multi_column_action));
576        self.workbook.delete_columns(start_col, effective_end_col)?;
577
578        self.workbook.recalculate_max_rows();
579        self.workbook.recalculate_max_cols();
580        let sheet = self.workbook.get_current_sheet();
581
582        if self.selected_cell.1 > sheet.max_cols {
583            self.selected_cell.1 = sheet.max_cols.max(1);
584        }
585
586        if self.selected_cell.0 > sheet.max_rows {
587            self.selected_cell.0 = sheet.max_rows.max(1);
588        }
589
590        for col in (start_col..=effective_end_col).rev() {
591            if self.column_widths.len() > col {
592                self.column_widths.remove(col);
593            }
594        }
595
596        self.adjust_column_widths(sheet.max_cols);
597
598        self.handle_scrolling();
599        self.search_results.clear();
600        self.current_search_idx = None;
601
602        self.add_notification(format!(
603            "Deleted columns {} to {}",
604            index_to_col_name(start_col),
605            index_to_col_name(effective_end_col)
606        ));
607        Ok(())
608    }
609
610    pub fn auto_adjust_column_width(&mut self, col: Option<usize>) {
611        // Get sheet information before any mutable operations
612        let is_loaded = self.workbook.get_current_sheet().is_loaded;
613        let max_cols = self.workbook.get_current_sheet().max_cols;
614        let default_min_width = 5;
615
616        if !is_loaded && max_cols == 0 {
617            self.add_notification(
618                "Cannot adjust column widths in lazy loading mode until sheet is loaded"
619                    .to_string(),
620            );
621            return;
622        }
623
624        match col {
625            // Adjust specific column
626            Some(column) => {
627                // Ensure column_widths is large enough
628                self.ensure_column_widths();
629
630                if column < self.column_widths.len() {
631                    // Calculate and set new column width
632                    let width = self.calculate_column_width(column);
633                    self.column_widths[column] = width.max(default_min_width);
634
635                    self.ensure_column_visible(column);
636
637                    self.add_notification(format!(
638                        "Column {} width adjusted",
639                        index_to_col_name(column)
640                    ));
641                }
642            }
643            // Adjust all columns
644            None => {
645                // Ensure column_widths is large enough
646                self.ensure_column_widths();
647
648                // Only process columns if there are any
649                if max_cols > 0 {
650                    for col_idx in 1..=max_cols {
651                        let width = self.calculate_column_width(col_idx);
652                        self.column_widths[col_idx] = width.max(default_min_width);
653                    }
654
655                    let column = self.selected_cell.1;
656                    self.ensure_column_visible(column);
657
658                    self.add_notification("All column widths adjusted".to_string());
659                }
660            }
661        }
662    }
663
664    fn calculate_column_width(&self, col: usize) -> usize {
665        let sheet = self.workbook.get_current_sheet();
666
667        // Start with minimum width and header width
668        let col_name = index_to_col_name(col);
669        let mut max_width = 3.max(col_name.len());
670
671        // Calculate max width from all cells in the column
672        for row in 1..=sheet.max_rows {
673            if row >= sheet.data.len() || col >= sheet.data[row].len() {
674                continue;
675            }
676
677            let content = &sheet.data[row][col].value;
678            if content.is_empty() {
679                continue;
680            }
681
682            let mut display_width = 0;
683
684            for c in content.chars() {
685                if c.is_ascii() {
686                    display_width += 1;
687                } else {
688                    display_width += 2;
689                }
690            }
691
692            max_width = max_width.max(display_width);
693        }
694        max_width
695    }
696
697    pub fn get_column_width(&self, col: usize) -> usize {
698        if col < self.column_widths.len() {
699            self.column_widths[col]
700        } else {
701            15 // Default width
702        }
703    }
704
705    pub fn ensure_column_widths(&mut self) {
706        let sheet = self.workbook.get_current_sheet();
707        self.adjust_column_widths(sheet.max_cols);
708    }
709
710    fn adjust_column_widths(&mut self, max_cols: usize) {
711        match self.column_widths.len().cmp(&(max_cols + 1)) {
712            std::cmp::Ordering::Greater => {
713                self.column_widths.truncate(max_cols + 1);
714            }
715            std::cmp::Ordering::Less => {
716                let additional = max_cols + 1 - self.column_widths.len();
717                self.column_widths.extend(vec![15; additional]);
718            }
719            std::cmp::Ordering::Equal => {
720                // Column widths are already correct, do nothing
721            }
722        }
723    }
724}