diff --git a/src/engine.rs b/src/engine.rs index 3e93a2a1..8b7bf6c3 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,15 +1,17 @@ use std::io::{Stdout, Write}; +use std::ops::Deref; use crossterm::{ - cursor::{position, MoveToColumn, RestorePosition, SavePosition}, + cursor::{MoveToColumn, RestorePosition, SavePosition}, event::{read, Event, KeyCode, KeyEvent, KeyModifiers}, style::{Color, Print, ResetColor, SetForegroundColor}, terminal::{Clear, ClearType}, - ExecutableCommand, QueueableCommand, Result, + QueueableCommand, Result, }; use std::collections::VecDeque; +use crate::history_search::{BasicSearch, BasicSearchCommand}; use crate::line_buffer::LineBuffer; const HISTORY_SIZE: usize = 100; @@ -27,6 +29,7 @@ pub enum EditCommand { AppendToHistory, PreviousHistory, NextHistory, + SearchHistory, Clear, CutFromStart, CutToEnd, @@ -50,6 +53,7 @@ pub struct Engine { history: VecDeque, history_cursor: i64, has_history: bool, + history_search: Option, // This could be have more features in the future (fzf, configurable?) } pub enum Signal { @@ -77,9 +81,22 @@ pub fn print_crlf(stdout: &mut Stdout) -> Result<()> { Ok(()) } -fn buffer_repaint(stdout: &mut Stdout, engine: &Engine, prompt_offset: u16) -> Result<()> { +fn queue_prompt(stdout: &mut Stdout) -> Result<()> { + // print our prompt + stdout + .queue(MoveToColumn(0))? + .queue(SetForegroundColor(Color::Blue))? + .queue(Print("〉"))? + .queue(ResetColor)?; + + Ok(()) +} + +fn buffer_paint(stdout: &mut Stdout, engine: &Engine) -> Result<()> { let new_index = engine.get_insertion_point(); + queue_prompt(stdout)?; + // Repaint logic: // // Start after the prompt @@ -88,8 +105,7 @@ fn buffer_repaint(stdout: &mut Stdout, engine: &Engine, prompt_offset: u16) -> R // Then draw the remainer of the buffer from above // Finally, reset the cursor to the saved position - stdout.queue(MoveToColumn(prompt_offset))?; - stdout.queue(Print(&engine.line_buffer[0..new_index]))?; + stdout.queue(Print(&engine.line_buffer[..new_index]))?; stdout.queue(SavePosition)?; stdout.queue(Print(&engine.line_buffer[new_index..]))?; stdout.queue(Clear(ClearType::UntilNewLine))?; @@ -100,6 +116,47 @@ fn buffer_repaint(stdout: &mut Stdout, engine: &Engine, prompt_offset: u16) -> R Ok(()) } +fn history_search_paint(stdout: &mut Stdout, engine: &Engine) -> Result<()> { + // Assuming we are currently searching + let search = engine.history_search.as_ref().unwrap(); + + let status = if search.result.is_none() && !search.search_string.is_empty() { + "failed " + } else { + "" + }; + + // print search prompt + stdout + .queue(MoveToColumn(0))? + .queue(SetForegroundColor(Color::Blue))? + .queue(Print(format!( + "({}reverse-search)`{}':", + status, search.search_string + )))? + .queue(ResetColor)?; + + match search.result { + Some((history_index, offset)) => { + let history_result = &engine.history[history_index]; + + stdout.queue(Print(&history_result[..offset]))?; + stdout.queue(SavePosition)?; + stdout.queue(Print(&history_result[offset..]))?; + stdout.queue(Clear(ClearType::UntilNewLine))?; + stdout.queue(RestorePosition)?; + } + + None => { + stdout.queue(Clear(ClearType::UntilNewLine))?; + } + } + + stdout.flush()?; + + Ok(()) +} + impl Engine { pub fn new() -> Engine { let history = VecDeque::with_capacity(HISTORY_SIZE); @@ -113,10 +170,37 @@ impl Engine { history, history_cursor, has_history, + history_search: None, } } pub fn run_edit_commands(&mut self, commands: &[EditCommand]) { + // Handle command for history inputs + if self.history_search.is_some() { + for command in commands { + match command { + EditCommand::InsertChar(c) => { + let search = self.history_search.as_mut().unwrap(); // We checked it is some + search.step(BasicSearchCommand::InsertChar(*c), &self.history); + } + EditCommand::Backspace => { + let search = self.history_search.as_mut().unwrap(); // We checked it is some + search.step(BasicSearchCommand::Backspace, &self.history); + } + EditCommand::SearchHistory => { + let search = self.history_search.as_mut().unwrap(); // We checked it is some + search.step(BasicSearchCommand::Next, &self.history); + } + EditCommand::MoveRight => { + // Ignore move right, it is currently emited with InsertChar + } + // Leave history search otherwise + _ => self.history_search = None, + } + } + return; + } + for command in commands { match command { EditCommand::MoveToStart => self.line_buffer.set_insertion_point(0), @@ -194,6 +278,10 @@ impl Engine { self.set_buffer(new_buffer.clone()); self.move_to_end(); } + EditCommand::SearchHistory => { + self.history_search = + Some(BasicSearch::new(String::from(self.line_buffer.deref()))); + } EditCommand::CutFromStart => { if self.get_insertion_point() > 0 { self.cut_buffer @@ -347,17 +435,9 @@ impl Engine { self.line_buffer.clear_range(range) } - pub fn read_line(&mut self, stdout: &mut Stdout) -> Result { - // print our prompt - stdout - .execute(SetForegroundColor(Color::Blue))? - .execute(Print("〉"))? - .execute(ResetColor)?; - - // set where the input begins - let (mut prompt_offset, _) = position()?; - prompt_offset += 1; + queue_prompt(stdout)?; + stdout.flush()?; loop { match read()? { @@ -415,6 +495,9 @@ impl Engine { KeyCode::Char('n') => { self.run_edit_commands(&[EditCommand::NextHistory]); } + KeyCode::Char('r') => { + self.run_edit_commands(&[EditCommand::SearchHistory]); + } KeyCode::Char('t') => { self.run_edit_commands(&[EditCommand::SwapGraphemes]); } @@ -473,16 +556,25 @@ impl Engine { KeyCode::End => { self.run_edit_commands(&[EditCommand::MoveToEnd]); } - KeyCode::Enter => { - let buffer = self.line_buffer.to_owned(); - - self.run_edit_commands(&[ - EditCommand::AppendToHistory, - EditCommand::Clear, - ]); - - return Ok(Signal::Success(buffer)); - } + KeyCode::Enter => match &self.history_search { + Some(search) => { + if let Some((history_index, _)) = search.result { + self.line_buffer + .set_buffer(self.history[history_index].clone()); + } + self.history_search = None; + } + None => { + let buffer = self.line_buffer.to_owned(); + + self.run_edit_commands(&[ + EditCommand::AppendToHistory, + EditCommand::Clear, + ]); + + return Ok(Signal::Success(buffer)); + } + }, KeyCode::Up => { self.run_edit_commands(&[EditCommand::PreviousHistory]); } @@ -508,7 +600,11 @@ impl Engine { print_message(stdout, &format!("width: {} and height: {}", width, height))?; } } - buffer_repaint(stdout, &self, prompt_offset)?; + if self.history_search.is_some() { + history_search_paint(stdout, &self)?; + } else { + buffer_paint(stdout, &self)?; + } } } } diff --git a/src/history_search.rs b/src/history_search.rs new file mode 100644 index 00000000..0532f3d6 --- /dev/null +++ b/src/history_search.rs @@ -0,0 +1,55 @@ +use std::collections::VecDeque; + +pub struct BasicSearch { + pub result: Option<(usize, usize)>, + pub search_string: String, +} + +pub enum BasicSearchCommand { + InsertChar(char), + Backspace, + Next, +} + +impl BasicSearch { + pub fn new(search_string: String) -> Self { + Self { + result: None, + search_string, + } + } + + pub fn step(&mut self, command: BasicSearchCommand, history: &VecDeque) { + let mut start = self + .result + .map(|(history_index, _)| history_index) + .unwrap_or(0); + + match command { + BasicSearchCommand::InsertChar(c) => { + self.search_string.push(c); + } + BasicSearchCommand::Backspace => { + self.search_string.pop(); // TODO: Unicode grapheme? + } + BasicSearchCommand::Next => { + start += 1; + } + } + + if self.search_string.is_empty() { + self.result = None; + } else { + self.result = history + .iter() + .enumerate() + .skip(start) + .filter_map(|(history_index, s)| { + s.match_indices(&self.search_string) + .next() + .map(|(offset, _)| (history_index, offset)) + }) + .next(); + } + } +} diff --git a/src/main.rs b/src/main.rs index 69ed6a0a..73d87bff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use engine::{print_crlf, print_message, Engine, Signal}; mod diagnostic; use diagnostic::print_events; +mod history_search; + fn main() -> Result<()> { let mut stdout = stdout();