Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix reverse import order for zsh and nu #2370

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion crates/atuin-client/src/import/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,23 @@ pub trait Loader: Sync + Send {
async fn push(&mut self, hist: History) -> eyre::Result<()>;
}

fn unix_byte_lines(input: &[u8]) -> impl Iterator<Item = &[u8]> {
fn unix_byte_lines(input: &[u8]) -> impl DoubleEndedIterator<Item = &[u8]> {
UnixByteLines {
iter: memchr::memchr_iter(b'\n', input),
bytes: input,
i: 0,
// Set to the last element
i_rev: input.len().saturating_sub(1),
}
}

struct UnixByteLines<'a> {
iter: Memchr<'a>,
bytes: &'a [u8],
// Index for iterating in regular order
i: usize,
// Index for iterating in reverse order
i_rev: usize,
}

impl<'a> Iterator for UnixByteLines<'a> {
Expand All @@ -64,6 +69,32 @@ impl<'a> Iterator for UnixByteLines<'a> {
}
}

impl<'a> DoubleEndedIterator for UnixByteLines<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
let needle_idx = match self.iter.next_back() {
Some(v) => {
if v == self.i_rev {
// 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_rev > 0 {
// Reached the very beginning of the input sequence
0
} else {
// Do not include the found newline in the range
needle_idx.map(|v| v + 1)?
};
let out = &self.bytes[range_start..self.i_rev];
self.i_rev = needle_idx.unwrap_or(0);
Some(out)
}
}

fn count_lines(input: &[u8]) -> usize {
unix_byte_lines(input).count()
}
Expand Down Expand Up @@ -96,6 +127,7 @@ fn is_file(p: PathBuf) -> Result<PathBuf> {
#[cfg(test)]
mod tests {
use super::*;
use itertools::assert_equal;

#[derive(Default)]
pub struct TestLoader {
Expand All @@ -109,4 +141,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"])
}
}
3 changes: 2 additions & 1 deletion crates/atuin-client/src/import/nu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 45 additions & 25 deletions crates/atuin-client/src/import/zsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,44 +57,52 @@ 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 {
add_command(&mut line, &mut counter, h).await?;
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?;
}
}
}

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();
Expand Down Expand Up @@ -203,13 +211,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 =
Expand All @@ -223,7 +243,7 @@ cargo update

assert_equal(
loader.buf.iter().map(|h| h.command.as_str()),
["echo 你好", "ls ~/音乐"],
["ls ~/音乐", "echo 你好"],
);
}
}