rustyline 0.2.0

Rustyline, a readline implementation based on Antirez's Linenoise
//! History API

use std::collections::VecDeque;
use std::path::Path;
use std::fs::File;

use super::Result;

pub struct History {
    entries: VecDeque<String>,
    max_len: usize,
}

const DEFAULT_HISTORY_MAX_LEN: usize = 100;

impl History {
    pub fn new() -> History {
        History {
            entries: VecDeque::new(),
            max_len: DEFAULT_HISTORY_MAX_LEN,
        }
    }

    /// Return the history entry at position `index`, starting from 0.
    pub fn get(&self, index: usize) -> Option<&String> {
        return self.entries.get(index);
    }

    /// Add a new entry in the history.
    pub fn add(&mut self, line: &str) -> bool {
        if self.max_len == 0 {
            return false;
        }
        if line.len() == 0 || line.chars().next().map_or(true, |c| c.is_whitespace()) {
            // ignorespace
            return false;
        }
        if let Some(s) = self.entries.back() {
            if s == line {
                // ignoredups
                return false;
            }
        }
        if self.entries.len() == self.max_len {
            self.entries.pop_front();
        }
        self.entries.push_back(String::from(line));
        return true;
    }

    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Set the maximum length for the history. This function can be called even
    /// if there is already some history, the function will make sure to retain
    /// just the latest `len` elements if the new history length value is smaller
    /// than the amount of items already inside the history.
    pub fn set_max_len(&mut self, len: usize) {
        self.max_len = len;
        if len == 0 {
            self.entries.clear();
            return;
        }
        loop {
            if self.entries.len() <= len {
                break;
            }
            self.entries.pop_front();
        }
    }

    /// Save the history in the specified file.
    pub fn save<P: AsRef<Path> + ?Sized>(&self, path: &P) -> Result<()> {
        use std::io::{BufWriter, Write};

        if self.entries.len() == 0 {
            return Ok(());
        }
        let file = try!(File::create(path));
        let mut wtr = BufWriter::new(file);
        for entry in self.entries.iter() {
            try!(wtr.write_all(&entry.as_bytes()));
            try!(wtr.write_all(b"\n"));
        }
        return Ok(());
    }

    /// Load the history from the specified file.
    pub fn load<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
        use std::io::{BufRead, BufReader};

        let file = try!(File::open(&path));
        let rdr = BufReader::new(file);
        for line in rdr.lines() {
            self.add(try!(line).as_ref()); // TODO truncate to MAX_LINE
        }
        return Ok(());
    }

    /// Clear history
    pub fn clear(&mut self) {
        self.entries.clear()
    }

    /// Search history (start position inclusive [0, len-1])
    pub fn search(&self, term: &str, start: usize, reverse: bool) -> Option<usize> {
        if term.is_empty() || start >= self.len() {
            return None;
        }
        if reverse {
            let index = self.entries
                            .iter()
                            .rev()
                            .skip(self.entries.len() - 1 - start)
                            .position(|entry| entry.contains(term));
            index.and_then(|index| Some(start - index))
        } else {
            let index = self.entries.iter().skip(start).position(|entry| entry.contains(term));
            index.and_then(|index| Some(index + start))
        }
    }
}

#[cfg(test)]
mod tests {
    extern crate tempdir;
    use std::path::Path;

    fn init() -> super::History {
        let mut history = super::History::new();
        assert!(history.add("line1"));
        assert!(history.add("line2"));
        assert!(history.add("line3"));
        return history;
    }

    #[test]
    fn new() {
        let history = super::History::new();
        assert_eq!(super::DEFAULT_HISTORY_MAX_LEN, history.max_len);
        assert_eq!(0, history.entries.len());
    }

    #[test]
    fn add() {
        let mut history = super::History::new();
        assert!(history.add("line1"));
        assert!(history.add("line2"));
        assert!(!history.add("line2"));
        assert!(!history.add(""));
        assert!(!history.add(" line3"));
    }

    #[test]
    fn set_max_len() {
        let mut history = init();
        history.set_max_len(1);
        assert_eq!(1, history.entries.len());
        assert_eq!(Some(&"line3".to_string()), history.entries.back());
    }

    #[test]
    fn save() {
        let mut history = init();
        let td = tempdir::TempDir::new_in(&Path::new("."), "histo").unwrap();
        let history_path = td.path().join(".history");

        history.save(&history_path).unwrap();
        history.load(&history_path).unwrap();
        td.close().unwrap();
    }

    #[test]
    fn search() {
        let history = init();
        assert_eq!(None, history.search("", 0, false));
        assert_eq!(None, history.search("none", 0, false));
        assert_eq!(None, history.search("line", 3, false));

        assert_eq!(Some(0), history.search("line", 0, false));
        assert_eq!(Some(1), history.search("line", 1, false));
        assert_eq!(Some(2), history.search("line3", 1, false));
    }

    #[test]
    fn reverse_search() {
        let history = init();
        assert_eq!(None, history.search("", 2, true));
        assert_eq!(None, history.search("none", 2, true));
        assert_eq!(None, history.search("line", 3, true));

        assert_eq!(Some(2), history.search("line", 2, true));
        assert_eq!(Some(1), history.search("line", 1, true));
        assert_eq!(Some(0), history.search("line1", 1, true));
    }
}