reedline/history/
cursor.rs

1use crate::{History, HistoryNavigationQuery, HistorySessionId};
2
3use super::base::CommandLineSearch;
4use super::base::SearchDirection;
5use super::base::SearchFilter;
6use super::HistoryItem;
7use super::SearchQuery;
8use crate::Result;
9
10/// Interface of a stateful navigation via [`HistoryNavigationQuery`].
11#[derive(Debug)]
12pub struct HistoryCursor {
13    query: HistoryNavigationQuery,
14    current: Option<HistoryItem>,
15    skip_dupes: bool,
16    session: Option<HistorySessionId>,
17}
18
19impl HistoryCursor {
20    pub fn new(query: HistoryNavigationQuery, session: Option<HistorySessionId>) -> HistoryCursor {
21        HistoryCursor {
22            query,
23            current: None,
24            skip_dupes: true,
25            session,
26        }
27    }
28
29    /// This moves the cursor backwards respecting the navigation query that is set
30    /// - Results in a no-op if the cursor is at the initial point
31    pub fn back(&mut self, history: &dyn History) -> Result<()> {
32        self.navigate_in_direction(history, SearchDirection::Backward)
33    }
34
35    /// This moves the cursor forwards respecting the navigation-query that is set
36    /// - Results in a no-op if the cursor is at the latest point
37    pub fn forward(&mut self, history: &dyn History) -> Result<()> {
38        self.navigate_in_direction(history, SearchDirection::Forward)
39    }
40
41    fn get_search_filter(&self) -> SearchFilter {
42        let filter = match self.query.clone() {
43            HistoryNavigationQuery::Normal(_) => SearchFilter::anything(self.session),
44            HistoryNavigationQuery::PrefixSearch(prefix) => {
45                SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix), self.session)
46            }
47            HistoryNavigationQuery::SubstringSearch(substring) => SearchFilter::from_text_search(
48                CommandLineSearch::Substring(substring),
49                self.session,
50            ),
51        };
52        if let (true, Some(current)) = (self.skip_dupes, &self.current) {
53            SearchFilter {
54                not_command_line: Some(current.command_line.clone()),
55                ..filter
56            }
57        } else {
58            filter
59        }
60    }
61    fn navigate_in_direction(
62        &mut self,
63        history: &dyn History,
64        direction: SearchDirection,
65    ) -> Result<()> {
66        if direction == SearchDirection::Forward && self.current.is_none() {
67            // if searching forward but we don't have a starting point, assume we are at the end
68            return Ok(());
69        }
70        let start_id = self.current.as_ref().and_then(|e| e.id);
71        let mut next = history.search(SearchQuery {
72            start_id,
73            end_id: None,
74            start_time: None,
75            end_time: None,
76            direction,
77            limit: Some(1),
78            filter: self.get_search_filter(),
79        })?;
80        if next.len() == 1 {
81            self.current = Some(next.swap_remove(0));
82        } else if direction == SearchDirection::Forward {
83            // no result and searching forward: we are at the end
84            self.current = None;
85        }
86        Ok(())
87    }
88
89    /// Returns the string (if present) at the cursor
90    pub fn string_at_cursor(&self) -> Option<String> {
91        self.current.as_ref().map(|e| e.command_line.to_string())
92    }
93
94    /// Poll the current [`HistoryNavigationQuery`] mode
95    pub fn get_navigation(&self) -> HistoryNavigationQuery {
96        self.query.clone()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use std::path::Path;
103
104    use pretty_assertions::assert_eq;
105
106    use crate::LineBuffer;
107
108    use super::super::*;
109    use super::*;
110
111    fn create_history() -> (Box<dyn History>, HistoryCursor) {
112        #[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
113        let hist = Box::new(SqliteBackedHistory::in_memory().unwrap());
114        #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
115        let hist = Box::<FileBackedHistory>::default();
116        (
117            hist,
118            HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
119        )
120    }
121    fn create_history_at(cap: usize, path: &Path) -> (Box<dyn History>, HistoryCursor) {
122        let hist = Box::new(FileBackedHistory::with_file(cap, path.to_owned()).unwrap());
123        (
124            hist,
125            HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
126        )
127    }
128
129    fn get_all_entry_texts(hist: &dyn History) -> Vec<String> {
130        let res = hist
131            .search(SearchQuery::everything(SearchDirection::Forward, None))
132            .unwrap();
133        let actual: Vec<_> = res.iter().map(|e| e.command_line.to_string()).collect();
134        actual
135    }
136    fn add_text_entries(hist: &mut dyn History, entries: &[impl AsRef<str>]) {
137        entries.iter().for_each(|e| {
138            hist.save(HistoryItem::from_command_line(e.as_ref()))
139                .unwrap();
140        });
141    }
142
143    #[test]
144    fn accessing_empty_history_returns_nothing() -> Result<()> {
145        let (_hist, cursor) = create_history();
146        assert_eq!(cursor.string_at_cursor(), None);
147        Ok(())
148    }
149
150    #[test]
151    fn going_forward_in_empty_history_does_not_error_out() -> Result<()> {
152        let (hist, mut cursor) = create_history();
153        cursor.forward(&*hist)?;
154        assert_eq!(cursor.string_at_cursor(), None);
155        Ok(())
156    }
157
158    #[test]
159    fn going_backwards_in_empty_history_does_not_error_out() -> Result<()> {
160        let (hist, mut cursor) = create_history();
161        cursor.back(&*hist)?;
162        assert_eq!(cursor.string_at_cursor(), None);
163        Ok(())
164    }
165
166    #[test]
167    fn going_backwards_bottoms_out() -> Result<()> {
168        let (mut hist, mut cursor) = create_history();
169        hist.save(HistoryItem::from_command_line("command1"))?;
170        hist.save(HistoryItem::from_command_line("command2"))?;
171        cursor.back(&*hist)?;
172        cursor.back(&*hist)?;
173        cursor.back(&*hist)?;
174        cursor.back(&*hist)?;
175        cursor.back(&*hist)?;
176        assert_eq!(cursor.string_at_cursor(), Some("command1".to_string()));
177        Ok(())
178    }
179
180    #[test]
181    fn going_forwards_bottoms_out() -> Result<()> {
182        let (mut hist, mut cursor) = create_history();
183        hist.save(HistoryItem::from_command_line("command1"))?;
184        hist.save(HistoryItem::from_command_line("command2"))?;
185        cursor.forward(&*hist)?;
186        cursor.forward(&*hist)?;
187        cursor.forward(&*hist)?;
188        cursor.forward(&*hist)?;
189        cursor.forward(&*hist)?;
190        assert_eq!(cursor.string_at_cursor(), None);
191        Ok(())
192    }
193
194    #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
195    #[test]
196    fn appends_only_unique() -> Result<()> {
197        let (mut hist, _) = create_history();
198        hist.save(HistoryItem::from_command_line("unique_old"))?;
199        hist.save(HistoryItem::from_command_line("test"))?;
200        hist.save(HistoryItem::from_command_line("test"))?;
201        hist.save(HistoryItem::from_command_line("unique"))?;
202        assert_eq!(hist.count_all()?, 3);
203        Ok(())
204    }
205
206    #[test]
207    fn prefix_search_works() -> Result<()> {
208        let (mut hist, _) = create_history();
209        hist.save(HistoryItem::from_command_line("find me as well"))?;
210        hist.save(HistoryItem::from_command_line("test"))?;
211        hist.save(HistoryItem::from_command_line("find me"))?;
212
213        let mut cursor = HistoryCursor::new(
214            HistoryNavigationQuery::PrefixSearch("find".to_string()),
215            None,
216        );
217
218        cursor.back(&*hist)?;
219        assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
220        cursor.back(&*hist)?;
221        assert_eq!(
222            cursor.string_at_cursor(),
223            Some("find me as well".to_string())
224        );
225        Ok(())
226    }
227
228    #[test]
229    fn prefix_search_bottoms_out() -> Result<()> {
230        let (mut hist, _) = create_history();
231        hist.save(HistoryItem::from_command_line("find me as well"))?;
232        hist.save(HistoryItem::from_command_line("test"))?;
233        hist.save(HistoryItem::from_command_line("find me"))?;
234
235        let mut cursor = HistoryCursor::new(
236            HistoryNavigationQuery::PrefixSearch("find".to_string()),
237            None,
238        );
239        cursor.back(&*hist)?;
240        assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
241        cursor.back(&*hist)?;
242        assert_eq!(
243            cursor.string_at_cursor(),
244            Some("find me as well".to_string())
245        );
246        cursor.back(&*hist)?;
247        cursor.back(&*hist)?;
248        cursor.back(&*hist)?;
249        cursor.back(&*hist)?;
250        assert_eq!(
251            cursor.string_at_cursor(),
252            Some("find me as well".to_string())
253        );
254        Ok(())
255    }
256    #[test]
257    fn prefix_search_returns_to_none() -> Result<()> {
258        let (mut hist, _) = create_history();
259        hist.save(HistoryItem::from_command_line("find me as well"))?;
260        hist.save(HistoryItem::from_command_line("test"))?;
261        hist.save(HistoryItem::from_command_line("find me"))?;
262
263        let mut cursor = HistoryCursor::new(
264            HistoryNavigationQuery::PrefixSearch("find".to_string()),
265            None,
266        );
267        cursor.back(&*hist)?;
268        assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
269        cursor.back(&*hist)?;
270        assert_eq!(
271            cursor.string_at_cursor(),
272            Some("find me as well".to_string())
273        );
274        cursor.forward(&*hist)?;
275        assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
276        cursor.forward(&*hist)?;
277        assert_eq!(cursor.string_at_cursor(), None);
278        cursor.forward(&*hist)?;
279        assert_eq!(cursor.string_at_cursor(), None);
280        Ok(())
281    }
282
283    #[test]
284    fn prefix_search_ignores_consecutive_equivalent_entries_going_backwards() -> Result<()> {
285        let (mut hist, _) = create_history();
286        hist.save(HistoryItem::from_command_line("find me as well"))?;
287        hist.save(HistoryItem::from_command_line("find me once"))?;
288        hist.save(HistoryItem::from_command_line("test"))?;
289        hist.save(HistoryItem::from_command_line("find me once"))?;
290
291        let mut cursor = HistoryCursor::new(
292            HistoryNavigationQuery::PrefixSearch("find".to_string()),
293            None,
294        );
295        cursor.back(&*hist)?;
296        assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
297        cursor.back(&*hist)?;
298        assert_eq!(
299            cursor.string_at_cursor(),
300            Some("find me as well".to_string())
301        );
302        Ok(())
303    }
304
305    #[test]
306    fn prefix_search_ignores_consecutive_equivalent_entries_going_forwards() -> Result<()> {
307        let (mut hist, _) = create_history();
308        hist.save(HistoryItem::from_command_line("find me once"))?;
309        hist.save(HistoryItem::from_command_line("test"))?;
310        hist.save(HistoryItem::from_command_line("find me once"))?;
311        hist.save(HistoryItem::from_command_line("find me as well"))?;
312
313        let mut cursor = HistoryCursor::new(
314            HistoryNavigationQuery::PrefixSearch("find".to_string()),
315            None,
316        );
317        cursor.back(&*hist)?;
318        assert_eq!(
319            cursor.string_at_cursor(),
320            Some("find me as well".to_string())
321        );
322        cursor.back(&*hist)?;
323        cursor.back(&*hist)?;
324        assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
325        cursor.forward(&*hist)?;
326        assert_eq!(
327            cursor.string_at_cursor(),
328            Some("find me as well".to_string())
329        );
330        cursor.forward(&*hist)?;
331        assert_eq!(cursor.string_at_cursor(), None);
332        Ok(())
333    }
334
335    #[test]
336    fn substring_search_works() -> Result<()> {
337        let (mut hist, _) = create_history();
338        hist.save(HistoryItem::from_command_line("substring"))?;
339        hist.save(HistoryItem::from_command_line("don't find me either"))?;
340        hist.save(HistoryItem::from_command_line("prefix substring"))?;
341        hist.save(HistoryItem::from_command_line("don't find me"))?;
342        hist.save(HistoryItem::from_command_line("prefix substring suffix"))?;
343
344        let mut cursor = HistoryCursor::new(
345            HistoryNavigationQuery::SubstringSearch("substring".to_string()),
346            None,
347        );
348        cursor.back(&*hist)?;
349        assert_eq!(
350            cursor.string_at_cursor(),
351            Some("prefix substring suffix".to_string())
352        );
353        cursor.back(&*hist)?;
354        assert_eq!(
355            cursor.string_at_cursor(),
356            Some("prefix substring".to_string())
357        );
358        cursor.back(&*hist)?;
359        assert_eq!(cursor.string_at_cursor(), Some("substring".to_string()));
360        Ok(())
361    }
362
363    #[test]
364    fn substring_search_with_empty_value_returns_none() -> Result<()> {
365        let (mut hist, _) = create_history();
366        hist.save(HistoryItem::from_command_line("substring"))?;
367
368        let cursor = HistoryCursor::new(
369            HistoryNavigationQuery::SubstringSearch("".to_string()),
370            None,
371        );
372
373        assert_eq!(cursor.string_at_cursor(), None);
374        Ok(())
375    }
376
377    #[test]
378    fn writes_to_new_file() -> Result<()> {
379        use tempfile::tempdir;
380
381        let tmp = tempdir().unwrap();
382        // check that it also works for a path where the directory has not been created yet
383        let histfile = tmp.path().join("nested_path").join(".history");
384
385        let entries = vec!["test", "text", "more test text"];
386
387        {
388            let (mut hist, _) = create_history_at(5, &histfile);
389
390            add_text_entries(hist.as_mut(), &entries);
391            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
392        }
393
394        let (reading_hist, _) = create_history_at(5, &histfile);
395        let actual = get_all_entry_texts(reading_hist.as_ref());
396        assert_eq!(entries, actual);
397
398        tmp.close().unwrap();
399        Ok(())
400    }
401
402    #[test]
403    fn persists_newlines_in_entries() -> Result<()> {
404        use tempfile::tempdir;
405
406        let tmp = tempdir().unwrap();
407        let histfile = tmp.path().join(".history");
408
409        let entries = vec![
410            "test",
411            "multiline\nentry\nunix",
412            "multiline\r\nentry\r\nwindows",
413            "more test text",
414        ];
415
416        {
417            let (mut writing_hist, _) = create_history_at(5, &histfile);
418            add_text_entries(writing_hist.as_mut(), &entries);
419            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
420        }
421
422        let (reading_hist, _) = create_history_at(5, &histfile);
423
424        let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
425        assert_eq!(entries, actual);
426
427        tmp.close().unwrap();
428        Ok(())
429    }
430
431    #[cfg(not(any(feature = "sqlite", feature = "sqlite-dynlib")))]
432    #[test]
433    fn truncates_file_to_capacity() -> Result<()> {
434        use tempfile::tempdir;
435
436        let tmp = tempdir().unwrap();
437        let histfile = tmp.path().join(".history");
438
439        let capacity = 5;
440        let initial_entries = vec!["test 1", "test 2"];
441        let appending_entries = vec!["test 3", "test 4"];
442        let expected_appended_entries = vec!["test 1", "test 2", "test 3", "test 4"];
443        let truncating_entries = vec!["test 5", "test 6", "test 7", "test 8"];
444        let expected_truncated_entries = vec!["test 4", "test 5", "test 6", "test 7", "test 8"];
445
446        {
447            let (mut writing_hist, _) = create_history_at(capacity, &histfile);
448            add_text_entries(writing_hist.as_mut(), &initial_entries);
449            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
450        }
451
452        {
453            let (mut appending_hist, _) = create_history_at(capacity, &histfile);
454            add_text_entries(appending_hist.as_mut(), &appending_entries);
455            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
456            let actual: Vec<_> = get_all_entry_texts(appending_hist.as_ref());
457            assert_eq!(expected_appended_entries, actual);
458        }
459
460        {
461            let (mut truncating_hist, _) = create_history_at(capacity, &histfile);
462            add_text_entries(truncating_hist.as_mut(), &truncating_entries);
463            let actual: Vec<_> = get_all_entry_texts(truncating_hist.as_ref());
464            assert_eq!(expected_truncated_entries, actual);
465            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
466        }
467
468        let (reading_hist, _) = create_history_at(capacity, &histfile);
469
470        let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
471        assert_eq!(expected_truncated_entries, actual);
472
473        tmp.close().unwrap();
474        Ok(())
475    }
476
477    #[test]
478    fn truncates_too_large_file() -> Result<()> {
479        use tempfile::tempdir;
480
481        let tmp = tempdir().unwrap();
482        let histfile = tmp.path().join(".history");
483
484        let overly_large_previous_entries = vec![
485            "test 1", "test 2", "test 3", "test 4", "test 5", "test 6", "test 7", "test 8",
486        ];
487        let expected_truncated_entries = vec!["test 4", "test 5", "test 6", "test 7", "test 8"];
488
489        {
490            let (mut writing_hist, _) = create_history_at(10, &histfile);
491            add_text_entries(writing_hist.as_mut(), &overly_large_previous_entries);
492            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
493        }
494
495        {
496            let (truncating_hist, _) = create_history_at(5, &histfile);
497
498            let actual: Vec<_> = get_all_entry_texts(truncating_hist.as_ref());
499            assert_eq!(expected_truncated_entries, actual);
500            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
501        }
502
503        let (reading_hist, _) = create_history_at(5, &histfile);
504
505        let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
506        assert_eq!(expected_truncated_entries, actual);
507
508        tmp.close().unwrap();
509        Ok(())
510    }
511
512    #[test]
513    fn concurrent_histories_dont_erase_eachother() -> Result<()> {
514        use tempfile::tempdir;
515
516        let tmp = tempdir().unwrap();
517        let histfile = tmp.path().join(".history");
518
519        let capacity = 7;
520        let initial_entries = vec!["test 1", "test 2", "test 3", "test 4", "test 5"];
521        let entries_a = vec!["A1", "A2", "A3"];
522        let entries_b = vec!["B1", "B2", "B3"];
523        let expected_entries = vec!["test 5", "B1", "B2", "B3", "A1", "A2", "A3"];
524
525        {
526            let (mut writing_hist, _) = create_history_at(capacity, &histfile);
527            add_text_entries(writing_hist.as_mut(), &initial_entries);
528            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
529        }
530
531        {
532            let (mut hist_a, _) = create_history_at(capacity, &histfile);
533
534            {
535                let (mut hist_b, _) = create_history_at(capacity, &histfile);
536
537                add_text_entries(hist_b.as_mut(), &entries_b);
538                // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
539            }
540            add_text_entries(hist_a.as_mut(), &entries_a);
541            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
542        }
543
544        let (reading_hist, _) = create_history_at(capacity, &histfile);
545
546        let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
547        assert_eq!(expected_entries, actual);
548
549        tmp.close().unwrap();
550        Ok(())
551    }
552
553    #[test]
554    fn concurrent_histories_are_threadsafe() -> Result<()> {
555        use tempfile::tempdir;
556
557        let tmp = tempdir().unwrap();
558        let histfile = tmp.path().join(".history");
559
560        let num_threads = 16;
561        let capacity = 2 * num_threads + 1;
562
563        let initial_entries: Vec<_> = (0..capacity).map(|i| format!("initial {i}")).collect();
564
565        {
566            let (mut writing_hist, _) = create_history_at(capacity, &histfile);
567            add_text_entries(writing_hist.as_mut(), &initial_entries);
568            // As `hist` goes out of scope and get's dropped, its contents are flushed to disk
569        }
570
571        let threads = (0..num_threads)
572            .map(|i| {
573                let cap = capacity;
574                let hfile = histfile.clone();
575                std::thread::spawn(move || {
576                    let (mut hist, _) = create_history_at(cap, &hfile);
577                    hist.save(HistoryItem::from_command_line(format!("A{i}")))
578                        .unwrap();
579                    hist.sync().unwrap();
580                    hist.save(HistoryItem::from_command_line(format!("B{i}")))
581                        .unwrap();
582                })
583            })
584            .collect::<Vec<_>>();
585
586        for t in threads {
587            t.join().unwrap();
588        }
589
590        let (reading_hist, _) = create_history_at(capacity, &histfile);
591
592        let actual: Vec<_> = get_all_entry_texts(reading_hist.as_ref());
593
594        assert!(
595            actual.contains(&format!("initial {}", capacity - 1)),
596            "Overwrote entry from before threading test"
597        );
598
599        for i in 0..num_threads {
600            assert!(actual.contains(&format!("A{i}")),);
601            assert!(actual.contains(&format!("B{i}")),);
602        }
603
604        tmp.close().unwrap();
605        Ok(())
606    }
607}