use std::fs;
use std::path::PathBuf;
const MAX_HISTORY_SIZE: usize = 500;
#[derive(Debug, Clone)]
pub struct InputHistory {
entries: Vec<String>,
max_size: usize,
current_index: Option<usize>,
saved_draft: Option<String>,
}
impl InputHistory {
pub fn new() -> Self {
Self {
entries: Vec::with_capacity(100),
max_size: MAX_HISTORY_SIZE,
current_index: None,
saved_draft: None,
}
}
pub fn with_max_size(max_size: usize) -> Self {
Self {
entries: Vec::with_capacity(100),
max_size,
current_index: None,
saved_draft: None,
}
}
pub fn add(&mut self, text: &str) {
let text = text.trim();
if text.is_empty() {
return;
}
if self.entries.first().map(|s| s.as_str()) == Some(text) {
return;
}
self.entries.insert(0, text.to_string());
if self.entries.len() > self.max_size {
self.entries.truncate(self.max_size);
}
self.current_index = None;
self.saved_draft = None;
}
pub fn navigate_up(&mut self, current_draft: &str) -> Option<&str> {
if self.entries.is_empty() {
return None;
}
if self.current_index.is_none() {
self.saved_draft = if current_draft.is_empty() {
None
} else {
Some(current_draft.to_string())
};
}
match self.current_index {
None => {
self.current_index = Some(0);
self.entries.first().map(|s| s.as_str())
}
Some(idx) if idx + 1 < self.entries.len() => {
self.current_index = Some(idx + 1);
self.entries.get(idx + 1).map(|s| s.as_str())
}
Some(_) => {
self.current()
}
}
}
pub fn navigate_down(&mut self) -> Option<&str> {
match self.current_index {
None => {
None
}
Some(0) => {
self.current_index = None;
None }
Some(idx) => {
self.current_index = Some(idx - 1);
self.current()
}
}
}
pub fn current(&self) -> Option<&str> {
self.current_index
.and_then(|idx| self.entries.get(idx).map(|s| s.as_str()))
}
pub fn is_navigating(&self) -> bool {
self.current_index.is_some()
}
pub fn saved_draft(&self) -> Option<&str> {
self.saved_draft.as_deref()
}
pub fn reset_navigation(&mut self) {
self.current_index = None;
self.saved_draft = None;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(&self) -> &[String] {
&self.entries
}
pub fn load(path: &PathBuf) -> Result<Self, String> {
if !path.exists() {
return Ok(Self::new());
}
let data =
fs::read_to_string(path).map_err(|e| format!("Failed to read history file: {}", e))?;
let entries: Vec<String> = serde_json::from_str(&data)
.map_err(|e| format!("Failed to deserialize history: {}", e))?;
Ok(Self {
entries,
max_size: MAX_HISTORY_SIZE,
current_index: None,
saved_draft: None,
})
}
pub fn save(&self, path: &PathBuf) -> Result<(), String> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create history directory: {}", e))?;
}
let serialized = serde_json::to_string_pretty(&self.entries)
.map_err(|e| format!("Failed to serialize history: {}", e))?;
fs::write(path, serialized).map_err(|e| format!("Failed to write history file: {}", e))?;
Ok(())
}
pub fn clear(&mut self) {
self.entries.clear();
self.current_index = None;
self.saved_draft = None;
}
}
impl Default for InputHistory {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_new_history_is_empty() {
let history = InputHistory::new();
assert!(history.is_empty());
assert_eq!(history.len(), 0);
assert!(!history.is_navigating());
}
#[test]
fn test_add_entry() {
let mut history = InputHistory::new();
history.add("hello");
assert_eq!(history.len(), 1);
assert_eq!(history.entries()[0], "hello");
}
#[test]
fn test_add_multiple_entries() {
let mut history = InputHistory::new();
history.add("first");
history.add("second");
history.add("third");
assert_eq!(history.len(), 3);
assert_eq!(history.entries()[0], "third");
assert_eq!(history.entries()[1], "second");
assert_eq!(history.entries()[2], "first");
}
#[test]
fn test_add_empty_ignored() {
let mut history = InputHistory::new();
history.add("");
history.add(" ");
assert!(history.is_empty());
}
#[test]
fn test_add_duplicate_most_recent_ignored() {
let mut history = InputHistory::new();
history.add("hello");
history.add("hello");
assert_eq!(history.len(), 1);
}
#[test]
fn test_add_duplicate_older_allowed() {
let mut history = InputHistory::new();
history.add("hello");
history.add("world");
history.add("hello");
assert_eq!(history.len(), 3);
assert_eq!(history.entries()[0], "hello"); assert_eq!(history.entries()[1], "world"); assert_eq!(history.entries()[2], "hello"); }
#[test]
fn test_max_size_truncation() {
let mut history = InputHistory::with_max_size(3);
history.add("first");
history.add("second");
history.add("third");
history.add("fourth");
assert_eq!(history.len(), 3);
assert_eq!(history.entries()[0], "fourth");
assert_eq!(history.entries()[2], "second");
}
#[test]
fn test_navigate_up_empty_history() {
let mut history = InputHistory::new();
let result = history.navigate_up("draft");
assert!(result.is_none());
assert!(!history.is_navigating());
}
#[test]
fn test_navigate_up_saves_draft() {
let mut history = InputHistory::new();
history.add("entry");
history.navigate_up("my draft");
assert!(history.is_navigating());
assert_eq!(history.saved_draft(), Some("my draft"));
}
#[test]
fn test_navigate_up_empty_draft_not_saved() {
let mut history = InputHistory::new();
history.add("entry");
history.navigate_up("");
assert!(history.is_navigating());
assert!(history.saved_draft().is_none());
}
#[test]
fn test_navigate_up_multiple() {
let mut history = InputHistory::new();
history.add("oldest");
history.add("middle");
history.add("newest");
let first = history.navigate_up("");
assert_eq!(first, Some("newest"));
assert_eq!(history.current(), Some("newest"));
let second = history.navigate_up("");
assert_eq!(second, Some("middle"));
let third = history.navigate_up("");
assert_eq!(third, Some("oldest"));
let fourth = history.navigate_up("");
assert_eq!(fourth, Some("oldest"));
}
#[test]
fn test_navigate_down_from_oldest() {
let mut history = InputHistory::new();
history.add("oldest");
history.add("newest");
history.navigate_up("");
history.navigate_up("");
assert_eq!(history.current(), Some("oldest"));
let result = history.navigate_down();
assert_eq!(result, Some("newest"));
let result = history.navigate_down();
assert!(result.is_none());
assert!(!history.is_navigating());
}
#[test]
fn test_navigate_down_without_navigation() {
let mut history = InputHistory::new();
history.add("entry");
let result = history.navigate_down();
assert!(result.is_none());
assert!(!history.is_navigating());
}
#[test]
fn test_reset_navigation() {
let mut history = InputHistory::new();
history.add("entry");
history.navigate_up("draft");
assert!(history.is_navigating());
history.reset_navigation();
assert!(!history.is_navigating());
assert!(history.saved_draft().is_none());
}
#[test]
fn test_add_resets_navigation() {
let mut history = InputHistory::new();
history.add("first");
history.navigate_up("draft");
assert!(history.is_navigating());
history.add("second");
assert!(!history.is_navigating());
assert!(history.saved_draft().is_none());
}
#[test]
fn test_roundtrip_save_load() {
let dir = tempdir().unwrap();
let path = dir.path().join("history.json");
let mut history = InputHistory::new();
history.add("first");
history.add("second");
history.add("third");
history.save(&path).unwrap();
let loaded = InputHistory::load(&path).unwrap();
assert_eq!(loaded.len(), 3);
assert_eq!(loaded.entries()[0], "third");
assert_eq!(loaded.entries()[1], "second");
assert_eq!(loaded.entries()[2], "first");
}
#[test]
fn test_load_missing_file_returns_empty() {
let dir = tempdir().unwrap();
let path = dir.path().join("nonexistent.json");
let history = InputHistory::load(&path).unwrap();
assert!(history.is_empty());
}
#[test]
fn test_save_creates_parent_directory() {
let dir = tempdir().unwrap();
let path = dir.path().join("nested").join("dir").join("history.json");
let mut history = InputHistory::new();
history.add("test");
history.save(&path).unwrap();
assert!(path.exists());
}
#[test]
fn test_clear() {
let mut history = InputHistory::new();
history.add("entry");
history.navigate_up("draft");
history.clear();
assert!(history.is_empty());
assert!(!history.is_navigating());
assert!(history.saved_draft().is_none());
}
#[test]
fn test_trim_on_add() {
let mut history = InputHistory::new();
history.add(" hello world ");
assert_eq!(history.entries()[0], "hello world");
}
#[test]
fn test_navigate_up_down_cycle() {
let mut history = InputHistory::new();
history.add("oldest");
history.add("middle");
history.add("newest");
history.navigate_up("my draft");
history.navigate_up("");
history.navigate_up("");
assert_eq!(history.current(), Some("oldest"));
history.navigate_down();
assert_eq!(history.current(), Some("middle"));
history.navigate_down();
assert_eq!(history.current(), Some("newest"));
let result = history.navigate_down();
assert!(result.is_none());
assert_eq!(history.saved_draft(), Some("my draft"));
}
}