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>,