//! Earlier command lines, for Up and Down and for the ghost text. /// How many lines are kept. const LIMIT: usize = 200; /// Earlier command lines, oldest first. A line is kept once: running it /// again moves it to the end. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct History { lines: Vec, } impl History { /// A history holding these lines, oldest first (as loaded from disk). pub fn from_lines(lines: Vec) -> History { let mut history = History::default(); for line in lines { history.add(&line); } history } /// The lines, oldest first (to save them). pub fn lines(&self) -> &[String] { &self.lines } /// Remembers a line that was run. pub fn add(&mut self, line: &str) { let line = line.trim(); if line.is_empty() { return; } self.lines.retain(|earlier| earlier != line); self.lines.push(line.to_string()); if self.lines.len() > LIMIT { self.lines.remove(0); } } /// The line at an index (from [`older`](History::older) or /// [`newer`](History::newer)). pub fn get(&self, index: usize) -> Option<&str> { self.lines.get(index).map(String::as_str) } /// The newest line before `before` (or before the end, for `None`) that /// starts with `start`, as Up finds it. pub fn older(&self, start: &str, before: Option) -> Option { let end = before.unwrap_or(self.lines.len()).min(self.lines.len()); self.lines[..end].iter().rposition(|line| line.starts_with(start)) } /// The oldest line after `after` that starts with `start`, as Down finds /// it. pub fn newer(&self, start: &str, after: usize) -> Option { let from = after + 1; self.lines.get(from..)?.iter().position(|line| line.starts_with(start)).map(|offset| from + offset) } /// The newest line that starts with `start` and goes further: what the /// ghost text suggests, like fish's. pub fn suggestion(&self, start: &str) -> Option<&str> { if start.trim().is_empty() { return None; } self.lines .iter() .rev() .find(|line| line.starts_with(start) && line.len() > start.len()) .map(String::as_str) } } #[cfg(test)] mod tests { use super::*; #[test] fn up_and_down_only_see_matching_lines() { let history = History::from_lines(vec!["fold open".into(), "e garden.cum".into(), "fold auto".into()]); let up = history.older("fold", None).unwrap(); assert_eq!(history.get(up), Some("fold auto")); let up_again = history.older("fold", Some(up)).unwrap(); assert_eq!(history.get(up_again), Some("fold open")); assert_eq!(history.older("fold", Some(up_again)), None); assert_eq!(history.newer("fold", up_again), Some(up)); assert_eq!(history.suggestion("e g"), Some("e garden.cum")); assert_eq!(history.suggestion(""), None); } #[test] fn running_a_line_again_moves_it_to_the_end() { let mut history = History::from_lines(vec!["q".into(), "w".into()]); history.add("q"); history.add(" "); assert_eq!(history.lines(), ["w", "q"]); } }