rucline/buffer/
navigation.rs

1pub(super) fn next_scalar_value(index: usize, string: &str) -> usize {
2    if let Some((offset, _)) = string[index..].char_indices().nth(1) {
3        index + offset
4    } else {
5        string.len()
6    }
7}
8
9pub(super) fn previous_scalar_value(index: usize, string: &str) -> usize {
10    if let Some((new_index, _)) = string[..index].char_indices().next_back() {
11        new_index
12    } else {
13        0
14    }
15}
16
17pub(super) fn next_word(pivot: usize, string: &str) -> usize {
18    let end = string.len();
19    if pivot == end {
20        pivot
21    } else {
22        unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
23            .find(|pair| {
24                pair.0 > pivot && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
25            })
26            .map_or(string.len(), |pair| pair.0)
27    }
28}
29
30pub(super) fn previous_word(pivot: usize, string: &str) -> usize {
31    if pivot == 0 {
32        pivot
33    } else {
34        unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
35            .rfind(|pair| {
36                pair.0 < pivot && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
37            })
38            .map_or(0, |pair| pair.0)
39    }
40}
41
42pub(super) fn previous_word_end(pivot: usize, string: &str) -> usize {
43    if pivot == 0 {
44        pivot
45    } else {
46        unicode_segmentation::UnicodeSegmentation::split_word_bound_indices(string)
47            .rfind(|pair| {
48                pair.0 + pair.1.len() < pivot
49                    && pair.1.chars().next().map_or(true, |c| !c.is_whitespace())
50            })
51            .map_or(0, |pair| pair.0 + pair.1.len())
52    }
53}
54
55// Allowed because it makes test clearer
56#[allow(clippy::non_ascii_literal)]
57#[cfg(test)]
58mod test {
59    #[derive(Copy, Clone)]
60    enum Direction {
61        Forward,
62        Backward,
63    }
64
65    impl Direction {
66        pub(super) fn start_for(self, scenario: &str) -> usize {
67            match self {
68                Direction::Forward => 0,
69                Direction::Backward => scenario.len(),
70            }
71        }
72    }
73
74    struct Tester {
75        direction: Direction,
76        scenarios: [&'static str; 10],
77    }
78
79    impl Tester {
80        pub(super) fn prepare(direction: Direction) -> Self {
81            Self {
82                direction,
83                scenarios: [
84                    "",
85                    "   \t   ",
86                    "AddZ   \t   ",
87                    "   \t   AddZ",
88                    "   \t   AddZ   \t   ",
89                    "AddZ AdZ  AZ \t  O AZ  AdZ   AddZ",
90                    "AddZ AdZ  AZ \t πŸ˜€ AZ  AdZ   AddZ",
91                    "AddZ AdZ  AZπŸ˜€AZ \t  O AZ  AdZ   AddZ",
92                    "AddZ AdZ  AZ \t πŸ‡§πŸ‡· AZ  AdZ   AddZ",
93                    "AddZ AdZ  AZπŸ‡§πŸ‡·AZ \t  O AZ  AdZ   AddZ",
94                ],
95            }
96        }
97
98        pub(super) fn test<F, V>(&self, uut: F, validator: V)
99        where
100            F: Fn(usize, &str) -> usize,
101            V: Fn(usize, &str) -> bool,
102        {
103            for scenario in &self.scenarios {
104                test_scenario(
105                    &uut,
106                    &validator,
107                    self.direction,
108                    &scenario,
109                    self.direction.start_for(&scenario),
110                    0,
111                )
112            }
113        }
114    }
115
116    fn test_scenario<F, V>(
117        uut: F,
118        validator: V,
119        direction: Direction,
120        scenario: &str,
121        start: usize,
122        iteration: usize,
123    ) where
124        F: Fn(usize, &str) -> usize,
125        V: Fn(usize, &str) -> bool,
126    {
127        let pivot = uut(start, scenario);
128
129        match direction {
130            Direction::Forward => {
131                if pivot == scenario.len() {
132                    return;
133                }
134                assert!(pivot > start);
135            }
136            Direction::Backward => {
137                if pivot == 0 {
138                    return;
139                }
140                assert!(pivot < start);
141            }
142        }
143
144        assert!(
145            validator(pivot, scenario),
146            "failed on iteration {} at index {} for \"{}\"",
147            iteration,
148            pivot,
149            scenario
150        );
151        test_scenario(uut, validator, direction, scenario, pivot, iteration + 1);
152    }
153
154    #[test]
155    fn next_word() {
156        let tester = Tester::prepare(Direction::Forward);
157        tester.test(super::next_word, |pivot, string| {
158            let c = string[pivot..].chars().next().unwrap();
159            c == 'A' || c == 'O' || c == 'πŸ˜€' || c == 'πŸ‡§'
160        });
161    }
162
163    #[test]
164    fn previous_word() {
165        let tester = Tester::prepare(Direction::Backward);
166        tester.test(super::previous_word, |pivot, string| {
167            let c = string[pivot..].chars().next().unwrap();
168            c == 'A' || c == 'O' || c == 'πŸ˜€' || c == 'πŸ‡§'
169        });
170    }
171
172    #[test]
173    fn previous_word_end() {
174        let tester = Tester::prepare(Direction::Backward);
175        tester.test(super::previous_word_end, |pivot, string| {
176            let c = string[..pivot].chars().next_back().unwrap();
177            c == 'Z' || c == 'O' || c == 'πŸ˜€' || c == 'πŸ‡·'
178        });
179    }
180
181    #[test]
182    fn within_multiple_unicode_scalar_values() {
183        let string = "ab πŸ‡§πŸ‡· cd";
184        let pivot = "ab πŸ‡§πŸ‡·".len() - 4;
185
186        assert_eq!(super::next_word(pivot, string), "ab πŸ‡§πŸ‡· ".len());
187        assert_eq!(super::previous_word(pivot, string), "ab ".len());
188        assert_eq!(super::previous_word_end(pivot, string), "ab".len());
189    }
190
191    #[test]
192    fn next_scalar_value() {
193        use super::next_scalar_value;
194
195        let string = "aπŸ˜€ΓΈΓ©πŸ‡§πŸ‡·";
196        let mut index = 0;
197        index = next_scalar_value(index, string);
198        assert_eq!(string[index..].chars().next().unwrap(), 'πŸ˜€');
199        index = next_scalar_value(index, string);
200        assert_eq!(string[index..].chars().next().unwrap(), 'ΓΈ');
201        index = next_scalar_value(index, string);
202        assert_eq!(string[index..].chars().next().unwrap(), 'Γ©');
203        index = next_scalar_value(index, string);
204        assert_eq!(
205            string[index..].chars().next().unwrap(),
206            "πŸ‡§πŸ‡·".chars().next().unwrap()
207        );
208        index = next_scalar_value(index, string);
209        assert_eq!(
210            string[index..].chars().next().unwrap(),
211            "πŸ‡§πŸ‡·".chars().nth(1).unwrap()
212        );
213        index = next_scalar_value(index, string);
214        assert_eq!(index, string.len());
215        index = next_scalar_value(index, string);
216        assert_eq!(index, string.len());
217    }
218
219    #[test]
220    fn previous_scalar_value() {
221        use super::previous_scalar_value;
222
223        let string = "aπŸ˜€ΓΈΓ©πŸ‡§πŸ‡·";
224        let mut index = string.len();
225        index = previous_scalar_value(index, string);
226        assert_eq!(
227            string[index..].chars().next().unwrap(),
228            "πŸ‡§πŸ‡·".chars().nth(1).unwrap()
229        );
230        index = previous_scalar_value(index, string);
231        assert_eq!(
232            string[index..].chars().next().unwrap(),
233            "πŸ‡§πŸ‡·".chars().next().unwrap()
234        );
235        index = previous_scalar_value(index, string);
236        assert_eq!(string[index..].chars().next().unwrap(), 'Γ©');
237        index = previous_scalar_value(index, string);
238        assert_eq!(string[index..].chars().next().unwrap(), 'ΓΈ');
239        index = previous_scalar_value(index, string);
240        assert_eq!(string[index..].chars().next().unwrap(), 'πŸ˜€');
241        index = previous_scalar_value(index, string);
242        assert_eq!(index, 0);
243        assert_eq!(string[index..].chars().next().unwrap(), 'a');
244        index = previous_scalar_value(index, string);
245        assert_eq!(index, 0);
246    }
247}