use crate::models::HistoryEntry;
use crate::parsers::HistoryParser;
#[derive(Debug, Default, Clone, Copy)]
pub struct DefaultHistoryParser;
impl DefaultHistoryParser {
pub const fn new() -> Self {
Self
}
}
impl HistoryParser for DefaultHistoryParser {
fn parse(&self, input: &str) -> Vec<HistoryEntry> {
let mut entries = Vec::new();
let mut pending_ts: Option<u64> = None;
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(rest) = trimmed.strip_prefix('#')
&& rest.chars().all(|c| c.is_ascii_digit())
&& !rest.is_empty()
{
pending_ts = rest.parse().ok();
continue;
}
let command = trimmed.split_whitespace().next().unwrap_or("").to_string();
let mut entry = HistoryEntry::new(trimmed, command);
if let Some(ts) = pending_ts.take() {
entry.timestamp = Some(ts);
}
entries.push(entry);
}
entries
}
}
pub fn parse_history(input: &str) -> Vec<HistoryEntry> {
DefaultHistoryParser::new().parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parser_parses_basic_commands() {
let parser = DefaultHistoryParser::new();
let entries = parser.parse("ls\ngit commit\ncd /home");
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].command, "ls");
assert_eq!(entries[1].command, "git");
assert_eq!(entries[2].command, "cd");
}
#[test]
fn parser_ignores_empty_lines() {
let parser = DefaultHistoryParser::new();
let entries = parser.parse("ls\n\n\ngit");
assert_eq!(entries.len(), 2);
}
#[test]
fn parser_trims_whitespace() {
let parser = DefaultHistoryParser::new();
let entries = parser.parse(" ls \n git status ");
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].raw, "ls");
assert_eq!(entries[0].command, "ls");
assert_eq!(entries[1].raw, "git status");
assert_eq!(entries[1].command, "git");
}
#[test]
fn parser_handles_empty_input() {
let parser = DefaultHistoryParser::new();
let entries = parser.parse("");
assert!(entries.is_empty());
}
#[test]
fn parser_extracts_first_token() {
let parser = DefaultHistoryParser::new();
let entries = parser.parse("git commit -m \"hello world\"");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].command, "git");
}
#[test]
fn parse_history_convenience_function() {
let entries = parse_history("echo hello");
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].command, "echo");
}
#[test]
fn parses_bash_timestamp_prefix() {
let input = "#1577836800\nls\n#1577836801\ngit status\n";
let entries = DefaultHistoryParser::new().parse(input);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].timestamp, Some(1577836800));
assert_eq!(entries[0].command, "ls");
assert_eq!(entries[1].timestamp, Some(1577836801));
assert_eq!(entries[1].command, "git");
}
#[test]
fn bash_timestamp_mixed_with_plain() {
let input = "ls\n#1577836800\ncd ..\n";
let entries = DefaultHistoryParser::new().parse(input);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].timestamp, None);
assert_eq!(entries[0].command, "ls");
assert_eq!(entries[1].timestamp, Some(1577836800));
assert_eq!(entries[1].command, "cd");
}
#[test]
fn bash_consecutive_timestamps_use_last() {
let input = "#1\n#2\nls\n";
let entries = DefaultHistoryParser::new().parse(input);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].timestamp, Some(2));
}
#[test]
fn bash_hash_not_digits_is_plain() {
let input = "# comment\nls\n";
let entries = DefaultHistoryParser::new().parse(input);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].command, "#");
assert_eq!(entries[1].command, "ls");
}
}