mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 13:29:48 -04:00
feat: range copy mode
This commit is contained in:
parent
626748db0f
commit
c62fd08043
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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!(
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user