diff --git a/TODO.txt b/TODO.txt index 12e582a4..6c9d988a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -25,7 +25,9 @@ X Up/down history X Home/end X Refactor keypresses to flush at the end X Ctrl-A, Ctrl-K, etc -* More Ctrl-??? key combinations +X More Ctrl-??? key combinations +X "engine"? +X Command pattern * Validation * Autocompletion * Multiline support diff --git a/src/engine.rs b/src/engine.rs new file mode 100644 index 00000000..5acdfc79 --- /dev/null +++ b/src/engine.rs @@ -0,0 +1,260 @@ +use std::collections::VecDeque; + +use crate::line_buffer::LineBuffer; + +const HISTORY_SIZE: usize = 100; + +pub enum EditCommand { + MoveToStart, + MoveToEnd, + MoveLeft, + MoveRight, + MoveWordLeft, + MoveWordRight, + InsertChar(char), + Backspace, + Delete, + AppendToHistory, + PreviousHistory, + NextHistory, + Clear, + CutFromStart, + CutToEnd, + CutWordLeft, + CutWordRight, + InsertCutBuffer, +} + +pub struct Engine { + line_buffer: LineBuffer, + + // Cut buffer + cut_buffer: String, + + // History + history: VecDeque, + history_cursor: i64, + has_history: bool, +} + +impl Engine { + pub fn new() -> Engine { + let history = VecDeque::with_capacity(HISTORY_SIZE); + let history_cursor = -1i64; + let has_history = false; + let cut_buffer = String::new(); + + Engine { + line_buffer: LineBuffer::new(), + cut_buffer, + history, + history_cursor, + has_history, + } + } + + pub fn run_edit_commands(&mut self, commands: &[EditCommand]) { + for command in commands { + match command { + EditCommand::MoveToStart => self.line_buffer.set_insertion_point(0), + EditCommand::MoveToEnd => { + self.line_buffer.move_to_end(); + } + EditCommand::MoveLeft => self.line_buffer.dec_insertion_point(), + EditCommand::MoveRight => self.line_buffer.inc_insertion_point(), + EditCommand::MoveWordLeft => { + self.line_buffer.move_word_left(); + } + EditCommand::MoveWordRight => { + self.line_buffer.move_word_right(); + } + EditCommand::InsertChar(c) => { + let insertion_point = self.line_buffer.get_insertion_point(); + self.line_buffer.insert_char(insertion_point, *c) + } + EditCommand::Backspace => { + let insertion_point = self.get_insertion_point(); + if insertion_point == self.get_buffer_len() && !self.is_empty() { + // buffer.dec_insertion_point(); + self.pop(); + } else if insertion_point < self.get_buffer_len() + && insertion_point > 0 + && !self.is_empty() + { + self.dec_insertion_point(); + let insertion_point = self.get_insertion_point(); + self.remove_char(insertion_point); + } + } + EditCommand::Delete => { + let insertion_point = self.get_insertion_point(); + if insertion_point < self.get_buffer_len() && !self.is_empty() { + self.remove_char(insertion_point); + } + } + EditCommand::Clear => { + self.line_buffer.clear(); + self.set_insertion_point(0); + } + EditCommand::AppendToHistory => { + if self.history.len() + 1 == HISTORY_SIZE { + // History is "full", so we delete the oldest entry first, + // before adding a new one. + self.history.pop_back(); + } + self.history.push_front(String::from(self.get_buffer())); + self.has_history = true; + // reset the history cursor - we want to start at the bottom of the + // history again. + self.history_cursor = -1; + } + EditCommand::PreviousHistory => { + if self.has_history && self.history_cursor < (self.history.len() as i64 - 1) { + self.history_cursor += 1; + let history_entry = self + .history + .get(self.history_cursor as usize) + .unwrap() + .clone(); + self.set_buffer(history_entry.clone()); + self.move_to_end(); + } + } + EditCommand::NextHistory => { + if self.history_cursor >= 0 { + self.history_cursor -= 1; + } + let new_buffer = if self.history_cursor < 0 { + String::new() + } else { + // We can be sure that we always have an entry on hand, that's why + // unwrap is fine. + self.history + .get(self.history_cursor as usize) + .unwrap() + .clone() + }; + + self.set_buffer(new_buffer.clone()); + self.move_to_end(); + } + EditCommand::CutFromStart => { + if self.get_insertion_point() > 0 { + let cut_slice = self.get_buffer()[..self.get_insertion_point()].to_string(); + + self.cut_buffer.replace_range(.., &cut_slice); + self.clear_to_insertion_point(); + } + } + EditCommand::CutToEnd => { + let cut_slice = &self.get_buffer()[self.get_insertion_point()..].to_string(); + if !cut_slice.is_empty() { + self.cut_buffer.replace_range(.., &cut_slice); + self.clear_to_end(); + } + } + EditCommand::CutWordLeft => { + let old_insertion_point = self.get_insertion_point(); + + self.move_word_left(); + + let cut_slice = self.get_buffer() + [self.get_insertion_point()..old_insertion_point] + .to_string(); + + if self.get_insertion_point() < old_insertion_point { + self.cut_buffer.replace_range(.., &cut_slice); + self.clear_range(self.get_insertion_point()..old_insertion_point); + } + } + EditCommand::CutWordRight => { + let old_insertion_point = self.get_insertion_point(); + + self.move_word_right(); + + let cut_slice = self.get_buffer() + [old_insertion_point..self.get_insertion_point()] + .to_string(); + + if self.get_insertion_point() > old_insertion_point { + self.cut_buffer.replace_range(.., &cut_slice); + self.clear_range(old_insertion_point..self.get_insertion_point()); + self.set_insertion_point(old_insertion_point); + } + } + EditCommand::InsertCutBuffer => { + let cut_buffer = self.cut_buffer.clone(); + self.insert_str(self.get_insertion_point(), &cut_buffer); + self.set_insertion_point(self.get_insertion_point() + self.cut_buffer.len()); + } + } + } + } + + pub fn set_insertion_point(&mut self, pos: usize) { + self.line_buffer.set_insertion_point(pos) + } + + pub fn get_insertion_point(&self) -> usize { + self.line_buffer.get_insertion_point() + } + + pub fn get_buffer(&self) -> &str { + &self.line_buffer.get_buffer() + } + + pub fn set_buffer(&mut self, buffer: String) { + self.line_buffer.set_buffer(buffer) + } + + pub fn move_to_end(&mut self) -> usize { + self.line_buffer.move_to_end() + } + + pub fn dec_insertion_point(&mut self) { + self.line_buffer.dec_insertion_point() + } + + pub fn get_buffer_len(&self) -> usize { + self.line_buffer.get_buffer_len() + } + + pub fn remove_char(&mut self, pos: usize) -> char { + self.line_buffer.remove_char(pos) + } + + pub fn insert_str(&mut self, idx: usize, string: &str) { + self.line_buffer.insert_str(idx, string) + } + + pub fn is_empty(&self) -> bool { + self.line_buffer.is_empty() + } + + pub fn pop(&mut self) -> Option { + self.line_buffer.pop() + } + + pub fn clear_to_end(&mut self) { + self.line_buffer.clear_to_end() + } + + pub fn clear_to_insertion_point(&mut self) { + self.line_buffer.clear_to_insertion_point() + } + + pub fn clear_range(&mut self, range: R) + where + R: std::ops::RangeBounds, + { + self.line_buffer.clear_range(range) + } + + pub fn move_word_left(&mut self) -> usize { + self.line_buffer.move_word_left() + } + + pub fn move_word_right(&mut self) -> usize { + self.line_buffer.move_word_right() + } +} diff --git a/src/main.rs b/src/main.rs index d0fdb8a5..a6108461 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,13 +9,12 @@ use crossterm::{ ExecutableCommand, QueueableCommand, Result, }; -use std::collections::VecDeque; use std::io::Stdout; mod line_buffer; -use line_buffer::LineBuffer; -const HISTORY_SIZE: usize = 100; +mod engine; +use engine::{EditCommand, Engine}; fn print_message(stdout: &mut Stdout, msg: &str) -> Result<()> { stdout @@ -29,9 +28,9 @@ fn print_message(stdout: &mut Stdout, msg: &str) -> Result<()> { Ok(()) } -fn buffer_repaint(stdout: &mut Stdout, buffer: &LineBuffer, prompt_offset: u16) -> Result<()> { - let raw_buffer = buffer.get_buffer(); - let new_index = buffer.get_insertion_point(); +fn buffer_repaint(stdout: &mut Stdout, engine: &Engine, prompt_offset: u16) -> Result<()> { + let raw_buffer = engine.get_buffer(); + let new_index = engine.get_insertion_point(); // Repaint logic: // @@ -96,11 +95,7 @@ fn main() -> Result<()> { return Ok(()); }; - let mut buffer = LineBuffer::new(); - let mut history = VecDeque::with_capacity(HISTORY_SIZE); - let mut history_cursor = -1i64; - let mut has_history = false; - let mut cut_buffer = String::new(); + let mut engine = Engine::new(); 'repl: loop { // print our prompt @@ -120,89 +115,50 @@ fn main() -> Result<()> { modifiers: KeyModifiers::CONTROL, }) => match code { KeyCode::Char('d') => { - if buffer.get_buffer().is_empty() { + if engine.get_buffer().is_empty() { stdout.queue(MoveToNextLine(1))?.queue(Print("exit"))?; break 'repl; } else { - let insertion_point = buffer.get_insertion_point(); - if insertion_point < buffer.get_buffer_len() && !buffer.is_empty() { - buffer.remove_char(insertion_point); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::Delete]); } } KeyCode::Char('a') => { - buffer.set_insertion_point(0); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveToStart]); } KeyCode::Char('e') => { - let buffer_len = buffer.get_buffer_len(); - buffer.set_insertion_point(buffer_len); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveToEnd]); } KeyCode::Char('k') => { - let cut_slice = &buffer.get_buffer()[buffer.get_insertion_point()..]; - if !cut_slice.is_empty() { - cut_buffer.replace_range(.., cut_slice); - buffer.clear_to_end(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::CutToEnd]); } KeyCode::Char('u') => { - if buffer.get_insertion_point() > 0 { - cut_buffer.replace_range( - .., - &buffer.get_buffer()[..buffer.get_insertion_point()], - ); - buffer.clear_to_insertion_point(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::CutFromStart]); } KeyCode::Char('y') => { - buffer.insert_str(buffer.get_insertion_point(), &cut_buffer); - buffer.set_insertion_point(buffer.get_insertion_point() + cut_buffer.len()); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::InsertCutBuffer]); } KeyCode::Char('b') => { - buffer.dec_insertion_point(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveLeft]); } KeyCode::Char('f') => { - buffer.inc_insertion_point(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveRight]); } KeyCode::Char('c') => { - buffer.clear(); + engine.run_edit_commands(&[EditCommand::Clear]); stdout.queue(Print('\n'))?.queue(MoveToColumn(1))?.flush()?; break 'input; } KeyCode::Char('h') => { - let insertion_point = buffer.get_insertion_point(); - if insertion_point == buffer.get_buffer_len() && !buffer.is_empty() { - buffer.pop(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } else if insertion_point < buffer.get_buffer_len() - && insertion_point > 0 - && !buffer.is_empty() - { - buffer.dec_insertion_point(); - let insertion_point = buffer.get_insertion_point(); - buffer.remove_char(insertion_point); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::Backspace]); } KeyCode::Char('w') => { - let old_insertion_point = buffer.get_insertion_point(); - buffer.move_word_left(); - if buffer.get_insertion_point() < old_insertion_point { - cut_buffer.replace_range( - .., - &buffer.get_buffer() - [buffer.get_insertion_point()..old_insertion_point], - ); - buffer.clear_range(buffer.get_insertion_point()..old_insertion_point); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::CutWordLeft]); + } + KeyCode::Left => { + engine.run_edit_commands(&[EditCommand::MoveWordLeft]); + } + KeyCode::Right => { + engine.run_edit_commands(&[EditCommand::MoveWordRight]); } _ => {} }, @@ -211,152 +167,71 @@ fn main() -> Result<()> { modifiers: KeyModifiers::ALT, }) => match code { KeyCode::Char('b') => { - buffer.move_word_left(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveWordLeft]); } KeyCode::Char('f') => { - buffer.move_word_right(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveWordRight]); } KeyCode::Char('d') => { - let old_insertion_point = buffer.get_insertion_point(); - buffer.move_word_right(); - if buffer.get_insertion_point() > old_insertion_point { - cut_buffer.replace_range( - .., - &buffer.get_buffer() - [old_insertion_point..buffer.get_insertion_point()], - ); - buffer.clear_range(old_insertion_point..buffer.get_insertion_point()); - buffer.set_insertion_point(old_insertion_point); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::CutWordRight]); } KeyCode::Left => { - if buffer.get_insertion_point() > 0 { - // If the ALT modifier is set, we want to jump words for more - // natural editing. Jumping words basically means: move to next - // whitespace in the given direction. - buffer.move_word_left(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::MoveWordLeft]); } KeyCode::Right => { - if buffer.get_insertion_point() < buffer.get_buffer_len() { - buffer.move_word_right(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::MoveWordRight]); } _ => {} }, Event::Key(KeyEvent { code, modifiers: _ }) => { match code { KeyCode::Char(c) => { - buffer.insert_char(buffer.get_insertion_point(), c); - buffer.inc_insertion_point(); - - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[ + EditCommand::InsertChar(c), + EditCommand::MoveRight, + ]); } KeyCode::Backspace => { - let insertion_point = buffer.get_insertion_point(); - if insertion_point == buffer.get_buffer_len() && !buffer.is_empty() { - // buffer.dec_insertion_point(); - buffer.pop(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } else if insertion_point < buffer.get_buffer_len() - && insertion_point > 0 - && !buffer.is_empty() - { - buffer.dec_insertion_point(); - let insertion_point = buffer.get_insertion_point(); - buffer.remove_char(insertion_point); - - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::Backspace]); } KeyCode::Delete => { - let insertion_point = buffer.get_insertion_point(); - if insertion_point < buffer.get_buffer_len() && !buffer.is_empty() { - buffer.remove_char(insertion_point); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::Delete]); } KeyCode::Home => { - buffer.set_insertion_point(0); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveToStart]); } KeyCode::End => { - let buffer_len = buffer.get_buffer_len(); - buffer.set_insertion_point(buffer_len); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::MoveToEnd]); } KeyCode::Enter => { - if buffer.get_buffer() == "exit" { + if engine.get_buffer() == "exit" { break 'repl; } else { - if history.len() + 1 == HISTORY_SIZE { - // History is "full", so we delete the oldest entry first, - // before adding a new one. - history.pop_back(); - } - history.push_front(String::from(buffer.get_buffer())); - has_history = true; - // reset the history cursor - we want to start at the bottom of the - // history again. - history_cursor = -1; - print_message( - &mut stdout, - &format!("Our buffer: {}", buffer.get_buffer()), - )?; - buffer.clear(); - buffer.set_insertion_point(0); + let buffer = String::from(engine.get_buffer()); + + engine.run_edit_commands(&[ + EditCommand::AppendToHistory, + EditCommand::Clear, + ]); + print_message(&mut stdout, &format!("Our buffer: {}", buffer))?; + break 'input; } } KeyCode::Up => { - // Up means: navigate through the history. - if has_history && history_cursor < (history.len() as i64 - 1) { - history_cursor += 1; - let history_entry = - history.get(history_cursor as usize).unwrap().clone(); - buffer.set_buffer(history_entry.clone()); - buffer.move_to_end(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::PreviousHistory]); } KeyCode::Down => { // Down means: navigate forward through the history. If we reached the // bottom of the history, we clear the buffer, to make it feel like // zsh/bash/whatever - if history_cursor >= 0 { - history_cursor -= 1; - } - let new_buffer = if history_cursor < 0 { - String::new() - } else { - // We can be sure that we always have an entry on hand, that's why - // unwrap is fine. - history.get(history_cursor as usize).unwrap().clone() - }; - - buffer.set_buffer(new_buffer.clone()); - buffer.move_to_end(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; + engine.run_edit_commands(&[EditCommand::NextHistory]); } KeyCode::Left => { - if buffer.get_insertion_point() > 0 { - // If the ALT modifier is set, we want to jump words for more - // natural editing. Jumping words basically means: move to next - // whitespace in the given direction. - buffer.dec_insertion_point(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::MoveLeft]); } KeyCode::Right => { - if buffer.get_insertion_point() < buffer.get_buffer_len() { - buffer.inc_insertion_point(); - buffer_repaint(&mut stdout, &buffer, prompt_offset)?; - } + engine.run_edit_commands(&[EditCommand::MoveRight]); } _ => {} }; @@ -371,6 +246,7 @@ fn main() -> Result<()> { )?; } } + buffer_repaint(&mut stdout, &engine, prompt_offset)?; } } terminal::disable_raw_mode()?;