From 3cca45bef232f377c006ed7358cfdc9cd0d69f2f Mon Sep 17 00:00:00 2001 From: Alex <32611784+ajesipow@users.noreply.github.com> Date: Sat, 24 Aug 2024 14:35:43 +0200 Subject: [PATCH] Fix reverse import order for zsh and nu --- crates/atuin-client/src/import/mod.rs | 53 ++++++++++++++++++-- crates/atuin-client/src/import/nu.rs | 3 +- crates/atuin-client/src/import/zsh.rs | 72 +++++++++++++++++---------- 3 files changed, 99 insertions(+), 29 deletions(-) diff --git a/crates/atuin-client/src/import/mod.rs b/crates/atuin-client/src/import/mod.rs index eb3ce04511c..d8667f22ccf 100644 --- a/crates/atuin-client/src/import/mod.rs +++ b/crates/atuin-client/src/import/mod.rs @@ -2,12 +2,11 @@ use std::fs::File; use std::io::Read; use std::path::PathBuf; +use crate::history::History; use async_trait::async_trait; use eyre::{bail, Result}; use memchr::Memchr; -use crate::history::History; - pub mod bash; pub mod fish; pub mod nu; @@ -32,11 +31,13 @@ pub trait Loader: Sync + Send { async fn push(&mut self, hist: History) -> eyre::Result<()>; } -fn unix_byte_lines(input: &[u8]) -> impl Iterator { +fn unix_byte_lines(input: &[u8]) -> impl DoubleEndedIterator { UnixByteLines { iter: memchr::memchr_iter(b'\n', input), bytes: input, i: 0, + // Index for iterating in reverse order, set to the last element + i_back: input.len().checked_sub(1).unwrap_or(0), } } @@ -44,6 +45,7 @@ struct UnixByteLines<'a> { iter: Memchr<'a>, bytes: &'a [u8], i: usize, + i_back: usize, } impl<'a> Iterator for UnixByteLines<'a> { @@ -64,6 +66,34 @@ impl<'a> Iterator for UnixByteLines<'a> { } } +impl<'a> DoubleEndedIterator for UnixByteLines<'a> { + fn next_back(&mut self) -> Option { + let needle_idx = match self.iter.next_back() { + Some(v) => { + if v == self.i_back { + // The first newline at the very end of the input sequence, skip + self.iter.next_back() + } else { + Some(v) + } + } + None => None, + }; + let range_start = if needle_idx.is_none() && self.i_back > 0 { + // Reached the very beginning of the input sequence + 0 + } else if needle_idx.is_none() && self.i_back == 0 { + return None; + } else { + // Do not include the found newline in the range + needle_idx.map(|v| v + 1)? + }; + let out = &self.bytes[range_start..self.i_back]; + self.i_back = needle_idx.unwrap_or(0); + Some(out) + } +} + fn count_lines(input: &[u8]) -> usize { unix_byte_lines(input).count() } @@ -96,6 +126,7 @@ fn is_file(p: PathBuf) -> Result { #[cfg(test)] mod tests { use super::*; + use itertools::assert_equal; #[derive(Default)] pub struct TestLoader { @@ -109,4 +140,20 @@ mod tests { Ok(()) } } + + #[test] + fn test_double_ended_iterator_unix_byte_lines() { + let input = "1\n2\n3\n4\n"; + let bytes = unix_byte_lines(input.as_bytes()); + + assert_equal(bytes, [b"1", b"2", b"3", b"4"]) + } + + #[test] + fn test_double_ended_iterator_unix_byte_lines_rev() { + let input = "1\n2\n3\n4\n"; + let bytes = unix_byte_lines(input.as_bytes()); + + assert_equal(bytes.rev(), [b"4", b"3", b"2", b"1"]) + } } diff --git a/crates/atuin-client/src/import/nu.rs b/crates/atuin-client/src/import/nu.rs index a45d83c5ee9..049aa584857 100644 --- a/crates/atuin-client/src/import/nu.rs +++ b/crates/atuin-client/src/import/nu.rs @@ -46,7 +46,8 @@ impl Importer for Nu { let now = OffsetDateTime::now_utc(); let mut counter = 0; - for b in unix_byte_lines(&self.bytes) { + // Reverse order so that recency is preserved + for b in unix_byte_lines(&self.bytes).rev() { let s = match std::str::from_utf8(b) { Ok(s) => s, Err(_) => continue, // we can skip past things like invalid utf8 diff --git a/crates/atuin-client/src/import/zsh.rs b/crates/atuin-client/src/import/zsh.rs index 5bc8fc16416..72808343f7c 100644 --- a/crates/atuin-client/src/import/zsh.rs +++ b/crates/atuin-client/src/import/zsh.rs @@ -57,44 +57,54 @@ impl Importer for Zsh { } async fn load(self, h: &mut impl Loader) -> Result<()> { - let now = OffsetDateTime::now_utc(); let mut line = String::new(); let mut counter = 0; - for b in unix_byte_lines(&self.bytes) { + // Reverse order so that recency of history is preserved + for b in unix_byte_lines(&self.bytes).rev() { let s = match unmetafy(b) { Some(s) => s, _ => continue, // we can skip past things like invalid utf8 }; - if let Some(s) = s.strip_suffix('\\') { - line.push_str(s); - line.push_str("\\\n"); + // Prepend command from previous line in the history + line.insert_str(0, "\\\n"); + line.insert_str(0, s); } else { - line.push_str(&s); - let command = std::mem::take(&mut line); - - if let Some(command) = command.strip_prefix(": ") { - counter += 1; - h.push(parse_extended(command, counter)).await?; - } else { - let offset = time::Duration::seconds(counter); - counter += 1; - - let imported = History::import() - // preserve ordering - .timestamp(now - offset) - .command(command.trim_end().to_string()); - - h.push(imported.build().into()).await?; + if !line.is_empty() { + add_command(&mut line, &mut counter, h).await?; } + line.push_str(&s); } } - + add_command(&mut line, &mut counter, h).await?; Ok(()) } } +async fn add_command(line: &mut String, counter: &mut i64, h: &mut impl Loader) -> Result<()> { + if line.is_empty() { + return Ok(()); + } + let now = OffsetDateTime::now_utc(); + let command = std::mem::take(line); + if let Some(command) = command.strip_prefix(": ") { + *counter += 1; + h.push(parse_extended(command, *counter)).await?; + } else { + let offset = time::Duration::seconds(*counter); + *counter += 1; + + let imported = History::import() + // preserve ordering + .timestamp(now - offset) + .command(command.trim_end().to_string()); + + h.push(imported.build().into()).await?; + } + Ok(()) +} + fn parse_extended(line: &str, counter: i64) -> History { let (time, duration) = line.split_once(':').unwrap(); let (duration, command) = duration.split_once(';').unwrap(); @@ -203,13 +213,25 @@ cargo update assert_equal( loader.buf.iter().map(|h| h.command.as_str()), [ - "cargo install atuin", - "cargo install atuin; \\\ncargo update", "cargo :b̷i̶t̴r̵o̴t̴ ̵i̷s̴ ̷r̶e̵a̸l̷", + "cargo install atuin; \\\ncargo update", + "cargo install atuin", ], ); } + #[tokio::test] + async fn test_parse_empty_file() { + let bytes = vec![]; + + let mut zsh = Zsh { bytes }; + assert_eq!(zsh.entries().await.unwrap(), 0); + + let mut loader = TestLoader::default(); + zsh.load(&mut loader).await.unwrap(); + assert!(loader.buf.is_empty()); + } + #[tokio::test] async fn test_parse_metafied() { let bytes = @@ -223,7 +245,7 @@ cargo update assert_equal( loader.buf.iter().map(|h| h.command.as_str()), - ["echo 你好", "ls ~/音乐"], + ["ls ~/音乐", "echo 你好"], ); } }