repeat x times

This commit is contained in:
alice pellerin
2026-03-21 16:41:10 -05:00
parent 823e186acd
commit 9ac66fc074
6 changed files with 483 additions and 383 deletions
+147 -40
View File
@@ -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<u8>],
window_size: WindowSize
) -> Option<AppAction> {
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<AppAction> {
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<u8>],
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 {