#[cfg(feature = "with-file-history")]
use fd_lock::RwLock;
#[cfg(feature = "with-file-history")]
use log::{debug, warn};
use std::borrow::Cow;
use std::collections::vec_deque;
use std::collections::VecDeque;
#[cfg(feature = "with-file-history")]
use std::fs::{File, OpenOptions};
#[cfg(feature = "with-file-history")]
use std::io::SeekFrom;
use std::ops::Index;
use std::path::Path;
#[cfg(feature = "with-file-history")]
use std::time::SystemTime;
use super::Result;
use crate::config::{Config, HistoryDuplicates};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SearchDirection {
Forward,
Reverse,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SearchResult<'a> {
pub entry: Cow<'a, str>,
pub idx: usize,
pub pos: usize,
}
pub trait History {
fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult>>;
fn add(&mut self, line: &str) -> Result<bool>;
fn add_owned(&mut self, line: String) -> Result<bool>; #[must_use]
fn len(&self) -> usize;
#[must_use]
fn is_empty(&self) -> bool;
fn set_max_len(&mut self, len: usize) -> Result<()>;
fn ignore_dups(&mut self, yes: bool) -> Result<()>;
fn ignore_space(&mut self, yes: bool);
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<()>;
fn search(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>>;
fn starts_with(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>>;
}
pub struct MemHistory {
entries: VecDeque<String>,
max_len: usize,
ignore_space: bool,
ignore_dups: bool,
}
impl MemHistory {
#[must_use]
pub fn new() -> Self {
Self::with_config(Config::default())
}
#[must_use]
pub fn with_config(config: Config) -> Self {
Self {
entries: VecDeque::new(),
max_len: config.max_history_size(),
ignore_space: config.history_ignore_space(),
ignore_dups: config.history_duplicates() == HistoryDuplicates::IgnoreConsecutive,
}
}
fn search_match<F>(
&self,
term: &str,
start: usize,
dir: SearchDirection,
test: F,
) -> Option<SearchResult>
where
F: Fn(&str) -> Option<usize>,
{
if term.is_empty() || start >= self.len() {
return None;
}
match dir {
SearchDirection::Reverse => {
for (idx, entry) in self
.entries
.iter()
.rev()
.skip(self.len() - 1 - start)
.enumerate()
{
if let Some(cursor) = test(entry) {
return Some(SearchResult {
idx: start - idx,
entry: Cow::Borrowed(entry),
pos: cursor,
});
}
}
None
}
SearchDirection::Forward => {
for (idx, entry) in self.entries.iter().skip(start).enumerate() {
if let Some(cursor) = test(entry) {
return Some(SearchResult {
idx: idx + start,
entry: Cow::Borrowed(entry),
pos: cursor,
});
}
}
None
}
}
}
fn ignore(&self, line: &str) -> bool {
if self.max_len == 0 {
return true;
}
if line.is_empty()
|| (self.ignore_space && line.chars().next().map_or(true, char::is_whitespace))
{
return true;
}
if self.ignore_dups {
if let Some(s) = self.entries.back() {
if s == line {
return true;
}
}
}
false
}
fn insert(&mut self, line: String) {
if self.entries.len() == self.max_len {
self.entries.pop_front();
}
self.entries.push_back(line);
}
}
impl Default for MemHistory {
fn default() -> Self {
Self::new()
}
}
impl History for MemHistory {
fn get(&self, index: usize, _: SearchDirection) -> Result<Option<SearchResult>> {
Ok(self
.entries
.get(index)
.map(String::as_ref)
.map(Cow::Borrowed)
.map(|entry| SearchResult {
entry,
idx: index,
pos: 0,
}))
}
fn add(&mut self, line: &str) -> Result<bool> {
if self.ignore(line) {
return Ok(false);
}
self.insert(line.to_owned());
Ok(true)
}
fn add_owned(&mut self, line: String) -> Result<bool> {
if self.ignore(&line) {
return Ok(false);
}
self.insert(line);
Ok(true)
}
fn len(&self) -> usize {
self.entries.len()
}
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn set_max_len(&mut self, len: usize) -> Result<()> {
self.max_len = len;
if self.len() > len {
self.entries.drain(..self.len() - len);
}
Ok(())
}
fn ignore_dups(&mut self, yes: bool) -> Result<()> {
self.ignore_dups = yes;
Ok(())
}
fn ignore_space(&mut self, yes: bool) {
self.ignore_space = yes;
}
fn save(&mut self, _: &Path) -> Result<()> {
unimplemented!();
}
fn append(&mut self, _: &Path) -> Result<()> {
unimplemented!();
}
fn load(&mut self, _: &Path) -> Result<()> {
unimplemented!();
}
fn clear(&mut self) -> Result<()> {
self.entries.clear();
Ok(())
}
fn search(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>> {
#[cfg(not(feature = "case_insensitive_history_search"))]
{
let test = |entry: &str| entry.find(term);
Ok(self.search_match(term, start, dir, test))
}
#[cfg(feature = "case_insensitive_history_search")]
{
use regex::{escape, RegexBuilder};
Ok(
if let Ok(re) = RegexBuilder::new(&escape(term))
.case_insensitive(true)
.build()
{
let test = |entry: &str| re.find(entry).map(|m| m.start());
self.search_match(term, start, dir, test)
} else {
None
},
)
}
}
fn starts_with(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>> {
#[cfg(not(feature = "case_insensitive_history_search"))]
{
let test = |entry: &str| {
if entry.starts_with(term) {
Some(term.len())
} else {
None
}
};
Ok(self.search_match(term, start, dir, test))
}
#[cfg(feature = "case_insensitive_history_search")]
{
use regex::{escape, RegexBuilder};
Ok(
if let Ok(re) = RegexBuilder::new(&escape(term))
.case_insensitive(true)
.build()
{
let test = |entry: &str| {
re.find(entry)
.and_then(|m| if m.start() == 0 { Some(m) } else { None })
.map(|m| m.end())
};
self.search_match(term, start, dir, test)
} else {
None
},
)
}
}
}
impl Index<usize> for MemHistory {
type Output = String;
fn index(&self, index: usize) -> &String {
&self.entries[index]
}
}
impl<'a> IntoIterator for &'a MemHistory {
type IntoIter = vec_deque::Iter<'a, String>;
type Item = &'a String;
fn into_iter(self) -> Self::IntoIter {
self.entries.iter()
}
}
#[derive(Default)]
#[cfg(feature = "with-file-history")]
pub struct FileHistory {
mem: MemHistory,
new_entries: usize,
path_info: Option<PathInfo>,
}
#[cfg(feature = "with-file-history")]
struct PathInfo(std::path::PathBuf, SystemTime, usize);
#[cfg(feature = "with-file-history")]
impl FileHistory {
const FILE_VERSION_V2: &'static str = "#V2";
#[must_use]
pub fn new() -> Self {
Self::with_config(Config::default())
}
#[must_use]
pub fn with_config(config: Config) -> Self {
Self {
mem: MemHistory::with_config(config),
new_entries: 0,
path_info: None,
}
}
fn save_to(&mut self, file: &File, append: bool) -> Result<()> {
use std::io::{BufWriter, Write};
fix_perm(file);
let mut wtr = BufWriter::new(file);
let first_new_entry = if append {
self.mem.len().saturating_sub(self.new_entries)
} else {
wtr.write_all(Self::FILE_VERSION_V2.as_bytes())?;
wtr.write_all(b"\n")?;
0
};
for entry in self.mem.entries.iter().skip(first_new_entry) {
let mut bytes = entry.as_bytes();
while let Some(i) = memchr::memchr2(b'\\', b'\n', bytes) {
let (head, tail) = bytes.split_at(i);
wtr.write_all(head)?;
let (&escapable_byte, tail) = tail
.split_first()
.expect("memchr guarantees i is a valid index");
if escapable_byte == b'\n' {
wtr.write_all(br"\n")?; } else {
debug_assert_eq!(escapable_byte, b'\\');
wtr.write_all(br"\\")?; }
bytes = tail;
}
wtr.write_all(bytes)?; wtr.write_all(b"\n")?;
}
wtr.flush()?;
Ok(())
}
fn load_from(&mut self, file: &File) -> Result<bool> {
use std::io::{BufRead, BufReader};
let rdr = BufReader::new(file);
let mut lines = rdr.lines();
let mut v2 = false;
if let Some(first) = lines.next() {
let line = first?;
if line == Self::FILE_VERSION_V2 {
v2 = true;
} else {
self.add_owned(line)?;
}
}
let mut appendable = v2;
for line in lines {
let mut line = line?;
if line.is_empty() {
continue;
}
if v2 {
let mut copy = None; let mut str = line.as_str();
while let Some(i) = str.find('\\') {
if copy.is_none() {
copy = Some(String::with_capacity(line.len()));
}
let s = copy.as_mut().unwrap();
s.push_str(&str[..i]);
let j = i + 1; let b = if j < str.len() {
str.as_bytes()[j]
} else {
0 };
match b {
b'n' => {
s.push('\n'); }
b'\\' => {
s.push('\\'); }
_ => {
warn!(target: "rustyline", "bad escaped line: {}", line);
copy = None;
break;
}
}
str = &str[j + 1..];
}
if let Some(mut s) = copy {
s.push_str(str); line = s;
}
}
appendable &= self.add_owned(line)?; }
self.new_entries = 0; Ok(appendable)
}
fn update_path(&mut self, path: &Path, file: &File, size: usize) -> Result<()> {
let modified = file.metadata()?.modified()?;
if let Some(PathInfo(
ref mut previous_path,
ref mut previous_modified,
ref mut previous_size,
)) = self.path_info
{
if previous_path.as_path() != path {
path.clone_into(previous_path);
}
*previous_modified = modified;
*previous_size = size;
} else {
self.path_info = Some(PathInfo(path.to_owned(), modified, size));
}
debug!(target: "rustyline", "PathInfo({:?}, {:?}, {})", path, modified, size);
Ok(())
}
fn can_just_append(&self, path: &Path, file: &File) -> Result<bool> {
if let Some(PathInfo(ref previous_path, ref previous_modified, ref previous_size)) =
self.path_info
{
if previous_path.as_path() != path {
debug!(target: "rustyline", "cannot append: {:?} <> {:?}", previous_path, path);
return Ok(false);
}
let modified = file.metadata()?.modified()?;
if *previous_modified != modified
|| self.mem.max_len <= *previous_size
|| self.mem.max_len < (*previous_size).saturating_add(self.new_entries)
{
debug!(target: "rustyline", "cannot append: {:?} < {:?} or {} < {} + {}",
previous_modified, modified, self.mem.max_len, previous_size, self.new_entries);
Ok(false)
} else {
Ok(true)
}
} else {
Ok(false)
}
}
#[must_use]
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &String> + '_ {
self.mem.entries.iter()
}
}
#[cfg(not(feature = "with-file-history"))]
pub type DefaultHistory = MemHistory;
#[cfg(feature = "with-file-history")]
pub type DefaultHistory = FileHistory;
#[cfg(feature = "with-file-history")]
impl History for FileHistory {
fn get(&self, index: usize, dir: SearchDirection) -> Result<Option<SearchResult>> {
self.mem.get(index, dir)
}
fn add(&mut self, line: &str) -> Result<bool> {
if self.mem.add(line)? {
self.new_entries = self.new_entries.saturating_add(1).min(self.len());
Ok(true)
} else {
Ok(false)
}
}
fn add_owned(&mut self, line: String) -> Result<bool> {
if self.mem.add_owned(line)? {
self.new_entries = self.new_entries.saturating_add(1).min(self.len());
Ok(true)
} else {
Ok(false)
}
}
fn len(&self) -> usize {
self.mem.len()
}
fn is_empty(&self) -> bool {
self.mem.is_empty()
}
fn set_max_len(&mut self, len: usize) -> Result<()> {
self.mem.set_max_len(len)?;
self.new_entries = self.new_entries.min(len);
Ok(())
}
fn ignore_dups(&mut self, yes: bool) -> Result<()> {
self.mem.ignore_dups(yes)
}
fn ignore_space(&mut self, yes: bool) {
self.mem.ignore_space(yes);
}
fn save(&mut self, path: &Path) -> Result<()> {
if self.is_empty() || self.new_entries == 0 {
return Ok(());
}
let old_umask = umask();
let f = File::create(path);
restore_umask(old_umask);
let file = f?;
let mut lock = RwLock::new(file);
let lock_guard = lock.write()?;
self.save_to(&lock_guard, false)?;
self.new_entries = 0;
self.update_path(path, &lock_guard, self.len())
}
fn append(&mut self, path: &Path) -> Result<()> {
use std::io::Seek;
if self.is_empty() || self.new_entries == 0 {
return Ok(());
}
if !path.exists() || self.new_entries == self.mem.max_len {
return self.save(path);
}
let file = OpenOptions::new().write(true).read(true).open(path)?;
let mut lock = RwLock::new(file);
let mut lock_guard = lock.write()?;
if self.can_just_append(path, &lock_guard)? {
lock_guard.seek(SeekFrom::End(0))?;
self.save_to(&lock_guard, true)?;
let size = self
.path_info
.as_ref()
.unwrap()
.2
.saturating_add(self.new_entries);
self.new_entries = 0;
return self.update_path(path, &lock_guard, size);
}
let mut other = Self {
mem: MemHistory {
entries: VecDeque::new(),
max_len: self.mem.max_len,
ignore_space: self.mem.ignore_space,
ignore_dups: self.mem.ignore_dups,
},
new_entries: 0,
path_info: None,
};
other.load_from(&lock_guard)?;
let first_new_entry = self.mem.len().saturating_sub(self.new_entries);
for entry in self.mem.entries.iter().skip(first_new_entry) {
other.add(entry)?;
}
lock_guard.seek(SeekFrom::Start(0))?;
lock_guard.set_len(0)?; other.save_to(&lock_guard, false)?;
self.update_path(path, &lock_guard, other.len())?;
self.new_entries = 0;
Ok(())
}
fn load(&mut self, path: &Path) -> Result<()> {
let file = File::open(path)?;
let lock = RwLock::new(file);
let lock_guard = lock.read()?;
let len = self.len();
if self.load_from(&lock_guard)? {
self.update_path(path, &lock_guard, self.len() - len)
} else {
self.path_info = None;
Ok(())
}
}
fn clear(&mut self) -> Result<()> {
self.mem.clear()?;
self.new_entries = 0;
Ok(())
}
fn search(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>> {
self.mem.search(term, start, dir)
}
fn starts_with(
&self,
term: &str,
start: usize,
dir: SearchDirection,
) -> Result<Option<SearchResult>> {
self.mem.starts_with(term, start, dir)
}
}
#[cfg(feature = "with-file-history")]
impl Index<usize> for FileHistory {
type Output = String;
fn index(&self, index: usize) -> &String {
&self.mem.entries[index]
}
}
#[cfg(feature = "with-file-history")]
impl<'a> IntoIterator for &'a FileHistory {
type IntoIter = vec_deque::Iter<'a, String>;
type Item = &'a String;
fn into_iter(self) -> Self::IntoIter {
self.mem.entries.iter()
}
}
#[cfg(feature = "with-file-history")]
cfg_if::cfg_if! {
if #[cfg(any(windows, target_arch = "wasm32"))] {
fn umask() -> u16 {
0
}
fn restore_umask(_: u16) {}
fn fix_perm(_: &File) {}
} else if #[cfg(unix)] {
use nix::sys::stat::{self, Mode, fchmod};
fn umask() -> Mode {
stat::umask(Mode::S_IXUSR | Mode::S_IRWXG | Mode::S_IRWXO)
}
fn restore_umask(old_umask: Mode) {
stat::umask(old_umask);
}
fn fix_perm(file: &File) {
use std::os::unix::io::AsRawFd;
let _ = fchmod(file.as_raw_fd(), Mode::S_IRUSR | Mode::S_IWUSR);
}
}
}
#[cfg(test)]
mod tests {
use super::{DefaultHistory, History, SearchDirection, SearchResult};
use crate::config::Config;
use crate::Result;
fn init() -> DefaultHistory {
let mut history = DefaultHistory::new();
assert!(history.add("line1").unwrap());
assert!(history.add("line2").unwrap());
assert!(history.add("line3").unwrap());
history
}
#[test]
fn new() {
let history = DefaultHistory::new();
assert_eq!(0, history.len());
}
#[test]
fn add() {
let config = Config::builder().history_ignore_space(true).build();
let mut history = DefaultHistory::with_config(config);
#[cfg(feature = "with-file-history")]
assert_eq!(config.max_history_size(), history.mem.max_len);
assert!(history.add("line1").unwrap());
assert!(history.add("line2").unwrap());
assert!(!history.add("line2").unwrap());
assert!(!history.add("").unwrap());
assert!(!history.add(" line3").unwrap());
}
#[test]
fn set_max_len() {
let mut history = init();
history.set_max_len(1).unwrap();
assert_eq!(1, history.len());
assert_eq!(Some(&"line3".to_owned()), history.into_iter().last());
}
#[test]
#[cfg(feature = "with-file-history")]
#[cfg_attr(miri, ignore)] fn save() -> Result<()> {
check_save("line\nfour \\ abc")
}
#[test]
#[cfg(feature = "with-file-history")]
#[cfg_attr(miri, ignore)] fn save_windows_path() -> Result<()> {
let path = "cd source\\repos\\forks\\nushell\\";
check_save(path)
}
#[cfg(feature = "with-file-history")]
fn check_save(line: &str) -> Result<()> {
let mut history = init();
assert!(history.add(line)?);
let tf = tempfile::NamedTempFile::new()?;
history.save(tf.path())?;
let mut history2 = DefaultHistory::new();
history2.load(tf.path())?;
for (a, b) in history.iter().zip(history2.iter()) {
assert_eq!(a, b);
}
tf.close()?;
Ok(())
}
#[test]
#[cfg(feature = "with-file-history")]
#[cfg_attr(miri, ignore)] fn load_legacy() -> Result<()> {
use std::io::Write;
let tf = tempfile::NamedTempFile::new()?;
{
let mut legacy = std::fs::File::create(tf.path())?;
let data = b"\
test\\n \\abc \\123\n\
123\\n\\\\n\n\
abcde
";
legacy.write_all(data)?;
legacy.flush()?;
}
let mut history = DefaultHistory::new();
history.load(tf.path())?;
assert_eq!(history[0], "test\\n \\abc \\123");
assert_eq!(history[1], "123\\n\\\\n");
assert_eq!(history[2], "abcde");
tf.close()?;
Ok(())
}
#[test]
#[cfg(feature = "with-file-history")]
#[cfg_attr(miri, ignore)] fn append() -> Result<()> {
let mut history = init();
let tf = tempfile::NamedTempFile::new()?;
history.append(tf.path())?;
let mut history2 = DefaultHistory::new();
history2.load(tf.path())?;
history2.add("line4")?;
history2.append(tf.path())?;
history.add("line5")?;
history.append(tf.path())?;
let mut history3 = DefaultHistory::new();
history3.load(tf.path())?;
assert_eq!(history3.len(), 5);
tf.close()?;
Ok(())
}
#[test]
#[cfg(feature = "with-file-history")]
#[cfg_attr(miri, ignore)] fn truncate() -> Result<()> {
let tf = tempfile::NamedTempFile::new()?;
let config = Config::builder().history_ignore_dups(false)?.build();
let mut history = DefaultHistory::with_config(config);
history.add("line1")?;
history.add("line1")?;
history.append(tf.path())?;
let mut history = DefaultHistory::new();
history.load(tf.path())?;
history.add("l")?;
history.append(tf.path())?;
let mut history = DefaultHistory::new();
history.load(tf.path())?;
assert_eq!(history.len(), 2);
assert_eq!(history[1], "l");
tf.close()?;
Ok(())
}
#[test]
fn search() -> Result<()> {
let history = init();
assert_eq!(None, history.search("", 0, SearchDirection::Forward)?);
assert_eq!(None, history.search("none", 0, SearchDirection::Forward)?);
assert_eq!(None, history.search("line", 3, SearchDirection::Forward)?);
assert_eq!(
Some(SearchResult {
idx: 0,
entry: history.get(0, SearchDirection::Forward)?.unwrap().entry,
pos: 0
}),
history.search("line", 0, SearchDirection::Forward)?
);
assert_eq!(
Some(SearchResult {
idx: 1,
entry: history.get(1, SearchDirection::Forward)?.unwrap().entry,
pos: 0
}),
history.search("line", 1, SearchDirection::Forward)?
);
assert_eq!(
Some(SearchResult {
idx: 2,
entry: history.get(2, SearchDirection::Forward)?.unwrap().entry,
pos: 0
}),
history.search("line3", 1, SearchDirection::Forward)?
);
Ok(())
}
#[test]
fn reverse_search() -> Result<()> {
let history = init();
assert_eq!(None, history.search("", 2, SearchDirection::Reverse)?);
assert_eq!(None, history.search("none", 2, SearchDirection::Reverse)?);
assert_eq!(None, history.search("line", 3, SearchDirection::Reverse)?);
assert_eq!(
Some(SearchResult {
idx: 2,
entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
pos: 0
}),
history.search("line", 2, SearchDirection::Reverse)?
);
assert_eq!(
Some(SearchResult {
idx: 1,
entry: history.get(1, SearchDirection::Reverse)?.unwrap().entry,
pos: 0
}),
history.search("line", 1, SearchDirection::Reverse)?
);
assert_eq!(
Some(SearchResult {
idx: 0,
entry: history.get(0, SearchDirection::Reverse)?.unwrap().entry,
pos: 0
}),
history.search("line1", 1, SearchDirection::Reverse)?
);
Ok(())
}
#[test]
#[cfg(feature = "case_insensitive_history_search")]
fn anchored_search() -> Result<()> {
let history = init();
assert_eq!(
Some(SearchResult {
idx: 2,
entry: history.get(2, SearchDirection::Reverse)?.unwrap().entry,
pos: 4
}),
history.starts_with("LiNe", 2, SearchDirection::Reverse)?
);
assert_eq!(
None,
history.starts_with("iNe", 2, SearchDirection::Reverse)?
);
Ok(())
}
}