Implemented very basic fuzzy search on the history buffer
This commit is contained in:
parent
3447bcef80
commit
d366f17ed4
2 changed files with 235 additions and 20 deletions
65
src/env.rs
65
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<str>;
|
||||
|
||||
/// Get a bound value from the env.
|
||||
fn get (&self, name: &str) -> Option<String>;
|
||||
fn get <N> (&self, name: N) -> Option<String>
|
||||
where
|
||||
N: AsRef<str>;
|
||||
|
||||
/// Get the current working directory.
|
||||
fn working_dir (&self) -> Result<std::path::PathBuf>;
|
||||
|
@ -132,7 +138,10 @@ impl<E: Env> Env for SetArgs<E> {
|
|||
self.parent.bind(name, value);
|
||||
}
|
||||
|
||||
fn get (&self, name: &str) -> Option<String> {
|
||||
fn get <N> (&self, name: N) -> Option<String>
|
||||
where
|
||||
N: AsRef<str>
|
||||
{
|
||||
self.parent.get(name)
|
||||
}
|
||||
|
||||
|
@ -188,9 +197,12 @@ impl<E: Env> Env for Scope<'_, E> {
|
|||
);
|
||||
}
|
||||
|
||||
fn get (&self, name: &str) -> Option<String> {
|
||||
fn get <N> (&self, name: N) -> Option<String>
|
||||
where
|
||||
N: AsRef<str>
|
||||
{
|
||||
self.bindings
|
||||
.get(name)
|
||||
.get(name.as_ref())
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
self.parent.get(name)
|
||||
|
@ -200,15 +212,20 @@ impl<E: Env> Env for Scope<'_, E> {
|
|||
|
||||
/// An iterator of arguments.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Args (Vec<String>);
|
||||
pub struct Args {
|
||||
args: Vec<String>,
|
||||
cur: usize
|
||||
}
|
||||
|
||||
impl<S: ToString> FromIterator<S> for Args {
|
||||
fn from_iter <T: IntoIterator<Item = S>> (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<Self::Item> {
|
||||
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<String> {
|
||||
self.bindings.get(name).cloned()
|
||||
fn get <N> (&self, name: N) -> Option<String>
|
||||
where
|
||||
N: AsRef<str>
|
||||
{
|
||||
self.bindings.get(name.as_ref()).cloned()
|
||||
}
|
||||
|
||||
fn stdout <R> (&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<String> {
|
||||
std::env::var(name).ok()
|
||||
fn get <N> (&self, name: N) -> Option<String>
|
||||
where
|
||||
N: AsRef<str>
|
||||
{
|
||||
std::env::var(name.as_ref()).ok()
|
||||
}
|
||||
|
||||
fn working_dir (&self) -> Result<std::path::PathBuf> {
|
||||
|
@ -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 (())
|
||||
|
|
190
src/env/history.rs
vendored
190
src/env/history.rs
vendored
|
@ -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<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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue