From c62fd0804365b26f7664d47c6e3627b8eee97338 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 4 Dec 2024 18:51:18 -0500 Subject: [PATCH] feat: range copy mode --- src/ui/mod.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++ src/ui/render/mod.rs | 1 + src/ui/test.rs | 63 ++++++++++++++++++++++++++++++++++- 3 files changed, 142 insertions(+), 1 deletion(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c12b2a7..a7a3e2e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -30,6 +30,7 @@ pub enum Modality { CellEdit, Command, Dialog, + RangeCopy, } #[derive(Debug)] @@ -38,6 +39,9 @@ pub struct AppState<'ws> { pub viewport_state: ViewportState, pub command_state: TextState<'ws>, pub numeric_prefix: Vec, + pub original_location: Option
, + pub start_range: Option
, + pub end_range: Option
, dirty: bool, popup: Vec, } @@ -49,6 +53,9 @@ impl<'ws> Default for AppState<'ws> { viewport_state: Default::default(), command_state: Default::default(), numeric_prefix: Default::default(), + original_location: Default::default(), + start_range: Default::default(), + end_range: Default::default(), dirty: Default::default(), popup: Default::default(), } @@ -197,6 +204,7 @@ impl<'ws> Workspace<'ws> { Modality::CellEdit => self.handle_edit_input(key)?, Modality::Command => self.handle_command_input(key)?, Modality::Dialog => self.handle_dialog_input(key)?, + Modality::RangeCopy => self.handle_range_copy_input(key)?, }; return Ok(result); } @@ -363,6 +371,57 @@ impl<'ws> Workspace<'ws> { self.state.numeric_prefix.push(digit); } + fn handle_range_copy_input(&mut self, key: event::KeyEvent) -> Result> { + if key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Esc => { + self.state.reset_n_prefix(); + } + KeyCode::Char(d) if d.is_ascii_digit() => { + self.handle_numeric_prefix(d); + } + KeyCode::Char('h') => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.move_left()?; + dbg!(&ws.book.location); + Ok(()) + })?; + } + KeyCode::Char('j') => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.move_down()?; + Ok(()) + })?; + } + KeyCode::Char('k') => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.move_up()?; + dbg!(&ws.book.location); + Ok(()) + })?; + } + KeyCode::Char('l') => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.move_right()?; + Ok(()) + })?; + } + KeyCode::Char(' ') => { + if self.state.start_range.is_none() { + self.state.start_range = dbg!(Some(self.book.location.clone())); + } else { + self.state.end_range = dbg!(Some(self.book.location.clone())); + self.exit_range_copy_mode()?; + } + } + _ => { + // moop + } + } + } + Ok(None) + } + fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { @@ -381,6 +440,9 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { self.save_file()?; } + KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { + self.enter_range_copy_mode(); + } KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { self.enter_dialog_mode(self.render_help_text()); } @@ -506,6 +568,14 @@ impl<'ws> Workspace<'ws> { self.state.popup = msg; self.state.modality_stack.push(Modality::Dialog); } + + fn enter_range_copy_mode(&mut self) { + self.state.original_location = Some(self.book.location.clone()); + self.state.start_range = None; + self.state.end_range = None; + self.state.modality_stack.push(Modality::RangeCopy); + } + fn enter_edit_mode(&mut self) { self.state.modality_stack.push(Modality::CellEdit); @@ -530,6 +600,14 @@ impl<'ws> Workspace<'ws> { self.state.pop_modality(); Ok(()) } + + fn exit_range_copy_mode(&mut self) -> Result<()> { + self.book.location = self.state.original_location.clone().expect("Missing original location after range copy"); + self.state.original_location = None; + self.state.pop_modality(); + Ok(()) + } + fn exit_edit_mode(&mut self, keep: bool) -> Result<()> { self.text_area.set_cursor_line_style(Style::default()); @@ -542,6 +620,7 @@ impl<'ws> Workspace<'ws> { self.text_area = reset_text_area(self.book.get_current_cell_contents()?); } self.state.dirty = false; + self.state.pop_modality(); Ok(()) } diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index 74f00c1..2c14f9e 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -95,6 +95,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { Modality::CellEdit => "edit", Modality::Command => "command", Modality::Dialog => "", + Modality::RangeCopy => "range-copy", }) .title_bottom( Line::from(format!( diff --git a/src/ui/test.rs b/src/ui/test.rs index dbba849..5ea6f66 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -1,6 +1,6 @@ use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; -use crate::ui::Modality; +use crate::ui::{Address, Modality}; use super::cmd::{parse, Cmd}; use super::Workspace; @@ -351,3 +351,64 @@ fn test_navigation_tab_next_numeric_prefix() .expect("Failed to handle 'Ctrl-n' key event"); assert_eq!("Sheet1", ws.book.get_sheet_name().expect("Failed to get sheet name")); } + +#[test] +fn test_range_copy() { + let mut ws = + Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook"); + assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); + + ws.book.move_to(&Address { row: 1, col: 1, }).expect("Failed to move to row"); + let original_loc = ws.book.location.clone(); + ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) + .expect("Failed to handle 'Ctrl-r' key event"); + assert_eq!(Some(&Modality::RangeCopy), ws.state.modality_stack.last()); + assert_eq!(Some(original_loc.clone()), ws.state.original_location); + assert!(ws.state.start_range.is_none()); + assert!(ws.state.end_range.is_none()); + + ws.handle_input(construct_key_event(KeyCode::Char('l'))) + .expect("Failed to handle 'l' key event"); + ws.handle_input(construct_key_event(KeyCode::Char(' '))) + .expect("Failed to handle ' ' key event"); + assert_eq!(Some(Address {row:1, col:2, }), ws.state.start_range); + + ws.handle_input(construct_key_event(KeyCode::Char('j'))) + .expect("Failed to handle 'j' key event"); + ws.handle_input(construct_key_event(KeyCode::Char(' '))) + .expect("Failed to handle ' ' key event"); + + assert!(ws.state.original_location.is_none()); + assert_eq!(Some(Address {row:1, col:2, }), ws.state.start_range); + assert_eq!(Some(Address {row:2, col:2, }), ws.state.end_range); + assert_eq!(original_loc, ws.book.location); + assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); + + ws.book.move_to(&Address { row: 5, col: 5, }).expect("Failed to move to row"); + let original_loc_2 = ws.book.location.clone(); + assert_eq!(Address { row: 5, col: 5 }, original_loc_2); + + ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) + .expect("Failed to handle 'Ctrl-r' key event"); + assert_eq!(Some(&Modality::RangeCopy), ws.state.modality_stack.last()); + assert_eq!(Some(original_loc_2.clone()), ws.state.original_location); + assert!(ws.state.start_range.is_none()); + assert!(ws.state.end_range.is_none()); + + ws.handle_input(construct_key_event(KeyCode::Char('h'))) + .expect("Failed to handle 'h' key event"); + ws.handle_input(construct_key_event(KeyCode::Char(' '))) + .expect("Failed to handle ' ' key event"); + assert_eq!(Some(Address {row:5, col:4, }), ws.state.start_range); + + ws.handle_input(construct_key_event(KeyCode::Char('k'))) + .expect("Failed to handle 'k' key event"); + ws.handle_input(construct_key_event(KeyCode::Char(' '))) + .expect("Failed to handle ' ' key event"); + + assert!(ws.state.original_location.is_none()); + assert_eq!(Some(Address {row:5, col:4, }), ws.state.start_range); + assert_eq!(Some(Address {row:4, col:4, }), ws.state.end_range); + assert_eq!(original_loc_2, ws.book.location); + assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); +}