diff --git a/src/engine.rs b/src/engine.rs index 0082d55c..b0853213 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,5 +1,5 @@ -use crate::line_buffer::LineBuffer; use crate::Prompt; +use crate::{history::History, line_buffer::LineBuffer}; use crate::{ history_search::{BasicSearch, BasicSearchCommand}, line_buffer::InsertionPoint, @@ -11,13 +11,11 @@ use crossterm::{ terminal::{self, Clear, ClearType}, QueueableCommand, Result, }; -use std::collections::VecDeque; use std::{ io::{stdout, Stdout, Write}, time::Duration, }; -const HISTORY_SIZE: usize = 100; static PROMPT_INDICATOR: &str = "〉"; const PROMPT_COLOR: Color = Color::Blue; @@ -55,9 +53,7 @@ pub struct Reedline { cut_buffer: String, // History - history: VecDeque, - history_cursor: i64, - has_history: bool, + history: History, history_search: Option, // This could be have more features in the future (fzf, configurable?) // Stdout @@ -79,9 +75,7 @@ impl Default for Reedline { impl Reedline { pub fn new() -> Reedline { - let history = VecDeque::with_capacity(HISTORY_SIZE); - let history_cursor = -1i64; - let has_history = false; + let history = History::default(); let cut_buffer = String::new(); let stdout = stdout(); @@ -89,8 +83,6 @@ impl Reedline { line_buffer: LineBuffer::new(), cut_buffer, history, - history_cursor, - has_history, history_search: None, stdout, } @@ -235,52 +227,21 @@ impl Reedline { 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(); - } - // Don't append if the preceding value is identical - if self - .history - .front() - .map_or(true, |entry| entry.as_str() != self.insertion_line()) - { - self.history.push_front(self.insertion_line().to_string()); - } - self.has_history = true; + self.history.append(self.insertion_line().to_string()); // reset the history cursor - we want to start at the bottom of the // history again. - self.history_cursor = -1; + self.history.reset_cursor(); } 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()); + if let Some(history_entry) = self.history.go_back() { + let new_buffer = history_entry.to_string(); + self.set_buffer(new_buffer); 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()); + let new_buffer = self.history.go_forward().unwrap_or_default().to_string(); + self.set_buffer(new_buffer); self.move_to_end(); } EditCommand::SearchHistory => { diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 00000000..080d98dc --- /dev/null +++ b/src/history.rs @@ -0,0 +1,128 @@ +use std::{collections::VecDeque, ops::Index}; + +/// Default size of the `History` +const HISTORY_SIZE: usize = 100; + +/// Stateful history that allows up/down-arrow browsing with an internal cursor +pub struct History { + capacity: usize, + entries: VecDeque, + cursor: isize, // -1 not browsing through history, >= 0 index into history +} + +impl Default for History { + fn default() -> Self { + Self::new(HISTORY_SIZE) + } +} + +impl History { + /// Creates an in-memory history that remembers `<= capacity` elements + pub fn new(capacity: usize) -> Self { + if capacity > isize::MAX as usize { + panic!("History capacity too large to be addressed safely"); + } + History { + capacity, + entries: VecDeque::with_capacity(capacity), + cursor: -1, + } + } + + /// Access the underlying entries (exported for possible fancy access to underlying VecDeque) + #[allow(dead_code)] + pub fn entries(&self) -> &VecDeque { + &self.entries + } + + /// Append an entry if non-empty and not repetition of the previous entry + pub fn append(&mut self, entry: String) { + if self.entries.len() + 1 == self.capacity { + // History is "full", so we delete the oldest entry first, + // before adding a new one. + self.entries.pop_back(); + } + // Don't append if the preceding value is identical or the string empty + if self + .entries + .front() + .map_or(true, |previous| previous != &entry) + && !entry.is_empty() + { + self.entries.push_front(entry); + } + } + + /// Reset the internal browsing cursor + pub fn reset_cursor(&mut self) { + self.cursor = -1 + } + + /// Try to move back in history. Returns `None` if history is exhausted. + pub fn go_back(&mut self) -> Option<&str> { + if self.cursor < (self.entries.len() as isize - 1) { + self.cursor += 1; + Some(self.entries.get(self.cursor as usize).unwrap()) + } else { + None + } + } + + /// Try to move forward in history. Returns `None` if history is exhausted (moving beyond most recent element). + pub fn go_forward(&mut self) -> Option<&str> { + if self.cursor >= 0 { + self.cursor -= 1; + } + if self.cursor >= 0 { + Some(self.entries.get(self.cursor as usize).unwrap()) + } else { + None + } + } + + /// Yields iterator to immutable references from the underlying data structure + pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, String> { + self.entries.iter() + } +} + +impl Index for History { + type Output = String; + + fn index(&self, index: usize) -> &Self::Output { + &self.entries[index] + } +} + +#[cfg(test)] +mod tests { + use super::History; + #[test] + fn navigates_safely() { + let mut hist = History::default(); + hist.append("test".to_string()); + assert_eq!(hist.go_forward(), None); // On empty line nothing to move forward to + assert_eq!(hist.go_back().unwrap(), "test"); // Back to the entry + assert_eq!(hist.go_back(), None); // Nothing to move back to + assert_eq!(hist.go_forward(), None); // Forward out of history to editing line + } + #[test] + fn appends_only_unique() { + let mut hist = History::default(); + hist.append("unique_old".to_string()); + hist.append("test".to_string()); + hist.append("test".to_string()); + hist.append("unique".to_string()); + assert_eq!(hist.entries().len(), 3); + assert_eq!(hist.go_back().unwrap(), "unique"); + assert_eq!(hist.go_back().unwrap(), "test"); + assert_eq!(hist.go_back().unwrap(), "unique_old"); + assert_eq!(hist.go_back(), None); + } + #[test] + fn appends_no_empties() { + let mut hist = History::default(); + hist.append("".to_string()); + assert_eq!(hist.entries().len(), 0); + } +} diff --git a/src/history_search.rs b/src/history_search.rs index 93f80c19..543be0e1 100644 --- a/src/history_search.rs +++ b/src/history_search.rs @@ -1,4 +1,4 @@ -use std::collections::VecDeque; +use crate::history::History; #[derive(Clone)] pub struct BasicSearch { @@ -20,7 +20,7 @@ impl BasicSearch { } } - pub fn step(&mut self, command: BasicSearchCommand, history: &VecDeque) { + pub fn step(&mut self, command: BasicSearchCommand, history: &History) { let mut start = self .result .map(|(history_index, _)| history_index) diff --git a/src/lib.rs b/src/lib.rs index 709f0584..205ff0f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ mod engine; pub use engine::{Reedline, Signal}; +mod history; + mod history_search; mod prompt;