rush/src/env/history.rs

192 lines
4.8 KiB
Rust

//! History control.
/// An entry in the [`History`] buffer.
#[derive(Debug)]
pub struct Entry {
raw: String,
}
impl Entry {
/// Returns `None` if the string does not match the given
/// fuzzy pattern. Returns `Some(score)` which can be used
/// to rank the results depending on how well they match.
pub fn fuzzy_match (&self, s: &str) -> Option<usize> {
let mut i = 0;
for c in s.chars() {
i = self.raw[i..].find(c)?;
}
Some (0)
}
/// Returns `true` if the entry contains the substring `s`.
pub fn exact_match (&self, s: &str) -> bool {
self.raw.contains(s)
}
}
/// A buffer that keeps track of the shell history.
#[derive(Default)]
pub struct History {
buf: Vec<Entry>,
cfg: Config,
cur: usize,
}
impl History {
/// Create a new empty history buffer with the given configuration.
///
/// To create one with the default configuration, you can use [`History::default`].
pub fn new (cfg: Config) -> History {
History {
buf: Vec::new(),
cur: 0,
cfg,
}
}
/// Move the cursor backwards in time.
pub fn prev (&mut self) -> &mut Self {
self.back(1)
}
/// Move the cursor forwards in time.
pub fn next (&mut self) -> &mut Self {
self.skip(1)
}
/// Time travel backwards.
fn back (&mut self, n: usize) -> &mut Self {
let History { buf, cur, .. } = self;
*cur = if *cur + n >= buf.len() { buf.len() - 1 } else { *cur + n };
self
}
/// Time travel into the "future".
fn skip (&mut self, n: usize) -> &mut Self {
let History { cur, .. } = self;
*cur = if *cur > n { *cur - n } else { 0 };
self
}
/// Append the given entry to the history buffer.
pub fn append (&mut self, entry: impl Into<Entry>) -> &mut Self {
let entry = entry.into();
self.buf.push(entry);
self
}
/// Remove the last entry of the buffer, if one exists.
pub fn pop (&mut self) -> Option<Entry> {
self.buf.pop()
}
/// Search the command history.
pub fn search <'s: 'x, 'x> (&'x self, s: &'s str) -> impl Iterator<Item = &'x Entry> {
self.buf.iter()
.rev()
.skip(self.cur)
.filter(move |Entry { raw }| {
raw.contains(s)
})
}
/// Search the command history using the fuzzy algorithm of
/// [`fuzzy_match`](Entry::fuzzy_match). The matching entries are
/// returned in the order in which they are entered in the buffer,
/// reversed. The fuzzy matching score is ignored.
pub fn fuzzy_search <'s: 'x, 'x> (&'x self, s: &'s str) -> impl Iterator<Item = &'x Entry> {
self.buf.iter()
.rev()
.skip(self.cur)
.filter(move |e| {
e.fuzzy_match(&s).is_some()
})
}
}
impl<S: ToString> From<S> for Entry {
fn from (text: S) -> Entry {
Entry { raw: text.to_string() }
}
}
/// Configuration for the [`History`] buffer.
#[derive(Clone)]
pub struct Config {
ignore: Vec<String>,
ignore_dups: bool,
limit: usize,
}
impl Config {
/// Create a new config.
///
/// This is an alias for [`Config::default`].
pub fn new () -> Config {
Config::default()
}
/// Configure the history to ignore consecutive exactly equal
/// history entries.
pub fn ignore_dups (mut self) -> Self {
self.ignore_dups = true;
self
}
/// Set the history size limit.
pub fn limit (mut self, limit: usize) -> Self {
self.limit = limit;
self
}
/// Create a [`History`] buffer which uses this configuration.
pub fn into_history (self) -> History {
History::new(self)
}
}
impl Default for Config {
fn default () -> Self {
Config {
ignore: Vec::new(),
ignore_dups: false,
limit: 10000,
}
}
}
#[test]
fn fuzzy_search () {
let mut hist = History::default();
hist.append("z /etc/nixos")
.append("exa /home/riley")
.append("bat /home/riley/default.nix")
.append("exa /etc")
.append("nv ~/.config/sway/config")
.append("exa /etc/nixos")
.append("nv /etc/nixos/configuration.nix");
assert!(hist.fuzzy_search("cosw").count() == 1);
assert!(hist.fuzzy_search("conf").count() == 2);
assert!(hist.fuzzy_search("nix").count() == 4);
// Matches `nv /etc/nixos/configuration.nix` and
// `nv ~/.config/sway/config`, in that order.
let mut iter = hist.fuzzy_search("cfg");
assert!(iter.next().unwrap().raw == "nv /etc/nixos/configuration.nix");
assert!(iter.next().unwrap().raw == "nv ~/.config/sway/config");
}
#[test]
fn fuzzy_match () {
Entry::from("exa /home/riley").fuzzy_match("exa").unwrap();
}