#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct InputHistory {
items: Vec<String>,
max_size: usize,
position: Option<usize>,
temp_input: Option<String>,
}
impl InputHistory {
pub const DEFAULT_MAX_SIZE: usize = 100;
pub fn new() -> Self {
Self::with_capacity(Self::DEFAULT_MAX_SIZE)
}
pub fn with_capacity(max_size: usize) -> Self {
assert!(max_size > 0, "History max_size must be greater than 0");
Self {
items: Vec::new(),
max_size,
position: None,
temp_input: None,
}
}
pub fn push(&mut self, item: String) {
if item.is_empty() {
return;
}
if self.items.last().map(|s| s.as_str()) == Some(item.as_str()) {
return;
}
self.items.push(item);
while self.items.len() > self.max_size {
self.items.remove(0);
}
self.reset_navigation();
}
pub fn navigate_prev(&mut self, current_input: &str) -> Option<String> {
if self.items.is_empty() {
return None;
}
match self.position {
None => {
self.temp_input = Some(current_input.to_string());
self.position = Some(self.items.len() - 1);
Some(self.items[self.items.len() - 1].clone())
}
Some(pos) if pos > 0 => {
self.position = Some(pos - 1);
Some(self.items[pos - 1].clone())
}
Some(_) => {
None
}
}
}
pub fn navigate_next(&mut self) -> Option<String> {
match self.position {
None => {
None
}
Some(pos) if pos < self.items.len() - 1 => {
self.position = Some(pos + 1);
Some(self.items[pos + 1].clone())
}
Some(_) => {
let original = self.temp_input.clone();
self.reset_navigation();
original
}
}
}
pub fn reset_navigation(&mut self) {
self.position = None;
self.temp_input = None;
}
pub fn last(&self) -> Option<&str> {
self.items.last().map(|s| s.as_str())
}
pub fn init_at_last(&mut self) {
if !self.items.is_empty() {
self.position = Some(self.items.len() - 1);
self.temp_input = None;
}
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn clear(&mut self) {
self.items.clear();
self.reset_navigation();
}
pub fn items(&self) -> &[String] {
&self.items
}
pub fn from_items(items: Vec<String>) -> Self {
let mut history = Self::new();
for item in items {
history.push(item);
}
history
}
pub fn save_to_file(&self, path: &std::path::Path) -> std::io::Result<()> {
let json = serde_json::to_string_pretty(&self.items).map_err(std::io::Error::other)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(path, json)?;
Ok(())
}
pub fn load_from_file(path: &std::path::Path) -> std::io::Result<Self> {
if !path.exists() {
return Ok(Self::new());
}
let json = std::fs::read_to_string(path)?;
let items: Vec<String> = serde_json::from_str(&json).map_err(std::io::Error::other)?;
let mut history = Self::new();
history.items = items;
if history.items.len() > history.max_size {
let excess = history.items.len() - history.max_size;
history.items.drain(0..excess);
}
Ok(history)
}
}
pub fn get_data_dir() -> std::io::Result<std::path::PathBuf> {
let data_dir = dirs::data_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine data directory",
)
})?;
Ok(data_dir.join("fresh"))
}
pub fn get_search_history_path() -> std::io::Result<std::path::PathBuf> {
Ok(get_data_dir()?.join("search_history.json"))
}
pub fn get_replace_history_path() -> std::io::Result<std::path::PathBuf> {
Ok(get_data_dir()?.join("replace_history.json"))
}
impl Default for InputHistory {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_history_is_empty() {
let history = InputHistory::new();
assert!(history.is_empty());
assert_eq!(history.len(), 0);
assert_eq!(history.last(), None);
}
#[test]
fn test_push_adds_items() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.push("third".to_string());
assert_eq!(history.len(), 3);
assert_eq!(history.last(), Some("third"));
}
#[test]
fn test_push_skips_empty_strings() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("".to_string());
history.push("second".to_string());
assert_eq!(history.len(), 2);
}
#[test]
fn test_push_skips_consecutive_duplicates() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.push("second".to_string());
history.push("second".to_string());
history.push("third".to_string());
assert_eq!(history.len(), 3);
assert_eq!(history.items, vec!["first", "second", "third"]);
}
#[test]
fn test_push_allows_non_consecutive_duplicates() {
let mut history = InputHistory::new();
history.push("search".to_string());
history.push("other".to_string());
history.push("search".to_string());
assert_eq!(history.len(), 3);
assert_eq!(history.items, vec!["search", "other", "search"]);
}
#[test]
fn test_navigate_prev_empty_history() {
let mut history = InputHistory::new();
let result = history.navigate_prev("current");
assert_eq!(result, None);
}
#[test]
fn test_navigate_prev_basic() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.push("third".to_string());
let prev = history.navigate_prev("typing...");
assert_eq!(prev, Some("third".to_string()));
let prev = history.navigate_prev("typing...");
assert_eq!(prev, Some("second".to_string()));
let prev = history.navigate_prev("typing...");
assert_eq!(prev, Some("first".to_string()));
let prev = history.navigate_prev("typing...");
assert_eq!(prev, None);
}
#[test]
fn test_navigate_next_without_prev() {
let mut history = InputHistory::new();
history.push("item".to_string());
let result = history.navigate_next();
assert_eq!(result, None);
}
#[test]
fn test_navigate_next_returns_to_original() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.navigate_prev("typing...");
history.navigate_prev("typing...");
let next = history.navigate_next();
assert_eq!(next, Some("second".to_string()));
let next = history.navigate_next();
assert_eq!(next, Some("typing...".to_string()));
let next = history.navigate_next();
assert_eq!(next, None);
}
#[test]
fn test_reset_navigation() {
let mut history = InputHistory::new();
history.push("item".to_string());
history.navigate_prev("current");
assert!(history.position.is_some());
assert!(history.temp_input.is_some());
history.reset_navigation();
assert!(history.position.is_none());
assert!(history.temp_input.is_none());
}
#[test]
fn test_max_size_enforcement() {
let mut history = InputHistory::with_capacity(3);
history.push("first".to_string());
history.push("second".to_string());
history.push("third".to_string());
assert_eq!(history.len(), 3);
history.push("fourth".to_string());
assert_eq!(history.len(), 3);
assert_eq!(history.items, vec!["second", "third", "fourth"]);
history.push("fifth".to_string());
assert_eq!(history.len(), 3);
assert_eq!(history.items, vec!["third", "fourth", "fifth"]);
}
#[test]
fn test_clear() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.navigate_prev("current");
history.clear();
assert!(history.is_empty());
assert_eq!(history.len(), 0);
assert!(history.position.is_none());
assert!(history.temp_input.is_none());
}
#[test]
fn test_up_down_up_down_sequence() {
let mut history = InputHistory::new();
history.push("first".to_string());
history.push("second".to_string());
history.push("third".to_string());
assert_eq!(history.navigate_prev("current"), Some("third".to_string()));
assert_eq!(history.navigate_prev("current"), Some("second".to_string()));
assert_eq!(history.navigate_next(), Some("third".to_string()));
assert_eq!(history.navigate_prev("current"), Some("second".to_string()));
}
#[test]
fn test_full_navigation_cycle() {
let mut history = InputHistory::new();
history.push("alpha".to_string());
history.push("beta".to_string());
history.push("gamma".to_string());
let original = "my search query";
assert_eq!(history.navigate_prev(original), Some("gamma".to_string()));
assert_eq!(history.navigate_prev(original), Some("beta".to_string()));
assert_eq!(history.navigate_prev(original), Some("alpha".to_string()));
assert_eq!(history.navigate_prev(original), None);
assert_eq!(history.navigate_next(), Some("beta".to_string()));
assert_eq!(history.navigate_next(), Some("gamma".to_string()));
assert_eq!(history.navigate_next(), Some(original.to_string())); assert_eq!(history.navigate_next(), None); }
#[test]
#[should_panic(expected = "History max_size must be greater than 0")]
fn test_zero_capacity_panics() {
InputHistory::with_capacity(0);
}
#[test]
fn test_single_item_history() {
let mut history = InputHistory::with_capacity(1);
history.push("first".to_string());
history.push("second".to_string());
history.push("third".to_string());
assert_eq!(history.len(), 1);
assert_eq!(history.last(), Some("third"));
}
}