1#[cfg(feature = "with-file-history")]
4use log::{debug, warn};
5use std::borrow::Cow;
6use std::collections::vec_deque;
7use std::collections::VecDeque;
8#[cfg(feature = "with-file-history")]
9use std::fs::{File, OpenOptions};
10#[cfg(feature = "with-file-history")]
11use std::io::SeekFrom;
12use std::ops::Index;
13use std::path::Path;
14#[cfg(feature = "with-file-history")]
15use std::time::SystemTime;
16
17use super::Result;
18use crate::config::{Config, HistoryDuplicates};
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum SearchDirection {
23 Forward,
25 Reverse,
27}
28
29#[derive(Debug, Clone, Eq, PartialEq)]
31pub struct SearchResult<'a> {
32 pub entry: Cow<'a, str>,
34 pub idx: usize,
36 pub pos: usize,
38}
39
40pub trait History {
43 fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult<'_>>>;
59
60 fn add(&mut self, line: &str) -> Result<bool>;
74 fn add_owned(&mut self, line: String) -> Result<bool>; #[must_use]
82 fn len(&self) -> usize;
83
84 #[must_use]
86 fn is_empty(&self) -> bool;
87
88 fn set_max_len(&mut self, len: usize) -> Result<()>;
114
115 fn ignore_dups(&mut self, yes: bool) -> Result<()>;
117
118 fn ignore_space(&mut self, yes: bool);
120
121 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<()>;
138
139 fn search(
158 &self,
159 term: &str,
160 start: usize,
161 dir: SearchDirection,
162 ) -> Result<Option<SearchResult<'_>>>;
163
164 fn starts_with(
166 &self,
167 term: &str,
168 start: usize,
169 dir: SearchDirection,
170 ) -> Result<Option<SearchResult<'_>>>;
171
172 }
178
179pub struct MemHistory {
181 entries: VecDeque<String>,
182 max_len: usize,
183 ignore_space: bool,
184 ignore_dups: bool,
185}
186
187impl MemHistory {
188 #[must_use]
190 pub fn new() -> Self {
191 Self::with_config(&Config::default())
192 }
193
194 #[must_use]
199 pub fn with_config(config: &Config) -> Self {
200 Self {
201 entries: VecDeque::new(),
202 max_len: config.max_history_size(),
203 ignore_space: config.history_ignore_space(),
204 ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
205 }
206 }
207
208 fn search_match<F>(
209 &self,
210 term: &str,
211 start: usize,
212 dir: SearchDirection,
213 test: F,
214 ) -> Option<SearchResult<'_>>
215 where
216 F: Fn(&str) -> Option<usize>,
217 {
218 if term.is_empty() || start >= self.len() {
219 return None;
220 }
221 match dir {
222 SearchDirection::Reverse => {
223 for (idx, entry) in self
224 .entries
225 .iter()
226 .rev()
227 .skip(self.len() - 1 - start)
228 .enumerate()
229 {
230 if let Some(cursor) = test(entry) {
231 return Some(SearchResult {
232 idx: start - idx,
233 entry: Cow::Borrowed(entry),
234 pos: cursor,
235 });
236 }
237 }
238 None
239 }
240 SearchDirection::Forward => {
241 for (idx, entry) in self.entries.iter().skip(start).enumerate() {
242 if let Some(cursor) = test(entry) {
243 return Some(SearchResult {
244 idx: idx + start,
245 entry: Cow::Borrowed(entry),
246 pos: cursor,
247 });
248 }
249 }
250 None
251 }
252 }
253 }
254
255 fn ignore(&self, line: &str) -> bool {
256 if self.max_len == 0 {
257 return true;
258 }
259 if line.is_empty()
260 || (self.ignore_space && line.chars().next().is_none_or(char::is_whitespace))
261 {
262 return true;
263 }
264 if self.ignore_dups {
265 if let Some(s) = self.entries.back() {
266 if s == line {
267 return true;
268 }
269 }
270 }
271 false
272 }
273
274 fn insert(&mut self, line: String) {
275 if self.entries.len() == self.max_len {
276 self.entries.pop_front();
277 }
278 self.entries.push_back(line);
279 }
280}
281
282impl Default for MemHistory {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288impl History for MemHistory {
289 fn get(&self, index: usize, _: SearchDirection) -> Result<Option<SearchResult<'_>>> {
290 Ok(self
291 .entries
292 .get(index)
293 .map(String::as_ref)
294 .map(Cow::Borrowed)
295 .map(|entry| SearchResult {
296 entry,
297 idx: index,
298 pos: 0,
299 }))
300 }
301
302 fn add(&mut self, line: &str) -> Result<bool> {
303 if self.ignore(line) {
304 return Ok(false);
305 }
306 self.insert(line.to_owned());
307 Ok(true)
308 }
309
310 fn add_owned(&mut self, line: String) -> Result<bool> {
311 if self.ignore(&line) {
312 return Ok(false);
313 }
314 self.insert(line);
315 Ok(true)
316 }
317
318 fn len(&self) -> usize {
319 self.entries.len()
320 }
321
322 fn is_empty(&self) -> bool {
323 self.entries.is_empty()
324 }
325
326 fn set_max_len(&mut self, len: usize) -> Result<()> {
327 self.max_len = len;
328 if self.len() > len {
329 self.entries.drain(..self.len() - len);
330 }
331 Ok(())
332 }
333
334 fn ignore_dups(&mut self, yes: bool) -> Result<()> {
335 self.ignore_dups = yes;
336 Ok(())
337 }
338
339 fn ignore_space(&mut self, yes: bool) {
340 self.ignore_space = yes;
341 }
342
343 fn save(&mut self, _: &Path) -> Result<()> {
344 unimplemented!();
345 }
346
347 fn append(&mut self, _: &Path) -> Result<()> {
348 unimplemented!();
349 }
350
351 fn load(&mut self, _: &Path) -> Result<()> {
352 unimplemented!();
353 }
354
355 fn clear(&mut self) -> Result<()> {
356 self.entries.clear();
357 Ok(())
358 }
359
360 fn search(
361 &self,
362 term: &str,
363 start: usize,
364 dir: SearchDirection,
365 ) -> Result<Option<SearchResult<'_>>> {
366 #[cfg(not(feature = "case_insensitive_history_search"))]
367 {
368 let test = |entry: &str| entry.find(term);
369 Ok(self.search_match(term, start, dir, test))
370 }
371 #[cfg(feature = "case_insensitive_history_search")]
372 {
373 use regex::{escape, RegexBuilder};
374 Ok(
375 if let Ok(re) = RegexBuilder::new(&escape(term))
376 .case_insensitive(true)
377 .build()
378 {
379 let test = |entry: &str| re.find(entry).map(|m| m.start());
380 self.search_match(term, start, dir, test)
381 } else {
382 None
383 },
384 )
385 }
386 }
387
388 fn starts_with(
389 &self,
390 term: &str,
391 start: usize,
392 dir: SearchDirection,
393 ) -> Result<Option<SearchResult<'_>>> {
394 #[cfg(not(feature = "case_insensitive_history_search"))]
395 {
396 let test = |entry: &str| {
397 if entry.starts_with(term) {
398 Some(term.len())
399 } else {
400 None
401 }
402 };
403 Ok(self.search_match(term, start, dir, test))
404 }
405 #[cfg(feature = "case_insensitive_history_search")]
406 {
407 use regex::{escape, RegexBuilder};
408 Ok(
409 if let Ok(re) = RegexBuilder::new(&escape(term))
410 .case_insensitive(true)
411 .build()
412 {
413 let test = |entry: &str| {
414 re.find(entry)
415 .and_then(|m| if m.start() == 0 { Some(m) } else { None })
416 .map(|m| m.end())
417 };
418 self.search_match(term, start, dir, test)
419 } else {
420 None
421 },
422 )
423 }
424 }
425}
426
427impl Index<usize> for MemHistory {
428 type Output = String;
429
430 fn index(&self, index: usize) -> &String {
431 &self.entries[index]
432 }
433}
434
435impl<'a> IntoIterator for &'a MemHistory {
436 type IntoIter = vec_deque::Iter<'a, String>;
437 type Item = &'a String;
438
439 fn into_iter(self) -> Self::IntoIter {
440 self.entries.iter()
441 }
442}
443
444#[derive(Default)]
446#[cfg(feature = "with-file-history")]
447pub struct FileHistory {
448 mem: MemHistory,
449 new_entries: usize,
451 path_info: Option<PathInfo>,
453}
454
455#[cfg(feature = "with-file-history")]
459struct PathInfo(std::path::PathBuf, SystemTime, usize);
460
461#[cfg(feature = "with-file-history")]
462impl FileHistory {
463 const FILE_VERSION_V2: &'static str = "#V2";
466
467 #[must_use]
469 pub fn new() -> Self {
470 Self::with_config(&Config::default())
471 }
472
473 #[must_use]
478 pub fn with_config(config: &Config) -> Self {
479 Self {
480 mem: MemHistory::with_config(config),
481 new_entries: 0,
482 path_info: None,
483 }
484 }
485
486 fn save_to(&mut self, file: &File, append: bool) -> Result<()> {
487 use std::io::{BufWriter, Write as _};
488
489 fix_perm(file);
490 let mut wtr = BufWriter::new(file);
491 let first_new_entry = if append {
492 self.mem.len().saturating_sub(self.new_entries)
493 } else {
494 wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
495 wtr.write_all(b"\n")?;
496 0
497 };
498 for entry in self.mem.entries.iter().skip(first_new_entry) {
499 let mut bytes = entry.as_bytes();
500 while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
501 let (head, tail) = bytes.split_at(i);
502 wtr.write_all(head)?;
503
504 let (&escapable_byte, tail) = tail
505 .split_first()
506 .expect("memchr guarantees i is a valid index");
507 if escapable_byte == b'\n' {
508 wtr.write_all(br"\n")?; } else {
510 debug_assert_eq!(escapable_byte, b'\\');
511 wtr.write_all(br"\\")?; }
513 bytes = tail;
514 }
515 wtr.write_all(bytes)?; wtr.write_all(b"\n")?;
517 }
518 wtr.flush()?;
520 Ok(())
521 }
522
523 fn load_from(&mut self, file: &File) -> Result<bool> {
524 use std::io::{BufRead as _, BufReader};
525
526 let rdr = BufReader::new(file);
527 let mut lines = rdr.lines();
528 let mut v2 = false;
529 if let Some(first) = lines.next() {
530 let line = first?;
531 if line == Self::FILE_VERSION_V2 {
532 v2 = true;
533 } else {
534 self.add_owned(line)?;
535 }
536 }
537 let mut appendable = v2;
538 for line in lines {
539 let mut line = line?;
540 if line.is_empty() {
541 continue;
542 }
543 if v2 {
544 let mut copy = None; let mut str = line.as_str();
546 while let Some(i) = str.find('\\') {
547 if copy.is_none() {
548 copy = Some(String::with_capacity(line.len()));
549 }
550 let s = copy.as_mut().unwrap();
551 s.push_str(&str[..i]);
552 let j = i + 1; let b = if j < str.len() {
554 str.as_bytes()[j]
555 } else {
556 0 };
558 match b {
559 b'n' => {
560 s.push('\n'); }
562 b'\\' => {
563 s.push('\\'); }
565 _ => {
566 warn!(target: "rustyline", "bad escaped line: {line}");
568 copy = None;
569 break;
570 }
571 }
572 str = &str[j + 1..];
573 }
574 if let Some(mut s) = copy {
575 s.push_str(str); line = s;
577 }
578 }
579 appendable &= self.add_owned(line)?; }
581 self.new_entries = 0; Ok(appendable)
583 }
584
585 fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> {
586 let modified = file.metadata()?.modified()?;
587 if let Some(PathInfo(
588 ref mut previous_path,
589 ref mut previous_modified,
590 ref mut previous_size,
591 )) = self.path_info
592 {
593 if previous_path.as_path() != path {
594 path.clone_into(previous_path);
595 }
596 *previous_modified = modified;
597 *previous_size = size;
598 } else {
599 self.path_info = Some(PathInfo(path.to_owned(), modified, size));
600 }
601 debug!(target: "rustyline", "PathInfo({path:?}, {modified:?}, {size})");
602 Ok(())
603 }
604
605 fn can_just_append(&self, path: &Path, file: &File) -> Result<bool> {
606 if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) =
607 self.path_info
608 {
609 if previous_path.as_path() != path {
610 debug!(target: "rustyline", "cannot append: {previous_path:?} <> {path:?}");
611 return Ok(false);
612 }
613 let modified = file.metadata()?.modified()?;
614 if *previous_modified != modified
615 || self.mem.max_len <= *previous_size
616 || self.mem.max_len < (*previous_size).saturating_add(self.new_entries)
617 {
618 debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}",
619 previous_modified, modified, self.mem.max_len, previous_size, self.new_entries);
620 Ok(false)
621 } else {
622 Ok(true)
623 }
624 } else {
625 Ok(false)
626 }
627 }
628
629 #[must_use]
631 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &String> + '_ {
632 self.mem.entries.iter()
633 }
634}
635
636#[cfg(not(feature = "with-file-history"))]
638pub type DefaultHistory = MemHistory;
639#[cfg(feature = "with-file-history")]
641pub type DefaultHistory = FileHistory;
642
643#[cfg(feature = "with-file-history")]
644impl History for FileHistory {
645 fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult<'_>>> {
646 self.mem.get(index, dir)
647 }
648
649 fn add(&mut self, line: &str) -> Result<bool> {
650 if self.mem.add(line)? {
651 self.new_entries = self.new_entries.saturating_add(1).min(self.len());
652 Ok(true)
653 } else {
654 Ok(false)
655 }
656 }
657
658 fn add_owned(&mut self, line: String) -> Result<bool> {
659 if self.mem.add_owned(line)? {
660 self.new_entries = self.new_entries.saturating_add(1).min(self.len());
661 Ok(true)
662 } else {
663 Ok(false)
664 }
665 }
666
667 fn len(&self) -> usize {
668 self.mem.len()
669 }
670
671 fn is_empty(&self) -> bool {
672 self.mem.is_empty()
673 }
674
675 fn set_max_len(&mut self, len: usize) -> Result<()> {
676 self.mem.set_max_len(len)?;
677 self.new_entries = self.new_entries.min(len);
678 Ok(())
679 }
680
681 fn ignore_dups(&mut self, yes: bool) -> Result<()> {
682 self.mem.ignore_dups(yes)
683 }
684
685 fn ignore_space(&mut self, yes: bool) {
686 self.mem.ignore_space(yes);
687 }
688
689 fn save(&mut self, path: &Path) -> Result<()> {
690 if self.is_empty() || self.new_entries == 0 {
691 return Ok(());
692 }
693 let old_umask = umask();
694 let f = File::create(path);
695 restore_umask(old_umask);
696 let file = f?;
697 file.lock()?;
698 self.save_to(&file, false)?;
699 self.new_entries = 0;
700 self.update_path(path, &file, self.len())
701 }
702
703 fn append(&mut self, path: &Path) -> Result<()> {
704 use std::io::Seek as _;
705
706 if self.is_empty() || self.new_entries == 0 {
707 return Ok(());
708 }
709 if !path.exists() || self.new_entries == self.mem.max_len {
710 return self.save(path);
711 }
712 let mut file = OpenOptions::new().write(true).read(true).open(path)?;
713 file.lock()?;
714 if self.can_just_append(path, &file)? {
715 file.seek(SeekFrom::End(0))?;
716 self.save_to(&file, true)?;
717 let size = self
718 .path_info
719 .as_ref()
720 .unwrap()
721 .2
722 .saturating_add(self.new_entries);
723 self.new_entries = 0;
724 return self.update_path(path, &file, size);
725 }
726 let mut other = Self {
728 mem: MemHistory {
729 entries: VecDeque::new(),
730 max_len: self.mem.max_len,
731 ignore_space: self.mem.ignore_space,
732 ignore_dups: self.mem.ignore_dups,
733 },
734 new_entries: 0,
735 path_info: None,
736 };
737 other.load_from(&file)?;
738 let first_new_entry = self.mem.len().saturating_sub(self.new_entries);
739 for entry in self.mem.entries.iter().skip(first_new_entry) {
740 other.add(entry)?;
741 }
742 file.seek(SeekFrom::Start(0))?;
743 file.set_len(0)?; other.save_to(&file, false)?;
745 self.update_path(path, &file, other.len())?;
746 self.new_entries = 0;
747 Ok(())
748 }
749
750 fn load(&mut self, path: &Path) -> Result<()> {
751 let file = File::open(path)?;
752 file.lock_shared()?;
753 let len = self.len();
754 if self.load_from(&file)? {
755 self.update_path(path, &file, self.len() - len)
756 } else {
757 self.path_info = None;
759 Ok(())
760 }
761 }
762
763 fn clear(&mut self) -> Result<()> {
764 self.mem.clear()?;
765 self.new_entries = 0;
766 Ok(())
767 }
768
769 fn search(
770 &self,
771 term: &str,
772 start: usize,
773 dir: SearchDirection,
774 ) -> Result<Option<SearchResult<'_>>> {
775 self.mem.search(term, start, dir)
776 }
777
778 fn starts_with(
779 &self,
780 term: &str,
781 start: usize,
782 dir: SearchDirection,
783 ) -> Result<Option<SearchResult<'_>>> {
784 self.mem.starts_with(term, start, dir)
785 }
786}
787
788#[cfg(feature = "with-file-history")]
789impl Index<usize> for FileHistory {
790 type Output = String;
791
792 fn index(&self, index: usize) -> &String {
793 &self.mem.entries[index]
794 }
795}
796
797#[cfg(feature = "with-file-history")]
798impl<'a> IntoIterator for &'a FileHistory {
799 type IntoIter = vec_deque::Iter<'a, String>;
800 type Item = &'a String;
801
802 fn into_iter(self) -> Self::IntoIter {
803 self.mem.entries.iter()
804 }
805}
806
807#[cfg(feature = "with-file-history")]
808cfg_if::cfg_if! {
809 if #[cfg(any(windows, target_arch = "wasm32"))] {
810 fn umask() -> u16 {
811 0
812 }
813
814 fn restore_umask(_: u16) {}
815
816 fn fix_perm(_: &File) {}
817 } else if #[cfg(unix)] {
818 use nix::sys::stat::{self, Mode, fchmod};
819 fn umask() -> Mode {
820 stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO)
821 }
822
823 fn restore_umask(old_umask: Mode) {
824 stat::umask(old_umask);
825 }
826
827 fn fix_perm(file: &File) {
828 let _ = fchmod(file, Mode::S_IRUSR | Mode::S_IWUSR);
829 }
830 }
831}
832
833#[cfg(test)]
834mod tests {
835 use super::{DefaultHistory, History as _, 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 as _;
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}