atuin-common 18.23.0

common library for atuin
Documentation
mod sealed {
    pub trait Sealed {}
}

/// A pattern that, unlike [`std::str::pattern::Pattern`], does not consume the pattern when used.
// Because `std::str::pattern::Pattern` is unstable, we cannot implement `PatternRef` in terms of
// it. Ideally this trait would be very simple -- an associated type `Self::Pattern<'a>` that
// implements `std::str::pattern::Pattern`, and a method to go from `&'a mut Self` to
// `Self::Pattern<'a>`. But because the standard library trait is unstable, we need to implement
// every `Pattern`-accepting `str` method we want to use here.
pub trait PatternRef: sealed::Sealed {
    fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str;
    fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str;
}

macro_rules! impl_pattern_ref {
    ([$($gen:tt)*], $ty:ty, $to_pattern:expr) => {
        impl<$($gen)*> sealed::Sealed for $ty {}

        impl<$($gen)*> PatternRef for $ty {
            fn trim_start_matches<'a>(&mut self, s: &'a str) -> &'a str {
                s.trim_start_matches(($to_pattern)(self))
            }

            fn trim_end_matches<'a>(&mut self, s: &'a str) -> &'a str {
                s.trim_end_matches(($to_pattern)(self))
            }
        }
    };
}

// Implement `PatternRef` for all of the types that implement `Pattern`.
impl_pattern_ref!([], char, Clone::clone);
impl_pattern_ref!([const N: usize], [char; N], Clone::clone);
impl_pattern_ref!([const N: usize], &[char; N], Clone::clone);
impl_pattern_ref!([], &[char], Clone::clone);
impl_pattern_ref!([], &str, Clone::clone);
impl_pattern_ref!([], &&str, Clone::clone);
impl_pattern_ref!([F: FnMut(char) -> bool], F, std::convert::identity);

pub trait TrimExt {
    /// Like [`str::trim_matches`], but modifies the [`String`] in-place instead of returning a
    /// substring.
    fn trim_matches_in_place<P: PatternRef>(&mut self, pattern: P);

    /// Like [`str::trim_start_matches`], but modifies the [`String`] in-place instead of returning
    /// a substring.
    fn trim_start_matches_in_place<P: PatternRef>(&mut self, pattern: P);

    /// Like [`str::trim_end_matches`], but modifies the [`String`] in-place instead of returning a
    /// substring.
    fn trim_end_matches_in_place<P: PatternRef>(&mut self, pattern: P);

    /// Like [`str::trim`], but modifies the [`String`] in-place instead of returning a substring.
    fn trim_in_place(&mut self) {
        self.trim_matches_in_place(char::is_whitespace);
    }

    /// Like [`str::trim_start`], but modifies the [`String`] in-place instead of returning a
    /// substring.
    fn trim_start_in_place(&mut self) {
        self.trim_start_matches_in_place(char::is_whitespace);
    }

    /// Like [`str::trim_end`], but modifies the [`String`] in-place instead of returning a
    /// substring.
    fn trim_end_in_place(&mut self) {
        self.trim_end_matches_in_place(char::is_whitespace);
    }
}

fn trim_start_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
    s.drain(..s.len() - pattern.trim_start_matches(s).len());
}

fn trim_end_matches_in_place<P: PatternRef>(s: &mut String, pattern: &mut P) {
    s.truncate(pattern.trim_end_matches(s).len());
}

impl TrimExt for String {
    fn trim_start_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
        trim_start_matches_in_place(self, &mut pattern);
    }

    fn trim_end_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
        trim_end_matches_in_place(self, &mut pattern);
    }

    fn trim_matches_in_place<P: PatternRef>(&mut self, mut pattern: P) {
        trim_start_matches_in_place(self, &mut pattern);
        trim_end_matches_in_place(self, &mut pattern);
    }
}

#[cfg(test)]
mod tests {
    use proptest::prelude::*;
    use rstest::rstest;

    use super::TrimExt;

    /// Run `trim_matches_in_place` over an owned copy of `input`.
    fn trimmed(input: &str, pattern: impl super::PatternRef) -> String {
        let mut string = input.to_string();
        string.trim_matches_in_place(pattern);
        string
    }

