From 9ac66fc0748061f34d1faf43cca00e29d0a4924f Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Sat, 21 Mar 2026 16:41:10 -0500 Subject: [PATCH] repeat x times --- src/action.rs | 345 +++++++++++++++------------------------------- src/app/mod.rs | 23 ++++ src/buffer/mod.rs | 187 +++++++++++++++++++------ src/config.rs | 264 +++++++++++++++++++++-------------- src/cursor.rs | 37 ++++- src/main.rs | 10 +- 6 files changed, 483 insertions(+), 383 deletions(-) diff --git a/src/action.rs b/src/action.rs index c525a83..d51f197 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,13 +1,34 @@ use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; use ratatui::{style::Stylize, text::Span}; - use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, app::WindowSize, buffer::{Buffer, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction}; #[derive(Clone, Copy)] pub enum Action { + App(AppAction), + Buffer(BufferAction), + Cursor(CursorAction), +} + +// actions that act on the app as a whole, not just one buffer +#[derive(Debug, Clone, Copy)] +pub enum AppAction { QuitIfSaved, Quit, + PreviousBuffer, + NextBuffer, + + Yank, +} + +impl From for Action { + fn from(app_action: AppAction) -> Self { + Self::App(app_action) + } +} + +#[derive(Clone, Copy)] +pub enum BufferAction { NormalMode, SelectMode, @@ -15,21 +36,7 @@ pub enum Action { View, Replace, Space, - - MoveByteUp, - MoveByteDown, - MoveByteLeft, - MoveByteRight, - - ExtendByteUp, - ExtendByteDown, - ExtendByteLeft, - ExtendByteRight, - - GotoLineStart, - GotoLineEnd, - GotoFileStart, - GotoFileEnd, + Repeat, ScrollDown, ScrollUp, @@ -40,20 +47,9 @@ pub enum Action { PageDown, PageUp, - MoveNextWordStart, - MoveNextWordEnd, - MovePreviousWordStart, - - ExtendNextWordStart, - ExtendNextWordEnd, - ExtendPreviousWordStart, - CollapseSelection, FlipSelections, - ExtendLineBelow, - ExtendLineAbove, - Delete, Undo, @@ -61,9 +57,6 @@ pub enum Action { Save, - PreviousBuffer, - NextBuffer, - CopySelectionOnNextLine, RotateSelectionsBackward, @@ -92,106 +85,105 @@ pub enum Action { AlignViewTop, } -// actions that act on the app as a whole, not just one buffer -pub enum AppAction { - QuitIfSaved, - Quit, +impl From for Action { + fn from(buffer_action: BufferAction) -> Self { + Self::Buffer(buffer_action) + } +} + +#[derive(Clone, Copy)] +pub enum CursorAction { + MoveByteUp, + MoveByteDown, + MoveByteLeft, + MoveByteRight, - PreviousBuffer, - NextBuffer, + ExtendByteUp, + ExtendByteDown, + ExtendByteLeft, + ExtendByteRight, + + GotoLineStart, + GotoLineEnd, + GotoFileStart, + GotoFileEnd, + + MoveNextWordStart, + MoveNextWordEnd, + MovePreviousWordStart, + + ExtendNextWordStart, + ExtendNextWordEnd, + ExtendPreviousWordStart, + + ExtendLineBelow, + ExtendLineAbove, +} + +impl From for Action { + fn from(cursor_action: CursorAction) -> Self { + Self::Cursor(cursor_action) + } } impl Buffer { - pub fn execute(&mut self, action: Action, window_size: WindowSize) -> Option { + pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) { match action { - Action::QuitIfSaved => return Some(AppAction::QuitIfSaved), - Action::Quit => return Some(AppAction::Quit), + BufferAction::NormalMode => self.normal_mode(), + BufferAction::SelectMode => self.select_mode(), - Action::NormalMode => self.normal_mode(), - Action::SelectMode => self.select_mode(), + BufferAction::Goto => self.goto(), + BufferAction::View => self.view(), + BufferAction::Replace => self.replace(), + BufferAction::Space => self.space(), + BufferAction::Repeat => self.repeat(), - Action::Goto => self.goto(), - Action::View => self.view(), - Action::Replace => self.replace(), - Action::Space => self.space(), + BufferAction::ScrollDown => self.scroll_down(window_size), + BufferAction::ScrollUp => self.scroll_up(window_size), - Action::MoveByteUp => self.move_byte_up(window_size), - Action::MoveByteDown => self.move_byte_down(window_size), - Action::MoveByteLeft => self.move_byte_left(window_size), - Action::MoveByteRight => self.move_byte_right(window_size), + BufferAction::PageCursorHalfDown => self.page_cursor_half_down(window_size), + BufferAction::PageCursorHalfUp => self.page_cursor_half_up(window_size), - Action::ExtendByteUp => self.extend_byte_up(window_size), - Action::ExtendByteDown => self.extend_byte_down(window_size), - Action::ExtendByteLeft => self.extend_byte_left(window_size), - Action::ExtendByteRight => self.extend_byte_right(window_size), + BufferAction::PageDown => self.page_down(window_size), + BufferAction::PageUp => self.page_up(window_size), - Action::GotoLineStart => self.goto_line_start(), - Action::GotoLineEnd => self.goto_line_end(), - Action::GotoFileStart => self.goto_file_start(window_size), - Action::GotoFileEnd => self.goto_file_end(window_size), + BufferAction::CollapseSelection => self.collapse_selection(), + BufferAction::FlipSelections => self.flip_selection(), - Action::ScrollDown => self.scroll_down(window_size), - Action::ScrollUp => self.scroll_up(window_size), + BufferAction::Delete => self.delete(window_size), - Action::PageCursorHalfDown => self.page_cursor_half_down(window_size), - Action::PageCursorHalfUp => self.page_cursor_half_up(window_size), + BufferAction::Undo => self.undo(window_size), + BufferAction::Redo => self.redo(window_size), - Action::PageDown => self.page_down(window_size), - Action::PageUp => self.page_up(window_size), + BufferAction::Save => self.save(), - Action::MoveNextWordStart => self.move_next_word_start(window_size), - Action::MoveNextWordEnd => self.move_next_word_end(window_size), - Action::MovePreviousWordStart => self.move_previous_word_start(window_size), + BufferAction::CopySelectionOnNextLine => self.copy_selection_on_next_line(), - Action::ExtendNextWordStart => self.extend_next_word_start(window_size), - Action::ExtendNextWordEnd => self.extend_next_word_end(window_size), - Action::ExtendPreviousWordStart => self.extend_previous_word_start(window_size), + BufferAction::RotateSelectionsBackward => self.rotate_selections_backward(), + BufferAction::RotateSelectionsForward => self.rotate_selections_forward(), - Action::CollapseSelection => self.collapse_selection(), - Action::FlipSelections => self.flip_selection(), + BufferAction::KeepPrimarySelection => self.keep_primary_selection(), + BufferAction::RemovePrimarySelection => self.remove_primary_selection(), - Action::ExtendLineBelow => self.extend_line_below(window_size), - Action::ExtendLineAbove => self.extend_line_above(window_size), + BufferAction::SplitSelectionsInto1s => self.split_selections_into_size(1), + BufferAction::SplitSelectionsInto2s => self.split_selections_into_size(2), + BufferAction::SplitSelectionsInto3s => self.split_selections_into_size(3), + BufferAction::SplitSelectionsInto4s => self.split_selections_into_size(4), + BufferAction::SplitSelectionsInto5s => self.split_selections_into_size(5), + BufferAction::SplitSelectionsInto6s => self.split_selections_into_size(6), + BufferAction::SplitSelectionsInto7s => self.split_selections_into_size(7), + BufferAction::SplitSelectionsInto8s => self.split_selections_into_size(8), + BufferAction::SplitSelectionsInto9s => self.split_selections_into_size(9), - Action::Delete => self.delete(window_size), + BufferAction::JumpToSelectedOffset => self.jump_to_selected_offset(window_size), + BufferAction::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size), - Action::Undo => self.undo(window_size), - Action::Redo => self.redo(window_size), + BufferAction::ToggleMark => self.toggle_mark(), - Action::Save => self.save(), - - Action::PreviousBuffer => return Some(AppAction::PreviousBuffer), - Action::NextBuffer => return Some(AppAction::NextBuffer), - - Action::CopySelectionOnNextLine => self.copy_selection_on_next_line(), - - Action::RotateSelectionsBackward => self.rotate_selections_backward(), - Action::RotateSelectionsForward => self.rotate_selections_forward(), - - Action::KeepPrimarySelection => self.keep_primary_selection(), - Action::RemovePrimarySelection => self.remove_primary_selection(), - - Action::SplitSelectionsInto1s => self.split_selections_into_size(1), - Action::SplitSelectionsInto2s => self.split_selections_into_size(2), - Action::SplitSelectionsInto3s => self.split_selections_into_size(3), - Action::SplitSelectionsInto4s => self.split_selections_into_size(4), - Action::SplitSelectionsInto5s => self.split_selections_into_size(5), - Action::SplitSelectionsInto6s => self.split_selections_into_size(6), - Action::SplitSelectionsInto7s => self.split_selections_into_size(7), - Action::SplitSelectionsInto8s => self.split_selections_into_size(8), - Action::SplitSelectionsInto9s => self.split_selections_into_size(9), - - Action::JumpToSelectedOffset => self.jump_to_selected_offset(window_size), - Action::JumpToSelectedOffsetRelativeToMark => self.jump_to_selected_offset_relative_to_mark(window_size), - - Action::ToggleMark => self.toggle_mark(), - - Action::AlignViewCenter => self.align_view_center(window_size), - Action::AlignViewBottom => self.align_view_bottom(window_size), - Action::AlignViewTop => self.align_view_top(), + BufferAction::AlignViewCenter => self.align_view_center(window_size), + BufferAction::AlignViewBottom => self.align_view_bottom(window_size), + BufferAction::AlignViewTop => self.align_view_top(), } - - None } const fn normal_mode(&mut self) { @@ -220,79 +212,8 @@ impl Buffer { self.partial_action = Some(PartialAction::Space); } - fn change_all_cursors(&mut self, transform: impl Fn(&mut Cursor)) { - transform(&mut self.primary_cursor); - - for cursor in &mut self.cursors { - transform(cursor); - } - self.cursors.sort_by_key(|cursor| cursor.head); - - self.combine_cursors_if_overlapping(); - } - - fn move_byte_up(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::move_byte_up); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn move_byte_down(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.move_byte_down(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn move_byte_left(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::move_byte_left); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn move_byte_right(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.move_byte_right(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_byte_up(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::extend_byte_up); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_byte_down(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_byte_down(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_byte_left(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::extend_byte_left); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_byte_right(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_byte_right(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn goto_line_start(&mut self) { - self.change_all_cursors(Cursor::goto_line_start); - } - - fn goto_line_end(&mut self) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.goto_line_end(max_contents_index)); - } - - fn goto_file_start(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::goto_file_start); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn goto_file_end(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.goto_file_end(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); + const fn repeat(&mut self) { + self.partial_action = Some(PartialAction::Repeat); } pub fn scroll_down(&mut self, window_size: WindowSize) { @@ -419,40 +340,6 @@ impl Buffer { self.combine_cursors_if_overlapping(); } - fn move_next_word_start(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.move_next_word_start(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn move_next_word_end(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.move_next_word_end(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn move_previous_word_start(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::move_previous_word_start); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_next_word_start(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_next_word_start(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_next_word_end(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_next_word_end(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_previous_word_start(&mut self, window_size: WindowSize) { - self.change_all_cursors(Cursor::extend_previous_word_start); - self.clamp_screen_to_primary_cursor(window_size); - } - fn collapse_selection(&mut self) { self.primary_cursor.collapse(); @@ -469,18 +356,6 @@ impl Buffer { } } - fn extend_line_below(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_line_below(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - - fn extend_line_above(&mut self, window_size: WindowSize) { - let max_contents_index = self.max_contents_index(); - self.change_all_cursors(|cursor| cursor.extend_line_above(max_contents_index)); - self.clamp_screen_to_primary_cursor(window_size); - } - fn delete(&mut self, window_size: WindowSize) { if !self.contents.is_empty() { self.execute_and_add( @@ -658,7 +533,7 @@ impl Buffer { if !iter::once(&self.primary_cursor) .chain(&self.cursors) .all(|cursor| { - bytes_as_nat(&self.contents[cursor.range()]) + bytes_to_nat(&self.contents[cursor.range()]) .is_some_and(|offset| offset < self.contents.len()) }) { @@ -675,12 +550,12 @@ impl Buffer { } self.primary_cursor = Cursor::at( - bytes_as_nat(&self.contents[self.primary_cursor.range()]).unwrap() + bytes_to_nat(&self.contents[self.primary_cursor.range()]).unwrap() ); for cursor in &mut self.cursors { *cursor = Cursor::at( - bytes_as_nat(&self.contents[cursor.range()]).unwrap() + bytes_to_nat(&self.contents[cursor.range()]).unwrap() ); } @@ -697,7 +572,7 @@ impl Buffer { if !iter::once(&self.primary_cursor) .chain(&self.cursors) .all(|cursor| { - bytes_as_nat(&self.contents[cursor.range()]) + bytes_to_nat(&self.contents[cursor.range()]) .map(|offset| mark_before(cursor.lower_bound(), &sorted_marks) + offset) .is_some_and(|offset| offset < self.contents.len()) }) @@ -715,7 +590,7 @@ impl Buffer { } self.primary_cursor = Cursor::at( - bytes_as_nat(&self.contents[self.primary_cursor.range()]) + bytes_to_nat(&self.contents[self.primary_cursor.range()]) .map(|offset| { mark_before(self.primary_cursor.lower_bound(), &sorted_marks) + offset }) @@ -724,7 +599,7 @@ impl Buffer { for cursor in &mut self.cursors { *cursor = Cursor::at( - bytes_as_nat(&self.contents[cursor.range()]) + bytes_to_nat(&self.contents[cursor.range()]) .map(|offset| { mark_before(cursor.lower_bound(), &sorted_marks) + offset }) @@ -789,7 +664,7 @@ impl Buffer { } } -fn bytes_as_nat(bytes: &[u8]) -> Option { +pub fn bytes_to_nat(bytes: &[u8]) -> Option { bytes .iter() .rev() // little-endian diff --git a/src/app/mod.rs b/src/app/mod.rs index 6dc0810..4cadbfa 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,6 +11,9 @@ pub struct App { pub buffers: Vec, pub current_buffer_index: usize, + pub primary_cursor_register: Vec, + pub other_cursor_registers: Vec>, + pub window_size: WindowSize, pub should_quit: bool, @@ -52,6 +55,9 @@ impl App { buffers, current_buffer_index: 0, + primary_cursor_register: Vec::new(), + other_cursor_registers: Vec::new(), + window_size, should_quit: false, @@ -84,6 +90,8 @@ impl App { let maybe_app_action = self.buffers[self.current_buffer_index].handle_key( key_event, &self.config, + &self.primary_cursor_register, + &self.other_cursor_registers, self.window_size ); @@ -94,6 +102,8 @@ impl App { AppAction::PreviousBuffer => self.previous_buffer(), AppAction::NextBuffer => self.next_buffer(), + + AppAction::Yank => self.yank(), } } } @@ -184,6 +194,19 @@ impl App { } } + fn yank(&mut self) { + self.primary_cursor_register = self.current_buffer() + .contents[self.current_buffer().primary_cursor.range()] + .to_vec(); + + self.other_cursor_registers = self.current_buffer().cursors + .iter() + .map(|cursor| { + self.current_buffer().contents[cursor.range()].to_vec() + }) + .collect(); + } + pub fn current_buffer(&self) -> &Buffer { &self.buffers[self.current_buffer_index] } diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 738c599..54599ca 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -1,8 +1,8 @@ use core::slice::GetDisjointMutIndex; use std::{collections::HashSet, fs::File, io::Read, path::PathBuf}; use crossterm::event::KeyEvent; -use ratatui::{style::Color, text::Span}; -use crate::{action::AppAction, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction}; +use ratatui::{style::{Color, Stylize}, text::Span}; +use crate::{BYTES_PER_LINE, action::{Action, AppAction, bytes_to_nat}, app::WindowSize, config::Config, cursor::Cursor, edit_action::EditAction}; mod widget; @@ -40,7 +40,7 @@ pub enum Mode { #[derive(Clone, Copy, Hash, PartialEq, Eq)] pub enum PartialAction { - Goto, View, Replace, Space + Goto, View, Replace, Space, Repeat } impl Mode { @@ -68,6 +68,7 @@ impl PartialAction { Self::View => "z", Self::Replace => "r", Self::Space => "␠", + Self::Repeat => "×", } } } @@ -108,54 +109,160 @@ impl Buffer { &mut self, event: KeyEvent, config: &Config, + primary_cursor_register: &[u8], + other_cursor_registers: &[Vec], window_size: WindowSize ) -> Option { self.alert_message = "".into(); - let mut app_action = None; + let app_action = match self.partial_action { + Some(PartialAction::Replace) => { + self.handle_replace(event, window_size); + None + }, + Some(PartialAction::Repeat) => { + self.handle_repeat( + event, + config, + primary_cursor_register, + other_cursor_registers, + window_size + ); + None + }, + _ => self.handle_other_modes(event, config, window_size), + }; - if self.partial_action == Some(PartialAction::Replace) { - if let Some(hex_character) = event.code.as_char() && - let Some(nybble) = nybble_from_hex(hex_character) - { - if let Some(partial_replace) = self.partial_replace.take() { - self.execute_and_add( - EditAction::Replace { - primary_cursor: self.primary_cursor, - cursors: self.cursors.clone(), - primary_old_data: self.contents[self.primary_cursor.range()].to_vec(), - old_data: self.cursors - .iter() - .map(|cursor| self.contents[cursor.range()].to_vec()) - .collect(), - new_byte: partial_replace << 4 | nybble - }, - window_size - ); - self.partial_action = None; - } else { - self.partial_replace = Some(nybble); - } - } else { + assert!(self.scroll_position.is_multiple_of(BYTES_PER_LINE)); + assert!(self.scroll_position < self.contents.len()); + assert!(self.primary_cursor.head < self.contents.len()); + assert!(self.primary_cursor.tail < self.contents.len()); + assert!(self.scroll_position <= self.primary_cursor.head); + assert!(self.primary_cursor.head < self.scroll_position + window_size.visible_byte_count()); + + app_action + } + + fn handle_replace(&mut self, event: KeyEvent, window_size: WindowSize) { + if let Some(hex_character) = event.code.as_char() && + let Some(nybble) = nybble_from_hex(hex_character) + { + if let Some(partial_replace) = self.partial_replace.take() { + self.execute_and_add( + EditAction::Replace { + primary_cursor: self.primary_cursor, + cursors: self.cursors.clone(), + primary_old_data: self.contents[self.primary_cursor.range()].to_vec(), + old_data: self.cursors + .iter() + .map(|cursor| self.contents[cursor.range()].to_vec()) + .collect(), + new_byte: partial_replace << 4 | nybble + }, + window_size + ); self.partial_action = None; - self.partial_replace = None; + } else { + self.partial_replace = Some(nybble); } } else { - let should_reset_partial = self.partial_action.is_some(); - - if let Some(mode_config) = config.0.get(&self.mode) && - let Some(keybinds) = mode_config.0.get(&self.partial_action) && - let Some(action) = keybinds.0.get(&event.into()) - { - app_action = self.execute(*action, window_size); - } - - if should_reset_partial { - self.partial_action = None; + self.partial_action = None; + self.partial_replace = None; + } + } + + fn handle_other_modes( + &mut self, + event: KeyEvent, + config: &Config, + window_size: WindowSize + ) -> Option { + let mut result = None; + + let should_reset_partial = self.partial_action.is_some(); + + if let Some(mode_config) = config.0.get(&self.mode) && + let Some(keybinds) = mode_config.0.get(&self.partial_action) && + let Some(action) = keybinds.0.get(&event.into()) + { + match action { + Action::App(app_action) => result = Some(*app_action), + Action::Buffer(buffer_action) => self.execute(*buffer_action, window_size), + Action::Cursor(cursor_action) => { + let max_contents_index = self.max_contents_index(); + + self.primary_cursor.execute(*cursor_action, max_contents_index); + + for cursor in &mut self.cursors { + cursor.execute(*cursor_action, max_contents_index); + } + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + self.clamp_screen_to_primary_cursor(window_size); + }, } } - app_action + if should_reset_partial { + self.partial_action = None; + } + + result + } + + fn handle_repeat( + &mut self, + event: KeyEvent, + config: &Config, + primary_cursor_register: &[u8], + other_cursor_registers: &[Vec], + window_size: WindowSize + ) { + self.partial_action = None; + + if let Some(mode_config) = config.0.get(&self.mode) && + let Some(keybinds) = mode_config.0.get(&Some(PartialAction::Repeat)) && + let Some(action) = keybinds.0.get(&event.into()) + { + match action { + Action::Cursor(cursor_action) => { + let Some(primary_repeat_count) = bytes_to_nat(primary_cursor_register) else { + self.alert_message = Span::from( + "repeat count is too large" + ).red(); + return; + }; + let other_repeat_counts = other_cursor_registers + .iter() + .map(|register| bytes_to_nat(register)); + + if other_repeat_counts.clone().any(|count| count.is_none()) { + self.alert_message = Span::from( + "repeat count is too large" + ).red(); + return; + } + + let max_contents_index = self.max_contents_index(); + + for _ in 0..primary_repeat_count { + self.primary_cursor.execute(*cursor_action, max_contents_index); + } + + for (cursor, repeat_count) in self.cursors.iter_mut().zip(other_repeat_counts) { + for _ in 0..repeat_count.unwrap() { + cursor.execute(*cursor_action, max_contents_index); + } + } + self.cursors.sort_by_key(|cursor| cursor.head); + + self.combine_cursors_if_overlapping(); + self.clamp_screen_to_primary_cursor(window_size); + }, + _ => panic!("repeated actions may only be cursor actions"), + } + } } pub const fn has_unsaved_changes(&self) -> bool { diff --git a/src/config.rs b/src/config.rs index 8930024..d2711e7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use crate::{action::Action, buffer::{Mode, PartialAction}}; +use crate::{action::{Action, AppAction, BufferAction, CursorAction}, buffer::{Mode, PartialAction}}; pub struct Config( pub HashMap @@ -97,159 +97,217 @@ impl Default for Config { [ (Mode::Normal, [ (None, [ - ("q".try_into().unwrap(), Action::QuitIfSaved), - ("Q".try_into().unwrap(), Action::Quit), + ("q".try_into().unwrap(), AppAction::QuitIfSaved.into()), + ("Q".try_into().unwrap(), AppAction::Quit.into()), - ("v".try_into().unwrap(), Action::SelectMode), + ("v".try_into().unwrap(), BufferAction::SelectMode.into()), - ("g".try_into().unwrap(), Action::Goto), - ("z".try_into().unwrap(), Action::View), - ("r".try_into().unwrap(), Action::Replace), - (" ".try_into().unwrap(), Action::Space), + ("g".try_into().unwrap(), BufferAction::Goto.into()), + ("z".try_into().unwrap(), BufferAction::View.into()), + ("r".try_into().unwrap(), BufferAction::Replace.into()), + (" ".try_into().unwrap(), BufferAction::Space.into()), + ("*".try_into().unwrap(), BufferAction::Repeat.into()), - ("i".try_into().unwrap(), Action::MoveByteUp), - ("k".try_into().unwrap(), Action::MoveByteDown), - ("j".try_into().unwrap(), Action::MoveByteLeft), - ("l".try_into().unwrap(), Action::MoveByteRight), + ("i".try_into().unwrap(), CursorAction::MoveByteUp.into()), + ("k".try_into().unwrap(), CursorAction::MoveByteDown.into()), + ("j".try_into().unwrap(), CursorAction::MoveByteLeft.into()), + ("l".try_into().unwrap(), CursorAction::MoveByteRight.into()), - ("G".try_into().unwrap(), Action::GotoFileEnd), + ("G".try_into().unwrap(), CursorAction::GotoFileEnd.into()), - ("C-e".try_into().unwrap(), Action::ScrollDown), - ("C-y".try_into().unwrap(), Action::ScrollUp), + ("C-e".try_into().unwrap(), BufferAction::ScrollDown.into()), + ("C-y".try_into().unwrap(), BufferAction::ScrollUp.into()), - ("C-d".try_into().unwrap(), Action::PageCursorHalfDown), - ("C-u".try_into().unwrap(), Action::PageCursorHalfUp), + ("C-d".try_into().unwrap(), BufferAction::PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), BufferAction::PageCursorHalfUp.into()), - ("C-f".try_into().unwrap(), Action::PageDown), - ("C-b".try_into().unwrap(), Action::PageUp), + ("C-f".try_into().unwrap(), BufferAction::PageDown.into()), + ("C-b".try_into().unwrap(), BufferAction::PageUp.into()), - ("w".try_into().unwrap(), Action::MoveNextWordStart), - ("e".try_into().unwrap(), Action::MoveNextWordEnd), - ("b".try_into().unwrap(), Action::MovePreviousWordStart), + ("w".try_into().unwrap(), CursorAction::MoveNextWordStart.into()), + ("e".try_into().unwrap(), CursorAction::MoveNextWordEnd.into()), + ("b".try_into().unwrap(), CursorAction::MovePreviousWordStart.into()), - (";".try_into().unwrap(), Action::CollapseSelection), - ("A-;".try_into().unwrap(), Action::FlipSelections), + (";".try_into().unwrap(), BufferAction::CollapseSelection.into()), + ("A-;".try_into().unwrap(), BufferAction::FlipSelections.into()), - ("x".try_into().unwrap(), Action::ExtendLineBelow), - ("X".try_into().unwrap(), Action::ExtendLineAbove), + ("x".try_into().unwrap(), CursorAction::ExtendLineBelow.into()), + ("X".try_into().unwrap(), CursorAction::ExtendLineAbove.into()), - ("d".try_into().unwrap(), Action::Delete), + ("d".try_into().unwrap(), BufferAction::Delete.into()), - ("u".try_into().unwrap(), Action::Undo), - ("U".try_into().unwrap(), Action::Redo), + ("u".try_into().unwrap(), BufferAction::Undo.into()), + ("U".try_into().unwrap(), BufferAction::Redo.into()), - ("C-j".try_into().unwrap(), Action::PreviousBuffer), - ("C-l".try_into().unwrap(), Action::NextBuffer), + ("C-j".try_into().unwrap(), AppAction::PreviousBuffer.into()), + ("C-l".try_into().unwrap(), AppAction::NextBuffer.into()), - ("C".try_into().unwrap(), Action::CopySelectionOnNextLine), + ("C".try_into().unwrap(), BufferAction::CopySelectionOnNextLine.into()), - ("(".try_into().unwrap(), Action::RotateSelectionsBackward), - (")".try_into().unwrap(), Action::RotateSelectionsForward), + ("(".try_into().unwrap(), BufferAction::RotateSelectionsBackward.into()), + (")".try_into().unwrap(), BufferAction::RotateSelectionsForward.into()), - (",".try_into().unwrap(), Action::KeepPrimarySelection), - ("A-,".try_into().unwrap(), Action::RemovePrimarySelection), + (",".try_into().unwrap(), BufferAction::KeepPrimarySelection.into()), + ("A-,".try_into().unwrap(), BufferAction::RemovePrimarySelection.into()), - ("1".try_into().unwrap(), Action::SplitSelectionsInto1s), - ("2".try_into().unwrap(), Action::SplitSelectionsInto2s), - ("3".try_into().unwrap(), Action::SplitSelectionsInto3s), - ("4".try_into().unwrap(), Action::SplitSelectionsInto4s), - ("5".try_into().unwrap(), Action::SplitSelectionsInto5s), - ("6".try_into().unwrap(), Action::SplitSelectionsInto6s), - ("7".try_into().unwrap(), Action::SplitSelectionsInto7s), - ("8".try_into().unwrap(), Action::SplitSelectionsInto8s), - ("9".try_into().unwrap(), Action::SplitSelectionsInto9s), + ("1".try_into().unwrap(), BufferAction::SplitSelectionsInto1s.into()), + ("2".try_into().unwrap(), BufferAction::SplitSelectionsInto2s.into()), + ("3".try_into().unwrap(), BufferAction::SplitSelectionsInto3s.into()), + ("4".try_into().unwrap(), BufferAction::SplitSelectionsInto4s.into()), + ("5".try_into().unwrap(), BufferAction::SplitSelectionsInto5s.into()), + ("6".try_into().unwrap(), BufferAction::SplitSelectionsInto6s.into()), + ("7".try_into().unwrap(), BufferAction::SplitSelectionsInto7s.into()), + ("8".try_into().unwrap(), BufferAction::SplitSelectionsInto8s.into()), + ("9".try_into().unwrap(), BufferAction::SplitSelectionsInto9s.into()), - ("J".try_into().unwrap(), Action::JumpToSelectedOffsetRelativeToMark), - ("A-J".try_into().unwrap(), Action::JumpToSelectedOffset), + ("J".try_into().unwrap(), BufferAction::JumpToSelectedOffsetRelativeToMark.into()), + ("A-J".try_into().unwrap(), BufferAction::JumpToSelectedOffset.into()), - ("m".try_into().unwrap(), Action::ToggleMark), + ("m".try_into().unwrap(), BufferAction::ToggleMark.into()), + + ("y".try_into().unwrap(), AppAction::Yank.into()), ].into()), (Some(PartialAction::Goto), [ - ("j".try_into().unwrap(), Action::GotoLineStart), - ("l".try_into().unwrap(), Action::GotoLineEnd), + ("j".try_into().unwrap(), CursorAction::GotoLineStart.into()), + ("l".try_into().unwrap(), CursorAction::GotoLineEnd.into()), - ("g".try_into().unwrap(), Action::GotoFileStart), + ("g".try_into().unwrap(), CursorAction::GotoFileStart.into()), ].into()), (Some(PartialAction::View), [ - ("z".try_into().unwrap(), Action::AlignViewCenter), - ("b".try_into().unwrap(), Action::AlignViewBottom), - ("t".try_into().unwrap(), Action::AlignViewTop), + ("z".try_into().unwrap(), BufferAction::AlignViewCenter.into()), + ("b".try_into().unwrap(), BufferAction::AlignViewBottom.into()), + ("t".try_into().unwrap(), BufferAction::AlignViewTop.into()), ].into()), (Some(PartialAction::Space), [ - ("w".try_into().unwrap(), Action::Save), + ("w".try_into().unwrap(), BufferAction::Save.into()), + ].into()), + (Some(PartialAction::Repeat), [ + ("i".try_into().unwrap(), CursorAction::MoveByteUp.into()), + ("k".try_into().unwrap(), CursorAction::MoveByteDown.into()), + ("j".try_into().unwrap(), CursorAction::MoveByteLeft.into()), + ("l".try_into().unwrap(), CursorAction::MoveByteRight.into()), + + ("C-e".try_into().unwrap(), BufferAction::ScrollDown.into()), + ("C-y".try_into().unwrap(), BufferAction::ScrollUp.into()), + + ("C-d".try_into().unwrap(), BufferAction::PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), BufferAction::PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), BufferAction::PageDown.into()), + ("C-b".try_into().unwrap(), BufferAction::PageUp.into()), + + ("w".try_into().unwrap(), CursorAction::MoveNextWordStart.into()), + ("e".try_into().unwrap(), CursorAction::MoveNextWordEnd.into()), + ("b".try_into().unwrap(), CursorAction::MovePreviousWordStart.into()), + + ("x".try_into().unwrap(), CursorAction::ExtendLineBelow.into()), + ("X".try_into().unwrap(), CursorAction::ExtendLineAbove.into()), + + ("d".try_into().unwrap(), BufferAction::Delete.into()), + + ("C".try_into().unwrap(), BufferAction::CopySelectionOnNextLine.into()), ].into()), ].into()), (Mode::Select, [ (None, [ - ("q".try_into().unwrap(), Action::QuitIfSaved), - ("Q".try_into().unwrap(), Action::Quit), + ("q".try_into().unwrap(), AppAction::QuitIfSaved.into()), + ("Q".try_into().unwrap(), AppAction::Quit.into()), - ("v".try_into().unwrap(), Action::NormalMode), + ("v".try_into().unwrap(), BufferAction::NormalMode.into()), - ("g".try_into().unwrap(), Action::Goto), - ("z".try_into().unwrap(), Action::View), - ("r".try_into().unwrap(), Action::Replace), - (" ".try_into().unwrap(), Action::Space), + ("g".try_into().unwrap(), BufferAction::Goto.into()), + ("z".try_into().unwrap(), BufferAction::View.into()), + ("r".try_into().unwrap(), BufferAction::Replace.into()), + (" ".try_into().unwrap(), BufferAction::Space.into()), + ("*".try_into().unwrap(), BufferAction::Repeat.into()), - ("i".try_into().unwrap(), Action::ExtendByteUp), - ("k".try_into().unwrap(), Action::ExtendByteDown), - ("j".try_into().unwrap(), Action::ExtendByteLeft), - ("l".try_into().unwrap(), Action::ExtendByteRight), + ("i".try_into().unwrap(), CursorAction::ExtendByteUp.into()), + ("k".try_into().unwrap(), CursorAction::ExtendByteDown.into()), + ("j".try_into().unwrap(), CursorAction::ExtendByteLeft.into()), + ("l".try_into().unwrap(), CursorAction::ExtendByteRight.into()), - ("C-e".try_into().unwrap(), Action::ScrollDown), - ("C-y".try_into().unwrap(), Action::ScrollUp), + ("C-e".try_into().unwrap(), BufferAction::ScrollDown.into()), + ("C-y".try_into().unwrap(), BufferAction::ScrollUp.into()), - ("C-d".try_into().unwrap(), Action::PageCursorHalfDown), - ("C-u".try_into().unwrap(), Action::PageCursorHalfUp), + ("C-d".try_into().unwrap(), BufferAction::PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), BufferAction::PageCursorHalfUp.into()), - ("C-f".try_into().unwrap(), Action::PageDown), - ("C-b".try_into().unwrap(), Action::PageUp), + ("C-f".try_into().unwrap(), BufferAction::PageDown.into()), + ("C-b".try_into().unwrap(), BufferAction::PageUp.into()), - ("w".try_into().unwrap(), Action::ExtendNextWordStart), - ("e".try_into().unwrap(), Action::ExtendNextWordEnd), - ("b".try_into().unwrap(), Action::ExtendPreviousWordStart), + ("w".try_into().unwrap(), CursorAction::ExtendNextWordStart.into()), + ("e".try_into().unwrap(), CursorAction::ExtendNextWordEnd.into()), + ("b".try_into().unwrap(), CursorAction::ExtendPreviousWordStart.into()), - (";".try_into().unwrap(), Action::CollapseSelection), - ("A-;".try_into().unwrap(), Action::FlipSelections), + (";".try_into().unwrap(), BufferAction::CollapseSelection.into()), + ("A-;".try_into().unwrap(), BufferAction::FlipSelections.into()), - ("x".try_into().unwrap(), Action::ExtendLineBelow), - ("X".try_into().unwrap(), Action::ExtendLineAbove), + ("x".try_into().unwrap(), CursorAction::ExtendLineBelow.into()), + ("X".try_into().unwrap(), CursorAction::ExtendLineAbove.into()), - ("d".try_into().unwrap(), Action::Delete), + ("d".try_into().unwrap(), BufferAction::Delete.into()), - ("u".try_into().unwrap(), Action::Undo), - ("U".try_into().unwrap(), Action::Redo), + ("u".try_into().unwrap(), BufferAction::Undo.into()), + ("U".try_into().unwrap(), BufferAction::Redo.into()), - ("C".try_into().unwrap(), Action::CopySelectionOnNextLine), + ("C".try_into().unwrap(), BufferAction::CopySelectionOnNextLine.into()), - ("(".try_into().unwrap(), Action::RotateSelectionsBackward), - (")".try_into().unwrap(), Action::RotateSelectionsForward), + ("(".try_into().unwrap(), BufferAction::RotateSelectionsBackward.into()), + (")".try_into().unwrap(), BufferAction::RotateSelectionsForward.into()), - (",".try_into().unwrap(), Action::KeepPrimarySelection), - ("A-,".try_into().unwrap(), Action::RemovePrimarySelection), + (",".try_into().unwrap(), BufferAction::KeepPrimarySelection.into()), + ("A-,".try_into().unwrap(), BufferAction::RemovePrimarySelection.into()), - ("1".try_into().unwrap(), Action::SplitSelectionsInto1s), - ("2".try_into().unwrap(), Action::SplitSelectionsInto2s), - ("3".try_into().unwrap(), Action::SplitSelectionsInto3s), - ("4".try_into().unwrap(), Action::SplitSelectionsInto4s), - ("5".try_into().unwrap(), Action::SplitSelectionsInto5s), - ("6".try_into().unwrap(), Action::SplitSelectionsInto6s), - ("7".try_into().unwrap(), Action::SplitSelectionsInto7s), - ("8".try_into().unwrap(), Action::SplitSelectionsInto8s), - ("9".try_into().unwrap(), Action::SplitSelectionsInto9s), + ("1".try_into().unwrap(), BufferAction::SplitSelectionsInto1s.into()), + ("2".try_into().unwrap(), BufferAction::SplitSelectionsInto2s.into()), + ("3".try_into().unwrap(), BufferAction::SplitSelectionsInto3s.into()), + ("4".try_into().unwrap(), BufferAction::SplitSelectionsInto4s.into()), + ("5".try_into().unwrap(), BufferAction::SplitSelectionsInto5s.into()), + ("6".try_into().unwrap(), BufferAction::SplitSelectionsInto6s.into()), + ("7".try_into().unwrap(), BufferAction::SplitSelectionsInto7s.into()), + ("8".try_into().unwrap(), BufferAction::SplitSelectionsInto8s.into()), + ("9".try_into().unwrap(), BufferAction::SplitSelectionsInto9s.into()), - ("J".try_into().unwrap(), Action::JumpToSelectedOffsetRelativeToMark), - ("A-J".try_into().unwrap(), Action::JumpToSelectedOffset), + ("J".try_into().unwrap(), BufferAction::JumpToSelectedOffsetRelativeToMark.into()), + ("A-J".try_into().unwrap(), BufferAction::JumpToSelectedOffset.into()), - ("m".try_into().unwrap(), Action::ToggleMark), + ("m".try_into().unwrap(), BufferAction::ToggleMark.into()), + + ("y".try_into().unwrap(), AppAction::Yank.into()), ].into()), (Some(PartialAction::View), [ - ("z".try_into().unwrap(), Action::AlignViewCenter), - ("b".try_into().unwrap(), Action::AlignViewBottom), - ("t".try_into().unwrap(), Action::AlignViewTop), + ("z".try_into().unwrap(), BufferAction::AlignViewCenter.into()), + ("b".try_into().unwrap(), BufferAction::AlignViewBottom.into()), + ("t".try_into().unwrap(), BufferAction::AlignViewTop.into()), ].into()), (Some(PartialAction::Space), [ - ("w".try_into().unwrap(), Action::Save), + ("w".try_into().unwrap(), BufferAction::Save.into()), + ].into()), + (Some(PartialAction::Repeat), [ + ("i".try_into().unwrap(), CursorAction::ExtendByteUp.into()), + ("k".try_into().unwrap(), CursorAction::ExtendByteDown.into()), + ("j".try_into().unwrap(), CursorAction::ExtendByteLeft.into()), + ("l".try_into().unwrap(), CursorAction::ExtendByteRight.into()), + + ("C-e".try_into().unwrap(), BufferAction::ScrollDown.into()), + ("C-y".try_into().unwrap(), BufferAction::ScrollUp.into()), + + ("C-d".try_into().unwrap(), BufferAction::PageCursorHalfDown.into()), + ("C-u".try_into().unwrap(), BufferAction::PageCursorHalfUp.into()), + + ("C-f".try_into().unwrap(), BufferAction::PageDown.into()), + ("C-b".try_into().unwrap(), BufferAction::PageUp.into()), + + ("w".try_into().unwrap(), CursorAction::ExtendNextWordStart.into()), + ("e".try_into().unwrap(), CursorAction::ExtendNextWordEnd.into()), + ("b".try_into().unwrap(), CursorAction::ExtendPreviousWordStart.into()), + + ("x".try_into().unwrap(), CursorAction::ExtendLineBelow.into()), + ("X".try_into().unwrap(), CursorAction::ExtendLineAbove.into()), + + ("d".try_into().unwrap(), BufferAction::Delete.into()), + + ("C".try_into().unwrap(), BufferAction::CopySelectionOnNextLine.into()), ].into()), ].into()) ].into() diff --git a/src/cursor.rs b/src/cursor.rs index 746da15..ee1c9b5 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,6 +1,5 @@ use std::{cmp::{max, min}, mem::swap, ops::RangeInclusive}; - -use crate::BYTES_PER_LINE; +use crate::{BYTES_PER_LINE, action::CursorAction}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Cursor { @@ -77,6 +76,40 @@ impl Cursor { } } + pub fn execute( + &mut self, + action: CursorAction, + max_contents_index: usize + ) { + match action { + CursorAction::MoveByteUp => self.move_byte_up(), + CursorAction::MoveByteDown => self.move_byte_down(max_contents_index), + CursorAction::MoveByteLeft => self.move_byte_left(), + CursorAction::MoveByteRight => self.move_byte_right(max_contents_index), + + CursorAction::ExtendByteUp => self.extend_byte_up(), + CursorAction::ExtendByteDown => self.extend_byte_down(max_contents_index), + CursorAction::ExtendByteLeft => self.extend_byte_left(), + CursorAction::ExtendByteRight => self.extend_byte_right(max_contents_index), + + CursorAction::GotoLineStart => self.goto_line_start(), + CursorAction::GotoLineEnd => self.goto_line_end(max_contents_index), + CursorAction::GotoFileStart => self.goto_file_start(), + CursorAction::GotoFileEnd => self.goto_file_end(max_contents_index), + + CursorAction::MoveNextWordStart => self.move_next_word_start(max_contents_index), + CursorAction::MoveNextWordEnd => self.move_next_word_end(max_contents_index), + CursorAction::MovePreviousWordStart => self.move_previous_word_start(), + + CursorAction::ExtendNextWordStart => self.extend_next_word_start(max_contents_index), + CursorAction::ExtendNextWordEnd => self.extend_next_word_end(max_contents_index), + CursorAction::ExtendPreviousWordStart => self.extend_previous_word_start(), + + CursorAction::ExtendLineBelow => self.extend_line_below(max_contents_index), + CursorAction::ExtendLineAbove => self.extend_line_above(max_contents_index), + } + } + pub const fn move_byte_up(&mut self) { if self.head >= BYTES_PER_LINE { self.head -= BYTES_PER_LINE; diff --git a/src/main.rs b/src/main.rs index 60a6c38..301ac4c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,10 @@ const LINES_OF_PADDING: usize = 5; const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // TODO: +// - extend to mark (tm?) +// - t0 can be to next null +// - tf can be to next FF +// - inspect selection // - resizing can move the cursor off the screen // - tab bar overflow // - search @@ -46,9 +50,6 @@ const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // - y/p // - [/] to cycle view offset? // - gj jump to entered offset -// - repeat X times (dec and hex) -// - from register?? -// - extend to mark (tm?) // future directions // - 'views' for bytes (i8/16/etc u8/16/etc 20.12/8.4/etc) @@ -79,6 +80,9 @@ fn main() { // dbg!(app.edit_history); + // dbg!(app.primary_cursor_register); + // dbg!(app.other_cursor_registers); + for log in app.logs { println!("{log}"); }