feat: range copy mode

This commit is contained in:
Jeremy Wall 2024-12-04 18:51:18 -05:00
parent 626748db0f
commit c62fd08043
3 changed files with 142 additions and 1 deletions

View File

@ -30,6 +30,7 @@ pub enum Modality {
CellEdit, CellEdit,
Command, Command,
Dialog, Dialog,
RangeCopy,
} }
#[derive(Debug)] #[derive(Debug)]
@ -38,6 +39,9 @@ pub struct AppState<'ws> {
pub viewport_state: ViewportState, pub viewport_state: ViewportState,
pub command_state: TextState<'ws>, pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>, pub numeric_prefix: Vec<char>,
pub original_location: Option<Address>,
pub start_range: Option<Address>,
pub end_range: Option<Address>,
dirty: bool, dirty: bool,
popup: Vec<String>, popup: Vec<String>,
} }
@ -49,6 +53,9 @@ impl<'ws> Default for AppState<'ws> {
viewport_state: Default::default(), viewport_state: Default::default(),
command_state: Default::default(), command_state: Default::default(),
numeric_prefix: Default::default(), numeric_prefix: Default::default(),
original_location: Default::default(),
start_range: Default::default(),
end_range: Default::default(),
dirty: Default::default(), dirty: Default::default(),
popup: Default::default(), popup: Default::default(),
} }
@ -197,6 +204,7 @@ impl<'ws> Workspace<'ws> {
Modality::CellEdit => self.handle_edit_input(key)?, Modality::CellEdit => self.handle_edit_input(key)?,
Modality::Command => self.handle_command_input(key)?, Modality::Command => self.handle_command_input(key)?,
Modality::Dialog => self.handle_dialog_input(key)?, Modality::Dialog => self.handle_dialog_input(key)?,
Modality::RangeCopy => self.handle_range_copy_input(key)?,
}; };
return Ok(result); return Ok(result);
} }
@ -363,6 +371,57 @@ impl<'ws> Workspace<'ws> {
self.state.numeric_prefix.push(digit); self.state.numeric_prefix.push(digit);
} }
fn handle_range_copy_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
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<Option<ExitCode>> { fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press { if key.kind == KeyEventKind::Press {
match key.code { match key.code {
@ -381,6 +440,9 @@ impl<'ws> Workspace<'ws> {
KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => { KeyCode::Char('s') if key.modifiers == KeyModifiers::CONTROL => {
self.save_file()?; self.save_file()?;
} }
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
self.enter_range_copy_mode();
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => { KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.enter_dialog_mode(self.render_help_text()); self.enter_dialog_mode(self.render_help_text());
} }
@ -507,6 +569,14 @@ impl<'ws> Workspace<'ws> {
self.state.modality_stack.push(Modality::Dialog); 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) { fn enter_edit_mode(&mut self) {
self.state.modality_stack.push(Modality::CellEdit); self.state.modality_stack.push(Modality::CellEdit);
self.text_area self.text_area
@ -531,6 +601,14 @@ impl<'ws> Workspace<'ws> {
Ok(()) 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<()> { fn exit_edit_mode(&mut self, keep: bool) -> Result<()> {
self.text_area.set_cursor_line_style(Style::default()); self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default()); self.text_area.set_cursor_style(Style::default());
@ -542,6 +620,7 @@ impl<'ws> Workspace<'ws> {
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.dirty = false;
self.state.pop_modality();
Ok(()) Ok(())
} }

View File

@ -95,6 +95,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
Modality::CellEdit => "edit", Modality::CellEdit => "edit",
Modality::Command => "command", Modality::Command => "command",
Modality::Dialog => "", Modality::Dialog => "",
Modality::RangeCopy => "range-copy",
}) })
.title_bottom( .title_bottom(
Line::from(format!( Line::from(format!(

View File

@ -1,6 +1,6 @@
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::ui::Modality; use crate::ui::{Address, Modality};
use super::cmd::{parse, Cmd}; use super::cmd::{parse, Cmd};
use super::Workspace; use super::Workspace;
@ -351,3 +351,64 @@ fn test_navigation_tab_next_numeric_prefix()
.expect("Failed to handle 'Ctrl-n' key event"); .expect("Failed to handle 'Ctrl-n' key event");
assert_eq!("Sheet1", ws.book.get_sheet_name().expect("Failed to get sheet name")); 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());
}