use core::slice::GetDisjointMutIndex; use std::{collections::HashSet, fs::File, io::{self, Read}, path::PathBuf}; use crossterm::event::KeyEvent; use ratatui::{style::Stylize, text::Span}; use serde::{Deserialize, Serialize}; use crate::{BYTES_PER_LINE, action::{Action, AppAction}, buffer::actions::bytes_to_nat, config::Config, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize}; mod widget; mod actions; pub struct Buffer { pub file_name: String, pub file_path: PathBuf, pub contents: Vec, pub scroll_position: usize, pub primary_cursor: Cursor, pub cursors: Vec, pub marks: HashSet, pub mode: Mode, pub partial_action: Option, pub partial_replace: Option, pub alert_message: Span<'static>, pub popups: Vec, pub inspection_status: Option, pub edit_history: Vec, // the index *after* the latest edit action pub time_traveling: Option, // the index *after* the last saved edit action pub last_saved_at: Option, pub logs: Vec, } #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug)] #[serde(rename_all = "snake_case")] pub enum Mode { Normal, Select, Insert } #[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug)] #[serde(rename_all = "snake_case")] pub enum PartialAction { Goto, View, Replace, Space, Repeat, Till } #[derive(Clone, Copy, PartialEq, Eq)] pub enum InspectionStatus { Normal, ColorsOnly } impl TryFrom<&str> for PartialAction { type Error = (); fn try_from(value: &str) -> Result { use PartialAction::*; match value { "goto" => Ok(Goto), "view" => Ok(View), "replace" => Ok(Replace), "space" => Ok(Space), "repeat" => Ok(Repeat), "to" => Ok(Till), _ => Err(()), } } } impl Buffer { pub fn from_file_at(file_path: PathBuf) -> io::Result { let mut file = File::open(&file_path)?; let mut contents = Vec::new(); file.read_to_end(&mut contents)?; Ok(Self::new(file_path, contents)) } pub fn new(file_path: PathBuf, contents: Vec) -> Self { Self { file_name: file_path.file_name().unwrap().to_str().unwrap().to_owned(), file_path, contents, scroll_position: 0, primary_cursor: Cursor::default(), cursors: Vec::new(), marks: HashSet::new(), mode: Mode::Normal, partial_action: None, partial_replace: None, alert_message: "".into(), popups: Vec::new(), inspection_status: None, edit_history: Vec::new(), time_traveling: None, last_saved_at: Some(0), logs: Vec::new(), } } pub fn handle_key( &mut self, event: KeyEvent, config: &Config, primary_cursor_register: &[u8], other_cursor_registers: &[Vec], window_size: WindowSize ) -> Option { self.alert_message = "".into(); // self.logs.push(format!("{event:?}")); 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), }; 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()); debug_assert!(self.cursors.is_sorted_by_key(|cursor| cursor.head)); 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; } else { self.partial_replace = Some(nybble); } } else { self.partial_action = None; self.partial_replace = None; } } fn handle_other_modes( &mut self, event: KeyEvent, config: &Config, window_size: WindowSize ) -> Option { use Action::*; 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()) { if action.clears_popups() { self.popups.clear(); } match action { App(app_action) => result = Some(*app_action), Buffer(buffer_action) => self.execute(*buffer_action, window_size), 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); }, } if action.clears_popups() && !action.is_inspection() { self.inspection_status = None; } } 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 { !self.all_changes_saved() } pub const fn all_changes_saved(&self) -> bool { if let Some(last_saved_at) = self.last_saved_at { if let Some(time_traveling) = self.time_traveling { last_saved_at == time_traveling } else { last_saved_at == self.edit_history.len() } } else { false } } // returns 0 if empty pub const fn max_contents_index(&self) -> usize { self.contents.len().saturating_sub(1) } pub fn combine_cursors_if_overlapping(&mut self) { let mut index = 0; // TODO: this can miss some in the WEIRD case that // [ *] // [ *] // [ *] // where * is the head. // the first one wont merge with the 2nd, but the 2nd will // merge with the 3rd, which would then overlap with the 1st, // but won't be checked while !self.cursors.is_empty() && index < self.cursors.len() { while index < self.cursors.len() - 1 && self.cursors[index].range().is_overlapping( &self.cursors[index + 1].range() ) { let next_cursor = self.cursors[index + 1]; self.cursors[index].combine_with(next_cursor); self.cursors.remove(index + 1); } if self.primary_cursor.range() .is_overlapping(&self.cursors[index].range()) { self.primary_cursor.combine_with(self.cursors[index]); self.cursors.remove(index); } else { index += 1; } } } } fn nybble_from_hex(hex: char) -> Option { if !hex.is_ascii() { return None; } match hex { '0'..='9' => Some(u8::try_from(hex).unwrap() - u8::try_from('0').unwrap()), 'a'..='f' => Some(u8::try_from(hex).unwrap() - u8::try_from('a').unwrap() + 10), 'A'..='F' => Some(u8::try_from(hex).unwrap() - u8::try_from('A').unwrap() + 10), _ => None } } mod tests { #[allow(unused_imports)] use crate::buffer::nybble_from_hex; #[test] fn nybble_from_hex_case_doesnt_matter() { for character in 'a'..='f' { assert_eq!(nybble_from_hex(character), nybble_from_hex(character.to_ascii_uppercase())); } } #[test] fn nybble_from_hex_digits_are_correct() { for (index, character) in ('0'..='9').enumerate() { assert_eq!(nybble_from_hex(character), Some(index as u8)); } } }