diff --git a/src/env.rs b/src/env.rs index 9f95fa5..cf088d2 100644 --- a/src/env.rs +++ b/src/env.rs @@ -3,7 +3,11 @@ use super::{ Error, Result, exec, }; -use std::{collections::HashMap, io::Write, iter::FromIterator}; +use std::{ + io::Write, + collections::HashMap, + iter::FromIterator +}; /// An environment used to [evaluate expressions](mod@super::eval) or /// [execute programs](mod@super::exec). @@ -19,7 +23,9 @@ pub trait Env { V: AsRef; /// Get a bound value from the env. - fn get (&self, name: &str) -> Option; + fn get (&self, name: N) -> Option + where + N: AsRef; /// Get the current working directory. fn working_dir (&self) -> Result; @@ -132,7 +138,10 @@ impl Env for SetArgs { self.parent.bind(name, value); } - fn get (&self, name: &str) -> Option { + fn get (&self, name: N) -> Option + where + N: AsRef + { self.parent.get(name) } @@ -188,9 +197,12 @@ impl Env for Scope<'_, E> { ); } - fn get (&self, name: &str) -> Option { + fn get (&self, name: N) -> Option + where + N: AsRef + { self.bindings - .get(name) + .get(name.as_ref()) .cloned() .or_else(|| { self.parent.get(name) @@ -200,15 +212,20 @@ impl Env for Scope<'_, E> { /// An iterator of arguments. #[derive(Default, Clone)] -pub struct Args (Vec); +pub struct Args { + args: Vec, + cur: usize +} impl FromIterator for Args { fn from_iter > (iter: T) -> Args { - Args ( - iter.into_iter() + Args { + args: iter + .into_iter() .map(|s| s.to_string()) - .collect() - ) + .collect(), + cur: 0 + } } } @@ -216,11 +233,12 @@ impl Iterator for Args { type Item = String; fn next (&mut self) -> Option { - if self.0.len() > 0 { - Some (self.0.remove(0)) - } else { - None - } + let Args { args, cur } = self; + + let val = args.get(*cur)?; + *cur += 1; + + Some (val.clone()) } } @@ -257,8 +275,11 @@ impl Env for Pure { ); } - fn get (&self, name: &str) -> Option { - self.bindings.get(name).cloned() + fn get (&self, name: N) -> Option + where + N: AsRef + { + self.bindings.get(name.as_ref()).cloned() } fn stdout (&mut self, data: R) -> Result<()> @@ -310,8 +331,11 @@ impl Env for Inherit { std::env::set_var(name.as_ref(), value.as_ref()); } - fn get (&self, name: &str) -> Option { - std::env::var(name).ok() + fn get (&self, name: N) -> Option + where + N: AsRef + { + std::env::var(name.as_ref()).ok() } fn working_dir (&self) -> Result { @@ -334,7 +358,8 @@ impl Env for Inherit { R: ToString { std::io::stdout().write( - data.to_string().as_bytes() + data.to_string() + .as_bytes() )?; Ok (()) diff --git a/src/env/history.rs b/src/env/history.rs index 487fb4c..ba511c6 100644 --- a/src/env/history.rs +++ b/src/env/history.rs @@ -1 +1,191 @@ //! 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 { + + 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, + 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) -> &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 { + self.buf.pop() + } + + /// Search the command history. + pub fn search <'s: 'x, 'x> (&'x self, s: &'s str) -> impl Iterator { + 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 { + self.buf.iter() + .rev() + .skip(self.cur) + .filter(move |e| { + e.fuzzy_match(&s).is_some() + }) + } +} + +impl From for Entry { + fn from (text: S) -> Entry { + Entry { raw: text.to_string() } + } +} + +/// Configuration for the [`History`] buffer. +#[derive(Clone)] +pub struct Config { + ignore: Vec, + 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(); +}