use crate::errors::LisaError;
use parking_lot::RwLock;
use std::{
collections::VecDeque,
fs::{File, OpenOptions},
io::{BufRead, BufReader, Write},
path::Path,
};
pub trait HistoryProvider: Send + Sync {
#[must_use]
fn get_previous_command(&self, command_back: usize) -> Option<String>;
fn complete_command(&self, input_so_far: &str) -> Option<String>;
fn insert_command(&self, new_command: &str);
fn attempt_to_do_full_sync(&self);
}
#[derive(Debug)]
pub struct SimpleHistoryProvider {
backing_file: RwLock<Option<File>>,
history: RwLock<VecDeque<String>>,
histsize: usize,
}
impl SimpleHistoryProvider {
#[must_use]
pub fn new_in_memory(histsize: usize) -> Self {
Self {
backing_file: RwLock::new(None),
history: RwLock::new(VecDeque::with_capacity(histsize)),
histsize,
}
}
pub fn new_file_backed<PathTy: AsRef<Path>>(
histsize: usize,
history_path: PathTy,
) -> Result<Self, LisaError> {
let path = history_path.as_ref();
if path.is_file() {
let fd = OpenOptions::new().write(true).read(true).open(path)?;
let mut history = VecDeque::with_capacity(histsize);
{
let reader = BufReader::new(&fd);
for line in reader.lines().collect::<Vec<_>>().into_iter().rev() {
if history.len() >= histsize {
break;
}
let Ok(full_line) = line else {
continue;
};
if full_line.is_empty() {
continue;
}
history.push_back(full_line);
}
}
Ok(Self {
backing_file: RwLock::new(Some(fd)),
history: RwLock::new(history),
histsize,
})
} else {
let fd = File::create_new(path)?;
Ok(Self {
backing_file: RwLock::new(Some(fd)),
history: RwLock::new(VecDeque::with_capacity(histsize)),
histsize,
})
}
}
}
impl HistoryProvider for SimpleHistoryProvider {
fn attempt_to_do_full_sync(&self) {
let mut backing_file_opt = self.backing_file.write();
if let Some(fd) = backing_file_opt.as_mut() {
let to_write = {
let read_locked = self.history.read();
read_locked
.iter()
.map(|data| data.replace('\n', " "))
.collect::<Vec<String>>()
.join("\n")
};
_ = fd.set_len(0);
_ = fd.write_all(to_write.as_bytes());
_ = fd.sync_data();
}
}
fn get_previous_command(&self, command_back: usize) -> Option<String> {
let guard = self.history.read();
guard
.iter()
.rev()
.nth(command_back - 1)
.map(ToOwned::to_owned)
}
fn complete_command(&self, input_so_far: &str) -> Option<String> {
let guard = self.history.read();
for command in guard.iter().rev() {
if command.len() > input_so_far.len() && command.starts_with(input_so_far) {
return Some((command[input_so_far.len()..]).to_owned());
}
}
None
}
fn insert_command(&self, new_command: &str) {
let mut guard = self.history.write();
if guard.len() >= self.histsize {
guard.pop_front();
}
guard.push_back(new_command.to_owned());
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
pub fn in_memory_history_provider() {
let mut memory_provider = SimpleHistoryProvider::new_in_memory(1);
memory_provider.insert_command("hi :)");
memory_provider.attempt_to_do_full_sync();
memory_provider = SimpleHistoryProvider::new_in_memory(1);
assert!(memory_provider.get_previous_command(1).is_none());
memory_provider.insert_command("hello :)");
assert_eq!(
memory_provider.get_previous_command(1),
Some("hello :)".to_owned()),
);
assert_eq!(memory_provider.get_previous_command(2), None);
memory_provider.insert_command("new");
assert_eq!(
memory_provider.get_previous_command(1),
Some("new".to_owned()),
);
assert_eq!(memory_provider.get_previous_command(2), None);
}
#[test]
pub fn file_backed_history_provider() {
let tempdir_guard = tempdir().expect("failed to create temporary directory for test!");
let mut history_path = PathBuf::from(&tempdir_guard.path());
history_path.push("example-history-file");
{
let file_provider_smol =
SimpleHistoryProvider::new_file_backed(1, history_path.clone())
.expect("Failed to create file backed provider with empty file!");
file_provider_smol.insert_command("hello");
assert_eq!(
file_provider_smol.get_previous_command(1),
Some("hello".to_owned()),
);
file_provider_smol.insert_command("hi");
assert_eq!(
file_provider_smol.get_previous_command(1),
Some("hi".to_owned()),
);
assert_eq!(file_provider_smol.get_previous_command(2), None);
file_provider_smol.attempt_to_do_full_sync();
}
{
let file_provider_lorg =
SimpleHistoryProvider::new_file_backed(2, history_path.clone())
.expect("Failed to create file backed provider from existing file!");
assert_eq!(
file_provider_lorg.get_previous_command(1),
Some("hi".to_owned())
);
file_provider_lorg.insert_command("other");
assert_eq!(
file_provider_lorg.get_previous_command(1),
Some("other".to_owned()),
);
assert_eq!(
file_provider_lorg.get_previous_command(2),
Some("hi".to_owned()),
);
file_provider_lorg.insert_command("even newer");
assert_eq!(
file_provider_lorg.get_previous_command(1),
Some("even newer".to_owned()),
);
assert_eq!(
file_provider_lorg.get_previous_command(2),
Some("other".to_owned()),
);
file_provider_lorg.attempt_to_do_full_sync();
}
{
let file_provider_smol = SimpleHistoryProvider::new_file_backed(1, history_path)
.expect("Failed to create file backed provider from existing large file!");
assert_eq!(
file_provider_smol.get_previous_command(1),
Some("even newer".to_owned()),
);
assert_eq!(file_provider_smol.get_previous_command(2), None);
file_provider_smol.insert_command("final");
assert_eq!(
file_provider_smol.get_previous_command(1),
Some("final".to_owned()),
);
assert_eq!(file_provider_smol.get_previous_command(2), None);
}
}
#[test]
pub fn complete_command_examples() {
let memory_provider = SimpleHistoryProvider::new_in_memory(3);
memory_provider.insert_command("test small");
memory_provider.insert_command("test large");
memory_provider.insert_command("other");
assert_eq!(
memory_provider.complete_command("test"),
Some(" large".to_owned()),
);
assert_eq!(
memory_provider.complete_command("test s"),
Some("mall".to_owned()),
);
assert_eq!(
memory_provider.complete_command("oth"),
Some("er".to_owned()),
);
assert_eq!(memory_provider.complete_command("no"), None);
}
}