    #[rstest]
    #[case::both_ends("xxhixx", "hi")]
    #[case::leading_only("xxhi", "hi")]
    #[case::trailing_only("hixx", "hi")]
    #[case::interior_kept("xhixhix", "hixhi")]
    #[case::no_match("hi", "hi")]
    #[case::all_pattern("xxxx", "")]
    #[case::empty("", "")]
    #[case::single_char("x", "")]
    fn trims_a_char_pattern(#[case] input: &str, #[case] expected: &str) {
        assert_eq!(trimmed(input, 'x'), expected);
    }

    #[rstest]
    #[case::blank_lines_and_spaces("\n\n  hi there  \n\n", "hi there")]
    #[case::mixed_run(" \n \n hi", "hi")]
    #[case::interior_newline_kept("\none\ntwo\n", "one\ntwo")]
    fn trims_a_char_array_pattern(#[case] input: &str, #[case] expected: &str) {
        assert_eq!(trimmed(input, ['\n', ' ']), expected);
        assert_eq!(trimmed(input, &['\n', ' ']), expected);
        // A slice, too: `str::trim_matches` accepts one, so `PatternRef` has to as well.
        assert_eq!(trimmed(input, &['\n', ' '][..]), expected);
    }

    #[rstest]
    #[case::str_pattern("abcXabc", "abc", "X")]
    #[case::repeated_str_pattern("abcabcXabcabc", "abc", "X")]
    #[case::partial_match_kept("abXab", "abc", "abXab")]
    fn trims_a_str_pattern(#[case] input: &str, #[case] pattern: &str, #[case] expected: &str) {
        assert_eq!(trimmed(input, pattern), expected);
        assert_eq!(trimmed(input, &pattern), expected);
    }

    #[rstest]
    #[case::digits("123hi456", "hi")]
    #[case::only_digits("123", "")]
    #[case::interior_digits_kept("1h2i3", "h2i")]
    #[case::nothing_to_trim("hi", "hi")]
    fn trims_a_closure_pattern(#[case] input: &str, #[case] expected: &str) {
        assert_eq!(trimmed(input, |c: char| c.is_ascii_digit()), expected);
    }

    #[rstest]
    #[case::multibyte_pattern("——hi——", '—', "hi")]
    #[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界")]
    #[case::multibyte_content_all_trimmed("🦀🦀", '🦀', "")]
    fn handles_multibyte_characters(
        #[case] input: &str,
        #[case] pattern: char,
        #[case] expected: &str,
    ) {
        assert_eq!(trimmed(input, pattern), expected);
    }

    #[rstest]
    #[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi")]
    #[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi")]
    #[case::interior_kept("  a b  ", "a b")]
    #[case::nothing_to_trim("hi", "hi")]
    fn trim_in_place_matches_str_trim(#[case] input: &str, #[case] expected: &str) {
        let mut string = input.to_string();
        string.trim_in_place();
        assert_eq!(string, expected);
        assert_eq!(string, input.trim());
    }

    // -- One end at a time ----------------------------------------------------
    //
    // A split capture trims its two halves differently: leading blank lines come off the start
    // chunk and trailing ones off the end chunk, but neither may lose the newlines facing the
    // discarded middle, since those are real output.

    /// Run each one-sided trim over an owned copy of `input`.
    fn trimmed_start(input: &str, pattern: impl super::PatternRef) -> String {
        let mut string = input.to_string();
        string.trim_start_matches_in_place(pattern);
        string
    }

    fn trimmed_end(input: &str, pattern: impl super::PatternRef) -> String {
        let mut string = input.to_string();
        string.trim_end_matches_in_place(pattern);
        string
    }

    #[rstest]
    #[case::both_ends("\n\nhi\n\n", "hi\n\n", "\n\nhi")]
    #[case::leading_only("\n\nhi", "hi", "\n\nhi")]
    #[case::trailing_only("hi\n\n", "hi\n\n", "hi")]
    #[case::interior_kept("\none\ntwo\n", "one\ntwo\n", "\none\ntwo")]
    #[case::all_pattern("\n\n", "", "")]
    #[case::empty("", "", "")]
    #[case::nothing_to_trim("hi", "hi", "hi")]
    fn trims_only_the_requested_end(
        #[case] input: &str,
        #[case] start_trimmed: &str,
        #[case] end_trimmed: &str,
    ) {
        assert_eq!(trimmed_start(input, '\n'), start_trimmed);
        assert_eq!(trimmed_end(input, '\n'), end_trimmed);
    }

