1#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum SearchDirection {
27 Forward,
29 Reverse,
31}
32
33#[derive(Debug, Clone, Eq, PartialEq)]
35pub struct SearchResult<'a> {
36 pub entry: &'a str,
38 pub idx: usize,
40 pub pos: usize,
42}
43
44#[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 new_entries: usize,
59 #[cfg(not(target_arch = "wasm32"))]
61 path_info: Option<PathInfo>,
62}
63
64#[cfg(not(target_arch = "wasm32"))]
66struct PathInfo(PathBuf, SystemTime, usize);
67
68impl History {
69 #[cfg(not(target_arch = "wasm32"))]
72 const FILE_VERSION_V2: &'static str = "#V2";
73
74 #[must_use]
76 pub fn new() -> Self {
77 Self::with_config(Config::default())
78 }
79
80 #[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 #[must_use]
99 pub fn get(&self, index: usize) -> Option<&String> {
100 self.entries.get(index)
101 }
102
103 #[must_use]
105 pub fn last(&self) -> Option<&String> {
106 self.entries.back()
107 }
108
109 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 #[must_use]
141 pub fn len(&self) -> usize {
142 self.entries.len()
143 }
144
145 #[must_use]
147 pub fn is_empty(&self) -> bool {
148 self.entries.is_empty()
149 }
150
151 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 #[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 #[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")?; } else {
219 debug_assert_eq!(escapable_byte, b'\\');
220 wtr.write_all(br"\\")?; }
222 bytes = tail;
223 }
224 wtr.write_all(bytes)?; wtr.write_all(b"\n")?;
226 }
227 wtr.flush()?;
229 Ok(())
230 }
231
232 #[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 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)?; 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 #[cfg(target_arch = "wasm32")]
286 pub fn append<P: AsRef<Path> + ?Sized>(&mut self, _path: &P) -> Result<()> {
287 todo!();
288 }
289
290 #[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 self.path_info = None;
306 Ok(())
307 }
308 }
309
310 #[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; 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; let b = if j < str.len() {
351 str.as_bytes()[j]
352 } else {
353 0 };
355 match b {
356 b'n' => {
357 s.push('\n'); }
359 b'\\' => {
360 s.push('\\'); }
362 _ => {
363 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); line = s;
374 }
375 }
376 appendable &= self.add(line); }
378 self.new_entries = 0; 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 pub fn clear(&mut self) {
430 self.entries.clear();
431 self.new_entries = 0;
432 }
433
434 #[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 #[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 #[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
573pub 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)] fn save() -> Result<()> {
663 check_save("line\nfour \\ abc")
664 }
665
666 #[test]
667 #[cfg_attr(miri, ignore)] 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)] 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 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)] 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)] 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}