mirror of
https://github.com/nushell/reedline.git
synced 2024-11-09 13:09:10 +03:00
Support history file (#44)
* Support history file History has a new constructor to remember a file to read from/ write to. Writing is deferred to the Drop. Binary supports history file via `REEDLINE_HISTFILE` env variable Additional changes: Inverted the ordering of the history VecDeque to simplify file IO and history internals. This makes the search code slightly more brittle. * Clarify history API * Harmonize return type of iterators
This commit is contained in:
parent
6c8ca05b4c
commit
32c9ac0c25
@ -90,8 +90,19 @@ impl Reedline {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_history(history: History) -> Self {
|
||||
let mut rl = Reedline::new();
|
||||
rl.history = history;
|
||||
rl
|
||||
}
|
||||
|
||||
pub fn print_history(&mut self) -> Result<()> {
|
||||
let history: Vec<_> = self.history.iter().rev().cloned().enumerate().collect();
|
||||
let history: Vec<_> = self
|
||||
.history
|
||||
.iter_chronologic()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.collect();
|
||||
|
||||
for (i, entry) in history {
|
||||
self.print_line(&format!("{}\t{}", i + 1, entry))?;
|
||||
@ -542,7 +553,7 @@ impl Reedline {
|
||||
|
||||
match search.result {
|
||||
Some((history_index, offset)) => {
|
||||
let history_result = &self.history[history_index];
|
||||
let history_result = self.history.get_nth_newest(history_index).unwrap();
|
||||
|
||||
self.stdout.queue(Print(&history_result[..offset]))?;
|
||||
self.stdout.queue(SavePosition)?;
|
||||
@ -732,8 +743,9 @@ impl Reedline {
|
||||
Some(search) => {
|
||||
self.queue_prompt_indicator()?;
|
||||
if let Some((history_index, _)) = search.result {
|
||||
self.line_buffer
|
||||
.set_buffer(self.history[history_index].clone());
|
||||
self.line_buffer.set_buffer(
|
||||
self.history.get_nth_newest(history_index).unwrap().clone(),
|
||||
);
|
||||
}
|
||||
self.history_search = None;
|
||||
}
|
||||
|
179
src/history.rs
179
src/history.rs
@ -1,13 +1,25 @@
|
||||
use std::{collections::VecDeque, ops::Index};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::BufReader;
|
||||
use std::io::{BufRead, BufWriter, Write};
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
|
||||
/// Default size of the `History`
|
||||
const HISTORY_SIZE: usize = 100;
|
||||
pub const HISTORY_SIZE: usize = 100;
|
||||
|
||||
/// Stateful history that allows up/down-arrow browsing with an internal cursor
|
||||
/// Stateful history that allows up/down-arrow browsing with an internal cursor.
|
||||
///
|
||||
/// Can optionally be associated with a newline separated history file.
|
||||
/// Similar to bash's behavior without HISTTIMEFORMAT.
|
||||
/// (See https://www.gnu.org/software/bash/manual/html_node/Bash-History-Facilities.html)
|
||||
/// All new changes within a certain History capacity will be written to disk when History is dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct History {
|
||||
capacity: usize,
|
||||
entries: VecDeque<String>,
|
||||
cursor: isize, // -1 not browsing through history, >= 0 index into history
|
||||
cursor: usize, // If cursor == entries.len() outside history browsing
|
||||
file: Option<PathBuf>,
|
||||
len_on_disk: usize, // Keep track what was previously written to disk
|
||||
truncate_file: bool, // as long as the file would not exceed capacity we can use appending writes
|
||||
}
|
||||
|
||||
impl Default for History {
|
||||
@ -17,18 +29,105 @@ impl Default for History {
|
||||
}
|
||||
|
||||
impl History {
|
||||
/// Creates an in-memory history that remembers `<= capacity` elements
|
||||
/// Creates a new in-memory history that remembers `<= capacity` elements
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
if capacity > isize::MAX as usize {
|
||||
if capacity == usize::MAX {
|
||||
panic!("History capacity too large to be addressed safely");
|
||||
}
|
||||
History {
|
||||
capacity,
|
||||
entries: VecDeque::with_capacity(capacity),
|
||||
cursor: -1,
|
||||
cursor: 0,
|
||||
file: None,
|
||||
len_on_disk: 0,
|
||||
truncate_file: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new history with an associated history file.
|
||||
///
|
||||
/// History file format: commands separated by new lines.
|
||||
/// If file exists file will be read otherwise empty file will be created.
|
||||
///
|
||||
/// Side effects: creates all nested directories to the file
|
||||
pub fn with_file(capacity: usize, file: PathBuf) -> std::io::Result<Self> {
|
||||
let mut hist = Self::new(capacity);
|
||||
if let Some(base_dir) = file.parent() {
|
||||
std::fs::create_dir_all(base_dir)?;
|
||||
}
|
||||
hist.file = Some(file);
|
||||
hist.load_file()?;
|
||||
Ok(hist)
|
||||
}
|
||||
|
||||
/// Loads history from the associated newline separated file
|
||||
///
|
||||
/// Expects the `History` to be empty.
|
||||
///
|
||||
/// Side effect: create/touch not yet existing file.
|
||||
fn load_file(&mut self) -> std::io::Result<()> {
|
||||
let f = File::open(
|
||||
self.file
|
||||
.as_ref()
|
||||
.expect("History::load_file should only be called if a filename is set"),
|
||||
);
|
||||
assert!(
|
||||
self.entries.is_empty(),
|
||||
"History currently designed to load file once in the constructor"
|
||||
);
|
||||
match f {
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
File::create(self.file.as_ref().unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(e),
|
||||
},
|
||||
Ok(file) => {
|
||||
let reader = BufReader::new(file);
|
||||
let mut from_file: VecDeque<String> = reader.lines().map(|s| s.unwrap()).collect();
|
||||
let from_file = if from_file.len() > self.capacity {
|
||||
from_file.split_off(from_file.len() - self.capacity)
|
||||
} else {
|
||||
from_file
|
||||
};
|
||||
self.len_on_disk = from_file.len();
|
||||
self.entries = from_file;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes unwritten history contents to disk.
|
||||
/// If file would exceed `capacity` truncates the oldest entries.
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
if self.file.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let file = if self.truncate_file {
|
||||
// Rewrite the whole file if we truncated the old output
|
||||
self.len_on_disk = 0;
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.open(self.file.as_ref().unwrap())?
|
||||
} else {
|
||||
// If the file is not beyond capacity just append new stuff
|
||||
// (use the stored self.len_on_disk as offset)
|
||||
OpenOptions::new()
|
||||
.append(true)
|
||||
.open(self.file.as_ref().unwrap())?
|
||||
};
|
||||
let mut writer = BufWriter::new(file);
|
||||
for line in self.entries.range(self.len_on_disk..) {
|
||||
writer.write_all(line.as_bytes())?;
|
||||
writer.write_all("\n".as_bytes())?;
|
||||
}
|
||||
writer.flush()?;
|
||||
self.len_on_disk = self.entries.len();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Access the underlying entries (exported for possible fancy access to underlying VecDeque)
|
||||
#[allow(dead_code)]
|
||||
pub fn entries(&self) -> &VecDeque<String> {
|
||||
@ -37,31 +136,38 @@ impl History {
|
||||
|
||||
/// Append an entry if non-empty and not repetition of the previous entry
|
||||
pub fn append(&mut self, entry: String) {
|
||||
if self.entries.len() + 1 == self.capacity {
|
||||
// History is "full", so we delete the oldest entry first,
|
||||
// before adding a new one.
|
||||
self.entries.pop_back();
|
||||
}
|
||||
// Don't append if the preceding value is identical or the string empty
|
||||
if self
|
||||
.entries
|
||||
.front()
|
||||
.back()
|
||||
.map_or(true, |previous| previous != &entry)
|
||||
&& !entry.is_empty()
|
||||
{
|
||||
self.entries.push_front(entry);
|
||||
if self.entries.len() == self.capacity {
|
||||
// History is "full", so we delete the oldest entry first,
|
||||
// before adding a new one.
|
||||
self.entries.pop_front();
|
||||
self.cursor = self.cursor.saturating_sub(1);
|
||||
self.len_on_disk = self.len_on_disk.saturating_sub(1);
|
||||
self.truncate_file = true;
|
||||
}
|
||||
// Keep the cursor meaning consistent if no call to `reset_cursor()` is done by the consumer
|
||||
if self.cursor == self.entries.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
self.entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the internal browsing cursor
|
||||
pub fn reset_cursor(&mut self) {
|
||||
self.cursor = -1
|
||||
self.cursor = self.entries.len()
|
||||
}
|
||||
|
||||
/// Try to move back in history. Returns `None` if history is exhausted.
|
||||
pub fn go_back(&mut self) -> Option<&str> {
|
||||
if self.cursor < (self.entries.len() as isize - 1) {
|
||||
self.cursor += 1;
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
Some(self.entries.get(self.cursor as usize).unwrap())
|
||||
} else {
|
||||
None
|
||||
@ -70,27 +176,46 @@ impl History {
|
||||
|
||||
/// Try to move forward in history. Returns `None` if history is exhausted (moving beyond most recent element).
|
||||
pub fn go_forward(&mut self) -> Option<&str> {
|
||||
if self.cursor >= 0 {
|
||||
self.cursor -= 1;
|
||||
if self.cursor < self.entries.len() {
|
||||
self.cursor += 1;
|
||||
}
|
||||
if self.cursor >= 0 {
|
||||
if self.cursor < self.entries.len() {
|
||||
Some(self.entries.get(self.cursor as usize).unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields iterator to immutable references from the underlying data structure
|
||||
pub fn iter(&self) -> std::collections::vec_deque::Iter<'_, String> {
|
||||
/// Yields iterator to immutable references from the underlying data structure.
|
||||
/// Order: Oldest entries first.
|
||||
pub fn iter_chronologic(
|
||||
&self,
|
||||
) -> impl Iterator<Item = &String> + DoubleEndedIterator + ExactSizeIterator + '_ {
|
||||
self.entries.iter()
|
||||
}
|
||||
|
||||
/// Yields iterator to immutable references from the underlying data structure.
|
||||
/// Order: Most recent entries first.
|
||||
pub fn iter_recent(
|
||||
&self,
|
||||
) -> impl Iterator<Item = &String> + DoubleEndedIterator + ExactSizeIterator + '_ {
|
||||
self.entries.iter().rev()
|
||||
}
|
||||
|
||||
/// Helper to get items on zero based index starting at the most recent.
|
||||
pub fn get_nth_newest(&self, idx: usize) -> Option<&String> {
|
||||
self.entries.get(self.entries().len() - idx - 1)
|
||||
}
|
||||
|
||||
/// Helper to get items on zero based index starting at the oldest entry.
|
||||
pub fn get_nth_oldest(&self, idx: usize) -> Option<&String> {
|
||||
self.entries.get(idx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<usize> for History {
|
||||
type Output = String;
|
||||
|
||||
fn index(&self, index: usize) -> &Self::Output {
|
||||
&self.entries[index]
|
||||
impl Drop for History {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ impl BasicSearch {
|
||||
self.result = None;
|
||||
} else {
|
||||
self.result = history
|
||||
.iter()
|
||||
.iter_recent()
|
||||
.enumerate()
|
||||
.skip(start)
|
||||
.filter_map(|(history_index, s)| {
|
||||
|
@ -2,6 +2,7 @@ mod engine;
|
||||
pub use engine::{Reedline, Signal};
|
||||
|
||||
mod history;
|
||||
pub use history::{History, HISTORY_SIZE};
|
||||
|
||||
mod history_search;
|
||||
|
||||
|
11
src/main.rs
11
src/main.rs
@ -1,9 +1,16 @@
|
||||
use crossterm::Result;
|
||||
|
||||
use reedline::{Reedline, Signal};
|
||||
use reedline::{History, Reedline, Signal, HISTORY_SIZE};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut line_editor = Reedline::new();
|
||||
let mut line_editor = match std::env::var("REEDLINE_HISTFILE") {
|
||||
Ok(histfile) if !histfile.is_empty() => {
|
||||
// TODO: Allow change of capacity and don't unwrap
|
||||
let history = History::with_file(HISTORY_SIZE, histfile.into()).unwrap();
|
||||
Reedline::with_history(history)
|
||||
}
|
||||
_ => Reedline::new(),
|
||||
};
|
||||
|
||||
// quick command like parameter handling
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
Loading…
Reference in New Issue
Block a user