yatima_rustyline/
history.rs

1//! History API
2
3#[cfg(not(target_arch = "wasm32"))]
4use {
5    fd_lock::RwLock,
6    log::{debug, warn},
7    std::{
8        fs::{File, OpenOptions},
9        io::SeekFrom,
10        path::PathBuf,
11        time::SystemTime,
12    },
13};
14
15use std::collections::vec_deque;
16use std::collections::VecDeque;
17use std::iter::DoubleEndedIterator;
18use std::ops::Index;
19use std::path::Path;
20
21use super::Result;
22use crate::config::{Config, HistoryDuplicates};
23
24/// Search direction
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum SearchDirection {
27    /// Search history forward
28    Forward,
29    /// Search history backward
30    Reverse,
31}
32
33/// History search result
34#[derive(Debug, Clone, Eq, PartialEq)]
35pub struct SearchResult<'a> {
36    /// history entry
37    pub entry: &'a str,
38    /// history index
39    pub idx: usize,
40    /// match position in `entry`
41    pub pos: usize,
42}
43
44// HistoryEntry: text + timestamp
45// TODO Make possible to customize how history is stored / loaded.
46// https://github.com/kkawakam/rustyline/issues/442
47// https://github.com/kkawakam/rustyline/issues/127
48// See https://python-prompt-toolkit.readthedocs.io/en/master/pages/reference.html#prompt_toolkit.history.History abstract methods
49
50/// Current state of the history.
51#[derive(Default)]
52pub struct History {
53    entries: VecDeque<String>,
54    max_len: usize,
55    pub(crate) ignore_space: bool,
56    pub(crate) ignore_dups: bool,
57    /// Number of entries inputted by user and not saved yet
58    new_entries: usize,
59    /// last path used by either `load` or `save`
60    #[cfg(not(target_arch = "wasm32"))]
61    path_info: Option<PathInfo>,
62}
63
64/// Last histo path, modified timestamp and size
65#[cfg(not(target_arch = "wasm32"))]
66struct PathInfo(PathBuf, SystemTime, usize);
67
68impl History {
69    // New multiline-aware history files start with `#V2\n` and have newlines
70    // and backslashes escaped in them.
71    #[cfg(not(target_arch = "wasm32"))]
72    const FILE_VERSION_V2: &'static str = "#V2";
73
74    /// Default constructor
75    #[must_use]
76    pub fn new() -> Self {
77        Self::with_config(Config::default())
78    }
79
80    /// Customized constructor with:
81    /// - `Config::max_history_size()`,
82    /// - `Config::history_ignore_space()`,
83    /// - `Config::history_duplicates()`.
84    #[must_use]
85    pub fn with_config(config: Config) -> Self {
86        Self {
87            entries: VecDeque::new(),
88            max_len: config.max_history_size(),
89            ignore_space: config.history_ignore_space(),
90            ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
91            new_entries: 0,
92            #[cfg(not(target_arch = "wasm32"))]
93            path_info: None,
94        }
95    }
96
97    /// Return the history entry at position `index`, starting from 0.
98    #[must_use]
99    pub fn get(&self, index: usize) -> Option<&String> {
100        self.entries.get(index)
101    }
102
103    /// Return the last history entry (i.e. previous command)
104    #[must_use]
105    pub fn last(&self) -> Option<&String> {
106        self.entries.back()
107    }
108
109    /// Add a new entry in the history.
110    pub fn add<S: AsRef<str> + Into<String>>(&mut self, line: S) -> bool {
111        if self.max_len == 0 {
112            return false;
113        }
114        if line.as_ref().is_empty()
115            || (self.ignore_space
116                && line
117                    .as_ref()
118                    .chars()
119                    .next()
120                    .map_or(true, char::is_whitespace))
121        {
122            return false;
123        }
124        if self.ignore_dups {
125            if let Some(s) = self.entries.back() {
126                if s == line.as_ref() {
127                    return false;
128                }
129            }
130        }
131        if self.entries.len() == self.max_len {
132            self.entries.pop_front();
133        }
134        self.entries.push_back(line.into());
135        self.new_entries = self.new_entries.saturating_add(1).min(self.len());
136        true
137    }
138
139    /// Return the number of entries in the history.
140    #[must_use]
141    pub fn len(&self) -> usize {
142        self.entries.len()
143    }
144
145    /// Return true if the history has no entry.
146    #[must_use]
147    pub fn is_empty(&self) -> bool {
148        self.entries.is_empty()
149    }
150
151    /// Set the maximum length for the history. This function can be called even
152    /// if there is already some history, the function will make sure to retain
153    /// just the latest `len` elements if the new history length value is
154    /// smaller than the amount of items already inside the history.
155    ///
156    /// Like [stifle_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX11).
157    pub fn set_max_len(&mut self, len: usize) {
158        self.max_len = len;
159        if self.len() > len {
160            self.entries.drain(..self.len() - len);
161            self.new_entries = self.new_entries.min(len);
162        }
163    }
164
165    /// Save the history in the specified file.
166    // TODO history_truncate_file
167    // https://tiswww.case.edu/php/chet/readline/history.html#IDX31
168    #[cfg(not(target_arch = "wasm32"))]
169    pub fn save<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
170        if self.is_empty() || self.new_entries == 0 {
171            return Ok(());
172        }
173        let path = path.as_ref();
174        let old_umask = umask();
175        let f = File::create(path);
176        restore_umask(old_umask);
177        let file = f?;
178        let mut lock = RwLock::new(file);
179        let lock_guard = lock.write()?;
180        self.save_to(&lock_guard, false)?;
181        self.new_entries = 0;
182        self.update_path(path, &lock_guard, self.len())
183    }
184
185    /// Save the history in the specified file.
186    /// Not implemented for wasm.
187    // TODO history_truncate_file
188    // https://tiswww.case.edu/php/chet/readline/history.html#IDX31
189    #[cfg(target_arch = "wasm32")]
190    pub fn save<P: AsRef<Path> + ?Sized>(&mut self, _path: &P) -> Result<()> {
191        todo!();
192    }
193
194    #[cfg(not(target_arch = "wasm32"))]
195    fn save_to(&mut self, file: &File, append: bool) -> Result<()> {
196        use std::io::{BufWriter, Write};
197
198        fix_perm(file);
199        let mut wtr = BufWriter::new(file);
200        let first_new_entry = if append {
201            self.entries.len().saturating_sub(self.new_entries)
202        } else {
203            wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
204            wtr.write_all(b"\n")?;
205            0
206        };
207        for entry in self.entries.iter().skip(first_new_entry) {
208            let mut bytes = entry.as_bytes();
209            while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
210                let (head, tail) = bytes.split_at(i);
211                wtr.write_all(head)?;
212
213                let (&escapable_byte, tail) = tail
214                    .split_first()
215                    .expect("memchr guarantees i is a valid index");
216                if escapable_byte == b'\n' {
217                    wtr.write_all(br"\n")?; // escaped line feed
218                } else {
219                    debug_assert_eq!(escapable_byte, b'\\');
220                    wtr.write_all(br"\\")?; // escaped backslash
221                }
222                bytes = tail;
223            }
224            wtr.write_all(bytes)?; // remaining bytes with no \n or \
225            wtr.write_all(b"\n")?;
226        }
227        // https://github.com/rust-lang/rust/issues/32677#issuecomment-204833485
228        wtr.flush()?;
229        Ok(())
230    }
231
232    /// Append new entries in the specified file.
233    // Like [append_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX30).
234    #[cfg(not(target_arch = "wasm32"))]
235    pub fn append<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
236        use std::io::Seek;
237
238        if self.is_empty() || self.new_entries == 0 {
239            return Ok(());
240        }
241        let path = path.as_ref();
242        if !path.exists() || self.new_entries == self.max_len {
243            return self.save(path);
244        }
245        let file = OpenOptions::new().write(true).read(true).open(path)?;
246        let mut lock = RwLock::new(file);
247        let mut lock_guard = lock.write()?;
248        if self.can_just_append(path, &lock_guard)? {
249            lock_guard.seek(SeekFrom::End(0))?;
250            self.save_to(&lock_guard, true)?;
251            let size = self
252                .path_info
253                .as_ref()
254                .unwrap()
255                .2
256                .saturating_add(self.new_entries);
257            self.new_entries = 0;
258            return self.update_path(path, &lock_guard, size);
259        }
260        // we may need to truncate file before appending new entries
261        let mut other = Self {
262            entries: VecDeque::new(),
263            max_len: self.max_len,
264            ignore_space: self.ignore_space,
265            ignore_dups: self.ignore_dups,
266            new_entries: 0,
267            path_info: None,
268        };
269        other.load_from(&lock_guard)?;
270        let first_new_entry = self.entries.len().saturating_sub(self.new_entries);
271        for entry in self.entries.iter().skip(first_new_entry) {
272            other.add(entry);
273        }
274        lock_guard.seek(SeekFrom::Start(0))?;
275        lock_guard.set_len(0)?; // if new size < old size
276        other.save_to(&lock_guard, false)?;
277        self.update_path(path, &lock_guard, other.len())?;
278        self.new_entries = 0;
279        Ok(())
280    }
281
282    /// Append new entries in the specified file.
283    // Like [append_history](http://tiswww.case.edu/php/chet/readline/history.html#IDX30).
284    /// Not implemented for wasm.
285    #[cfg(target_arch = "wasm32")]
286    pub fn append<P: AsRef<Path> + ?Sized>(&mut self, _path: &P) -> Result<()> {
287        todo!();
288    }
289
290    /// Load the history from the specified file.
291    ///
292    /// # Errors
293    /// Will return `Err` if path does not already exist or could not be read.
294    #[cfg(not(target_arch = "wasm32"))]
295    pub fn load<P: AsRef<Path> + ?Sized>(&mut self, path: &P) -> Result<()> {
296        let path = path.as_ref();
297        let file = File::open(path)?;
298        let lock = RwLock::new(file);
299        let lock_guard = lock.read()?;
300        let len = self.len();
301        if self.load_from(&lock_guard)? {
302            self.update_path(path, &lock_guard, self.len() - len)
303        } else {
304            // discard old version on next save
305            self.path_info = None;
306            Ok(())
307        }
308    }
309
310    /// Load the history from the specified file.
311    /// Not implemented for wasm.
312    /// # Errors
313    /// Will return `Err` if path does not already exist or could not be read.
314    #[cfg(target_arch = "wasm32")]
315    pub fn load<P: AsRef<Path> + ?Sized>(&mut self, _path: &P) -> Result<()> {
316        todo!();
317    }
318
319    #[cfg(not(target_arch = "wasm32"))]
320    fn load_from(&mut self, file: &File) -> Result<bool> {
321        use std::io::{BufRead, BufReader};
322
323        let rdr = BufReader::new(file);
324        let mut lines = rdr.lines();
325        let mut v2 = false;
326        if let Some(first) = lines.next() {
327            let line = first?;
328            if line == Self::FILE_VERSION_V2 {
329                v2 = true;
330            } else {
331                self.add(line);
332            }
333        }
334        let mut appendable = v2;
335        for line in lines {
336            let mut line = line?;
337            if line.is_empty() {
338                continue;
339            }
340            if v2 {
341                let mut copy = None; // lazily copy line if unescaping is needed
342                let mut str = line.as_str();
343                while let Some(i) = str.find('\\') {
344                    if copy.is_none() {
345                        copy = Some(String::with_capacity(line.len()));
346                    }
347                    let s = copy.as_mut().unwrap();
348                    s.push_str(&str[..i]);
349                    let j = i + 1; // escaped char idx
350                    let b = if j < str.len() {
351                        str.as_bytes()[j]
352                    } else {
353                        0 // unexpected if History::save works properly
354                    };
355                    match b {
356                        b'n' => {
357                            s.push('\n'); // unescaped line feed
358                        }
359                        b'\\' => {
360                            s.push('\\'); // unescaped back slash
361                        }
362                        _ => {
363                            // only line feed and back slash should have been escaped
364                            warn!(target: "rustyline", "bad escaped line: {}", line);
365                            copy = None;
366                            break;
367                        }
368                    }
369                    str = &str[j + 1..];
370                }
371                if let Some(mut s) = copy {
372                    s.push_str(str); // remaining bytes with no escaped char
373                    line = s;
374                }
375            }
376            appendable &= self.add(line); // TODO truncate to MAX_LINE
377        }
378        self.new_entries = 0; // TODO we may lost new entries if loaded lines < max_len
379        Ok(appendable)
380    }
381
382    #[cfg(not(target_arch = "wasm32"))]
383    fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> {
384        let modified = file.metadata()?.modified()?;
385        if let Some(PathInfo(
386            ref mut previous_path,
387            ref mut previous_modified,
388            ref mut previous_size,
389        )) = self.path_info
390        {
391            if previous_path.as_path() != path {
392                *previous_path = path.to_owned();
393            }
394            *previous_modified = modified;
395            *previous_size = size;
396        } else {
397            self.path_info = Some(PathInfo(path.to_owned(), modified, size));
398        }
399        debug!(target: "rustyline", "PathInfo({:?}, {:?}, {})", path, modified, size);
400        Ok(())
401    }
402
403    #[cfg(not(target_arch = "wasm32"))]
404    fn can_just_append(&self, path: &Path, file: &File) -> Result<bool> {
405        if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) =
406            self.path_info
407        {
408            if previous_path.as_path() != path {
409                debug!(target: "rustyline", "cannot append: {:?} <> {:?}", previous_path, path);
410                return Ok(false);
411            }
412            let modified = file.metadata()?.modified()?;
413            if *previous_modified != modified
414                || self.max_len <= *previous_size
415                || self.max_len < (*previous_size).saturating_add(self.new_entries)
416            {
417                debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}",
418                       previous_modified, modified, self.max_len, previous_size, self.new_entries);
419                Ok(false)
420            } else {
421                Ok(true)
422            }
423        } else {
424            Ok(false)
425        }
426    }
427
428    /// Clear history
429    pub fn clear(&mut self) {
430        self.entries.clear();
431        self.new_entries = 0;
432    }
433
434    /// Search history (start position inclusive [0, len-1]).
435    ///
436    /// Return the absolute index of the nearest history entry that matches
437    /// `term`.
438    ///
439    /// Return None if no entry contains `term` between [start, len -1] for
440    /// forward search
441    /// or between [0, start] for reverse search.
442    #[must_use]
443    pub fn search(&self, term: &str, start: usize, dir: SearchDirection) -> Option<SearchResult> {
444        #[cfg(not(feature = "case_insensitive_history_search"))]
445        {
446            let test = |entry: &str| entry.find(term);
447            self.search_match(term, start, dir, test)
448        }
449        #[cfg(feature = "case_insensitive_history_search")]
450        {
451            use regex::{escape, RegexBuilder};
452            if let Ok(re) = RegexBuilder::new(&escape(term))
453                .case_insensitive(true)
454                .build()
455            {
456                let test = |entry: &str| re.find(entry).map(|m| m.start());
457                self.search_match(term, start, dir, test)
458            } else {
459                None
460            }
461        }
462    }
463
464    /// Anchored search
465    #[must_use]
466    pub fn starts_with(
467        &self,
468        term: &str,
469        start: usize,
470        dir: SearchDirection,
471    ) -> Option<SearchResult> {
472        #[cfg(not(feature = "case_insensitive_history_search"))]
473        {
474            let test = |entry: &str| {
475                if entry.starts_with(term) {
476                    Some(term.len())
477                } else {
478                    None
479                }
480            };
481            self.search_match(term, start, dir, test)
482        }
483        #[cfg(feature = "case_insensitive_history_search")]
484        {
485            use regex::{escape, RegexBuilder};
486            if let Ok(re) = RegexBuilder::new(&escape(term))
487                .case_insensitive(true)
488                .build()
489            {
490                let test = |entry: &str| {
491                    re.find(entry)
492                        .and_then(|m| if m.start() == 0 { Some(m) } else { None })
493                        .map(|m| m.end())
494                };
495                self.search_match(term, start, dir, test)
496            } else {
497                None
498            }
499        }
500    }
501
502    fn search_match<F>(
503        &self,
504        term: &str,
505        start: usize,
506        dir: SearchDirection,
507        test: F,
508    ) -> Option<SearchResult>
509    where
510        F: Fn(&str) -> Option<usize>,
511    {
512        if term.is_empty() || start >= self.len() {
513            return None;
514        }
515        match dir {
516            SearchDirection::Reverse => {
517                for (idx, entry) in self
518                    .entries
519                    .iter()
520                    .rev()
521                    .skip(self.entries.len() - 1 - start)
522                    .enumerate()
523                {
524                    if let Some(cursor) = test(entry) {
525                        return Some(SearchResult {
526                            idx: start - idx,
527                            entry,
528                            pos: cursor,
529                        });
530                    }
531                }
532                None
533            }
534            SearchDirection::Forward => {
535                for (idx, entry) in self.entries.iter().skip(start).enumerate() {
536                    if let Some(cursor) = test(entry) {
537                        return Some(SearchResult {
538                            idx: idx + start,
539                            entry,
540                            pos: cursor,
541                        });
542                    }
543                }
544                None
545            }
546        }
547    }
548
549    /// Return a forward iterator.
550    #[must_use]
551    pub fn iter(&self) -> Iter<'_> {
552        Iter(self.entries.iter())
553    }
554}
555
556impl Index<usize> for History {
557    type Output = String;
558
559    fn index(&self, index: usize) -> &String {
560        &self.entries[index]
561    }
562}
563
564impl<'a> IntoIterator for &'a History {
565    type IntoIter = Iter<'a>;
566    type Item = &'a String;
567
568    fn into_iter(self) -> Iter<'a> {
569        self.iter()
570    }
571}
572
573/// History iterator.
574pub struct Iter<'a>(vec_deque::Iter<'a, String>);
575
576impl<'a> Iterator for Iter<'a> {
577    type Item = &'a String;
578
579    fn next(&mut self) -> Option<&'a String> {
580        self.0.next()
581    }
582
583    fn size_hint(&self) -> (usize, Option<usize>) {
584        self.0.size_hint()
585    }
586}
587
588impl<'a> DoubleEndedIterator for Iter<'a> {
589    fn next_back(&mut self) -> Option<&'a String> {
590        self.0.next_back()
591    }
592}
593
594cfg_if::cfg_if! {
595    if #[cfg(windows)] {
596       fn umask() -> u16 {
597           0
598       }
599
600       fn restore_umask(_: u16) {}
601
602       fn fix_perm(_: &std::fs::File) {}
603    } else if #[cfg(unix)] {
604        use nix::sys::stat::{self, Mode, fchmod};
605        fn umask() -> Mode {
606            stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO)
607        }
608
609        fn restore_umask(old_umask: Mode) {
610            stat::umask(old_umask);
611        }
612
613        fn fix_perm(file: &File) {
614            use std::os::unix::io::AsRawFd;
615            let _ = fchmod(file.as_raw_fd(), Mode::S_IRUSR | Mode::S_IWUSR);
616        }
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::{History, SearchDirection, SearchResult};
623    use crate::config::Config;
624    use crate::Result;
625
626    fn init() -> History {
627        let mut history = History::new();
628        assert!(history.add("line1"));
629        assert!(history.add("line2"));
630        assert!(history.add("line3"));
631        history
632    }
633
634    #[test]
635    fn new() {
636        let history = History::new();
637        assert_eq!(0, history.entries.len());
638    }
639
640    #[test]
641    fn add() {
642        let config = Config::builder().history_ignore_space(true).build();
643        let mut history = History::with_config(config);
644        assert_eq!(config.max_history_size(), history.max_len);
645        assert!(history.add("line1"));
646        assert!(history.add("line2"));
647        assert!(!history.add("line2"));
648        assert!(!history.add(""));
649        assert!(!history.add(" line3"));
650    }
651
652    #[test]
653    fn set_max_len() {
654        let mut history = init();
655        history.set_max_len(1);
656        assert_eq!(1, history.entries.len());
657        assert_eq!(Some(&"line3".to_owned()), history.last());
658    }
659
660    #[test]
661    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
662    fn save() -> Result<()> {
663        check_save("line\nfour \\ abc")
664    }
665
666    #[test]
667    #[cfg_attr(miri, ignore)] // unsupported operation: `open` not available when isolation is enabled
668    fn save_windows_path() -> Result<()> {
669        let path = "cd source\\repos\\forks\\nushell\\";
670        check_save(path)
671    }
672
673    fn check_save(line: &str) -> Result<()> {
674        let mut history = init();
675        assert!(history.add(line));
676        let tf = tempfile::NamedTempFile::new()?;
677
678        history.save(tf.path())?;
679        let mut history2 = History::new();
680        history2.load(tf.path())?;
681        for (a, b) in history.entries.iter().zip(history2.entries.iter()) {
682            assert_eq!(a, b);
683        }
684        tf.close()?;
685        Ok(())
686    }
687
688    #[test]
689    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
690    fn load_legacy() -> Result<()> {
691        use std::io::Write;
692        let tf = tempfile::NamedTempFile::new()?;
693        {
694            let mut legacy = std::fs::File::create(tf.path())?;
695            // Some data we'd accidentally corrupt if we got the version wrong
696            let data = b"\
697                test\\n \\abc \\123\n\
698                123\\n\\\\n\n\
699                abcde
700            ";
701            legacy.write_all(data)?;
702            legacy.flush()?;
703        }
704        let mut history = History::new();
705        history.load(tf.path())?;
706        assert_eq!(history.entries[0], "test\\n \\abc \\123");
707        assert_eq!(history.entries[1], "123\\n\\\\n");
708        assert_eq!(history.entries[2], "abcde");
709
710        tf.close()?;
711        Ok(())
712    }
713
714    #[test]
715    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
716    fn append() -> Result<()> {
717        let mut history = init();
718        let tf = tempfile::NamedTempFile::new()?;
719
720        history.append(tf.path())?;
721
722        let mut history2 = History::new();
723        history2.load(tf.path())?;
724        history2.add("line4");
725        history2.append(tf.path())?;
726
727        history.add("line5");
728        history.append(tf.path())?;
729
730        let mut history3 = History::new();
731        history3.load(tf.path())?;
732        assert_eq!(history3.len(), 5);
733
734        tf.close()?;
735        Ok(())
736    }
737
738    #[test]
739    #[cfg_attr(miri, ignore)] // unsupported operation: `getcwd` not available when isolation is enabled
740    fn truncate() -> Result<()> {
741        let tf = tempfile::NamedTempFile::new()?;
742
743        let config = Config::builder().history_ignore_dups(false).build();
744        let mut history = History::with_config(config);
745        history.add("line1");
746        history.add("line1");
747        history.append(tf.path())?;
748
749        let mut history = History::new();
750        history.load(tf.path())?;
751        history.add("l");
752        history.append(tf.path())?;
753
754        let mut history = History::new();
755        history.load(tf.path())?;
756        assert_eq!(history.len(), 2);
757        assert_eq!(history.entries[1], "l");
758
759        tf.close()?;
760        Ok(())
761    }
762
763    #[test]
764    fn search() {
765        let history = init();
766        assert_eq!(None, history.search("", 0, SearchDirection::Forward));
767        assert_eq!(None, history.search("none", 0, SearchDirection::Forward));
768        assert_eq!(None, history.search("line", 3, SearchDirection::Forward));
769
770        assert_eq!(
771            Some(SearchResult {
772                idx: 0,
773                entry: history.get(0).unwrap(),
774                pos: 0
775            }),
776            history.search("line", 0, SearchDirection::Forward)
777        );
778        assert_eq!(
779            Some(SearchResult {
780                idx: 1,
781                entry: history.get(1).unwrap(),
782                pos: 0
783            }),
784            history.search("line", 1, SearchDirection::Forward)
785        );
786        assert_eq!(
787            Some(SearchResult {
788                idx: 2,
789                entry: history.get(2).unwrap(),
790                pos: 0
791            }),
792            history.search("line3", 1, SearchDirection::Forward)
793        );
794    }
795
796    #[test]
797    fn reverse_search() {
798        let history = init();
799        assert_eq!(None, history.search("", 2, SearchDirection::Reverse));
800        assert_eq!(None, history.search("none", 2, SearchDirection::Reverse));
801        assert_eq!(None, history.search("line", 3, SearchDirection::Reverse));
802
803        assert_eq!(
804            Some(SearchResult {
805                idx: 2,
806                entry: history.get(2).unwrap(),
807                pos: 0
808            }),
809            history.search("line", 2, SearchDirection::Reverse)
810        );
811        assert_eq!(
812            Some(SearchResult {
813                idx: 1,
814                entry: history.get(1).unwrap(),
815                pos: 0
816            }),
817            history.search("line", 1, SearchDirection::Reverse)
818        );
819        assert_eq!(
820            Some(SearchResult {
821                idx: 0,
822                entry: history.get(0).unwrap(),
823                pos: 0
824            }),
825            history.search("line1", 1, SearchDirection::Reverse)
826        );
827    }
828
829    #[test]
830    #[cfg(feature = "case_insensitive_history_search")]
831    fn anchored_search() {
832        let history = init();
833        assert_eq!(
834            Some(SearchResult {
835                idx: 2,
836                entry: history.get(2).unwrap(),
837                pos: 4
838            }),
839            history.starts_with("LiNe", 2, SearchDirection::Reverse)
840        );
841        assert_eq!(
842            None,
843            history.starts_with("iNe", 2, SearchDirection::Reverse)
844        );
845    }
846}