History filter (#566)

* add `HistoryFilter` and use it in engine, to allow not storing items with a given prefix

* use `with_history_exclusion_prefix` in demo

* review

* impl history filter on engine

* keep 1 filterered history item

* don't impl History on Box<T: History>
This commit is contained in:
samlich 2023-05-03 21:25:42 +00:00 committed by GitHub
parent 65c4e7a419
commit 97f754425a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 100 additions and 17 deletions

View File

@ -78,6 +78,7 @@ fn main() -> Result<()> {
let mut line_editor = Reedline::create()
.with_history_session_id(history_session_id)
.with_history(history)
.with_history_exclusion_prefix(Some(" ".to_string()))
.with_completer(completer)
.with_quick_completions(true)
.with_partial_completions(true)

View File

@ -99,6 +99,9 @@ pub struct Reedline {
history_session_id: Option<HistorySessionId>,
// none if history doesn't support this
history_last_run_id: Option<HistoryItemId>,
history_exclusion_prefix: Option<String>,
history_excluded_item: Option<HistoryItem>,
history_cursor_on_excluded: bool,
input_mode: InputMode,
// Validator
@ -166,6 +169,8 @@ impl Drop for Reedline {
}
impl Reedline {
const FILTERED_ITEM_ID: HistoryItemId = HistoryItemId(i64::MAX);
/// Create a new [`Reedline`] engine with a local [`History`] that is not synchronized to a file.
#[must_use]
pub fn create() -> Self {
@ -187,6 +192,9 @@ impl Reedline {
),
history_session_id: hist_session_id,
history_last_run_id: None,
history_exclusion_prefix: None,
history_excluded_item: None,
history_cursor_on_excluded: false,
input_mode: InputMode::Regular,
painter,
edit_mode,
@ -357,6 +365,27 @@ impl Reedline {
self
}
/// A builder which configures history exclusion for your instance of the Reedline engine
/// # Example
/// ```rust,no_run
/// // Create a reedline instance with history that will *not* include commands starting with a space
///
/// use reedline::{FileBackedHistory, Reedline};
///
/// let history = Box::new(
/// FileBackedHistory::with_file(5, "history.txt".into())
/// .expect("Error configuring history with file"),
/// );
/// let mut line_editor = Reedline::create()
/// .with_history(history)
/// .with_history_exclusion_prefix(Some(" ".into()));
/// ```
#[must_use]
pub fn with_history_exclusion_prefix(mut self, ignore_prefix: Option<String>) -> Self {
self.history_exclusion_prefix = ignore_prefix;
self
}
/// A builder that configures the validator for your instance of the Reedline engine
/// # Example
/// ```rust
@ -513,14 +542,16 @@ impl Reedline {
&mut self,
f: &dyn Fn(HistoryItem) -> HistoryItem,
) -> crate::Result<()> {
if let Some(r) = &self.history_last_run_id {
self.history.update(*r, f)?;
} else {
return Err(ReedlineError(ReedlineErrorVariants::OtherHistoryError(
match &self.history_last_run_id {
Some(Self::FILTERED_ITEM_ID) => {
self.history_excluded_item = Some(f(self.history_excluded_item.take().unwrap()));
Ok(())
}
Some(r) => self.history.update(*r, f),
None => Err(ReedlineError(ReedlineErrorVariants::OtherHistoryError(
"No command run",
)));
))),
}
Ok(())
}
/// Wait for input and provide the user with a specified [`Prompt`].
@ -1147,17 +1178,26 @@ impl Reedline {
}
fn previous_history(&mut self) {
if self.history_cursor_on_excluded {
self.history_cursor_on_excluded = false;
}
if self.input_mode != InputMode::HistoryTraversal {
self.input_mode = InputMode::HistoryTraversal;
self.history_cursor = HistoryCursor::new(
self.get_history_navigation_based_on_line_buffer(),
self.get_history_session_id(),
);
if self.history_excluded_item.is_some() {
self.history_cursor_on_excluded = true;
}
}
self.history_cursor
.back(self.history.as_ref())
.expect("todo: error handling");
if !self.history_cursor_on_excluded {
self.history_cursor
.back(self.history.as_ref())
.expect("todo: error handling");
}
self.update_buffer_from_history();
self.editor.move_to_start(UndoBehavior::HistoryNavigation);
self.editor
@ -1173,9 +1213,25 @@ impl Reedline {
);
}
self.history_cursor
.forward(self.history.as_ref())
.expect("todo: error handling");
if self.history_cursor_on_excluded {
self.history_cursor_on_excluded = false;
} else {
let cursor_was_on_item = self.history_cursor.string_at_cursor().is_some();
self.history_cursor
.forward(self.history.as_ref())
.expect("todo: error handling");
if cursor_was_on_item
&& self.history_cursor.string_at_cursor().is_none()
&& self.history_excluded_item.is_some()
{
self.history_cursor_on_excluded = true;
}
}
if self.history_cursor.string_at_cursor().is_none() && !self.history_cursor_on_excluded {
self.input_mode = InputMode::Regular;
}
self.update_buffer_from_history();
self.editor.move_to_end(UndoBehavior::HistoryNavigation);
}
@ -1264,6 +1320,14 @@ impl Reedline {
/// Not used for the separate modal reverse search!
fn update_buffer_from_history(&mut self) {
match self.history_cursor.get_navigation() {
_ if self.history_cursor_on_excluded => self.editor.set_buffer(
self.history_excluded_item
.as_ref()
.unwrap()
.command_line
.clone(),
UndoBehavior::HistoryNavigation,
),
HistoryNavigationQuery::Normal(original) => {
if let Some(buffer_to_paint) = self.history_cursor.string_at_cursor() {
self.editor
@ -1629,8 +1693,21 @@ impl Reedline {
if !buffer.is_empty() {
let mut entry = HistoryItem::from_command_line(&buffer);
entry.session_id = self.get_history_session_id();
let entry = self.history.save(entry).expect("todo: error handling");
self.history_last_run_id = entry.id;
if self
.history_exclusion_prefix
.as_ref()
.map(|prefix| buffer.starts_with(prefix))
.unwrap_or(false)
{
entry.id = Some(Self::FILTERED_ITEM_ID);
self.history_last_run_id = entry.id;
self.history_excluded_item = Some(entry);
} else {
entry = self.history.save(entry).expect("todo: error handling");
self.history_last_run_id = entry.id;
self.history_excluded_item = None;
}
}
self.run_edit_commands(&[EditCommand::Clear]);
self.editor.reset_undo_stack();

View File

@ -78,7 +78,12 @@ impl History for FileBackedHistory {
fn load(&self, id: HistoryItemId) -> Result<super::HistoryItem> {
Ok(FileBackedHistory::construct_entry(
Some(id),
self.entries[id.0 as usize].clone(),
self.entries
.get(id.0 as usize)
.ok_or(ReedlineError(ReedlineErrorVariants::OtherHistoryError(
"Item does not exist",
)))?
.clone(),
))
}

View File

@ -4,7 +4,7 @@ use rusqlite::ToSql;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt::Display, time::Duration};
/// Unique ID for the [`HistoryItem`]
/// Unique ID for the [`HistoryItem`]. More recent items have higher ids than older ones.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct HistoryItemId(pub(crate) i64);
impl HistoryItemId {
@ -52,7 +52,7 @@ impl From<HistorySessionId> for i64 {
/// This trait represents additional arbitrary context to be added to a history (optional, see [`HistoryItem`])
pub trait HistoryItemExtraInfo: Serialize + DeserializeOwned + Default + Send {}
#[derive(Default, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
/// something that is serialized as null and deserialized by ignoring everything
pub struct IgnoreAllExtraInfo;