From c62fd0804365b26f7664d47c6e3627b8eee97338 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 4 Dec 2024 18:51:18 -0500 Subject: [PATCH 1/6] 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()); +} From bb5d81106eb6711cfbde1f7a952a9a286a162a32 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 4 Dec 2024 20:02:08 -0500 Subject: [PATCH 2/6] docs: help text, spelling --- docs/index.md | 15 +++++ src/book/mod.rs | 7 +++ src/ui/mod.rs | 134 ++++++++++++++++++++++++++++++++++++------- src/ui/render/mod.rs | 2 +- src/ui/test.rs | 17 +++++- 5 files changed, 152 insertions(+), 23 deletions(-) diff --git a/docs/index.md b/docs/index.md index 83db44d..d5b807a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -76,6 +76,9 @@ For the most part this should work the same way you expect a spreadsheet to work * `Enter` will update the cell contents. * `Esc` will cancel editing the cell and leave it unedited. +`Ctrl-r` will enter range select mode when editing a formula. You can navigate around the +sheet and hit space to select that cell in the sheet to set the start of the range. Navigate some more and hit space to set the end of the range. + You can find the functions we support documented here: [ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html) ### Command Mode @@ -94,3 +97,15 @@ The currently supported commands are: * `quit` Quits the application. `q` is a shorthand alias for this command. + +### Range Select Mode + +Range Select mode copies a range reference for use later. You can enter range select mode from CellEdit mode with `CTRL-r`. + +* `h`, `j`, `k`, `l` will navigate around the sheet. +* `Ctrl-n`, `Ctrl-p` will navigate between sheets. +* ` ` the spacebar will select the start and end of the range respectively. + +When you have selected the end of the range you will exit range select mode and the range reference will be placed into the cell contents you are editing. + + diff --git a/src/book/mod.rs b/src/book/mod.rs index 5e481b7..8602ea2 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -287,6 +287,13 @@ impl Book { .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?) } + pub(crate) fn get_sheet_name_by_idx(&self, idx: usize) -> Result<&str> { + Ok(&self + .model + .workbook + .worksheet(idx as u32) + .map_err(|s| anyhow!("Invalid Worksheet: {}", s))?.name) + } pub(crate) fn get_sheet_by_idx_mut(&mut self, idx: usize) -> Result<&mut Worksheet> { Ok(self .model diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a7a3e2e..4f467e2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -30,7 +30,7 @@ pub enum Modality { CellEdit, Command, Dialog, - RangeCopy, + RangeSelect, } #[derive(Debug)] @@ -40,6 +40,8 @@ pub struct AppState<'ws> { pub command_state: TextState<'ws>, pub numeric_prefix: Vec, pub original_location: Option
, + pub original_sheet: Option, + pub range_sheet: Option, pub start_range: Option
, pub end_range: Option
, dirty: bool, @@ -54,6 +56,8 @@ impl<'ws> Default for AppState<'ws> { command_state: Default::default(), numeric_prefix: Default::default(), original_location: Default::default(), + original_sheet: Default::default(), + range_sheet: Default::default(), start_range: Default::default(), end_range: Default::default(), dirty: Default::default(), @@ -104,6 +108,19 @@ impl Address { pub fn new(row: usize, col: usize) -> Self { Self { row, col } } + + pub fn to_range_part(&self) -> String { + let count = if self.col == 26 { + 1 + } else { + (self.col / 26) + 1 + }; + format!( + "{}{}", + render::viewport::COLNAMES[(self.col - 1) % 26].repeat(count), + self.row + ) + } } impl Default for Address { @@ -156,6 +173,33 @@ impl<'ws> Workspace<'ws> { Ok(()) } + pub fn selected_range_to_string(&self) -> String { + let state = &self.state; + let start = state + .start_range + .as_ref() + .map(|addr| addr.to_range_part()) + .unwrap_or_else(|| String::new()); + let end = state + .end_range + .as_ref() + .map(|addr| format!(":{}", addr.to_range_part())) + .unwrap_or_else(|| String::new()); + if let Some(range_sheet) = state.range_sheet { + if range_sheet != self.book.current_sheet { + return format!( + "{}!{}{}", + self.book + .get_sheet_name_by_idx(range_sheet as usize) + .expect("No such sheet index"), + start, + end + ); + } + } + format!("{}:{}", start, end) + } + /// Move a row down in the current sheet. pub fn move_down(&mut self) -> Result<()> { let mut loc = self.book.location.clone(); @@ -204,7 +248,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)?, + Modality::RangeSelect => self.handle_range_select_input(key)?, }; return Ok(result); } @@ -225,13 +269,14 @@ impl<'ws> Workspace<'ws> { "* CTRl-h: Shrink column width by 1".to_string(), "* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(), "* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(), - "* CTRl-?: Previous sheet. Starts over at end if at beginning.".to_string(), + "* ALT-h: Previous sheet. Starts over at end if at beginning.".to_string(), "* q exit".to_string(), "* Ctrl-S Save sheet".to_string(), ], Modality::CellEdit => vec![ "Edit Mode:".to_string(), "* ENTER/RETURN: Exit edit mode and save changes".to_string(), + "* Ctrl-r: Enter Range Selection mode".to_string(), "* ESC: Exit edit mode and discard changes".to_string(), "Otherwise edit as normal".to_string(), ], @@ -241,6 +286,14 @@ impl<'ws> Workspace<'ws> { "* CTRL-?: Exit command mode".to_string(), "* ENTER/RETURN: run command and exit command mode".to_string(), ], + Modality::RangeSelect => vec![ + "Range Selection Mode:".to_string(), + "* ESC: Exit command mode".to_string(), + "* h,j,k,l: vim style navigation".to_string(), + "* Spacebar: Select start and end of range".to_string(), + "* CTRl-n: Next sheet. Starts over at beginning if at end.".to_string(), + "* CTRl-p: Previous sheet. Starts over at end if at beginning.".to_string(), + ], _ => vec!["General help".to_string()], } } @@ -284,6 +337,15 @@ impl<'ws> Workspace<'ws> { self.enter_dialog_mode(self.render_help_text()); return Ok(None); } + KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { + self.enter_range_select_mode(); + return Ok(None); + } + KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { + self.text_area.set_yank_text(self.selected_range_to_string()); + self.text_area.paste(); + return Ok(None); + } KeyCode::Enter => self.exit_edit_mode(true)?, KeyCode::Esc => self.exit_edit_mode(false)?, _ => { @@ -371,19 +433,22 @@ impl<'ws> Workspace<'ws> { self.state.numeric_prefix.push(digit); } - fn handle_range_copy_input(&mut self, key: event::KeyEvent) -> Result> { + fn handle_range_select_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Esc => { self.state.reset_n_prefix(); } + KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { + self.enter_dialog_mode(self.render_help_text()); + return Ok(None); + } 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(()) })?; } @@ -396,7 +461,6 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('k') => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.move_up()?; - dbg!(&ws.book.location); Ok(()) })?; } @@ -408,12 +472,26 @@ impl<'ws> Workspace<'ws> { } KeyCode::Char(' ') => { if self.state.start_range.is_none() { - self.state.start_range = dbg!(Some(self.book.location.clone())); + self.state.start_range = Some(self.book.location.clone()); } else { - self.state.end_range = dbg!(Some(self.book.location.clone())); - self.exit_range_copy_mode()?; + self.state.end_range = Some(self.book.location.clone()); + self.exit_range_select_mode()?; } } + KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.book.select_next_sheet(); + Ok(()) + })?; + self.state.range_sheet = Some(self.book.current_sheet); + } + KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { + self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { + ws.book.select_prev_sheet(); + Ok(()) + })?; + self.state.range_sheet = Some(self.book.current_sheet); + } _ => { // moop } @@ -421,7 +499,7 @@ impl<'ws> Workspace<'ws> { } Ok(None) } - + fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { @@ -441,7 +519,7 @@ impl<'ws> Workspace<'ws> { self.save_file()?; } KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { - self.enter_range_copy_mode(); + self.enter_range_select_mode(); } KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { self.enter_dialog_mode(self.render_help_text()); @@ -549,7 +627,10 @@ impl<'ws> Workspace<'ws> { return Ok(None); } - fn run_with_prefix(&mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>) -> Result<(), anyhow::Error> { + fn run_with_prefix( + &mut self, + action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>, + ) -> Result<(), anyhow::Error> { for _ in 1..=self.state.get_n_prefix() { action(self)?; } @@ -568,15 +649,16 @@ impl<'ws> Workspace<'ws> { self.state.popup = msg; self.state.modality_stack.push(Modality::Dialog); } - - fn enter_range_copy_mode(&mut self) { + + fn enter_range_select_mode(&mut self) { + self.state.range_sheet = Some(self.book.current_sheet); + self.state.original_sheet = Some(self.book.current_sheet); 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); + self.state.modality_stack.push(Modality::RangeSelect); } - fn enter_edit_mode(&mut self) { self.state.modality_stack.push(Modality::CellEdit); self.text_area @@ -600,15 +682,27 @@ 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"); + + fn exit_range_select_mode(&mut self) -> Result<()> { + self.book.current_sheet = self + .state + .original_sheet + .clone() + .expect("Missing original sheet"); + self.book.location = self + .state + .original_location + .clone() + .expect("Missing original location after range copy"); self.state.original_location = None; self.state.pop_modality(); + if self.state.modality() == &Modality::CellEdit { + self.text_area.set_yank_text(self.selected_range_to_string()); + self.text_area.paste(); + } Ok(()) } - fn exit_edit_mode(&mut self, keep: bool) -> Result<()> { self.text_area.set_cursor_line_style(Style::default()); self.text_area.set_cursor_style(Style::default()); diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index 2c14f9e..af68aa8 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -95,7 +95,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> { Modality::CellEdit => "edit", Modality::Command => "command", Modality::Dialog => "", - Modality::RangeCopy => "range-copy", + Modality::RangeSelect => "range-copy", }) .title_bottom( Line::from(format!( diff --git a/src/ui/test.rs b/src/ui/test.rs index 5ea6f66..b5879b1 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -362,7 +362,7 @@ fn test_range_copy() { 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(&Modality::RangeSelect), 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()); @@ -390,7 +390,7 @@ fn test_range_copy() { 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(&Modality::RangeSelect), 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()); @@ -412,3 +412,16 @@ fn test_range_copy() { assert_eq!(original_loc_2, ws.book.location); assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); } + +#[test] +fn test_range_copy_mode_from_edit_mode() { + 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.handle_input(construct_key_event(KeyCode::Char('e'))) + .expect("Failed to handle 'e' key event"); + assert_eq!(Some(&Modality::CellEdit), ws.state.modality_stack.last()); + ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) + .expect("Failed to handle 'Ctrl-r' key event"); + assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last()); +} From 5f6f45141cadc4523c041360312b0ff022e33177 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 5 Dec 2024 14:26:52 -0500 Subject: [PATCH 3/6] fix: various bugs around cell edit state and docs --- docs/index.md | 60 ++++++++++++++++++++++++++++++++++++--------------- src/ui/mod.rs | 2 ++ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/docs/index.md b/docs/index.md index d5b807a..aea95d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,11 +22,14 @@ Options: ## User Interface -The sheetui user interface is loosely inspired by vim. It is a modal interface that is entirely keyboard driven. At nearly any time you can type `Alt-h` to get some context sensitive help. +The sheetui user interface is loosely inspired by vim. It is a modal interface +that is entirely keyboard driven. At nearly any time you can type `Alt-h` to +get some context sensitive help. ### Navigation Mode -The interface will start out in navigation mode. You can navigate around the table and between the sheets using the following keybinds: +The interface will start out in navigation mode. You can navigate around the +table and between the sheets using the following keybinds: **Cell Navigation** @@ -44,7 +47,9 @@ Sheet navigation moving will loop around when you reach the ends. **Numeric prefixes** -You can prefix each of the keybinds above with a numeric prefix to do them that many times. So typing `123h` will move to the left 123 times. Hitting `Esc` will clear the numeric prefix if you want to cancel it. +You can prefix each of the keybinds above with a numeric prefix to do them that +many times. So typing `123h` will move to the left 123 times. Hitting `Esc` +will clear the numeric prefix if you want to cancel it. **Modifying the Sheet or Cells** @@ -54,36 +59,51 @@ You can prefix each of the keybinds above with a numeric prefix to do them that **Other Keybindings** +* `Ctrl-r` will enter range selection mode * `Ctrl-s` will save the sheet. * `q` will exit the application. * `:` will enter CommandMode. - - + +Range selections made from navigation mode will be available to paste into a Cell Edit. + + ### CellEdit Mode -You enter CellEdit mode by hitting `e` or `i` while in navigation mode. Type what you want into the cell. +You enter CellEdit mode by hitting `e` or `i` while in navigation mode. Type +what you want into the cell. Starting with: * `=` will treat what you type as a formula. * `$` will treat it as us currency. -Typing a number will treat the contents as a number. While typing non-numeric text will treat it as text content. +Typing a number will treat the contents as a number. While typing non-numeric +text will treat it as text content. -For the most part this should work the same way you expect a spreadsheet to work. + + +For the most part this should work the same way you expect a spreadsheet to +work. * `Enter` will update the cell contents. * `Esc` will cancel editing the cell and leave it unedited. +* `Ctrl-p` will paste the range selection if it exists into the cell. -`Ctrl-r` will enter range select mode when editing a formula. You can navigate around the -sheet and hit space to select that cell in the sheet to set the start of the range. Navigate some more and hit space to set the end of the range. +`Ctrl-r` will enter range select mode when editing a formula. You can navigate +around the sheet and hit space to select that cell in the sheet to set the +start of the range. Navigate some more and hit space to set the end of the +range. -You can find the functions we support documented here: [ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html) +You can find the functions we support documented here: +[ironcalc docs](https://docs.ironcalc.com/functions/lookup-and-reference.html) ### Command Mode -You enter command mode by typing `:` while in navigation mode. You can then type a command and hit `Enter` to execute it or `Esc` to cancel. +You enter command mode by typing `:` while in navigation mode. You can then +type a command and hit `Enter` to execute it or `Esc` to cancel. The currently supported commands are: @@ -96,16 +116,22 @@ The currently supported commands are: * `edit ` Edit a new spreadsheet at the current path. `e` is a shorthand alias for this command. * `quit` Quits the application. `q` is a shorthand alias for this command. - + ### Range Select Mode -Range Select mode copies a range reference for use later. You can enter range select mode from CellEdit mode with `CTRL-r`. +Range Select mode copies a range reference for use later. You can enter range +select mode from CellEdit mode with `CTRL-r`. * `h`, `j`, `k`, `l` will navigate around the sheet. * `Ctrl-n`, `Ctrl-p` will navigate between sheets. -* ` ` the spacebar will select the start and end of the range respectively. +* `The spacebar will select the start and end of the range respectively. -When you have selected the end of the range you will exit range select mode and the range reference will be placed into the cell contents you are editing. +When you have selected the end of the range you will exit range select mode and +the range reference will be placed into the cell contents you are editing. - + diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4f467e2..bbd1163 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -344,6 +344,7 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { self.text_area.set_yank_text(self.selected_range_to_string()); self.text_area.paste(); + self.state.dirty = true; return Ok(None); } KeyCode::Enter => self.exit_edit_mode(true)?, @@ -699,6 +700,7 @@ impl<'ws> Workspace<'ws> { if self.state.modality() == &Modality::CellEdit { self.text_area.set_yank_text(self.selected_range_to_string()); self.text_area.paste(); + self.state.dirty = true; } Ok(()) } From 61a6e935155f312d7dcd75f605ed574400b1797f Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 5 Dec 2024 17:27:06 -0500 Subject: [PATCH 4/6] wip: bits of polish * show what you are selecting in range mode * Fix error with cell edit not updating after Enter --- src/ui/mod.rs | 129 ++++++++++++++++++++++++-------------- src/ui/render/mod.rs | 2 +- src/ui/render/test.rs | 50 +++++++++++---- src/ui/render/viewport.rs | 30 ++++++--- src/ui/test.rs | 28 ++++----- 5 files changed, 155 insertions(+), 84 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bbd1163..1e18d85 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -33,17 +33,40 @@ pub enum Modality { RangeSelect, } +#[derive(Debug, Default)] +pub struct RangeSelection { + pub original_location: Option
, + pub original_sheet: Option, + pub sheet: Option, + pub start: Option
, + pub end: Option
, +} + +impl RangeSelection { + pub fn get_range(&self) -> Option<(Address, Address)> { + if let (Some(start), Some(end)) = (&self.start, &self.end) { + return Some(( + Address { + row: std::cmp::min(start.row, end.row), + col: std::cmp::min(start.col, end.col), + }, + Address { + row: std::cmp::max(start.row, end.row), + col: std::cmp::max(start.col, end.col), + }, + )); + } + None + } +} + #[derive(Debug)] pub struct AppState<'ws> { pub modality_stack: Vec, pub viewport_state: ViewportState, pub command_state: TextState<'ws>, pub numeric_prefix: Vec, - pub original_location: Option
, - pub original_sheet: Option, - pub range_sheet: Option, - pub start_range: Option
, - pub end_range: Option
, + pub range_select: RangeSelection, dirty: bool, popup: Vec, } @@ -55,11 +78,7 @@ impl<'ws> Default for AppState<'ws> { viewport_state: Default::default(), command_state: Default::default(), numeric_prefix: Default::default(), - original_location: Default::default(), - original_sheet: Default::default(), - range_sheet: Default::default(), - start_range: Default::default(), - end_range: Default::default(), + range_select: Default::default(), dirty: Default::default(), popup: Default::default(), } @@ -175,29 +194,22 @@ impl<'ws> Workspace<'ws> { pub fn selected_range_to_string(&self) -> String { let state = &self.state; - let start = state - .start_range - .as_ref() - .map(|addr| addr.to_range_part()) - .unwrap_or_else(|| String::new()); - let end = state - .end_range - .as_ref() - .map(|addr| format!(":{}", addr.to_range_part())) - .unwrap_or_else(|| String::new()); - if let Some(range_sheet) = state.range_sheet { - if range_sheet != self.book.current_sheet { - return format!( - "{}!{}{}", - self.book - .get_sheet_name_by_idx(range_sheet as usize) - .expect("No such sheet index"), - start, - end - ); + if let Some((start, end)) = state.range_select.get_range() { + let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part())); + if let Some(range_sheet) = state.range_select.sheet { + if range_sheet != self.book.current_sheet { + return format!( + "{}!{}", + self.book + .get_sheet_name_by_idx(range_sheet as usize) + .expect("No such sheet index"), + a1 + ); + } } + return a1; } - format!("{}:{}", start, end) + return String::new() } /// Move a row down in the current sheet. @@ -342,7 +354,8 @@ impl<'ws> Workspace<'ws> { return Ok(None); } KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { - self.text_area.set_yank_text(self.selected_range_to_string()); + self.text_area + .set_yank_text(self.selected_range_to_string()); self.text_area.paste(); self.state.dirty = true; return Ok(None); @@ -438,7 +451,13 @@ impl<'ws> Workspace<'ws> { if key.kind == KeyEventKind::Press { match key.code { KeyCode::Esc => { - self.state.reset_n_prefix(); + if self.state.numeric_prefix.len() > 0 { + self.state.reset_n_prefix(); + } else { + self.state.range_select.start = None; + self.state.range_select.end = None; + self.exit_range_select_mode()?; + } } KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { self.enter_dialog_mode(self.render_help_text()); @@ -452,46 +471,52 @@ impl<'ws> Workspace<'ws> { ws.move_left()?; Ok(()) })?; + self.maybe_update_range_end(); } KeyCode::Char('j') => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.move_down()?; Ok(()) })?; + self.maybe_update_range_end(); } KeyCode::Char('k') => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.move_up()?; Ok(()) })?; + self.maybe_update_range_end(); } KeyCode::Char('l') => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.move_right()?; Ok(()) })?; + self.maybe_update_range_end(); } - KeyCode::Char(' ') => { - if self.state.start_range.is_none() { - self.state.start_range = Some(self.book.location.clone()); + KeyCode::Char(' ') | KeyCode::Enter => { + if self.state.range_select.start.is_none() { + self.state.range_select.start = Some(self.book.location.clone()); } else { - self.state.end_range = Some(self.book.location.clone()); + self.state.range_select.end = Some(self.book.location.clone()); self.exit_range_select_mode()?; } } KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { + // TODO(jwall): We should reset our range selections. self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_next_sheet(); Ok(()) })?; - self.state.range_sheet = Some(self.book.current_sheet); + self.state.range_select.sheet = Some(self.book.current_sheet); } KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { + // TODO(jwall): We should reset our range selections. self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_prev_sheet(); Ok(()) })?; - self.state.range_sheet = Some(self.book.current_sheet); + self.state.range_select.sheet = Some(self.book.current_sheet); } _ => { // moop @@ -501,6 +526,12 @@ impl<'ws> Workspace<'ws> { Ok(None) } + fn maybe_update_range_end(&mut self) { + if self.state.range_select.start.is_some() { + self.state.range_select.end = Some(self.book.location.clone()); + } + } + fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result> { if key.kind == KeyEventKind::Press { match key.code { @@ -652,11 +683,11 @@ impl<'ws> Workspace<'ws> { } fn enter_range_select_mode(&mut self) { - self.state.range_sheet = Some(self.book.current_sheet); - self.state.original_sheet = Some(self.book.current_sheet); - self.state.original_location = Some(self.book.location.clone()); - self.state.start_range = None; - self.state.end_range = None; + self.state.range_select.sheet = Some(self.book.current_sheet); + self.state.range_select.original_sheet = Some(self.book.current_sheet); + self.state.range_select.original_location = Some(self.book.location.clone()); + self.state.range_select.start = None; + self.state.range_select.end = None; self.state.modality_stack.push(Modality::RangeSelect); } @@ -687,18 +718,21 @@ impl<'ws> Workspace<'ws> { fn exit_range_select_mode(&mut self) -> Result<()> { self.book.current_sheet = self .state + .range_select .original_sheet .clone() .expect("Missing original sheet"); self.book.location = self .state + .range_select .original_location .clone() .expect("Missing original location after range copy"); - self.state.original_location = None; + self.state.range_select.original_location = None; self.state.pop_modality(); if self.state.modality() == &Modality::CellEdit { - self.text_area.set_yank_text(self.selected_range_to_string()); + self.text_area + .set_yank_text(self.selected_range_to_string()); self.text_area.paste(); self.state.dirty = true; } @@ -712,9 +746,8 @@ impl<'ws> Workspace<'ws> { if self.state.dirty && keep { self.book.edit_current_cell(contents)?; self.book.evaluate(); - } else { - self.text_area = reset_text_area(self.book.get_current_cell_contents()?); } + 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 af68aa8..bedf8e6 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -44,7 +44,7 @@ impl<'ws> Workspace<'ws> { Box::new(move |rect: Rect, buf: &mut Buffer, ws: &mut Self| { let sheet_name = ws.book.get_sheet_name().unwrap_or("Unknown"); let table_block = Block::bordered().title_top(sheet_name); - let viewport = Viewport::new(&ws.book) + let viewport = Viewport::new(&ws.book, &ws.state.range_select) .with_selected(ws.book.location.clone()) .block(table_block); StatefulWidget::render(viewport, rect, buf, &mut ws.state.viewport_state); diff --git a/src/ui/render/test.rs b/src/ui/render/test.rs index 68a5006..001e389 100644 --- a/src/ui/render/test.rs +++ b/src/ui/render/test.rs @@ -1,15 +1,23 @@ use ironcalc::base::Model; +use crate::ui::AppState; + use super::{Address, Book, Viewport, ViewportState}; #[test] fn test_viewport_get_visible_columns() { let mut state = ViewportState::default(); - let book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model")); + let book = Book::new( + Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"), + ); let default_size = book.get_col_size(1).expect("Failed to get column size"); let width = dbg!(dbg!(default_size) * 12 / 2); - let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 }); - let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); + let app_state = AppState::default(); + let viewport = + Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 17 }); + let cols = viewport + .get_visible_columns((width + 5) as u16, &mut state) + .expect("Failed to get visible columns"); assert_eq!(5, cols.len()); assert_eq!(17, cols.last().expect("Failed to get last column").idx); } @@ -17,32 +25,50 @@ fn test_viewport_get_visible_columns() { #[test] fn test_viewport_get_visible_rows() { let mut state = dbg!(ViewportState::default()); - let book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model")); + let book = Book::new( + Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"), + ); let height = 6; - let viewport = Viewport::new(&book).with_selected(Address { row: 17, col: 1 }); + let app_state = AppState::default(); + let viewport = + Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 17, col: 1 }); let rows = dbg!(viewport.get_visible_rows(height as u16, &mut state)); assert_eq!(height - 1, rows.len()); - assert_eq!(17 - (height - 2), *rows.first().expect("Failed to get first row")); + assert_eq!( + 17 - (height - 2), + *rows.first().expect("Failed to get first row") + ); assert_eq!(17, *rows.last().expect("Failed to get last row")); } #[test] fn test_viewport_visible_columns_after_length_change() { let mut state = ViewportState::default(); - let mut book = Book::new(Model::new_empty("test", "en", "America/New_York").expect("Failed to make model")); + let mut book = Book::new( + Model::new_empty("test", "en", "America/New_York").expect("Failed to make model"), + ); let default_size = book.get_col_size(1).expect("Failed to get column size"); let width = dbg!(dbg!(default_size) * 12 / 2); { - let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 17 }); - let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); + let app_state = AppState::default(); + let viewport = Viewport::new(&book, &app_state.range_select) + .with_selected(Address { row: 1, col: 17 }); + let cols = viewport + .get_visible_columns((width + 5) as u16, &mut state) + .expect("Failed to get visible columns"); assert_eq!(5, cols.len()); assert_eq!(17, cols.last().expect("Failed to get last column").idx); } - book.set_col_size(1, default_size * 6).expect("Failed to set column size"); + book.set_col_size(1, default_size * 6) + .expect("Failed to set column size"); { - let viewport = Viewport::new(&book).with_selected(Address { row: 1, col: 1 }); - let cols = viewport.get_visible_columns((width + 5) as u16, &mut state).expect("Failed to get visible columns"); + let app_state = AppState::default(); + let viewport = + Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 1 }); + let cols = viewport + .get_visible_columns((width + 5) as u16, &mut state) + .expect("Failed to get visible columns"); assert_eq!(1, cols.len()); assert_eq!(1, cols.last().expect("Failed to get last column").idx); } diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index 38b7653..aaed5d3 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -7,7 +7,7 @@ use ratatui::{ widgets::{Block, Cell, Row, StatefulWidget, Table, Widget}, }; -use super::{Address, Book}; +use super::{Address, AppState, Book, RangeSelection}; // TODO(zaphar): Move this to the book module. // NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it @@ -34,10 +34,11 @@ pub struct ViewportState { } /// A renderable viewport over a book. -pub struct Viewport<'book> { +pub struct Viewport<'ws> { pub(crate) selected: Address, - book: &'book Book, - block: Option>, + book: &'ws Book, + range_selection: &'ws RangeSelection, + block: Option>, } pub(crate) const COLNAMES: [&'static str; 26] = [ @@ -45,10 +46,11 @@ pub(crate) const COLNAMES: [&'static str; 26] = [ "T", "U", "V", "W", "X", "Y", "Z", ]; -impl<'book> Viewport<'book> { - pub fn new(book: &'book Book) -> Self { +impl<'ws> Viewport<'ws> { + pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self { Self { book, + range_selection: app_state, selected: Default::default(), block: None, } @@ -127,7 +129,7 @@ impl<'book> Viewport<'book> { return Ok(visible); } - pub fn block(mut self, block: Block<'book>) -> Self { + pub fn block(mut self, block: Block<'ws>) -> Self { self.block = Some(block); self } @@ -158,7 +160,17 @@ impl<'book> Viewport<'book> { .book .get_cell_addr_rendered(&Address { row: ri, col: *ci }) .unwrap(); - let cell = Cell::new(Text::raw(content)); + let mut cell = Cell::new(Text::raw(content)); + if let Some((start, end)) = &self.range_selection.get_range() { + if ri >= start.row + && ri <= end.row + && *ci >= start.col + && *ci <= end.col + { + // This is a selected range + cell = cell.fg(Color::Black).bg(Color::LightBlue) + } + } match (self.book.location.row == ri, self.book.location.col == *ci) { (true, true) => cell.fg(Color::White).bg(Color::Rgb(57, 61, 71)), _ => cell, @@ -197,7 +209,7 @@ impl<'book> Viewport<'book> { } } -impl<'book> StatefulWidget for Viewport<'book> { +impl<'ws> StatefulWidget for Viewport<'ws> { type State = ViewportState; fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { diff --git a/src/ui/test.rs b/src/ui/test.rs index b5879b1..47ed76e 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -363,24 +363,24 @@ fn test_range_copy() { ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) .expect("Failed to handle 'Ctrl-r' key event"); assert_eq!(Some(&Modality::RangeSelect), 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()); + assert_eq!(Some(original_loc.clone()), ws.state.range_select.original_location); + assert!(ws.state.range_select.start.is_none()); + assert!(ws.state.range_select.end.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); + assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start); 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!(ws.state.range_select.original_location.is_none()); + assert_eq!(Some(Address {row:1, col:2, }), ws.state.range_select.start); + assert_eq!(Some(Address {row:2, col:2, }), ws.state.range_select.end); assert_eq!(original_loc, ws.book.location); assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); @@ -391,24 +391,24 @@ fn test_range_copy() { ws.handle_input(construct_modified_key_event(KeyCode::Char('r'), KeyModifiers::CONTROL)) .expect("Failed to handle 'Ctrl-r' key event"); assert_eq!(Some(&Modality::RangeSelect), 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()); + assert_eq!(Some(original_loc_2.clone()), ws.state.range_select.original_location); + assert!(ws.state.range_select.start.is_none()); + assert!(ws.state.range_select.end.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); + assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start); 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!(ws.state.range_select.original_location.is_none()); + assert_eq!(Some(Address {row:5, col:4, }), ws.state.range_select.start); + assert_eq!(Some(Address {row:4, col:4, }), ws.state.range_select.end); assert_eq!(original_loc_2, ws.book.location); assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last()); } From 4b3e25895d9f66470e25a451303bda324601ccfb Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 5 Dec 2024 17:41:36 -0500 Subject: [PATCH 5/6] chore: cleanup unused imports --- src/ui/render/viewport.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index aaed5d3..32de8b6 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -7,7 +7,7 @@ use ratatui::{ widgets::{Block, Cell, Row, StatefulWidget, Table, Widget}, }; -use super::{Address, AppState, Book, RangeSelection}; +use super::{Address, Book, RangeSelection}; // TODO(zaphar): Move this to the book module. // NOTE(zaphar): This is stolen from ironcalc but ironcalc doesn't expose it From a85c5121ee1bf1d999037fca2c355dee79983dc9 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Thu, 5 Dec 2024 17:41:36 -0500 Subject: [PATCH 6/6] wip: handle range resets when switching sheets --- src/ui/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1e18d85..01ff5c1 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -58,6 +58,12 @@ impl RangeSelection { } None } + + pub fn reset_range_selection(&mut self) { + self.start = None; + self.end = None; + self.sheet = None; + } } #[derive(Debug)] @@ -503,7 +509,7 @@ impl<'ws> Workspace<'ws> { } } KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { - // TODO(jwall): We should reset our range selections. + self.state.range_select.reset_range_selection(); self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_next_sheet(); Ok(()) @@ -511,7 +517,7 @@ impl<'ws> Workspace<'ws> { self.state.range_select.sheet = Some(self.book.current_sheet); } KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { - // TODO(jwall): We should reset our range selections. + self.state.range_select.reset_range_selection(); self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_prev_sheet(); Ok(())