1use std::path::Path;
2
3use crate::app::AppState;
4use crate::excel::{EXCEL_MAX_COLS, EXCEL_MAX_ROWS};
5use crate::json_export::{export_all_sheets_json, export_json, HeaderDirection};
6use crate::utils::{cell_reference, col_name_to_index, index_to_col_name, parse_cell_reference};
7
8impl AppState<'_> {
9 pub fn execute_command(&mut self) {
10 let command = self.input_buffer.clone();
11 self.input_mode = crate::app::InputMode::Normal;
12 self.input_buffer = String::new();
13
14 if command.is_empty() {
15 return;
16 }
17
18 if let Some(cell_ref) = parse_cell_reference(&command) {
20 self.jump_to_cell(cell_ref);
21 return;
22 }
23
24 match command.as_str() {
26 "w" => {
27 if let Err(e) = self.save() {
28 self.add_notification(format!("Save failed: {e}"));
29 }
30 }
31 "wq" | "x" => self.save_and_exit(),
32 "q" => {
33 if self.workbook.is_modified() {
34 self.add_notification(
35 "File has unsaved changes. Use :q! to force quit or :wq to save and quit."
36 .to_string(),
37 );
38 } else {
39 self.should_quit = true;
40 }
41 }
42 "q!" => self.exit_without_saving(),
43 "y" => self.copy_cell(),
44 "d" => {
45 if let Err(e) = self.cut_cell() {
46 self.add_notification(format!("Cut failed: {e}"));
47 }
48 }
49 "put" | "pu" => {
50 if let Err(e) = self.paste_cell() {
51 self.add_notification(format!("Paste failed: {e}"));
52 }
53 }
54 "nohlsearch" | "noh" => self.disable_search_highlight(),
55 "help" => self.show_help(),
56 "delsheet" => self.delete_current_sheet(),
57 "freeze" => self.freeze_at_cell(self.selected_cell),
58 "unfreeze" => self.clear_freeze_panes(),
59 "addsheet" => self.add_notification("Usage: :addsheet <name>".to_string()),
60 _ => {
61 if command.starts_with("cw ") {
63 self.handle_column_width_command(&command);
64 } else if command.starts_with("ej") {
65 self.handle_json_export_command(&command);
66 } else if let Some(sheet_name) = command.strip_prefix("addsheet ") {
67 self.create_sheet(sheet_name.trim());
68 } else if command.starts_with("sheet ") {
69 let sheet_name = command.strip_prefix("sheet ").unwrap().trim();
70 self.switch_to_sheet(sheet_name);
71 } else if command.starts_with("dr") {
72 self.handle_delete_row_command(&command);
73 } else if command.starts_with("dc") {
74 self.handle_delete_column_command(&command);
75 } else if let Some(cell_ref) = command.strip_prefix("freeze ") {
76 self.handle_freeze_command(cell_ref.trim());
77 } else {
78 self.add_notification(format!("Unknown command: {}", command));
79 }
80 }
81 }
82 }
83
84 fn handle_freeze_command(&mut self, cell_ref: &str) {
85 let Some(cell) = parse_cell_reference(cell_ref) else {
86 self.add_notification("Usage: :freeze [cell]".to_string());
87 return;
88 };
89
90 self.freeze_at_cell(cell);
91 }
92
93 fn freeze_at_cell(&mut self, cell: (usize, usize)) {
94 let (row, col) = cell;
95 if row == 1 && col == 1 {
96 self.clear_freeze_panes();
97 return;
98 }
99
100 self.workbook
101 .set_freeze_panes(row.saturating_sub(1), col.saturating_sub(1));
102 self.handle_scrolling();
103 self.add_notification(format!("Frozen panes at {}", cell_reference(cell)));
104 }
105
106 fn clear_freeze_panes(&mut self) {
107 self.workbook.clear_freeze_panes();
108 self.handle_scrolling();
109 self.add_notification("Freeze panes cleared".to_string());
110 }
111
112 fn handle_column_width_command(&mut self, cmd: &str) {
113 let parts: Vec<&str> = cmd.split_whitespace().collect();
114
115 if parts.len() < 2 {
116 self.add_notification("Usage: :cw [fit|min|number] [all]".to_string());
117 return;
118 }
119
120 let action = parts[1];
121 let apply_to_all = parts.len() > 2 && parts[2] == "all";
122
123 match action {
124 "fit" => {
125 if apply_to_all {
126 self.auto_adjust_column_width(None);
127 } else {
128 self.auto_adjust_column_width(Some(self.selected_cell.1));
129 }
130 }
131 "min" => {
132 if apply_to_all {
133 let sheet = self.workbook.get_current_sheet();
135 for col in 1..=sheet.max_cols {
136 self.column_widths[col] = 5; }
138 self.add_notification("All columns set to minimum width".to_string());
139 } else {
140 let col = self.selected_cell.1;
142 self.column_widths[col] = 5; self.add_notification(format!("Column {} set to minimum width", col));
144 }
145 }
146 _ => {
147 if let Ok(width) = action.parse::<usize>() {
149 let col = self.selected_cell.1;
150 self.column_widths[col] = width.clamp(5, 50); self.add_notification(format!("Column {} width set to {}", col, width));
152 } else {
153 self.add_notification(format!("Invalid column width: {}", action));
154 }
155 }
156 }
157 }
158
159 fn handle_delete_row_command(&mut self, cmd: &str) {
160 let parts: Vec<&str> = cmd.split_whitespace().collect();
161
162 if parts.len() == 1 {
163 if let Err(e) = self.delete_current_row() {
165 self.add_notification(format!("Failed to delete row: {e}"));
166 }
167 return;
168 }
169
170 if parts.len() == 2 {
171 if let Ok(row) = parts[1].parse::<usize>() {
173 if let Err(e) = self.delete_row(row) {
174 self.add_notification(format!("Failed to delete row {}: {}", row, e));
175 }
176 } else {
177 self.add_notification(format!("Invalid row number: {}", parts[1]));
178 }
179 return;
180 }
181
182 if parts.len() == 3 {
183 if let (Ok(start_row), Ok(end_row)) =
185 (parts[1].parse::<usize>(), parts[2].parse::<usize>())
186 {
187 if let Err(e) = self.delete_rows(start_row, end_row) {
188 self.add_notification(format!(
189 "Failed to delete rows {} to {}: {}",
190 start_row, end_row, e
191 ));
192 }
193 } else {
194 self.add_notification("Invalid row range".to_string());
195 }
196 return;
197 }
198
199 self.add_notification("Usage: :dr [row] [end_row]".to_string());
200 }
201
202 fn handle_delete_column_command(&mut self, cmd: &str) {
203 let parts: Vec<&str> = cmd.split_whitespace().collect();
204
205 if parts.len() == 1 {
206 if let Err(e) = self.delete_current_column() {
208 self.add_notification(format!("Failed to delete column: {e}"));
209 }
210 return;
211 }
212
213 if parts.len() == 2 {
214 let col_str = parts[1].to_uppercase();
216
217 if let Some(col) = col_name_to_index(&col_str) {
219 if let Err(e) = self.delete_column(col) {
220 self.add_notification(format!("Failed to delete column {}: {}", col_str, e));
221 }
222 return;
223 }
224
225 if let Ok(col) = col_str.parse::<usize>() {
227 if let Err(e) = self.delete_column(col) {
228 self.add_notification(format!("Failed to delete column {}: {}", col, e));
229 }
230 return;
231 }
232
233 self.add_notification(format!("Invalid column: {}", col_str));
234 return;
235 }
236
237 if parts.len() == 3 {
238 let start_col_str = parts[1].to_uppercase();
240 let end_col_str = parts[2].to_uppercase();
241
242 let start_col =
243 col_name_to_index(&start_col_str).or_else(|| start_col_str.parse::<usize>().ok());
244 let end_col =
245 col_name_to_index(&end_col_str).or_else(|| end_col_str.parse::<usize>().ok());
246
247 if let (Some(start), Some(end)) = (start_col, end_col) {
248 if let Err(e) = self.delete_columns(start, end) {
249 self.add_notification(format!(
250 "Failed to delete columns {} to {}: {}",
251 start_col_str, end_col_str, e
252 ));
253 }
254 } else {
255 self.add_notification("Invalid column range".to_string());
256 }
257 return;
258 }
259
260 self.add_notification("Usage: :dc [col] [end_col]".to_string());
261 }
262
263 fn handle_json_export_command(&mut self, cmd: &str) {
264 let export_all = cmd.starts_with("eja ") || cmd == "eja";
266
267 let parts: Vec<&str> = if cmd.starts_with("ej ") {
269 cmd.strip_prefix("ej ")
270 .unwrap()
271 .split_whitespace()
272 .collect()
273 } else if cmd == "ej" {
274 vec!["h", "1"] } else if cmd.starts_with("eja ") {
277 cmd.strip_prefix("eja ")
278 .unwrap()
279 .split_whitespace()
280 .collect()
281 } else if cmd == "eja" {
282 vec!["h", "1"] } else {
285 self.add_notification("Invalid JSON export command".to_string());
286 return;
287 };
288
289 if parts.len() < 2 {
291 if export_all {
292 self.add_notification("Usage: :eja [h|v] [rows]".to_string());
293 } else {
294 self.add_notification("Usage: :ej [h|v] [rows]".to_string());
295 }
296 return;
297 }
298
299 let direction_str = parts[0];
300 let header_count_str = parts[1];
301
302 let direction = match direction_str.parse::<HeaderDirection>() {
303 Ok(dir) => dir,
304 Err(_) => {
305 self.add_notification(format!(
306 "Invalid header direction: {}. Use 'h' or 'v'",
307 direction_str
308 ));
309 return;
310 }
311 };
312
313 let header_count = match header_count_str.parse::<usize>() {
314 Ok(count) => count,
315 Err(_) => {
316 self.add_notification(format!("Invalid header count: {}", header_count_str));
317 return;
318 }
319 };
320
321 let sheet_name = self.workbook.get_current_sheet_name();
322
323 let file_path = self.workbook.get_file_path().to_string();
324 let original_file = Path::new(&file_path);
325 let file_stem = original_file
326 .file_stem()
327 .and_then(|s| s.to_str())
328 .unwrap_or("export");
329
330 let parent_dir = original_file.parent().unwrap_or_else(|| Path::new(""));
331
332 let now = chrono::Local::now();
333 let timestamp = now.format("%Y%m%d_%H%M%S").to_string();
334
335 let filename = if export_all {
336 format!("{}_all_sheets_{}.json", file_stem, timestamp)
337 } else {
338 format!("{}_sheet_{}_{}.json", file_stem, sheet_name, timestamp)
339 };
340
341 let new_filepath = parent_dir.join(filename);
343
344 let result = if export_all {
346 export_all_sheets_json(&self.workbook, direction, header_count, &new_filepath)
347 } else {
348 export_json(
349 self.workbook.get_current_sheet(),
350 direction,
351 header_count,
352 &new_filepath,
353 )
354 };
355
356 match result {
357 Ok(_) => {
358 self.add_notification(format!("Exported to {}", new_filepath.display()));
359 }
360 Err(e) => {
361 self.add_notification(format!("Export failed: {e}"));
362 }
363 }
364 }
365
366 fn jump_to_cell(&mut self, cell_ref: (usize, usize)) {
367 let (row, col) = cell_ref; if row > EXCEL_MAX_ROWS || col > EXCEL_MAX_COLS {
370 self.add_notification(format!(
371 "Cell reference out of range: {}",
372 cell_reference(cell_ref)
373 ));
374 return;
375 }
376
377 self.selected_cell = (row, col);
378 self.handle_scrolling();
379
380 self.add_notification(format!("Jumped to cell {}{}", index_to_col_name(col), row));
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::parse_cell_reference;
387 use crate::app::AppState;
388 use crate::excel::{Cell, FreezePanes, Sheet, Workbook, EXCEL_MAX_COLS, EXCEL_MAX_ROWS};
389 use std::path::PathBuf;
390
391 fn app_with_sheet() -> AppState<'static> {
392 let mut data = vec![vec![Cell::empty(); 3]; 3];
393 data[1][1] = Cell::new("Name".to_string(), false);
394 data[1][2] = Cell::new("Name".to_string(), false);
395 data[2][1] = Cell::new("Ada".to_string(), false);
396 data[2][2] = Cell::new("10".to_string(), false);
397 let sheet = Sheet {
398 name: "Data".to_string(),
399 data,
400 max_rows: 2,
401 max_cols: 2,
402 is_loaded: true,
403 freeze_panes: FreezePanes::none(),
404 };
405
406 AppState::new(
407 Workbook::from_sheets_for_test(vec![sheet]),
408 PathBuf::from("test.xlsx"),
409 )
410 .unwrap()
411 }
412
413 #[test]
414 fn parses_valid_cell_references() {
415 assert_eq!(parse_cell_reference("A1"), Some((1, 1)));
416 assert_eq!(parse_cell_reference("BC12"), Some((12, 55)));
417 }
418
419 #[test]
420 fn ignores_commands_with_non_ascii_arguments() {
421 assert_eq!(parse_cell_reference("addsheet 测试1"), None);
422 assert_eq!(parse_cell_reference("测试1"), None);
423 }
424
425 #[test]
426 fn cell_reference_command_can_jump_to_blank_cell_beyond_used_range() {
427 let mut app = app_with_sheet();
428 app.input_buffer = "A3".to_string();
429
430 app.execute_command();
431
432 assert_eq!(app.selected_cell, (3, 1));
433 assert_eq!(app.get_cell_content(3, 1), "");
434 assert_eq!(
435 app.notification_messages.last().map(String::as_str),
436 Some("Jumped to cell A3")
437 );
438 }
439
440 #[test]
441 fn cell_reference_command_can_jump_to_excel_bottom_right_cell() {
442 let mut app = app_with_sheet();
443 app.input_buffer = "XFD1048576".to_string();
444
445 app.execute_command();
446
447 assert_eq!(app.selected_cell, (EXCEL_MAX_ROWS, EXCEL_MAX_COLS));
448 assert_eq!(app.get_cell_content(EXCEL_MAX_ROWS, EXCEL_MAX_COLS), "");
449 }
450
451 #[test]
452 fn cell_reference_command_rejects_cells_beyond_excel_bounds() {
453 let mut app = app_with_sheet();
454 app.input_buffer = "XFE1048577".to_string();
455
456 app.execute_command();
457
458 assert_eq!(app.selected_cell, (1, 1));
459 assert_eq!(
460 app.notification_messages.last().map(String::as_str),
461 Some("Cell reference out of range: XFE1048577")
462 );
463 }
464
465 #[test]
466 fn freeze_command_uses_current_cell_and_marks_workbook_modified() {
467 let mut app = app_with_sheet();
468 app.selected_cell = (2, 2);
469 app.input_buffer = "freeze".to_string();
470
471 app.execute_command();
472
473 let sheet = app.workbook.get_current_sheet();
474 assert_eq!(sheet.freeze_panes.rows, 1);
475 assert_eq!(sheet.freeze_panes.cols, 1);
476 assert!(app.workbook.is_modified());
477 assert!(app.undo_history.all_undone());
478 }
479
480 #[test]
481 fn freeze_command_accepts_explicit_cell_and_a1_clears() {
482 let mut app = app_with_sheet();
483
484 app.input_buffer = "freeze B2".to_string();
485 app.execute_command();
486 assert_eq!(
487 app.workbook.get_current_sheet().freeze_panes.split_cell(),
488 (2, 2)
489 );
490
491 app.input_buffer = "freeze A1".to_string();
492 app.execute_command();
493 assert!(!app.workbook.get_current_sheet().freeze_panes.is_frozen());
494 }
495
496 #[test]
497 fn unfreeze_command_clears_freeze_panes() {
498 let mut app = app_with_sheet();
499 app.workbook.set_freeze_panes(1, 1);
500
501 app.input_buffer = "unfreeze".to_string();
502 app.execute_command();
503
504 assert!(!app.workbook.get_current_sheet().freeze_panes.is_frozen());
505 }
506}