1#[cfg(feature = "with-file-history")]
4use fd_lock::RwLock;
5#[cfg(feature = "with-file-history")]
6use log::{debug, warn};
7use std::borrow::Cow;
8use std::collections::vec_deque;
9use std::collections::VecDeque;
10#[cfg(feature = "with-file-history")]
11use std::fs::{File, OpenOptions};
12#[cfg(feature = "with-file-history")]
13use std::io::SeekFrom;
14use std::ops::Index;
15use std::path::Path;
16#[cfg(feature = "with-file-history")]
17use std::time::SystemTime;
18
19use super::Result;
20use crate::config::{Config, HistoryDuplicates};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum SearchDirection {
25 Forward,
27 Reverse,
29}
30
31#[derive(Debug, Clone, Eq, PartialEq)]
33pub struct SearchResult<'a> {
34 pub entry: Cow<'a, str>,
36 pub idx: usize,
38 pub pos: usize,
40}
41
42pub trait History {
45 fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult>>;
61
62 fn add(&mut self, line: &str) -> Result<bool>;
73 fn add_owned(&mut self, line: String) -> Result<bool>; #[must_use]
78 fn len(&self) -> usize;
79
80 #[must_use]
82 fn is_empty(&self) -> bool;
83
84 fn set_max_len(&mut self, len: usize) -> Result<()>;
110
111 fn ignore_dups(&mut self, yes: bool) -> Result<()>;
113
114 fn ignore_space(&mut self, yes: bool);
116
117 fn save(&mut self, path: &Path) -> Result<()>; fn append(&mut self, path: &Path) -> Result<()>; fn load(&mut self, path: &Path) -> Result<()>; fn clear(&mut self) -> Result<()>;
134
135 fn search(
154 &self,
155 term: &str,
156 start: usize,
157 dir: SearchDirection,
158 ) -> Result<Option<SearchResult>>;
159
160 fn starts_with(
162 &self,
163 term: &str,
164 start: usize,
165 dir: SearchDirection,
166 ) -> Result<Option<SearchResult>>;
167
168 }
174
175pub struct MemHistory {
177 entries: VecDeque<String>,
178 max_len: usize,
179 ignore_space: bool,
180 ignore_dups: bool,
181}
182
183impl MemHistory {
184 #[must_use]
186 pub fn new() -> Self {
187 Self::with_config(Config::default())
188 }
189
190 #[must_use]
195 pub fn with_config(config: Config) -> Self {
196 Self {
197 entries: VecDeque::new(),
198 max_len: config.max_history_size(),
199 ignore_space: config.history_ignore_space(),
200 ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
201 }
202 }
203
204 fn search_match<F>(
205 &self,
206 term: &str,
207 start: usize,
208 dir: SearchDirection,
209 test: F,
210 ) -> Option<SearchResult>
211 where
212 F: Fn(&str) -> Option<usize>,
213 {
214 if term.is_empty() || start >= self.len() {
215 return None;
216 }
217 match dir {
218 SearchDirection::Reverse => {
219 for (idx, entry) in self
220 .entries
221 .iter()
222 .rev()
223 .skip(self.len() - 1 - start)
224 .enumerate()
225 {
226 if let Some(cursor) = test(entry) {
227 return Some(SearchResult {
228 idx: start - idx,
229 entry: Cow::Borrowed(entry),
230 pos: cursor,
231 });
232 }
233 }
234 None
235 }
236 SearchDirection::Forward => {
237 for (idx, entry) in self.entries.iter().skip(start).enumerate() {
238 if let Some(cursor) = test(entry) {
239 return Some(SearchResult {
240 idx: idx + start,
241 entry: Cow::Borrowed(entry),
242 pos: cursor,
243 });
244 }
245 }
246 None
247 }
248 }
249 }
250
251 fn ignore(&self, line: &str) -> bool {
252 if self.max_len == 0 {
253 return true;
254 }
255 if line.is_empty()
256 || (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace))
257 {
258 return true;
259 }
260 if self.ignore_dups {
261 if let Some(s) = self.entries.back() {
262 if s == line {
263 return true;
264 }
265 }
266 }
267 false
268 }
269
270 fn insert(&mut self, line: String) {
271 if self.entries.len() == self.max_len {
272 self.entries.pop_front();
273 }
274 self.entries.push_back(line);
275 }
276}
277
278impl Default for MemHistory {
279 fn default() -> Self {
280 Self::new()
281 }
282}
283
284impl History for MemHistory {
285 fn get(&self, index: usize, _: SearchDirection) -> Result<Option<SearchResult>> {
286 Ok(self
287 .entries
288 .get(index)
289 .map(String::as_ref)
290 .map(Cow::Borrowed)
291 .map(|entry| SearchResult {
292 entry,
293 idx: index,
294 pos: 0,
295 }))
296 }
297
298 fn add(&mut self, line: &str) -> Result<bool> {
299 if self.ignore(line) {
300 return Ok(false);
301 }
302 self.insert(line.to_owned());
303 Ok(true)
304 }
305
306 fn add_owned(&mut self, line: String) -> Result<bool> {
307 if self.ignore(&line) {
308 return Ok(false);
309 }
310 self.insert(line);
311 Ok(true)
312 }
313
314 fn len(&self) -> usize {
315 self.entries.len()
316 }
317
318 fn is_empty(&self) -> bool {
319 self.entries.is_empty()
320 }
321
322 fn set_max_len(&mut self, len: usize) -> Result<()> {
323 self.max_len = len;
324 if self.len() > len {
325 self.entries.drain(..self.len() - len);
326 }
327 Ok(())
328 }
329
330 fn ignore_dups(&mut self, yes: bool) -> Result<()> {
331 self.ignore_dups = yes;
332 Ok(())
333 }
334
335 fn ignore_space(&mut self, yes: bool) {
336 self.ignore_space = yes;
337 }
338
339 fn save(&mut self, _: &Path) -> Result<()> {
340 unimplemented!();
341 }
342
343 fn append(&mut self, _: &Path) -> Result<()> {
344 unimplemented!();
345 }
346
347 fn load(&mut self, _: &Path) -> Result<()> {
348 unimplemented!();
349 }
350
351 fn clear(&mut self) -> Result<()> {
352 self.entries.clear();
353 Ok(())
354 }
355
356 fn search(
357 &self,
358 term: &str,
359 start: usize,
360 dir: SearchDirection,
361 ) -> Result<Option<SearchResult>> {
362 #[cfg(not(feature = "case_insensitive_history_search"))]
363 {
364 let test = |entry: &str| entry.find(term);
365 Ok(self.search_match(term, start, dir, test))
366 }
367 #[cfg(feature = "case_insensitive_history_search")]
368 {
369 use regex::{escape, RegexBuilder};
370 Ok(
371 if let Ok(re) = RegexBuilder::new(&escape(term))
372 .case_insensitive(true)
373 .build()
374 {
375 let test = |entry: &str| re.find(entry).map(|m| m.start());
376 self.search_match(term, start, dir, test)
377 } else {
378 None
379 },
380 )
381 }
382 }
383
384 fn starts_with(
385 &self,
386 term: &str,
387 start: usize,
388 dir: SearchDirection,
389 ) -> Result<Option<SearchResult>> {
390 #[cfg(not(feature = "case_insensitive_history_search"))]
391 {
392 let test = |entry: &str| {
393 if entry.starts_with(term) {
394 Some(term.len())
395 } else {
396 None
397 }
398 };
399 Ok(self.search_match(term, start, dir, test))
400 }
401 #[cfg(feature = "case_insensitive_history_search")]
402 {
403 use regex::{escape, RegexBuilder};
404 Ok(
405 if let Ok(re) = RegexBuilder::new(&escape(term))
406 .case_insensitive(true)
407 .build()
408 {
409 let test = |entry: &str| {
410 re.find(entry)
411 .and_then(|m| if m.start() == 0 { Some(m) } else { None })
412 .map(|m| m.end())
413 };
414 self.search_match(term, start, dir, test)
415 } else {
416 None
417 },
418 )
419 }
420 }
421}
422
423impl Index<usize> for MemHistory {
424 type Output = String;
425
426 fn index(&self, index: usize) -> &String {
427 &self.entries[index]
428 }
429}
430
431impl<'a> IntoIterator for &'a MemHistory {
432 type IntoIter = vec_deque::Iter<'a, String>;
433 type Item = &'a String;
434
435 fn into_iter(self) -> Self::IntoIter {
436 self.entries.iter()
437 }
438}
439
440#[derive(Default)]
442#[cfg(feature = "with-file-history")]
443pub struct FileHistory {
444 mem: MemHistory,
445 new_entries: usize,
447 path_info: Option<PathInfo>,
449}
450
451#[cfg(feature = "with-file-history")]
455struct PathInfo(std::path::PathBuf, SystemTime, usize);
456
457#[cfg(feature = "with-file-history")]
458impl FileHistory {
459 const FILE_VERSION_V2: &'static str = "#V2";
462
463 #[must_use]
465 pub fn new() -> Self {
466 Self::with_config(Config::default())
467 }
468
469 #[must_use]
474 pub fn with_config(config: Config) -> Self {
475 Self {
476 mem: MemHistory::with_config(config),
477 new_entries: 0,
478 path_info: None,
479 }
480 }
481
482 fn save_to(&mut self, file: &File, append: bool) -> Result<()> {
483 use std::io::{BufWriter, Write};
484
485 fix_perm(file);
486 let mut wtr = BufWriter::new(file);
487 let first_new_entry = if append {
488 self.mem.len().saturating_sub(self.new_entries)
489 } else {
490 wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
491 wtr.write_all(b"\n")?;
492 0
493 };
494 for entry in self.mem.entries.iter().skip(first_new_entry) {
495 let mut bytes = entry.as_bytes();
496 while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
497 let (head, tail) = bytes.split_at(i);
498 wtr.write_all(head)?;
499
500 let (&escapable_byte, tail) = tail
501 .split_first()
502 .expect("memchr guarantees i is a valid index");
503 if escapable_byte == b'\n' {
504 wtr.write_all(br"\n")?; } else {
506 debug_assert_eq!(escapable_byte, b'\\');
507 wtr.write_all(br"\\")?; }
509 bytes = tail;
510 }
511 wtr.write_all(bytes)?; wtr.write_all(b"\n")?;
513 }
514 wtr.flush()?;
516 Ok(())
517 }
518
519 fn load_from(&mut self, file: &File) -> Result<bool> {
520 use std::io::{BufRead, BufReader};
521
522 let rdr = BufReader::new(file);
523 let mut lines = rdr.lines();
524 let mut v2 = false;
525 if let Some(first) = lines.next() {
526 let line = first?;
527 if line == Self::FILE_VERSION_V2 {
528 v2 = true;
529 } else {
530 self.add_owned(line)?;
531 }
532 }
533 let mut appendable = v2;
534 for line in lines {
535 let mut line = line?;
536 if line.is_empty() {
537 continue;
538 }
539 if v2 {
540 let mut copy = None; let mut str = line.as_str();
542 while let Some(i) = str.find('\\') {
543 if copy.is_none() {
544 copy = Some(String::with_capacity(line.len()));
545 }
546 let s = copy.as_mut().unwrap();
547 s.push_str(&str[..i]);
548 let j = i + 1; let b = if j < str.len() {
550 str.as_bytes()[j]
551 } else {
552 0 };
554 match b {
555 b'n' => {
556 s.push('\n'); }
558 b'\\' => {
559 s.push('\\'); }
561 _ => {
562 warn!(target: "rustyline", "bad escaped line: {}", line);
564 copy = None;
565 break;
566 }
567 }
568 str = &str[j + 1..];
569 }
570 if let Some(mut s) = copy {
571 s.push_str(str); line = s;
573 }
574 }
575 appendable &= self.add_owned(line)?; }
577 self.new_entries = 0; Ok(appendable)
579 }
580
581 fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> {
582 let modified = file.metadata()?.modified()?;
583 if let Some(PathInfo(
584 ref mut previous_path,
585 ref mut previous_modified,
586 ref mut previous_size,
587 )) = self.path_info
588 {
589 if previous_path.as_path() != path {
590 *previous_path = path.to_owned();
591 }
592 *previous_modified = modified;
593 *previous_size = size;
594 } else {
595 self.path_info = Some(PathInfo(path.to_owned(), modified, size));
596 }
597 debug!(target: "rustyline", "PathInfo({:?}, {:?}, {})", path, modified, size);
598 Ok(())
599 }
600
601 fn can_just_append(&self, path: &Path, file: &File) -> Result<bool> {
602 if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) =
603 self.path_info
604 {
605 if previous_path.as_path() != path {
606 debug!(target: "rustyline", "cannot append: {:?} <> {:?}", previous_path, path);
607 return Ok(false);
608 }
609 let modified = file.metadata()?.modified()?;
610 if *previous_modified != modified
611 || self.mem.max_len <= *previous_size
612 || self.mem.max_len < (*previous_size).saturating_add(self.new_entries)
613 {
614 debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}",
615 previous_modified, modified, self.mem.max_len, previous_size, self.new_entries);
616 Ok(false)
617 } else {
618 Ok(true)
619 }
620 } else {
621 Ok(false)
622 }
623 }
624
625 #[must_use]
627 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &String> + '_ {
628 self.mem.entries.iter()
629 }
630}
631
632#[cfg(not(feature = "with-file-history"))]
634pub type DefaultHistory = MemHistory;
635#[cfg(feature = "with-file-history")]
637pub type DefaultHistory = FileHistory;
638
639#[cfg(feature = "with-file-history")]
640impl History for FileHistory {
641 fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult>> {
642 self.mem.get(index, dir)
643 }
644
645 fn add(&mut self, line: &str) -> Result<bool> {
646 if self.mem.add(line)? {
647 self.new_entries = self.new_entries.saturating_add(1).min(self.len());
648 Ok(true)
649 } else {
650 Ok(false)
651 }
652 }
653
654 fn add_owned(&mut self, line: String) -> Result<bool> {
655 if self.mem.add_owned(line)? {
656 self.new_entries = self.new_entries.saturating_add(1).min(self.len());
657 Ok(true)
658 } else {
659 Ok(false)
660 }
661 }
662
663 fn len(&self) -> usize {
664 self.mem.len()
665 }
666
667 fn is_empty(&self) -> bool {
668 self.mem.is_empty()
669 }
670
671 fn set_max_len(&mut self, len: usize) -> Result<()> {
672 self.mem.set_max_len(len)?;
673 self.new_entries = self.new_entries.min(len);
674 Ok(())
675 }
676
677 fn ignore_dups(&mut self, yes: bool) -> Result<()> {
678 self.mem.ignore_dups(yes)
679 }
680
681 fn ignore_space(&mut self, yes: bool) {
682 self.mem.ignore_space(yes);
683 }
684
685 fn save(&mut self, path: &Path) -> Result<()> {
686 if self.is_empty() || self.new_entries == 0 {
687 return Ok(());
688 }
689 let old_umask = umask();
690 let f = File::create(path);
691 restore_umask(old_umask);
692 let file = f?;
693 let mut lock = RwLock::new(file);
694 let lock_guard = lock.write()?;
695 self.save_to(&lock_guard, false)?;
696 self.new_entries = 0;
697 self.update_path(path, &lock_guard, self.len())
698 }
699
700 fn append(&mut self, path: &Path) -> Result<()> {
701 use std::io::Seek;
702
703 if self.is_empty() || self.new_entries == 0 {
704 return Ok(());
705 }
706 if !path.exists() || self.new_entries == self.mem.max_len {
707 return self.save(path);
708 }
709 let file = OpenOptions::new().write(true).read(true).open(path)?;
710 let mut lock = RwLock::new(file);
711 let mut lock_guard = lock.write()?;
712 if self.can_just_append(path, &lock_guard)? {
713 lock_guard.seek(SeekFrom::End(0))?;
714 self.save_to(&lock_guard, true)?;
715 let size = self
716 .path_info
717 .as_ref()
718 .unwrap()
719 .2
720 .saturating_add(self.new_entries);
721 self.new_entries = 0;
722 return self.update_path(path, &lock_guard, size);
723 }
724 let mut other = Self {
726 mem: MemHistory {
727 entries: VecDeque::new(),
728 max_len: self.mem.max_len,
729 ignore_space: self.mem.ignore_space,
730 ignore_dups: self.mem.ignore_dups,
731 },
732 new_entries: 0,
733 path_info: None,
734 };
735 other.load_from(&lock_guard)?;
736 let first_new_entry = self.mem.len().saturating_sub(self.new_entries);
737 for entry in self.mem.entries.iter().skip(first_new_entry) {
738 other.add(entry)?;
739 }
740 lock_guard.seek(SeekFrom::Start(0))?;
741 lock_guard.set_len(0)?; other.save_to(&lock_guard, false)?;
743 self.update_path(path, &lock_guard, other.len())?;
744 self.new_entries = 0;
745 Ok(())
746 }
747
748 fn load(&mut self, path: &Path) -> Result<()> {
749 let file = File::open(path)?;
750 let lock = RwLock::new(file);
751 let lock_guard = lock.read()?;
752 let len = self.len();
753 if self.load_from(&lock_guard)? {
754 self.update_path(path, &lock_guard, self.len() - len)
755 } else {
756 self.path_info = None;
758 Ok(())
759 }
760 }
761
762 fn clear(&mut self) -> Result<()> {
763 self.mem.clear()?;
764 self.new_entries = 0;
765 Ok(())
766 }
767
768 fn search(
769 &self,
770 term: &str,
771 start: usize,
772 dir: SearchDirection,
773 ) -> Result<Option<SearchResult>> {
774 self.mem.search(term, start, dir)
775 }
776
777 fn starts_with(
778 &self,
779 term: &str,
780 start: usize,
781 dir: SearchDirection,
782 ) -> Result<Option<SearchResult>> {
783 self.mem.starts_with(term, start, dir)
784 }
785}
786
787#[cfg(feature = "with-file-history")]
788impl Index<usize> for FileHistory {
789 type Output = String;
790
791 fn index(&self, index: usize) -> &String {
792 &self.mem.entries[index]
793 }
794}
795
796#[cfg(feature = "with-file-history")]
797impl<'a> IntoIterator for &'a FileHistory {
798 type IntoIter = vec_deque::Iter<'a, String>;
799 type Item = &'a String;
800
801 fn into_iter(self) -> Self::IntoIter {
802 self.mem.entries.iter()
803 }
804}
805
806#[cfg(feature = "with-file-history")]
807cfg_if::cfg_if! {
808 if #[cfg(any(windows, target_arch = "wasm32"))] {
809 fn umask() -> u16 {
810 0
811 }
812
813 fn restore_umask(_: u16) {}
814
815 fn fix_perm(_: &File) {}
816 } else if #[cfg(unix)] {
817 use nix::sys::stat::{self, Mode, fchmod};
818 fn umask() -> Mode {
819 stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO)
820 }
821
822 fn restore_umask(old_umask: Mode) {
823 stat::umask(old_umask);
824 }
825
826 fn fix_perm(file: &File) {
827 use std::os::unix::io::AsRawFd;
828 let _ = fchmod(file.as_raw_fd(), Mode::S_IRUSR | Mode::S_IWUSR);
829 }
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::{DefaultHistory, History, SearchDirection, SearchResult};
836 use crate::config::Config;
837 use crate::Result;
838
839 fn init() -> DefaultHistory {
840 let mut history = DefaultHistory::new();
841 assert!(history.add("line1").unwrap());
842 assert!(history.add("line2").unwrap());
843 assert!(history.add("line3").unwrap());
844 history
845 }
846
847 #[test]
848 fn new() {
849 let history = DefaultHistory::new();
850 assert_eq!(0, history.len());
851 }
852
853 #[test]
854 fn add() {
855 let config = Config::builder().history_ignore_space(true).build();
856 let mut history = DefaultHistory::with_config(config);
857 #[cfg(feature = "with-file-history")]
858 assert_eq!(config.max_history_size(), history.mem.max_len);
859 assert!(history.add("line1").unwrap());
860 assert!(history.add("line2").unwrap());
861 assert!(!history.add("line2").unwrap());
862 assert!(!history.add("").unwrap());
863 assert!(!history.add(" line3").unwrap());
864 }
865
866 #[test]
867 fn set_max_len() {
868 let mut history = init();
869 history.set_max_len(1).unwrap();
870 assert_eq!(1, history.len());
871 assert_eq!(Some(&"line3".to_owned()), history.into_iter().last());
872 }
873
874 #[test]
875 #[cfg(feature = "with-file-history")]
876 #[cfg_attr(miri, ignore)] fn save() -> Result<()> {
878 check_save("line\nfour \\ abc")
879 }
880
881 #[test]
882 #[cfg(feature = "with-file-history")]
883 #[cfg_attr(miri, ignore)] fn save_windows_path() -> Result<()> {
885 let path = "cd source\\repos\\forks\\nushell\\";
886 check_save(path)
887 }
888
889 #[cfg(feature = "with-file-history")]
890 fn check_save(line: &str) -> Result<()> {
891 let mut history = init();
892 assert!(history.add(line)?);
893 let tf = tempfile::NamedTempFile::new()?;
894
895 history.save(tf.path())?;
896 let mut history2 = DefaultHistory::new();
897 history2.load(tf.path())?;
898 for (a, b) in history.iter().zip(history2.iter()) {
899 assert_eq!(a, b);
900 }
901 tf.close()?;
902 Ok(())
903 }
904
905 #[test]
906 #[cfg(feature = "with-file-history")]
907 #[cfg_attr(miri, ignore)] fn load_legacy() -> Result<()> {
909 use std::io::Write;
910 let tf = tempfile::NamedTempFile::new()?;
911 {
912 let mut legacy = std::fs::File::create(tf.path())?;
913 let data = b"\
915 test\\n \\abc \\123\n\
916 123\\n\\\\n\n\
917 abcde
918 ";
919 legacy.write_all(data)?;
920 legacy.flush()?;
921 }
922 let mut history = DefaultHistory::new();
923 history.load(tf.path())?;
924 assert_eq!(history[0], "test\\n \\abc \\123");
925 assert_eq!(history[1], "123\\n\\\\n");
926 assert_eq!(history[2], "abcde");
927
928 tf.close()?;
929 Ok(())
930 }
931
932 #[test]
933 #[cfg(feature = "with-file-history")]
934 #[cfg_attr(miri, ignore)] fn append() -> Result<()> {
936 let mut history = init();
937 let tf = tempfile::NamedTempFile::new()?;
938
939 history.append(tf.path())?;
940
941 let mut history2 = DefaultHistory::new();
942 history2.load(tf.path())?;
943 history2.add("line4")?;
944 history2.append(tf.path())?;
945
946 history.add("line5")?;
947 history.append(tf.path())?;
948
949 let mut history3 = DefaultHistory::new();
950 history3.load(tf.path())?;
951 assert_eq!(history3.len(), 5);
952
953 tf.close()?;
954 Ok(())
955 }
956
957 #[test]
958 #[cfg(feature = "with-file-history")]
959 #[cfg_attr(miri, ignore)] fn truncate() -> Result<()> {
961 let tf = tempfile::NamedTempFile::new()?;
962
963 let config = Config::builder().history_ignore_dups(false)?.build();
964 let mut history = DefaultHistory::with_config(config);
965 history.add("line1")?;
966 history.add("line1")?;
967 history.append(tf.path())?;
968
969 let mut history = DefaultHistory::new();
970 history.load(tf.path())?;
971 history.add("l")?;
972 history.append(tf.path())?;
973
974 let mut history = DefaultHistory::new();
975 history.load(tf.path())?;
976 assert_eq!(history.len(), 2);
977 assert_eq!(history[1], "l");
978
979 tf.close()?;
980 Ok(())
981 }
982
983 #[test]
984 fn search() -> Result<()> {
985 let history = init();
986 assert_eq!(None, history.search("", 0, SearchDirection::Forward)?);
987 assert_eq!(None, history.search("none", 0, SearchDirection::Forward)?);
988 assert_eq!(None, history.search("line", 3, SearchDirection::Forward)?);
989
990 assert_eq!(
991 Some(SearchResult {
992 idx: 0,
993 entry: history.get(0, SearchDirection::Forward)?.unwrap().entry,
994 pos: 0
995 }),
996 history.search("line", 0, SearchDirection::Forward)?
997 );
998 assert_eq!(
999 Some(SearchResult {
1000 idx: 1,
1001 entry: history.get(1, SearchDirection::Forward)?.unwrap().entry,
1002 pos: 0
1003 }),
1004 history.search("line", 1, SearchDirection::Forward)?
1005 );
1006 assert_eq!(
1007 Some(SearchResult {
1008 idx: 2,
1009 entry: history.get(2, SearchDirection::Forward)?.unwrap().entry,
1010 pos: 0
1011 }),
1012 history.search("line3", 1, SearchDirection::Forward)?
1013 );
1014 Ok(())
1015 }
1016
1017 #[test]
1018 fn reverse_search() -> Result<()> {
1019 let history = init();
1020 assert_eq!(None, history.search("", 2, SearchDirection::Reverse)?);
1021 assert_eq!(None, history.search("none", 2, SearchDirection::Reverse)?);
1022 assert_eq!(None, history.search("line", 3, SearchDirection::Reverse)?);
1023
1024 assert_eq!(
1025 Some(SearchResult {
1026 idx: 2,
1027 entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
1028 pos: 0
1029 }),
1030 history.search("line", 2, SearchDirection::Reverse)?
1031 );
1032 assert_eq!(
1033 Some(SearchResult {
1034 idx: 1,
1035 entry: history.get(1, SearchDirection::Reverse)?.unwrap().entry,
1036 pos: 0
1037 }),
1038 history.search("line", 1, SearchDirection::Reverse)?
1039 );
1040 assert_eq!(
1041 Some(SearchResult {
1042 idx: 0,
1043 entry: history.get(0, SearchDirection::Reverse)?.unwrap().entry,
1044 pos: 0
1045 }),
1046 history.search("line1", 1, SearchDirection::Reverse)?
1047 );
1048 Ok(())
1049 }
1050
1051 #[test]
1052 #[cfg(feature = "case_insensitive_history_search")]
1053 fn anchored_search() -> Result<()> {
1054 let history = init();
1055 assert_eq!(
1056 Some(SearchResult {
1057 idx: 2,
1058 entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
1059 pos: 4
1060 }),
1061 history.starts_with("LiNe", 2, SearchDirection::Reverse)?
1062 );
1063 assert_eq!(
1064 None,
1065 history.starts_with("iNe", 2, SearchDirection::Reverse)?
1066 );
1067 Ok(())
1068 }
1069}