use std::{
collections::VecDeque,
fs::File,
io::{BufRead, BufReader, Write},
path::Path,
};
#[derive(Clone)]
pub struct History {
entries: VecDeque<String>,
position: usize,
pub limit_size: Option<usize>,
}
impl Default for History {
fn default() -> Self {
Self {
entries: VecDeque::from([String::new()]),
position: 0,
limit_size: None,
}
}
}
impl History {
pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> anyhow::Result<()> {
let mut file = File::create(path)?;
let mut contents = self.entries.clone();
contents.pop_back();
let items_to_save: Vec<_> = if let Some(limit) = self.limit_size {
contents.iter().rev().take(limit).collect()
} else {
contents.iter().rev().collect()
};
for item in items_to_save {
writeln!(file, "{}", item)?;
}
Ok(())
}
pub fn load_from_file<P: AsRef<Path>>(
path: P,
limit_size: Option<usize>,
) -> anyhow::Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut ret = Self {
limit_size,
..Default::default()
};
for line in reader.lines() {
let line = line?;
if !line.is_empty() {
ret.entries.push_back(line);
}
}
ret.entries.push_back(String::new());
ret.move_to_tail(); Ok(ret)
}
pub fn insert<T: AsRef<str>>(&mut self, item: T) {
let item = item.as_ref().to_string();
if !self.exists(&item) {
let init_state = self.entries.pop_back().unwrap();
self.entries.push_back(item);
if let Some(limit) = self.limit_size
&& limit < self.entries.len()
{
self.entries.pop_front();
}
self.entries.push_back(init_state);
}
self.move_to_tail();
}
pub fn get(&self) -> String {
self.entries
.get(self.position)
.unwrap_or(&String::new())
.to_string()
}
fn exists<T: AsRef<str>>(&self, item: T) -> bool {
self.entries.iter().any(|i| i == item.as_ref())
}
pub fn backward(&mut self) -> bool {
let Some(position) = self.position.checked_sub(1) else {
return false;
};
self.position = position;
true
}
pub fn forward(&mut self) -> bool {
if self.position.saturating_add(1) >= self.entries.len() {
return false;
}
self.position += 1;
true
}
pub fn move_to_tail(&mut self) {
self.position = self.entries.len() - 1;
}
}
#[cfg(test)]
mod tests {
mod backward {
use super::super::*;
#[test]
fn stops_at_the_oldest_entry() {
let mut history = History::default();
history.insert("first");
history.insert("second");
assert!(history.backward());
assert_eq!(history.position, 1);
assert_eq!(history.get(), "second");
assert!(history.backward());
assert_eq!(history.position, 0);
assert_eq!(history.get(), "first");
assert!(!history.backward());
assert_eq!(history.position, 0);
}
}
mod forward {
use super::super::*;
#[test]
fn stops_at_the_editing_slot() {
let mut history = History::default();
history.insert("first");
assert!(!history.forward());
assert_eq!(history.position, 1);
assert!(history.backward());
assert!(history.forward());
assert_eq!(history.position, 1);
}
}
mod move_to_tail {
use super::super::*;
#[test]
fn selects_the_editing_slot() {
let mut history = History::default();
history.insert("first");
history.backward();
history.move_to_tail();
assert_eq!(history.position, 1);
}
}
mod insert {
use super::super::*;
#[test]
fn appends_an_entry_before_the_editing_slot() {
let mut h = History::default();
h.insert("item");
assert_eq!(
VecDeque::from([String::from("item"), String::new()]),
h.entries
);
}
#[test]
fn preserves_insertion_order() {
let mut h = History::default();
h.insert("item1");
h.insert("item2");
assert_eq!(
VecDeque::from([String::from("item1"), String::from("item2"), String::new()]),
h.entries
);
}
#[test]
fn evicts_the_oldest_entry_at_the_size_limit() {
let mut h = History {
limit_size: Some(2),
..Default::default()
};
h.insert("item1");
h.insert("item2");
h.insert("item3");
assert_eq!(
VecDeque::from([String::from("item2"), String::from("item3"), String::new()]),
h.entries
);
}
}
mod exists {
use super::super::*;
#[test]
fn reports_whether_an_entry_exists() {
let mut h = History::default();
h.insert("existed");
assert!(h.exists("existed"));
assert!(!h.exists("not_found"));
}
}
}