From 29270c800cb644538a5718932d1ffb4676e3863f Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Fri, 6 Dec 2024 17:23:04 -0500 Subject: [PATCH 1/8] wip: cell deletions --- docs/index.md | 6 +++++- src/book/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index c4fcc75..dc9762d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,6 +44,8 @@ table and between the sheets using the following keybinds: * `l` and, ➡️ will move one cell to the right. * `j`, ⬇️, and `Enter` will move one cell down. * `k` ⬆️, will move one cell up. +* `d` will delete the contents of the selected cell leaving style untouched +* `D` will delete the contents of the selected cell including any style **Sheet Navigation** @@ -129,12 +131,14 @@ will be discarded if you have not saved first. ### Range Select Mode -Range Select mode copies a range reference for use later. You can enter range +Range Select mode copies a range reference for use later or delete a range's contents. 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. +* `d` will delete the contents of the range leaving any style untouched +* `D` will delete the contents of the range including any style 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 8602ea2..6c00a4f 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -104,6 +104,48 @@ impl Book { Ok(()) } + pub fn clear_current_cell(&mut self) -> Result<()> { + self.clear_cell_contents(self.current_sheet as u32, self.location.clone()) + } + + pub fn clear_current_cell_all(&mut self) -> Result<()> { + self.clear_cell_all(self.current_sheet as u32, self.location.clone()) + } + + + pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> { + Ok(self + .model + .cell_clear_contents(sheet, row as i32, col as i32) + .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?) + } + + pub fn clear_cell_range(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> { + for row in start.row..=end.row { + for col in start.col..=end.col { + self.clear_cell_contents(sheet, Address { row, col })?; + } + } + Ok(()) + } + + pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> { + Ok(self + .model + .cell_clear_all(sheet, row as i32, col as i32) + .map_err(|s| anyhow!("Unable to clear cell contents {}", s))?) + } + + pub fn clear_cell_range_all(&mut self, sheet: u32, start: Address, end: Address) -> Result<()> { + for row in start.row..=end.row { + for col in start.col..=end.col { + self.clear_cell_all(sheet, Address { row, col })?; + } + } + Ok(()) + } + + /// Get a cells formatted content. pub fn get_current_cell_rendered(&self) -> Result { Ok(self.get_cell_addr_rendered(&self.location)?) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 4536687..9975c94 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -201,7 +201,11 @@ impl<'ws> Workspace<'ws> { pub fn selected_range_to_string(&self) -> String { let state = &self.state; if let Some((start, end)) = state.range_select.get_range() { - let a1 = format!("{}{}", start.to_range_part(), format!(":{}", end.to_range_part())); + 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!( @@ -215,7 +219,7 @@ impl<'ws> Workspace<'ws> { } return a1; } - return String::new() + return String::new(); } /// Move a row down in the current sheet. @@ -281,6 +285,8 @@ impl<'ws> Workspace<'ws> { "* ENTER/RETURN: Go down one cell".to_string(), "* TAB: Go over one cell".to_string(), "* h,j,k,l: vim style navigation".to_string(), + "* d: clear cell contents leaving style untouched".to_string(), + "* D: clear cell contents including style".to_string(), "* CTRl-r: Add a row".to_string(), "* CTRl-c: Add a column".to_string(), "* CTRl-l: Grow column width by 1".to_string(), @@ -308,6 +314,8 @@ impl<'ws> Workspace<'ws> { "Range Selection Mode:".to_string(), "* ESC: Exit command mode".to_string(), "* h,j,k,l: vim style navigation".to_string(), + "* d: delete the contents of the range leaving style untouched".to_string(), + "* D: clear cell contents including style".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(), @@ -471,6 +479,30 @@ impl<'ws> Workspace<'ws> { KeyCode::Char(d) if d.is_ascii_digit() => { self.handle_numeric_prefix(d); } + KeyCode::Char('D') => { + if let Some((start, end)) = self.state.range_select.get_range() { + self.book.clear_cell_range_all( + self.state + .range_select + .sheet + .unwrap_or_else(|| self.book.current_sheet), + start, + end, + )?; + } + } + KeyCode::Char('d') => { + if let Some((start, end)) = self.state.range_select.get_range() { + self.book.clear_cell_range( + self.state + .range_select + .sheet + .unwrap_or_else(|| self.book.current_sheet), + start, + end, + )?; + } + } KeyCode::Char('h') => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.move_left()?; @@ -567,6 +599,12 @@ impl<'ws> Workspace<'ws> { Ok(()) })?; } + KeyCode::Char('d') => { + self.book.clear_current_cell()?; + } + KeyCode::Char('D') => { + self.book.clear_current_cell_all()?; + } KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> { ws.book.select_prev_sheet(); From 37ce1ba7af474e06aa75c2fca8bf6ed10ec8789b Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sun, 8 Dec 2024 20:34:08 -0500 Subject: [PATCH 2/8] wip: copy-paste internally --- docs/index.md | 5 ++ src/book/mod.rs | 15 ++++- src/book/test.rs | 10 +-- src/ui/mod.rs | 168 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 184 insertions(+), 14 deletions(-) diff --git a/docs/index.md b/docs/index.md index dc9762d..186123e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -70,6 +70,9 @@ will clear the numeric prefix if you want to cancel it. * `Ctrl-r` will enter range selection mode * `Ctrl-s` will save the sheet. +* `Ctrl-c`, `y` Copy the cell or range contents. +* `Ctrl-v`, `p` Paste into the sheet. +* `Ctrl-Shift-C` Copy the cell or range formatted content. * `q` will exit the application. * `:` will enter CommandMode. @@ -136,6 +139,8 @@ 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. +* `Ctrl-c`, `y` Copy the cell or range contents. +* `Ctrl-Shift-C`, 'Y' Copy the cell or range formatted content. * `The spacebar will select the start and end of the range respectively. * `d` will delete the contents of the range leaving any style untouched * `D` will delete the contents of the range including any style diff --git a/src/book/mod.rs b/src/book/mod.rs index 6c00a4f..8b345d6 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -158,6 +158,15 @@ impl Book { .get_formatted_cell_value(self.current_sheet, *row as i32, *col as i32) .map_err(|s| anyhow!("Unable to format cell {}", s))?) } + + /// Get a cells actual content unformatted as a string. + pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result { + Ok(self + .model + .get_cell_content(self.current_sheet, *row as i32, *col as i32) + .map_err(|s| anyhow!("Unable to format cell {}", s))?) + } + /// Get a cells actual content as a string. pub fn get_current_cell_contents(&self) -> Result { @@ -174,13 +183,13 @@ impl Book { /// Update the current cell in a book. /// This update won't be reflected until you call `Book::evaluate`. pub fn edit_current_cell>(&mut self, value: S) -> Result<()> { - self.update_entry(&self.location.clone(), value)?; + self.update_cell(&self.location.clone(), value)?; Ok(()) } /// Update an entry in the current sheet for a book. /// This update won't be reflected until you call `Book::evaluate`. - pub fn update_entry>(&mut self, location: &Address, value: S) -> Result<()> { + pub fn update_cell>(&mut self, location: &Address, value: S) -> Result<()> { self.model .set_user_input( self.current_sheet, @@ -349,7 +358,7 @@ impl Default for Book { fn default() -> Self { let mut book = Book::new(Model::new_empty("default_name", "en", "America/New_York").unwrap()); - book.update_entry(&Address { row: 1, col: 1 }, "").unwrap(); + book.update_cell(&Address { row: 1, col: 1 }, "").unwrap(); book } } diff --git a/src/book/test.rs b/src/book/test.rs index 510e1b6..f1cf22d 100644 --- a/src/book/test.rs +++ b/src/book/test.rs @@ -36,7 +36,7 @@ fn test_book_default() { #[test] fn test_book_insert_cell_new_row() { let mut book = Book::default(); - book.update_entry(&Address { row: 2, col: 1 }, "1") + book.update_cell(&Address { row: 2, col: 1 }, "1") .expect("failed to edit cell"); book.evaluate(); let WorksheetDimension { @@ -52,7 +52,7 @@ fn test_book_insert_cell_new_row() { #[test] fn test_book_insert_cell_new_column() { let mut book = Book::default(); - book.update_entry(&Address { row: 1, col: 2 }, "1") + book.update_cell(&Address { row: 1, col: 2 }, "1") .expect("failed to edit cell"); let WorksheetDimension { min_row, @@ -67,7 +67,7 @@ fn test_book_insert_cell_new_column() { #[test] fn test_book_insert_rows() { let mut book = Book::default(); - book.update_entry(&Address { row: 2, col: 2 }, "1") + book.update_cell(&Address { row: 2, col: 2 }, "1") .expect("failed to edit cell"); book.move_to(&Address { row: 2, col: 2 }) .expect("Failed to move to location"); @@ -85,7 +85,7 @@ fn test_book_insert_rows() { #[test] fn test_book_insert_columns() { let mut book = Book::default(); - book.update_entry(&Address { row: 2, col: 2 }, "1") + book.update_cell(&Address { row: 2, col: 2 }, "1") .expect("failed to edit cell"); book.move_to(&Address { row: 2, col: 2 }) .expect("Failed to move to location"); @@ -103,7 +103,7 @@ fn test_book_insert_columns() { #[test] fn test_book_col_size() { let mut book = Book::default(); - book.update_entry(&Address { row: 2, col: 2 }, "1") + book.update_cell(&Address { row: 2, col: 2 }, "1") .expect("failed to edit cell"); book.set_col_size(1, 20).expect("Failed to set column size"); assert_eq!(20, book.get_col_size(1).expect("Failed to get column size")); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9975c94..385d8d3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -66,6 +66,12 @@ impl RangeSelection { } } +#[derive(Debug)] +pub enum ClipboardContents { + Cell(String), + Range(Vec>), +} + #[derive(Debug)] pub struct AppState<'ws> { pub modality_stack: Vec, @@ -75,6 +81,7 @@ pub struct AppState<'ws> { pub range_select: RangeSelection, dirty: bool, popup: Vec, + clipboard: Option, } impl<'ws> Default for AppState<'ws> { @@ -87,6 +94,7 @@ impl<'ws> Default for AppState<'ws> { range_select: Default::default(), dirty: Default::default(), popup: Default::default(), + clipboard: Default::default(), } } } @@ -532,12 +540,7 @@ impl<'ws> Workspace<'ws> { self.maybe_update_range_end(); } 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.range_select.end = Some(self.book.location.clone()); - self.exit_range_select_mode()?; - } + self.update_range_selection()?; } KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => { self.state.range_select.reset_range_selection(); @@ -555,6 +558,19 @@ impl<'ws> Workspace<'ws> { })?; self.state.range_select.sheet = Some(self.book.current_sheet); } + KeyCode::Char('C') + if key + .modifiers + .contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) => + { + // TODO(zaphar): Share the algorithm below between both copies + self.copy_range_formatted()?; + } + KeyCode::Char('Y') => self.copy_range_formatted()?, + KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { + self.copy_range_contents()?; + } + KeyCode::Char('y') => self.copy_range_contents()?, _ => { // moop } @@ -563,6 +579,88 @@ impl<'ws> Workspace<'ws> { Ok(None) } + fn copy_range_formatted(&mut self) -> Result<(), anyhow::Error> { + self.update_range_selection()?; + match &self.state.range_select.get_range() { + Some(( + Address { + row: row_start, + col: col_start, + }, + Address { + row: row_end, + col: col_end, + }, + )) => { + let mut rows = Vec::new(); + for ri in (*row_start)..=(*row_end) { + let mut cols = Vec::new(); + for ci in (*col_start)..=(*col_end) { + cols.push( + self.book + .get_cell_addr_rendered(&Address { row: ri, col: ci })?, + ); + } + rows.push(cols); + } + self.state.clipboard = Some(ClipboardContents::Range(rows)); + } + None => { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_rendered()?, + )); + } + } + self.exit_range_select_mode()?; + Ok(()) + } + + fn copy_range_contents(&mut self) -> Result<(), anyhow::Error> { + self.update_range_selection()?; + match &self.state.range_select.get_range() { + Some(( + Address { + row: row_start, + col: col_start, + }, + Address { + row: row_end, + col: col_end, + }, + )) => { + let mut rows = Vec::new(); + for ri in (*row_start)..=(*row_end) { + let mut cols = Vec::new(); + for ci in (*col_start)..=(*col_end) { + cols.push( + self.book + .get_cell_addr_contents(&Address { row: ri, col: ci })?, + ); + } + rows.push(cols); + } + self.state.clipboard = Some(ClipboardContents::Range(rows)); + } + None => { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_contents()?, + )); + } + } + self.exit_range_select_mode()?; + Ok(()) + } + + fn update_range_selection(&mut self) -> Result<(), anyhow::Error> { + Ok(if self.state.range_select.start.is_none() { + self.state.range_select.start = Some(self.book.location.clone()); + self.state.range_select.end = Some(self.book.location.clone()); + } else { + self.state.range_select.end = Some(self.book.location.clone()); + self.exit_range_select_mode()?; + }) + } + 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()); @@ -590,6 +688,34 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { self.enter_range_select_mode(); } + KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_contents()?, + )); + } + KeyCode::Char('y') => { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_contents()?, + )); + } + KeyCode::Char('C') + if key + .modifiers + .contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) => + { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_rendered()?, + )); + } + KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => { + self.enter_range_select_mode() + } + KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => { + self.paste_range()?; + } + KeyCode::Char('v') if key.modifiers == KeyModifiers::CONTROL => { + self.paste_range()?; + } KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { self.enter_dialog_mode(self.render_help_text()); } @@ -702,6 +828,36 @@ impl<'ws> Workspace<'ws> { return Ok(None); } + fn paste_range(&mut self) -> Result<(), anyhow::Error> { + match &self.state.clipboard { + Some(ClipboardContents::Cell(contents)) => { + self.book.edit_current_cell(contents)?; + } + Some(ClipboardContents::Range(ref rows)) => { + let Address { row, col } = self.book.location.clone(); + let row_len = rows.len(); + for ri in 0..row_len { + let columns = &rows[ri]; + let col_len = columns.len(); + for ci in 0..col_len { + self.book.update_cell( + &Address { + row: ri + row, + col: ci + col, + }, + columns[ci].clone(), + )?; + } + } + } + None => { + // NOOP + } + } + self.state.clipboard = None; + Ok(()) + } + fn run_with_prefix( &mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>, From df936af04605673f1e5461993bbe6d95bb80c974 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 14 Dec 2024 07:49:00 -0500 Subject: [PATCH 3/8] fix: Only show range selection when in range-select mode --- src/ui/mod.rs | 70 +++++++++++++-------------------------- src/ui/render/mod.rs | 29 ++++++++++++---- src/ui/render/test.rs | 8 ++--- src/ui/render/viewport.rs | 8 +++-- 4 files changed, 54 insertions(+), 61 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 385d8d3..d013e9a 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -564,13 +564,13 @@ impl<'ws> Workspace<'ws> { .contains(KeyModifiers::CONTROL | KeyModifiers::SHIFT) => { // TODO(zaphar): Share the algorithm below between both copies - self.copy_range_formatted()?; + self.copy_range(true)?; } - KeyCode::Char('Y') => self.copy_range_formatted()?, + KeyCode::Char('Y') => self.copy_range(true)?, KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { - self.copy_range_contents()?; + self.copy_range(false)?; } - KeyCode::Char('y') => self.copy_range_contents()?, + KeyCode::Char('y') => self.copy_range(false)?, _ => { // moop } @@ -579,7 +579,7 @@ impl<'ws> Workspace<'ws> { Ok(None) } - fn copy_range_formatted(&mut self) -> Result<(), anyhow::Error> { + fn copy_range(&mut self, formatted: bool) -> Result<(), anyhow::Error> { self.update_range_selection()?; match &self.state.range_select.get_range() { Some(( @@ -596,55 +596,26 @@ impl<'ws> Workspace<'ws> { for ri in (*row_start)..=(*row_end) { let mut cols = Vec::new(); for ci in (*col_start)..=(*col_end) { - cols.push( + cols.push(if formatted { self.book - .get_cell_addr_rendered(&Address { row: ri, col: ci })?, - ); + .get_cell_addr_rendered(&Address { row: ri, col: ci })? + } else { + self.book + .get_cell_addr_contents(&Address { row: ri, col: ci })? + }); } rows.push(cols); } self.state.clipboard = Some(ClipboardContents::Range(rows)); } None => { - self.state.clipboard = Some(ClipboardContents::Cell( - self.book.get_current_cell_rendered()?, - )); - } - } - self.exit_range_select_mode()?; - Ok(()) - } - - fn copy_range_contents(&mut self) -> Result<(), anyhow::Error> { - self.update_range_selection()?; - match &self.state.range_select.get_range() { - Some(( - Address { - row: row_start, - col: col_start, - }, - Address { - row: row_end, - col: col_end, - }, - )) => { - let mut rows = Vec::new(); - for ri in (*row_start)..=(*row_end) { - let mut cols = Vec::new(); - for ci in (*col_start)..=(*col_end) { - cols.push( - self.book - .get_cell_addr_contents(&Address { row: ri, col: ci })?, - ); - } - rows.push(cols); - } - self.state.clipboard = Some(ClipboardContents::Range(rows)); - } - None => { - self.state.clipboard = Some(ClipboardContents::Cell( - self.book.get_current_cell_contents()?, - )); + self.state.clipboard = Some(ClipboardContents::Cell(if formatted { + self.book + .get_current_cell_rendered()? + } else { + self.book + .get_current_cell_contents()? + })); } } self.exit_range_select_mode()?; @@ -698,6 +669,11 @@ impl<'ws> Workspace<'ws> { self.book.get_current_cell_contents()?, )); } + KeyCode::Char('Y') => { + self.state.clipboard = Some(ClipboardContents::Cell( + self.book.get_current_cell_rendered()?, + )); + } KeyCode::Char('C') if key .modifiers diff --git a/src/ui/render/mod.rs b/src/ui/render/mod.rs index bedf8e6..2655307 100644 --- a/src/ui/render/mod.rs +++ b/src/ui/render/mod.rs @@ -28,25 +28,40 @@ impl<'ws> Workspace<'ws> { ]; let mut rs: Vec> = vec![ Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| { - let tabs = Tabs::new(ws.book.get_sheet_names().iter().enumerate().map(|(idx, name)| format!("{} {}", name, idx)).collect::>()) - .select(Some(ws.book.current_sheet as usize)); + let tabs = Tabs::new( + ws.book + .get_sheet_names() + .iter() + .enumerate() + .map(|(idx, name)| format!("{} {}", name, idx)) + .collect::>(), + ) + .select(Some(ws.book.current_sheet as usize)); tabs.render(rect, buf); }), Box::new(|rect: Rect, buf: &mut Buffer, ws: &mut Self| { - let [text_rect, info_rect] = Layout::horizontal(vec![Constraint::Fill(1),Constraint::Fill(1)]).areas(rect); + let [text_rect, info_rect] = + Layout::horizontal(vec![Constraint::Fill(1), Constraint::Fill(1)]).areas(rect); ws.text_area.render(text_rect, buf); let hint = Paragraph::new(vec![ Line::from(""), - Line::from("ALT-h to toggle help dialog").centered() + Line::from("ALT-h to toggle help dialog").centered(), ]); hint.render(info_rect, buf); }), 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, &ws.state.range_select) - .with_selected(ws.book.location.clone()) - .block(table_block); + let viewport = Viewport::new( + &ws.book, + if ws.state.modality() == &Modality::RangeSelect { + Some(&ws.state.range_select) + } else { + None + }, + ) + .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 001e389..46c42cf 100644 --- a/src/ui/render/test.rs +++ b/src/ui/render/test.rs @@ -14,7 +14,7 @@ fn test_viewport_get_visible_columns() { let width = dbg!(dbg!(default_size) * 12 / 2); let app_state = AppState::default(); let viewport = - Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 17 }); + Viewport::new(&book, Some(&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"); @@ -31,7 +31,7 @@ fn test_viewport_get_visible_rows() { let height = 6; let app_state = AppState::default(); let viewport = - Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 17, col: 1 }); + Viewport::new(&book, Some(&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!( @@ -51,7 +51,7 @@ fn test_viewport_visible_columns_after_length_change() { let width = dbg!(dbg!(default_size) * 12 / 2); { let app_state = AppState::default(); - let viewport = Viewport::new(&book, &app_state.range_select) + let viewport = Viewport::new(&book, Some(&app_state.range_select)) .with_selected(Address { row: 1, col: 17 }); let cols = viewport .get_visible_columns((width + 5) as u16, &mut state) @@ -65,7 +65,7 @@ fn test_viewport_visible_columns_after_length_change() { { let app_state = AppState::default(); let viewport = - Viewport::new(&book, &app_state.range_select).with_selected(Address { row: 1, col: 1 }); + Viewport::new(&book, Some(&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"); diff --git a/src/ui/render/viewport.rs b/src/ui/render/viewport.rs index 1b33e9b..c91d975 100644 --- a/src/ui/render/viewport.rs +++ b/src/ui/render/viewport.rs @@ -37,7 +37,7 @@ pub struct ViewportState { pub struct Viewport<'ws> { pub(crate) selected: Address, book: &'ws Book, - range_selection: &'ws RangeSelection, + range_selection: Option<&'ws RangeSelection>, block: Option>, } @@ -47,7 +47,7 @@ pub(crate) const COLNAMES: [&'static str; 26] = [ ]; impl<'ws> Viewport<'ws> { - pub fn new(book: &'ws Book, app_state: &'ws RangeSelection) -> Self { + pub fn new(book: &'ws Book, app_state: Option<&'ws RangeSelection>) -> Self { Self { book, range_selection: app_state, @@ -160,7 +160,9 @@ impl<'ws> Viewport<'ws> { .get_cell_addr_rendered(&Address { row: ri, col: *ci }) .unwrap(); let mut cell = Cell::new(Text::raw(content)); - if let Some((start, end)) = &self.range_selection.get_range() { + if let Some((start, end)) = + &self.range_selection.map_or(None, |r| r.get_range()) + { if ri >= start.row && ri <= end.row && *ci >= start.col From 9b2f2fbbc36695bb02f230ac7cbbc3f9e6b333ae Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 14 Dec 2024 08:09:20 -0500 Subject: [PATCH 4/8] chore: renames and other housekeeping --- src/ui/cmd.rs | 4 ++-- src/ui/mod.rs | 2 +- src/ui/test.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ui/cmd.rs b/src/ui/cmd.rs index f6d7b31..edaeed3 100644 --- a/src/ui/cmd.rs +++ b/src/ui/cmd.rs @@ -5,7 +5,7 @@ use slice_utils::{Measured, Peekable, Seekable, Span, StrCursor}; #[derive(Debug, PartialEq, Eq)] pub enum Cmd<'a> { Write(Option<&'a str>), - InsertRow(usize), + InsertRows(usize), InsertColumns(usize), RenameSheet(Option, &'a str), NewSheet(Option<&'a str>), @@ -155,7 +155,7 @@ fn try_consume_insert_row<'cmd, 'i: 'cmd>( return Err("Invalid command: Did you mean to type `insert-rows `?"); } let arg = input.span(0..).trim(); - return Ok(Some(Cmd::InsertRow(if arg.is_empty() { + return Ok(Some(Cmd::InsertRows(if arg.is_empty() { 1 } else { if let Ok(count) = arg.parse() { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d013e9a..bc208c9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -425,7 +425,7 @@ impl<'ws> Workspace<'ws> { self.book.evaluate(); Ok(None) } - Ok(Some(Cmd::InsertRow(count))) => { + Ok(Some(Cmd::InsertRows(count))) => { self.book.insert_rows(self.book.location.row, count)?; self.book.evaluate(); Ok(None) diff --git a/src/ui/test.rs b/src/ui/test.rs index 47ed76e..fce996e 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -35,7 +35,7 @@ fn test_insert_rows_cmd() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::InsertRow(1)); + assert_eq!(cmd, Cmd::InsertRows(1)); } #[test] @@ -46,7 +46,7 @@ fn test_insert_rows_cmd_short() { let output = result.unwrap(); assert!(output.is_some()); let cmd = output.unwrap(); - assert_eq!(cmd, Cmd::InsertRow(1)); + assert_eq!(cmd, Cmd::InsertRows(1)); } #[test] From a8d42321ce031def432aa1bd30aa241361025e8b Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Sat, 14 Dec 2024 11:08:36 -0500 Subject: [PATCH 5/8] feat: `s` replaces a cells contents --- src/ui/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index bc208c9..2ee3ad3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -656,6 +656,11 @@ impl<'ws> Workspace<'ws> { KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { self.save_file()?; } + KeyCode::Char('s') if key.modifiers != KeyModifiers::CONTROL => { + self.book.clear_current_cell()?; + self.text_area = reset_text_area(String::new()); + self.enter_edit_mode(); + } KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { self.enter_range_select_mode(); } From f0dda7e345207e51930266c0dbae8231c1e34804 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Tue, 17 Dec 2024 22:21:13 -0500 Subject: [PATCH 6/8] feat: `gg` will move to the top row --- src/ui/mod.rs | 16 ++++++++++++++++ src/ui/test.rs | 20 +++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2ee3ad3..8b09161 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -78,6 +78,7 @@ pub struct AppState<'ws> { pub viewport_state: ViewportState, pub command_state: TextState<'ws>, pub numeric_prefix: Vec, + pub char_queue: Vec, pub range_select: RangeSelection, dirty: bool, popup: Vec, @@ -91,6 +92,7 @@ impl<'ws> Default for AppState<'ws> { viewport_state: Default::default(), command_state: Default::default(), numeric_prefix: Default::default(), + char_queue: Default::default(), range_select: Default::default(), dirty: Default::default(), popup: Default::default(), @@ -240,6 +242,12 @@ impl<'ws> Workspace<'ws> { Ok(()) } + /// Move to the top row without changing columns + pub fn move_to_top(&mut self) -> Result<()> { + self.book.move_to(&Address { row: 1, col: self.book.location.col })?; + Ok(()) + } + /// Move a row up in the current sheet. pub fn move_up(&mut self) -> Result<()> { let mut loc = self.book.location.clone(); @@ -801,6 +809,14 @@ impl<'ws> Workspace<'ws> { Ok(()) })?; } + KeyCode::Char('g') => { + if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) { + self.state.char_queue.pop(); + self.move_to_top()?; + } else { + self.state.char_queue.push('g'); + } + } _ => { // noop } diff --git a/src/ui/test.rs b/src/ui/test.rs index fce996e..72e4280 100644 --- a/src/ui/test.rs +++ b/src/ui/test.rs @@ -200,7 +200,6 @@ fn construct_modified_key_event(code: KeyCode, mods: KeyModifiers) -> Event { Event::Key(KeyEvent::new(code, mods)) } -// TODO(zaphar): Interaction testing for input. #[test] fn test_input_navitation_enter_key() { let mut ws = @@ -425,3 +424,22 @@ fn test_range_copy_mode_from_edit_mode() { .expect("Failed to handle 'Ctrl-r' key event"); assert_eq!(Some(&Modality::RangeSelect), ws.state.modality_stack.last()); } + +#[test] +fn test_gg_movement() { + 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('j'))) + .expect("Failed to handle 'e' key event"); + ws.handle_input(construct_key_event(KeyCode::Char('j'))) + .expect("Failed to handle 'e' key event"); + assert_eq!(ws.book.location, Address { row: 3, col: 1 }); + ws.handle_input(construct_key_event(KeyCode::Char('l'))) + .expect("Failed to handle 'e' key event"); + ws.handle_input(construct_key_event(KeyCode::Char('g'))) + .expect("Failed to handle 'e' key event"); + ws.handle_input(construct_key_event(KeyCode::Char('g'))) + .expect("Failed to handle 'e' key event"); + assert_eq!(ws.book.location, Address { row: 1, col: 2 }); +} From 909a77b1f8d97a9d45821dac4f7b377d29503646 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 18 Dec 2024 19:30:19 -0500 Subject: [PATCH 7/8] feat: `x` in range mode will extend the range --- docs/index.md | 3 ++- examples/test.icalc | Bin 3920 -> 0 bytes examples/test.xlsx | Bin 3898 -> 3872 bytes src/book/mod.rs | 18 +++++++++++++++++- src/ui/mod.rs | 25 ++++++++++++++++++++----- 5 files changed, 39 insertions(+), 7 deletions(-) delete mode 100644 examples/test.icalc diff --git a/docs/index.md b/docs/index.md index 186123e..36ebf95 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,7 +68,8 @@ will clear the numeric prefix if you want to cancel it. **Other Keybindings** -* `Ctrl-r` will enter range selection mode +* `Ctrl-r` will enter range selection mode. +* `v` will enter range selection mode with the start of the range already selected. * `Ctrl-s` will save the sheet. * `Ctrl-c`, `y` Copy the cell or range contents. * `Ctrl-v`, `p` Paste into the sheet. diff --git a/examples/test.icalc b/examples/test.icalc deleted file mode 100644 index c45659964fcf604ea57fc411bad6bab750a8fd20..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3920 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 diff --git a/examples/test.xlsx b/examples/test.xlsx index a2b60d1cb1c09534a33d6358408b85cb356ed988..ac5e7f903956b8f562daadacf378b5c8505ab5bb 100644 GIT binary patch delta 1203 zcmdlbw?K|Jz?+#xgn@y9gF%UT@&`n&TQ$k4C)2AV$X7yXfscUm@HE+z%-+qwc>A~&y?*6aa z`>(qB;=U8jvJvWAr6%nVpU5C`-D%a;2|CXve`sXkn<=DoovUpLle^o^vkkMSWOL_- zNpH|N$SSua=G55u-sg_T=B-$`Eq>zJ(#BNpZ2M zfr;!ynF&qh9-FsTG7HRc;$zh)F?o0N%M#ZOt-Ehs2st;U^~k9Mp)WVySoz(r?9e8i z>eT%cfBj7UWmr4c)Sq|hsj! z)aE(t-0a%|Wwp1~zm#6#mD~FHulU62oCSBhn*XUs|8hOFy7*batNmq~jy9qD^?zxv zsMdM2B)e1m;=vbXW&(}o8o6(6!tVWk{QPH2iszI&ZG}R|`af%UO}@S5{{J<{cfYV_ z>^}VWzxutU7w<}b0Tf}A zU04)B5w$skg&z`gRjhI#fyuL3_k-xov1~KJ5}SXri-Q@Hg*m}-yV;794r0;^=gbw!2Jr|o|2Bel9uHfQpI z+sW#xT^0-G*_^h&uakCreb{8Vx#8EQOjvu(;etoBX0g`8w_jhrW9;BR6yg-!tXi|U zU3Y(Cb7}g01vVkZW799RT*%eBDdyG&cznaM*-ae$G`W!vMC5#Im7$6Kzk|Y?wrl~4Mk>Ub>S92 zT754mxo4k0S-H-ugPr5ik0zFGICz~7Gj+{A}?#K84L|0^3&!$KH zJ$hYt+kt|7vKo&Gn2zPKM#?-3dCb6~w|Jx_jaV2M;)_yq ziuKXW;Nz8M44bUQD-{5W5e6i{jKqHHn8C`xP?3YKJ@WA8XWJ$)FfgoVU|;|_45nvE zW9Q^4yvkq)?BbQS@n!>SFD|LfNiEi^$jw1>N2H*cg85qp28Jd^1_pkZDGUq@OB$a~ z7UYwYM|T}3lA5=UjFbKNB*2zs@<}ska8CB*l;KBM0ZFwC!gh$1seuWd`t`s U5=e$EX-u8W$*;|}hZ`ga0N@S5cmMzZ delta 1222 zcmZ1=w@Z#Uz?+#xgn@y9gTYF?cq4BnBeRuw@#IQI6)?Sk@hzv7cyVNvu3&ZREmZj=>eHbNiOEtnJI|hXJ7kn7v&Sm= z;6f&O)(PKyZWu-?M7gaL=hoOM&(g|wbY`LIae?|>mRn}l-@FjUH)ZLeTZcSfuDh|?JnrkERjb&p zZkXEkGx*oXPk*l2nTGA!=*4hy@k9|GQLzdAFaAmR)qVZS%e1>hzGZX4fr~r-oRq9R z&n@U{yCZDtYLoh9`U}_fOt$+X@9L;_kbAjK+x#~#+&dTO?KnRD!&YO~PwW2GUW|9V z{5WB4jk`_PHn;N4jXh0{Yc>~$SIXbB_mT6Qr+Tw;&a%QP+mmfNdy}3YKA*N{@ju%Q zckca{e{@#mW&`UU$z*>%X-3`2*?gKYpag^D6b3#d&Ra)2F0dix`9;~q8L6oy gn0|sMHYNrJ2_(Hs8nd`2H*!f$=HeG%y9V|x04|`u!vFvP diff --git a/src/book/mod.rs b/src/book/mod.rs index 8b345d6..f6dce1a 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -96,13 +96,29 @@ impl Book { Ok(&self.get_sheet()?.sheet_data) } - /// Move to a specific sheel location in the current sheet + /// Move to a specific sheet location in the current sheet pub fn move_to(&mut self, Address { row, col }: &Address) -> Result<()> { // FIXME(zaphar): Check that this is safe first. self.location.row = *row; self.location.col = *col; Ok(()) } + + /// Extend a cell to the rest of the range. + pub fn extend_to(&mut self, from: &Address, to: &Address) -> Result<()> { + for ri in from.row..=to.row { + for ci in from.col..=to.col { + if ri == from.row && ci == from.col { + continue; + } + let contents = self.model.extend_to(self.current_sheet, from.row as i32, from.col as i32, ri as i32, ci as i32).map_err(|e| anyhow!(e))?; + self.model.set_user_input(self.current_sheet, ri as i32, ci as i32, contents) + .map_err(|e| anyhow!(e))?; + } + } + self.evaluate(); + Ok(()) + } pub fn clear_current_cell(&mut self) -> Result<()> { self.clear_cell_contents(self.current_sheet as u32, self.location.clone()) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8b09161..c84a51e 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -317,6 +317,7 @@ impl<'ws> Workspace<'ws> { "Edit Mode:".to_string(), "* ENTER/RETURN: Exit edit mode and save changes".to_string(), "* Ctrl-r: Enter Range Selection mode".to_string(), + "* v: Enter Range Selection mode with the start of the range already selected".to_string(), "* ESC: Exit edit mode and discard changes".to_string(), "Otherwise edit as normal".to_string(), ], @@ -380,7 +381,7 @@ impl<'ws> Workspace<'ws> { return Ok(None); } KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { - self.enter_range_select_mode(); + self.enter_range_select_mode(false); return Ok(None); } KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => { @@ -579,6 +580,12 @@ impl<'ws> Workspace<'ws> { self.copy_range(false)?; } KeyCode::Char('y') => self.copy_range(false)?, + KeyCode::Char('x') => { + if let (Some(from), Some(to)) = (self.state.range_select.start.as_ref(), self.state.range_select.end.as_ref()) { + self.book.extend_to(from, to)?; + } + self.exit_range_select_mode()?; + } _ => { // moop } @@ -670,7 +677,7 @@ impl<'ws> Workspace<'ws> { self.enter_edit_mode(); } KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => { - self.enter_range_select_mode(); + self.enter_range_select_mode(false); } KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => { self.state.clipboard = Some(ClipboardContents::Cell( @@ -697,7 +704,7 @@ impl<'ws> Workspace<'ws> { )); } KeyCode::Char('v') if key.modifiers != KeyModifiers::CONTROL => { - self.enter_range_select_mode() + self.enter_range_select_mode(true) } KeyCode::Char('p') if key.modifiers != KeyModifiers::CONTROL => { self.paste_range()?; @@ -810,6 +817,7 @@ impl<'ws> Workspace<'ws> { })?; } KeyCode::Char('g') => { + // TODO(zaphar): This really needs a better state machine. if self.state.char_queue.first().map(|c| *c == 'g').unwrap_or(false) { self.state.char_queue.pop(); self.move_to_top()?; @@ -819,6 +827,7 @@ impl<'ws> Workspace<'ws> { } _ => { // noop + self.state.char_queue.clear(); } } } @@ -829,6 +838,7 @@ impl<'ws> Workspace<'ws> { match &self.state.clipboard { Some(ClipboardContents::Cell(contents)) => { self.book.edit_current_cell(contents)?; + self.book.evaluate(); } Some(ClipboardContents::Range(ref rows)) => { let Address { row, col } = self.book.location.clone(); @@ -846,6 +856,7 @@ impl<'ws> Workspace<'ws> { )?; } } + self.book.evaluate(); } None => { // NOOP @@ -878,11 +889,15 @@ impl<'ws> Workspace<'ws> { self.state.modality_stack.push(Modality::Dialog); } - fn enter_range_select_mode(&mut self) { + fn enter_range_select_mode(&mut self, init_start: bool) { 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; + if init_start { + self.state.range_select.start = Some(self.book.location.clone()); + } else { + self.state.range_select.start = None; + } self.state.range_select.end = None; self.state.modality_stack.push(Modality::RangeSelect); } From 4ae7f357c13cf744e8234da2474186511f5e9d20 Mon Sep 17 00:00:00 2001 From: Jeremy Wall Date: Wed, 18 Dec 2024 20:29:56 -0500 Subject: [PATCH 8/8] chore: utility address range helper --- src/book/mod.rs | 118 ++++++++++++++++++++++++++++++++++++++---------- src/ui/mod.rs | 20 +++----- 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/src/book/mod.rs b/src/book/mod.rs index f6dce1a..91c11b9 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -18,6 +18,64 @@ mod test; const COL_PIXELS: f64 = 5.0; +#[derive(Debug, Clone)] +pub struct AddressRange<'book> { + pub start: &'book Address, + pub end: &'book Address, +} + +impl<'book> AddressRange<'book> { + pub fn as_rows(&self) -> Vec> { + let (row_range, col_range) = self.get_ranges(); + let mut rows = Vec::with_capacity(row_range.len()); + for ri in row_range.iter() { + let mut row = Vec::with_capacity(col_range.len()); + for ci in col_range.iter() { + row.push(Address { row: *ri, col: *ci }); + } + rows.push(row); + } + rows + } + + pub fn as_series(&self) -> Vec
{ + let (row_range, col_range) = self.get_ranges(); + let mut rows = Vec::with_capacity(row_range.len() * col_range.len()); + for ri in row_range.iter() { + for ci in col_range.iter() { + rows.push(Address { row: *ri, col: *ci }); + } + } + rows + } + + fn get_ranges(&self) -> (Vec, Vec) { + let row_range = if self.start.row <= self.end.row { + (self.start.row..=self.end.row) + .into_iter() + .collect::>() + } else { + let mut v = (self.start.row..=self.end.row) + .into_iter() + .collect::>(); + v.reverse(); + v + }; + let col_range = if self.start.col <= self.end.col { + (self.start.col..=self.end.col) + .into_iter() + .collect::>() + } else { + let mut v = (self.start.col..=self.end.col) + .into_iter() + .collect::>(); + v.reverse(); + v + }; + (row_range, col_range) + } +} + /// A spreadsheet book with some internal state tracking. pub struct Book { pub(crate) model: Model, @@ -103,18 +161,35 @@ impl Book { self.location.col = *col; Ok(()) } - + /// Extend a cell to the rest of the range. pub fn extend_to(&mut self, from: &Address, to: &Address) -> Result<()> { - for ri in from.row..=to.row { - for ci in from.col..=to.col { - if ri == from.row && ci == from.col { - continue; - } - let contents = self.model.extend_to(self.current_sheet, from.row as i32, from.col as i32, ri as i32, ci as i32).map_err(|e| anyhow!(e))?; - self.model.set_user_input(self.current_sheet, ri as i32, ci as i32, contents) - .map_err(|e| anyhow!(e))?; - } + for cell in (AddressRange { + start: from, + end: to, + }) + .as_series() + .iter() + .skip(1) + { + let contents = self + .model + .extend_to( + self.current_sheet, + from.row as i32, + from.col as i32, + cell.row as i32, + cell.col as i32, + ) + .map_err(|e| anyhow!(e))?; + self.model + .set_user_input( + self.current_sheet, + cell.row as i32, + cell.col as i32, + contents, + ) + .map_err(|e| anyhow!(e))?; } self.evaluate(); Ok(()) @@ -123,13 +198,12 @@ impl Book { pub fn clear_current_cell(&mut self) -> Result<()> { self.clear_cell_contents(self.current_sheet as u32, self.location.clone()) } - + pub fn clear_current_cell_all(&mut self) -> Result<()> { self.clear_cell_all(self.current_sheet as u32, self.location.clone()) } - - pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> { + pub fn clear_cell_contents(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> { Ok(self .model .cell_clear_contents(sheet, row as i32, col as i32) @@ -144,8 +218,8 @@ impl Book { } Ok(()) } - - pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col, }: Address) -> Result<()> { + + pub fn clear_cell_all(&mut self, sheet: u32, Address { row, col }: Address) -> Result<()> { Ok(self .model .cell_clear_all(sheet, row as i32, col as i32) @@ -161,7 +235,6 @@ impl Book { Ok(()) } - /// Get a cells formatted content. pub fn get_current_cell_rendered(&self) -> Result { Ok(self.get_cell_addr_rendered(&self.location)?) @@ -174,7 +247,7 @@ impl Book { .get_formatted_cell_value(self.current_sheet, *row as i32, *col as i32) .map_err(|s| anyhow!("Unable to format cell {}", s))?) } - + /// Get a cells actual content unformatted as a string. pub fn get_cell_addr_contents(&self, Address { row, col }: &Address) -> Result { Ok(self @@ -183,7 +256,6 @@ impl Book { .map_err(|s| anyhow!("Unable to format cell {}", s))?) } - /// Get a cells actual content as a string. pub fn get_current_cell_contents(&self) -> Result { Ok(self @@ -290,7 +362,7 @@ impl Book { .enumerate() .find(|(_idx, sheet)| sheet.name == name) { - self.current_sheet =idx as u32; + self.current_sheet = idx as u32; return true; } false @@ -309,7 +381,7 @@ impl Book { } self.current_sheet = next; } - + pub fn select_prev_sheet(&mut self) { let len = self.model.workbook.worksheets.len() as u32; let next = if self.current_sheet == 0 { @@ -320,7 +392,6 @@ impl Book { self.current_sheet = next; } - /// Select a sheet by id. pub fn select_sheet_by_id(&mut self, id: u32) -> bool { if let Some((idx, _sheet)) = self @@ -353,13 +424,14 @@ impl Book { .worksheet_mut(self.current_sheet) .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) + .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 diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c84a51e..53974e5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,7 +1,7 @@ //! Ui rendering logic use std::{path::PathBuf, process::ExitCode}; -use crate::book::Book; +use crate::book::{AddressRange, Book}; use anyhow::{anyhow, Result}; use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; @@ -598,25 +598,19 @@ impl<'ws> Workspace<'ws> { self.update_range_selection()?; match &self.state.range_select.get_range() { Some(( - Address { - row: row_start, - col: col_start, - }, - Address { - row: row_end, - col: col_end, - }, + start, + end, )) => { let mut rows = Vec::new(); - for ri in (*row_start)..=(*row_end) { + for row in (AddressRange { start, end, }).as_rows() { let mut cols = Vec::new(); - for ci in (*col_start)..=(*col_end) { + for cell in row { cols.push(if formatted { self.book - .get_cell_addr_rendered(&Address { row: ri, col: ci })? + .get_cell_addr_rendered(&cell)? } else { self.book - .get_cell_addr_contents(&Address { row: ri, col: ci })? + .get_cell_addr_contents(&cell)? }); } rows.push(cols);