mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-22 04:39:48 -04:00
wip: copy-paste internally
This commit is contained in:
parent
d09e8d4902
commit
65b0fc4bba
@ -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
|
||||
|
@ -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<String> {
|
||||
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<String> {
|
||||
@ -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<S: Into<String>>(&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<S: Into<String>>(&mut self, location: &Address, value: S) -> Result<()> {
|
||||
pub fn update_cell<S: Into<String>>(&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
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
|
168
src/ui/mod.rs
168
src/ui/mod.rs
@ -66,6 +66,12 @@ impl RangeSelection {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ClipboardContents {
|
||||
Cell(String),
|
||||
Range(Vec<Vec<String>>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppState<'ws> {
|
||||
pub modality_stack: Vec<Modality>,
|
||||
@ -75,6 +81,7 @@ pub struct AppState<'ws> {
|
||||
pub range_select: RangeSelection,
|
||||
dirty: bool,
|
||||
popup: Vec<String>,
|
||||
clipboard: Option<ClipboardContents>,
|
||||
}
|
||||
|
||||
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>,
|
||||
|
Loading…
x
Reference in New Issue
Block a user