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