    #[rstest]
    #[case::multibyte_pattern("——hi——", '—', "hi——", "——hi")]
    #[case::multibyte_content_preserved("xx🦀 世界xx", 'x', "🦀 世界xx", "xx🦀 世界")]
    fn one_sided_trims_handle_multibyte_characters(
        #[case] input: &str,
        #[case] pattern: char,
        #[case] start_trimmed: &str,
        #[case] end_trimmed: &str,
    ) {
        assert_eq!(trimmed_start(input, pattern), start_trimmed);
        assert_eq!(trimmed_end(input, pattern), end_trimmed);
    }

    #[rstest]
    #[case::ascii_whitespace(" \t\r\nhi \t\r\n", "hi \t\r\n", " \t\r\nhi")]
    #[case::unicode_whitespace("\u{3000}hi\u{3000}", "hi\u{3000}", "\u{3000}hi")]
    #[case::nothing_to_trim("hi", "hi", "hi")]
    fn one_sided_whitespace_trims_match_str(
        #[case] input: &str,
        #[case] start_trimmed: &str,
        #[case] end_trimmed: &str,
    ) {
        let mut start = input.to_string();
        start.trim_start_in_place();
        assert_eq!(start, start_trimmed);
        assert_eq!(start, input.trim_start());

        let mut end = input.to_string();
        end.trim_end_in_place();
        assert_eq!(end, end_trimmed);
        assert_eq!(end, input.trim_end());
    }

    #[rstest]
    fn a_stateful_pattern_is_reused_rather_than_consumed() {
        // The point of `PatternRef`: a single `FnMut` drives both ends.
        let mut string = "abhixy".to_string();
        let mut chars: std::collections::HashSet<char> = string.chars().collect();
        string.trim_matches_in_place(|c| {
            assert!(chars.remove(&c), "matcher called on nonexistent char, or same char twice");
            !c.is_ascii_alphabetic() || "abxy".contains(c)
        });
        assert_eq!(string, "hi");
        assert!(chars.is_empty(), "matcher not called on every char");
    }

    proptest! {
        /// However the pattern and haystack are chosen, trimming in place must agree with
        /// `str::trim_matches` and never leave the string on a non-char boundary.
        #[rstest]
        fn agrees_with_str_trim_matches(input in ".{0,64}", pattern in prop::char::range('a', 'e')) {
            let mut string = input.clone();
            string.trim_matches_in_place(pattern);
            prop_assert_eq!(string, input.trim_matches(pattern));
        }

        #[rstest]
        fn agrees_with_str_trim(input in ".{0,64}") {
            let mut string = input.clone();
            string.trim_in_place();
            prop_assert_eq!(string, input.trim());
        }

        #[rstest]
        fn agrees_with_str_trim_start_matches(
            input in ".{0,64}",
            pattern in prop::char::range('a', 'e'),
        ) {
            let mut string = input.clone();
            string.trim_start_matches_in_place(pattern);
            prop_assert_eq!(string, input.trim_start_matches(pattern));
        }

        #[rstest]
        fn agrees_with_str_trim_end_matches(
            input in ".{0,64}",
            pattern in prop::char::range('a', 'e'),
        ) {
            let mut string = input.clone();
            string.trim_end_matches_in_place(pattern);
            prop_assert_eq!(string, input.trim_end_matches(pattern));
        }

        #[rstest]
        fn agrees_with_str_trim_start(input in ".{0,64}") {
            let mut string = input.clone();
            string.trim_start_in_place();
            prop_assert_eq!(string, input.trim_start());
        }

        #[rstest]
        fn agrees_with_str_trim_end(input in ".{0,64}") {
            let mut string = input.clone();
            string.trim_end_in_place();
            prop_assert_eq!(string, input.trim_end());
        }

        /// Trimming both ends is exactly trimming each end in turn, however they are ordered.
        #[rstest]
        fn both_ends_is_the_two_one_sided_trims(
            input in ".{0,64}",
            pattern in prop::char::range('a', 'e'),
        ) {
            let mut both = input.clone();
            both.trim_matches_in_place(pattern);

            let mut start_then_end = input.clone();
            start_then_end.trim_start_matches_in_place(pattern);
            start_then_end.trim_end_matches_in_place(pattern);

            let mut end_then_start = input;
            end_then_start.trim_end_matches_in_place(pattern);
            end_then_start.trim_start_matches_in_place(pattern);

            prop_assert_eq!(&both, &start_then_end);
            prop_assert_eq!(&both, &end_then_start);
        }
    }
}