1use crate::actions::{
2 ActionCommand, ColumnAction, MultiColumnAction, MultiRowAction, RowAction, SheetAction,
3 SheetOperation,
4};
5use crate::app::AppState;
6use crate::utils::index_to_col_name;
7use anyhow::Result;
8
9impl AppState<'_> {
10 pub fn next_sheet(&mut self) -> Result<()> {
11 let sheet_count = self.workbook.get_sheet_names().len();
12 let current_index = self.workbook.get_current_sheet_index();
13
14 if current_index >= sheet_count - 1 {
15 self.add_notification("Already at the last sheet".to_string());
16 return Ok(());
17 }
18
19 self.switch_sheet_by_index(current_index + 1)
20 }
21
22 pub fn prev_sheet(&mut self) -> Result<()> {
23 let current_index = self.workbook.get_current_sheet_index();
24
25 if current_index == 0 {
26 self.add_notification("Already at the first sheet".to_string());
27 return Ok(());
28 }
29
30 self.switch_sheet_by_index(current_index - 1)
31 }
32
33 pub fn switch_sheet_by_index(&mut self, index: usize) -> Result<()> {
34 let current_sheet_name = self.workbook.get_current_sheet_name();
35
36 if !self.sheet_column_widths.contains_key(¤t_sheet_name)
38 || self.sheet_column_widths[¤t_sheet_name] != self.column_widths
39 {
40 self.sheet_column_widths
41 .insert(current_sheet_name.clone(), self.column_widths.clone());
42 }
43
44 let current_position = crate::app::CellPosition {
46 selected: self.selected_cell,
47 view: (self.start_row, self.start_col),
48 };
49 self.sheet_cell_positions
50 .insert(current_sheet_name, current_position);
51
52 self.workbook.switch_sheet(index)?;
53
54 let new_sheet_name = self.workbook.get_current_sheet_name();
55
56 if let Some(saved_widths) = self.sheet_column_widths.get(&new_sheet_name) {
58 if &self.column_widths != saved_widths {
59 self.column_widths = saved_widths.clone();
60 }
61 } else {
62 let max_cols = self.workbook.get_current_sheet().max_cols;
63 let default_width = 15;
64 self.column_widths = vec![default_width; max_cols + 1];
65
66 self.sheet_column_widths
67 .insert(new_sheet_name.clone(), self.column_widths.clone());
68 }
69
70 if let Some(saved_position) = self.sheet_cell_positions.get(&new_sheet_name) {
72 self.selected_cell = Self::clamp_cell_to_excel_bounds(saved_position.selected);
73 self.start_row = saved_position.view.0;
74 self.start_col = saved_position.view.1;
75 self.clamp_selected_cell_to_excel_bounds();
76
77 self.handle_scrolling();
79 } else {
80 self.selected_cell = (1, 1);
82 self.start_row = 1;
83 self.start_col = 1;
84 }
85
86 if !self.search_results.is_empty() {
88 self.search_results.clear();
89 self.current_search_idx = None;
90 }
91
92 self.update_row_number_width();
93
94 let is_lazy_loading = self.workbook.is_lazy_loading();
96 let is_sheet_loaded = self.workbook.is_sheet_loaded(index);
97
98 if is_lazy_loading && !is_sheet_loaded {
99 self.input_mode = crate::app::InputMode::LazyLoading;
101 self.add_notification(format!(
102 "Switched to sheet: {new_sheet_name} (press Enter to load)"
103 ));
104 } else {
105 self.add_notification(format!("Switched to sheet: {new_sheet_name}"));
106 }
107
108 Ok(())
109 }
110
111 pub fn switch_to_sheet(&mut self, name_or_index: &str) {
112 let sheet_names = self.workbook.get_sheet_names();
114
115 if let Ok(index) = name_or_index.parse::<usize>() {
117 let zero_based_index = index.saturating_sub(1);
119
120 if zero_based_index < sheet_names.len() {
121 match self.switch_sheet_by_index(zero_based_index) {
122 Ok(()) => return,
123 Err(e) => {
124 self.add_notification(format!("Failed to switch to sheet {index}: {e}"));
125 return;
126 }
127 }
128 }
129 }
130
131 for (i, name) in sheet_names.iter().enumerate() {
133 if name.eq_ignore_ascii_case(name_or_index) {
134 match self.switch_sheet_by_index(i) {
135 Ok(()) => return,
136 Err(e) => {
137 self.add_notification(format!(
138 "Failed to switch to sheet '{name_or_index}': {e}"
139 ));
140 return;
141 }
142 }
143 }
144 }
145
146 self.add_notification(format!("Sheet '{name_or_index}' not found"));
148 }
149
150 pub fn create_sheet(&mut self, name: &str) {
151 let insert_index = self.workbook.get_current_sheet_index() + 1;
152
153 match self.workbook.add_sheet(name, insert_index) {
154 Ok(sheet_name) => {
155 let default_width = 15;
156 let max_cols = self
157 .workbook
158 .get_sheet_by_index(insert_index)
159 .map(|sheet| sheet.max_cols)
160 .unwrap_or(1);
161
162 self.sheet_column_widths
163 .insert(sheet_name.clone(), vec![default_width; max_cols + 1]);
164 self.sheet_cell_positions.insert(
165 sheet_name.clone(),
166 crate::app::CellPosition {
167 selected: (1, 1),
168 view: (1, 1),
169 },
170 );
171
172 if let Err(e) = self.switch_sheet_by_index(insert_index) {
173 self.sheet_column_widths.remove(&sheet_name);
174 self.sheet_cell_positions.remove(&sheet_name);
175 let _ = self.workbook.delete_sheet_at_index(insert_index);
176 self.add_notification(format!("Failed to switch to new sheet: {e}"));
177 return;
178 }
179
180 self.notification_messages.pop();
181
182 let sheet_action = SheetAction {
183 sheet_index: insert_index,
184 sheet_name: sheet_name.clone(),
185 sheet_data: self.workbook.get_current_sheet().clone(),
186 column_widths: self.column_widths.clone(),
187 operation: SheetOperation::Create,
188 };
189
190 self.undo_history.push(ActionCommand::Sheet(sheet_action));
191 self.input_mode = crate::app::InputMode::Normal;
192 self.add_notification(format!("Created sheet: {sheet_name}"));
193 }
194 Err(e) => {
195 self.add_notification(format!("Failed to add sheet: {e}"));
196 }
197 }
198 }
199
200 pub fn delete_current_sheet(&mut self) {
201 let current_sheet_name = self.workbook.get_current_sheet_name();
202 let sheet_index = self.workbook.get_current_sheet_index();
203
204 let sheet_data = self.workbook.get_current_sheet().clone();
206 let column_widths = self.column_widths.clone();
207
208 match self.workbook.delete_current_sheet() {
209 Ok(()) => {
210 let sheet_action = SheetAction {
212 sheet_index,
213 sheet_name: current_sheet_name.clone(),
214 sheet_data,
215 column_widths,
216 operation: SheetOperation::Delete,
217 };
218
219 self.undo_history.push(ActionCommand::Sheet(sheet_action));
220 self.sheet_column_widths.remove(¤t_sheet_name);
221 self.sheet_cell_positions.remove(¤t_sheet_name);
222
223 let new_sheet_name = self.workbook.get_current_sheet_name();
224 let new_sheet_index = self.workbook.get_current_sheet_index();
225 let is_new_sheet_loaded = self.workbook.is_sheet_loaded(new_sheet_index);
226
227 if let Some(saved_position) = self.sheet_cell_positions.get(&new_sheet_name) {
229 self.selected_cell = Self::clamp_cell_to_excel_bounds(saved_position.selected);
230 self.start_row = saved_position.view.0;
231 self.start_col = saved_position.view.1;
232 self.clamp_selected_cell_to_excel_bounds();
233
234 self.handle_scrolling();
236 } else {
237 self.selected_cell = (1, 1);
239 self.start_row = 1;
240 self.start_col = 1;
241 }
242
243 if let Some(saved_widths) = self.sheet_column_widths.get(&new_sheet_name) {
244 self.column_widths = saved_widths.clone();
245 } else {
246 let max_cols = self.workbook.get_current_sheet().max_cols;
247 let default_width = 15;
248 self.column_widths = vec![default_width; max_cols + 1];
249
250 self.sheet_column_widths
251 .insert(new_sheet_name.clone(), self.column_widths.clone());
252 }
253
254 self.search_results.clear();
256 self.current_search_idx = None;
257 self.update_row_number_width();
258
259 if self.workbook.is_lazy_loading() && !is_new_sheet_loaded {
261 self.input_mode = crate::app::InputMode::LazyLoading;
263 self.add_notification(format!(
264 "Deleted sheet: {current_sheet_name}. Switched to sheet: {new_sheet_name} (press Enter to load)"
265 ));
266 } else {
267 self.add_notification(format!("Deleted sheet: {current_sheet_name}"));
268 }
269 }
270 Err(e) => {
271 self.add_notification(format!("Failed to delete sheet: {e}"));
272 }
273 }
274 }
275
276 pub fn delete_current_row(&mut self) -> Result<()> {
277 let row = self.selected_cell.0;
278 let sheet = self.workbook.get_current_sheet();
279
280 if row < 1 || row > sheet.max_rows {
282 return Ok(());
283 }
284
285 let sheet_index = self.workbook.get_current_sheet_index();
287 let sheet_name = self.workbook.get_current_sheet_name();
288
289 let row_data = if row < sheet.data.len() {
291 sheet.data[row].clone()
292 } else {
293 Vec::new()
294 };
295
296 let row_action = RowAction {
298 sheet_index,
299 sheet_name,
300 row,
301 row_data,
302 };
303
304 self.undo_history.push(ActionCommand::Row(row_action));
305 self.workbook.delete_row(row)?;
306
307 self.workbook.recalculate_max_rows();
308 self.workbook.recalculate_max_cols();
309 self.clamp_selected_cell_to_excel_bounds();
310
311 self.handle_scrolling();
312 self.search_results.clear();
313 self.current_search_idx = None;
314
315 self.add_notification(format!("Deleted row {row}"));
316 Ok(())
317 }
318
319 pub fn delete_row(&mut self, row: usize) -> Result<()> {
320 let sheet = self.workbook.get_current_sheet();
321
322 if row < 1 || row > sheet.max_rows {
324 return Ok(());
325 }
326
327 let sheet_index = self.workbook.get_current_sheet_index();
329 let sheet_name = self.workbook.get_current_sheet_name();
330
331 let row_data = if row < sheet.data.len() {
333 sheet.data[row].clone()
334 } else {
335 Vec::new()
336 };
337
338 let row_action = RowAction {
340 sheet_index,
341 sheet_name,
342 row,
343 row_data,
344 };
345
346 self.undo_history.push(ActionCommand::Row(row_action));
347 self.workbook.delete_row(row)?;
348
349 self.workbook.recalculate_max_rows();
350 self.workbook.recalculate_max_cols();
351 self.clamp_selected_cell_to_excel_bounds();
352
353 self.handle_scrolling();
354 self.search_results.clear();
355 self.current_search_idx = None;
356
357 self.add_notification(format!("Deleted row {row}"));
358 Ok(())
359 }
360
361 pub fn delete_rows(&mut self, start_row: usize, end_row: usize) -> Result<()> {
362 if start_row == end_row {
363 return self.delete_row(start_row);
364 }
365
366 let sheet = self.workbook.get_current_sheet();
367
368 if start_row < 1 || start_row > sheet.max_rows || start_row > end_row {
370 return Ok(());
371 }
372
373 let effective_end_row = end_row.min(sheet.max_rows);
375
376 let sheet_index = self.workbook.get_current_sheet_index();
378 let sheet_name = self.workbook.get_current_sheet_name();
379
380 let rows_to_save = effective_end_row - start_row + 1;
382 let mut rows_data = Vec::with_capacity(rows_to_save);
383
384 for row in start_row..=effective_end_row {
385 if row < sheet.data.len() {
386 rows_data.push(sheet.data[row].clone());
387 } else {
388 rows_data.push(Vec::new());
389 }
390 }
391
392 let multi_row_action = MultiRowAction {
394 sheet_index,
395 sheet_name,
396 start_row,
397 end_row: effective_end_row,
398 rows_data,
399 };
400
401 self.undo_history
402 .push(ActionCommand::MultiRow(multi_row_action));
403 self.workbook.delete_rows(start_row, effective_end_row)?;
404
405 self.workbook.recalculate_max_rows();
406 self.workbook.recalculate_max_cols();
407 self.clamp_selected_cell_to_excel_bounds();
408
409 self.handle_scrolling();
410 self.search_results.clear();
411 self.current_search_idx = None;
412
413 self.add_notification(format!("Deleted rows {start_row} to {effective_end_row}"));
414 Ok(())
415 }
416
417 pub fn delete_current_column(&mut self) -> Result<()> {
418 let col = self.selected_cell.1;
419 let sheet = self.workbook.get_current_sheet();
420
421 if col < 1 || col > sheet.max_cols {
423 return Ok(());
424 }
425
426 let sheet_index = self.workbook.get_current_sheet_index();
428 let sheet_name = self.workbook.get_current_sheet_name();
429
430 let mut column_data = Vec::with_capacity(sheet.data.len());
432 for row in &sheet.data {
433 if col < row.len() {
434 column_data.push(row[col].clone());
435 } else {
436 column_data.push(crate::excel::Cell::empty());
437 }
438 }
439
440 let column_width = if col < self.column_widths.len() {
442 self.column_widths[col]
443 } else {
444 15 };
446
447 let column_action = ColumnAction {
448 sheet_index,
449 sheet_name,
450 col,
451 column_data,
452 column_width,
453 };
454
455 self.undo_history.push(ActionCommand::Column(column_action));
456 self.workbook.delete_column(col)?;
457
458 self.workbook.recalculate_max_rows();
459 self.workbook.recalculate_max_cols();
460 let max_cols = self.workbook.get_current_sheet().max_cols;
461 self.clamp_selected_cell_to_excel_bounds();
462
463 if self.column_widths.len() > col {
464 self.column_widths.remove(col);
465 }
466
467 self.adjust_column_widths(max_cols);
468
469 self.handle_scrolling();
470 self.search_results.clear();
471 self.current_search_idx = None;
472
473 let col_name = index_to_col_name(col);
474 self.add_notification(format!("Deleted column {col_name}"));
475 Ok(())
476 }
477
478 pub fn delete_column(&mut self, col: usize) -> Result<()> {
479 let sheet = self.workbook.get_current_sheet();
480
481 if col < 1 || col > sheet.max_cols {
483 return Ok(());
484 }
485
486 let sheet_index = self.workbook.get_current_sheet_index();
488 let sheet_name = self.workbook.get_current_sheet_name();
489
490 let mut column_data = Vec::with_capacity(sheet.data.len());
492 for row in &sheet.data {
493 if col < row.len() {
494 column_data.push(row[col].clone());
495 } else {
496 column_data.push(crate::excel::Cell::empty());
497 }
498 }
499
500 let column_width = if col < self.column_widths.len() {
502 self.column_widths[col]
503 } else {
504 15 };
506
507 let column_action = ColumnAction {
508 sheet_index,
509 sheet_name,
510 col,
511 column_data,
512 column_width,
513 };
514
515 self.undo_history.push(ActionCommand::Column(column_action));
516 self.workbook.delete_column(col)?;
517
518 self.workbook.recalculate_max_rows();
519 self.workbook.recalculate_max_cols();
520 let max_cols = self.workbook.get_current_sheet().max_cols;
521 self.clamp_selected_cell_to_excel_bounds();
522
523 if self.column_widths.len() > col {
524 self.column_widths.remove(col);
525 }
526
527 self.adjust_column_widths(max_cols);
528
529 self.handle_scrolling();
530 self.search_results.clear();
531 self.current_search_idx = None;
532
533 let col_name = index_to_col_name(col);
534 self.add_notification(format!("Deleted column {col_name}"));
535 Ok(())
536 }
537
538 pub fn delete_columns(&mut self, start_col: usize, end_col: usize) -> Result<()> {
539 if start_col == end_col {
540 return self.delete_column(start_col);
541 }
542
543 let sheet = self.workbook.get_current_sheet();
544
545 if start_col < 1 || start_col > sheet.max_cols || start_col > end_col {
547 return Ok(());
548 }
549
550 let effective_end_col = end_col.min(sheet.max_cols);
552
553 let sheet_index = self.workbook.get_current_sheet_index();
555 let sheet_name = self.workbook.get_current_sheet_name();
556
557 let cols_to_save = effective_end_col - start_col + 1;
559 let mut columns_data = Vec::with_capacity(cols_to_save);
560 let mut column_widths = Vec::with_capacity(cols_to_save);
561
562 for col in start_col..=effective_end_col {
563 let mut column_data = Vec::with_capacity(sheet.data.len());
565 for row in &sheet.data {
566 if col < row.len() {
567 column_data.push(row[col].clone());
568 } else {
569 column_data.push(crate::excel::Cell::empty());
570 }
571 }
572 columns_data.push(column_data);
573
574 let column_width = if col < self.column_widths.len() {
576 self.column_widths[col]
577 } else {
578 15 };
580 column_widths.push(column_width);
581 }
582
583 let multi_column_action = MultiColumnAction {
585 sheet_index,
586 sheet_name,
587 start_col,
588 end_col: effective_end_col,
589 columns_data,
590 column_widths,
591 };
592
593 self.undo_history
594 .push(ActionCommand::MultiColumn(multi_column_action));
595 self.workbook.delete_columns(start_col, effective_end_col)?;
596
597 self.workbook.recalculate_max_rows();
598 self.workbook.recalculate_max_cols();
599 let max_cols = self.workbook.get_current_sheet().max_cols;
600 self.clamp_selected_cell_to_excel_bounds();
601
602 for col in (start_col..=effective_end_col).rev() {
603 if self.column_widths.len() > col {
604 self.column_widths.remove(col);
605 }
606 }
607
608 self.adjust_column_widths(max_cols);
609
610 self.handle_scrolling();
611 self.search_results.clear();
612 self.current_search_idx = None;
613
614 self.add_notification(format!(
615 "Deleted columns {} to {}",
616 index_to_col_name(start_col),
617 index_to_col_name(effective_end_col)
618 ));
619 Ok(())
620 }
621
622 pub fn auto_adjust_column_width(&mut self, col: Option<usize>) {
623 let is_loaded = self.workbook.get_current_sheet().is_loaded;
625 let max_cols = self.workbook.get_current_sheet().max_cols;
626 let default_min_width = 5;
627
628 if !is_loaded && max_cols == 0 {
629 self.add_notification(
630 "Cannot adjust column widths in lazy loading mode until sheet is loaded"
631 .to_string(),
632 );
633 return;
634 }
635
636 match col {
637 Some(column) => {
639 self.ensure_column_widths();
641
642 if column < self.column_widths.len() {
643 let width = self.calculate_column_width(column);
645 self.column_widths[column] = width.max(default_min_width);
646
647 self.ensure_column_visible(column);
648
649 self.add_notification(format!(
650 "Column {} width adjusted",
651 index_to_col_name(column)
652 ));
653 }
654 }
655 None => {
657 self.ensure_column_widths();
659
660 if max_cols > 0 {
662 for col_idx in 1..=max_cols {
663 let width = self.calculate_column_width(col_idx);
664 self.column_widths[col_idx] = width.max(default_min_width);
665 }
666
667 let column = self.selected_cell.1;
668 self.ensure_column_visible(column);
669
670 self.add_notification("All column widths adjusted".to_string());
671 }
672 }
673 }
674 }
675
676 fn calculate_column_width(&self, col: usize) -> usize {
677 let sheet = self.workbook.get_current_sheet();
678
679 let col_name = index_to_col_name(col);
681 let mut max_width = 3.max(col_name.len());
682
683 for row in 1..=sheet.max_rows {
685 if row >= sheet.data.len() || col >= sheet.data[row].len() {
686 continue;
687 }
688
689 let content = &sheet.data[row][col].value;
690 if content.is_empty() {
691 continue;
692 }
693
694 let mut display_width = 0;
695
696 for c in content.chars() {
697 if c.is_ascii() {
698 display_width += 1;
699 } else {
700 display_width += 2;
701 }
702 }
703
704 max_width = max_width.max(display_width);
705 }
706 max_width
707 }
708
709 pub fn get_column_width(&self, col: usize) -> usize {
710 if col < self.column_widths.len() {
711 self.column_widths[col]
712 } else {
713 15 }
715 }
716
717 pub fn ensure_column_widths(&mut self) {
718 let sheet = self.workbook.get_current_sheet();
719 self.adjust_column_widths(sheet.max_cols);
720 }
721
722 fn adjust_column_widths(&mut self, max_cols: usize) {
723 match self.column_widths.len().cmp(&(max_cols + 1)) {
724 std::cmp::Ordering::Greater => {
725 self.column_widths.truncate(max_cols + 1);
726 }
727 std::cmp::Ordering::Less => {
728 let additional = max_cols + 1 - self.column_widths.len();
729 self.column_widths.extend(vec![15; additional]);
730 }
731 std::cmp::Ordering::Equal => {
732 }
734 }
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use crate::app::AppState;
741 use crate::excel::{Sheet, Workbook};
742 use std::path::PathBuf;
743
744 #[test]
745 fn create_sheet_can_be_undone_and_redone() {
746 let workbook = Workbook::from_sheets_for_test(vec![Sheet::blank("Sheet1".to_string())]);
747 let mut app = AppState::new(workbook, PathBuf::from("test.xlsx")).unwrap();
748
749 app.create_sheet("Report");
750 assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1", "Report"]);
751 assert_eq!(app.workbook.get_current_sheet_name(), "Report");
752 assert!(app.workbook.is_modified());
753
754 app.undo().unwrap();
755 assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1"]);
756 assert_eq!(app.workbook.get_current_sheet_name(), "Sheet1");
757 assert!(!app.workbook.is_modified());
758
759 app.redo().unwrap();
760 assert_eq!(app.workbook.get_sheet_names(), vec!["Sheet1", "Report"]);
761 assert_eq!(app.workbook.get_current_sheet_name(), "Report");
762 assert!(app.workbook.is_modified());
763 }
764}