feat: ui: numeric prefixes for navigation commands

This commit is contained in:
Jeremy Wall 2024-12-02 18:13:13 -05:00
parent 20faa0c0f3
commit 444bbf3c6d
2 changed files with 160 additions and 69 deletions

View File

@ -37,6 +37,7 @@ pub struct AppState<'ws> {
pub modality_stack: Vec<Modality>,
pub viewport_state: ViewportState,
pub command_state: TextState<'ws>,
pub numeric_prefix: Vec<char>,
dirty: bool,
popup: Vec<String>,
}
@ -47,6 +48,7 @@ impl<'ws> Default for AppState<'ws> {
modality_stack: vec![Modality::default()],
viewport_state: Default::default(),
command_state: Default::default(),
numeric_prefix: Default::default(),
dirty: Default::default(),
popup: Default::default(),
}
@ -62,6 +64,25 @@ impl<'ws> AppState<'ws> {
self.modality_stack.pop();
}
}
pub fn get_n_prefix(&self) -> usize {
let prefix = self
.numeric_prefix
.iter()
.map(|c| c.to_digit(10).unwrap())
.fold(Some(0 as usize), |acc, n| {
acc?.checked_mul(10)?.checked_add(n as usize)
})
.unwrap_or(1);
if prefix == 0 {
return 1;
}
prefix
}
pub fn reset_n_prefix(&mut self) {
self.numeric_prefix.clear();
}
}
// TODO(jwall): This should probably move to a different module.
@ -237,7 +258,9 @@ impl<'ws> Workspace<'ws> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => self.exit_dialog_mode()?,
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => self.exit_dialog_mode()?,
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
self.exit_dialog_mode()?
}
_ => {
// NOOP
}
@ -336,9 +359,16 @@ impl<'ws> Workspace<'ws> {
}
}
fn handle_numeric_prefix(&mut self, digit: char) {
self.state.numeric_prefix.push(digit);
}
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char(d) if d.is_ascii_digit() => {
self.handle_numeric_prefix(d);
}
KeyCode::Char('e') | KeyCode::Char('i') => {
self.enter_edit_mode();
}
@ -352,11 +382,17 @@ impl<'ws> Workspace<'ws> {
self.enter_dialog_mode(self.render_help_text());
}
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
for _ in 1..=self.state.get_n_prefix() {
self.book.select_next_sheet();
}
self.state.reset_n_prefix();
}
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
for _ in 1..=self.state.get_n_prefix() {
self.book.select_prev_sheet();
}
self.state.reset_n_prefix();
}
KeyCode::Char('s')
if key.modifiers == KeyModifiers::HYPER
|| key.modifiers == KeyModifiers::SUPER =>
@ -364,18 +400,25 @@ impl<'ws> Workspace<'ws> {
self.save_file()?;
}
KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => {
for _ in 1..=self.state.get_n_prefix() {
let Address { row: _, col } = &self.book.location;
self.book
.set_col_size(*col, self.book.get_col_size(*col)? + 1)?;
}
self.state.reset_n_prefix();
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
for _ in 1..=self.state.get_n_prefix() {
let Address { row: _, col } = &self.book.location;
let curr_size = self.book.get_col_size(*col)?;
if curr_size > 1 {
self.book.set_col_size(*col, curr_size - 1)?;
}
}
self.state.reset_n_prefix();
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
for _ in 1..=self.state.get_n_prefix() {
let (row_count, _) = self.book.get_size()?;
self.book.update_entry(
&Address {
@ -392,62 +435,66 @@ impl<'ws> Workspace<'ws> {
}
self.handle_movement_change();
}
KeyCode::Char('t') if key.modifiers == KeyModifiers::CONTROL => {
let (_, col_count) = self.book.get_size()?;
self.book.update_entry(
&Address {
row: 1,
col: col_count + 1,
},
"",
)?;
self.state.reset_n_prefix();
}
KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS));
}
KeyCode::Char('j') | KeyCode::Down
if key.modifiers != KeyModifiers::CONTROL =>
{
self.move_down()?;
self.handle_movement_change();
KeyCode::Char('j') | KeyCode::Down if key.modifiers != KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_down()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Enter
if key.modifiers != KeyModifiers::SHIFT =>
{
self.move_down()?;
self.handle_movement_change();
KeyCode::Enter if key.modifiers != KeyModifiers::SHIFT => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_down()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Enter
if key.modifiers == KeyModifiers::SHIFT =>
{
self.move_up()?;
self.handle_movement_change();
KeyCode::Enter if key.modifiers == KeyModifiers::SHIFT => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_up()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Char('k') | KeyCode::Up if key.modifiers != KeyModifiers::CONTROL => {
self.move_up()?;
self.handle_movement_change();
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_up()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Char('h') | KeyCode::Left if key.modifiers != KeyModifiers::CONTROL => {
self.move_left()?;
self.handle_movement_change();
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_left()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Char('l') | KeyCode::Right
if key.modifiers != KeyModifiers::CONTROL =>
{
self.move_right()?;
self.handle_movement_change();
KeyCode::Char('l') | KeyCode::Right if key.modifiers != KeyModifiers::CONTROL => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_right()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Tab
if key.modifiers != KeyModifiers::SHIFT =>
{
self.move_right()?;
self.handle_movement_change();
KeyCode::Tab if key.modifiers != KeyModifiers::SHIFT => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_right()?;
ws.handle_movement_change();
Ok(())
})?;
}
KeyCode::Tab
if key.modifiers == KeyModifiers::SHIFT =>
{
self.move_left()?;
self.handle_movement_change();
KeyCode::Tab if key.modifiers == KeyModifiers::SHIFT => {
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
ws.move_left()?;
ws.handle_movement_change();
Ok(())
})?;
}
_ => {
// noop
@ -457,6 +504,14 @@ impl<'ws> Workspace<'ws> {
return Ok(None);
}
fn run_with_prefix(&mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>) -> Result<(), anyhow::Error> {
for _ in 1..=self.state.get_n_prefix() {
action(self)?;
}
self.state.reset_n_prefix();
Ok(())
}
fn enter_navigation_mode(&mut self) {
self.state.modality_stack.push(Modality::Navigate);
}

View File

@ -315,3 +315,39 @@ fn test_edit_mode_esc_keycode() {
assert_eq!("", ws.text_area.lines().join("\n"));
}
#[test]
fn test_navigation_numeric_prefix()
{
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.new_sheet(Some("Sheet2")).expect("failed to create sheet2");
ws.book.new_sheet(Some("Sheet3")).expect("failed to create sheet3");
ws.handle_input(construct_key_event(KeyCode::Char('2')))
.expect("Failed to handle '3' key event");
ws.handle_input(construct_key_event(KeyCode::Char('3')))
.expect("Failed to handle '3' key event");
ws.handle_input(construct_key_event(KeyCode::Char('9')))
.expect("Failed to handle '3' key event");
assert_eq!(239, ws.state.get_n_prefix());
}
#[test]
fn test_navigation_tab_next_numeric_prefix()
{
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.new_sheet(Some("Sheet2")).expect("failed to create sheet2");
ws.book.new_sheet(Some("Sheet3")).expect("failed to create sheet3");
ws.handle_input(construct_key_event(KeyCode::Char('2')))
.expect("Failed to handle '3' key event");
assert_eq!(2, ws.state.get_n_prefix());
ws.handle_input(construct_modified_key_event(KeyCode::Char('n'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-n' key event");
assert_eq!("Sheet3", ws.book.get_sheet_name().expect("Failed to get sheet name"));
assert_eq!(1, ws.state.get_n_prefix());
ws.handle_input(construct_modified_key_event(KeyCode::Char('n'), KeyModifiers::CONTROL))
.expect("Failed to handle 'Ctrl-n' key event");
assert_eq!("Sheet1", ws.book.get_sheet_name().expect("Failed to get sheet name"));
}