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