mirror of
https://github.com/nushell/reedline.git
synced 2024-09-11 15:55:50 +03:00
Allow history searching via session id (#562)
* add the ability to search history with session id * clippy
This commit is contained in:
parent
27f4417191
commit
61c6409fb7
@ -66,7 +66,17 @@ fn main() -> Result<()> {
|
|||||||
emacs: None,
|
emacs: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Setting history_per_session to true will allow the history to be isolated to the current session
|
||||||
|
// Setting history_per_session to false will allow the history to be shared across all sessions
|
||||||
|
let history_per_session = false;
|
||||||
|
let mut history_session_id = if history_per_session {
|
||||||
|
Reedline::create_history_session_id()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut line_editor = Reedline::create()
|
let mut line_editor = Reedline::create()
|
||||||
|
.with_history_session_id(history_session_id)
|
||||||
.with_history(history)
|
.with_history(history)
|
||||||
.with_completer(completer)
|
.with_completer(completer)
|
||||||
.with_quick_completions(true)
|
.with_quick_completions(true)
|
||||||
@ -145,10 +155,34 @@ fn main() -> Result<()> {
|
|||||||
line_editor.clear_scrollback()?;
|
line_editor.clear_scrollback()?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Get the full history
|
||||||
if buffer.trim() == "history" {
|
if buffer.trim() == "history" {
|
||||||
line_editor.print_history()?;
|
line_editor.print_history()?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Get the history only pertinent to the current session
|
||||||
|
if buffer.trim() == "history session" {
|
||||||
|
line_editor.print_history_session()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Get this history session identifier
|
||||||
|
if buffer.trim() == "history sessionid" {
|
||||||
|
line_editor.print_history_session_id()?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Toggle between the full history and the history pertinent to the current session
|
||||||
|
if buffer.trim() == "toggle history_session" {
|
||||||
|
let hist_session_id = if history_session_id.is_none() {
|
||||||
|
// If we never created a history session ID, create one now
|
||||||
|
let sesh = Reedline::create_history_session_id();
|
||||||
|
history_session_id = sesh;
|
||||||
|
sesh
|
||||||
|
} else {
|
||||||
|
history_session_id
|
||||||
|
};
|
||||||
|
line_editor.toggle_history_session_matching(hist_session_id)?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if buffer.trim() == "clear-history" {
|
if buffer.trim() == "clear-history" {
|
||||||
let hstry = Box::new(line_editor.history_mut());
|
let hstry = Box::new(line_editor.history_mut());
|
||||||
hstry
|
hstry
|
||||||
|
100
src/engine.rs
100
src/engine.rs
@ -158,14 +158,15 @@ impl Reedline {
|
|||||||
let hinter = None;
|
let hinter = None;
|
||||||
let validator = None;
|
let validator = None;
|
||||||
let edit_mode = Box::<Emacs>::default();
|
let edit_mode = Box::<Emacs>::default();
|
||||||
let hist_session_id = Self::create_history_session_id();
|
let hist_session_id = None;
|
||||||
|
|
||||||
Reedline {
|
Reedline {
|
||||||
editor: Editor::default(),
|
editor: Editor::default(),
|
||||||
history,
|
history,
|
||||||
history_cursor: HistoryCursor::new(HistoryNavigationQuery::Normal(
|
history_cursor: HistoryCursor::new(
|
||||||
LineBuffer::default(),
|
HistoryNavigationQuery::Normal(LineBuffer::default()),
|
||||||
)),
|
hist_session_id,
|
||||||
|
),
|
||||||
history_session_id: hist_session_id,
|
history_session_id: hist_session_id,
|
||||||
history_last_run_id: None,
|
history_last_run_id: None,
|
||||||
input_mode: InputMode::Regular,
|
input_mode: InputMode::Regular,
|
||||||
@ -188,7 +189,7 @@ impl Reedline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a new history session id based on the current time and the first commit datetime of reedline
|
/// Get a new history session id based on the current time and the first commit datetime of reedline
|
||||||
fn create_history_session_id() -> Option<HistorySessionId> {
|
pub fn create_history_session_id() -> Option<HistorySessionId> {
|
||||||
let nanos = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
let nanos = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
||||||
Ok(n) => n.as_nanos() as i64,
|
Ok(n) => n.as_nanos() as i64,
|
||||||
Err(_) => 0,
|
Err(_) => 0,
|
||||||
@ -202,6 +203,14 @@ impl Reedline {
|
|||||||
self.history_session_id
|
self.history_session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set a new history session id
|
||||||
|
/// This should be used in situations where the user initially did not have a history_session_id
|
||||||
|
/// and then later realized they want to have one without restarting the application.
|
||||||
|
pub fn set_history_session_id(&mut self, session: Option<HistorySessionId>) -> Result<()> {
|
||||||
|
self.history_session_id = session;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// A builder to include a [`Hinter`] in your instance of the Reedline engine
|
/// A builder to include a [`Hinter`] in your instance of the Reedline engine
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust
|
/// ```rust
|
||||||
@ -380,6 +389,13 @@ impl Reedline {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A builder that adds the history item id
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_history_session_id(mut self, session: Option<HistorySessionId>) -> Self {
|
||||||
|
self.history_session_id = session;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// A builder that enables reedline changing the cursor shape based on the current edit mode.
|
/// A builder that enables reedline changing the cursor shape based on the current edit mode.
|
||||||
/// The current implementation sets the cursor shape when drawing the prompt.
|
/// The current implementation sets the cursor shape when drawing the prompt.
|
||||||
/// Do not use this if the cursor shape is set elsewhere, e.g. in the terminal settings or by ansi escape sequences.
|
/// Do not use this if the cursor shape is set elsewhere, e.g. in the terminal settings or by ansi escape sequences.
|
||||||
@ -397,7 +413,7 @@ impl Reedline {
|
|||||||
pub fn print_history(&mut self) -> Result<()> {
|
pub fn print_history(&mut self) -> Result<()> {
|
||||||
let history: Vec<_> = self
|
let history: Vec<_> = self
|
||||||
.history
|
.history
|
||||||
.search(SearchQuery::everything(SearchDirection::Forward))
|
.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
.expect("todo: error handling");
|
.expect("todo: error handling");
|
||||||
|
|
||||||
for (i, entry) in history.iter().enumerate() {
|
for (i, entry) in history.iter().enumerate() {
|
||||||
@ -406,6 +422,40 @@ impl Reedline {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Output the complete [`History`] for this session, chronologically with numbering to the terminal
|
||||||
|
pub fn print_history_session(&mut self) -> Result<()> {
|
||||||
|
let history: Vec<_> = self
|
||||||
|
.history
|
||||||
|
.search(SearchQuery::everything(
|
||||||
|
SearchDirection::Forward,
|
||||||
|
self.get_history_session_id(),
|
||||||
|
))
|
||||||
|
.expect("todo: error handling");
|
||||||
|
|
||||||
|
for (i, entry) in history.iter().enumerate() {
|
||||||
|
self.print_line(&format!("{}\t{}", i, entry.command_line))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print the history session id
|
||||||
|
pub fn print_history_session_id(&mut self) -> Result<()> {
|
||||||
|
println!("History Session Id: {:?}", self.get_history_session_id());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle between having a history that uses the history session id and one that does not
|
||||||
|
pub fn toggle_history_session_matching(
|
||||||
|
&mut self,
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.history_session_id = match self.get_history_session_id() {
|
||||||
|
Some(_) => None,
|
||||||
|
None => session,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Read-only view of the history
|
/// Read-only view of the history
|
||||||
pub fn history(&self) -> &dyn History {
|
pub fn history(&self) -> &dyn History {
|
||||||
&*self.history
|
&*self.history
|
||||||
@ -1066,8 +1116,10 @@ impl Reedline {
|
|||||||
fn previous_history(&mut self) {
|
fn previous_history(&mut self) {
|
||||||
if self.input_mode != InputMode::HistoryTraversal {
|
if self.input_mode != InputMode::HistoryTraversal {
|
||||||
self.input_mode = InputMode::HistoryTraversal;
|
self.input_mode = InputMode::HistoryTraversal;
|
||||||
self.history_cursor =
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(self.get_history_navigation_based_on_line_buffer());
|
self.get_history_navigation_based_on_line_buffer(),
|
||||||
|
self.get_history_session_id(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.history_cursor
|
self.history_cursor
|
||||||
@ -1082,8 +1134,10 @@ impl Reedline {
|
|||||||
fn next_history(&mut self) {
|
fn next_history(&mut self) {
|
||||||
if self.input_mode != InputMode::HistoryTraversal {
|
if self.input_mode != InputMode::HistoryTraversal {
|
||||||
self.input_mode = InputMode::HistoryTraversal;
|
self.input_mode = InputMode::HistoryTraversal;
|
||||||
self.history_cursor =
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(self.get_history_navigation_based_on_line_buffer());
|
self.get_history_navigation_based_on_line_buffer(),
|
||||||
|
self.get_history_session_id(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.history_cursor
|
self.history_cursor
|
||||||
@ -1118,8 +1172,10 @@ impl Reedline {
|
|||||||
///
|
///
|
||||||
/// This mode uses a separate prompt and handles keybindings slightly differently!
|
/// This mode uses a separate prompt and handles keybindings slightly differently!
|
||||||
fn enter_history_search(&mut self) {
|
fn enter_history_search(&mut self) {
|
||||||
self.history_cursor =
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::SubstringSearch("".to_string()));
|
HistoryNavigationQuery::SubstringSearch("".to_string()),
|
||||||
|
self.get_history_session_id(),
|
||||||
|
);
|
||||||
self.input_mode = InputMode::HistorySearch;
|
self.input_mode = InputMode::HistorySearch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1133,11 +1189,14 @@ impl Reedline {
|
|||||||
let navigation = self.history_cursor.get_navigation();
|
let navigation = self.history_cursor.get_navigation();
|
||||||
if let HistoryNavigationQuery::SubstringSearch(mut substring) = navigation {
|
if let HistoryNavigationQuery::SubstringSearch(mut substring) = navigation {
|
||||||
substring.push(*c);
|
substring.push(*c);
|
||||||
self.history_cursor =
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::SubstringSearch(substring));
|
HistoryNavigationQuery::SubstringSearch(substring),
|
||||||
|
self.get_history_session_id(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
self.history_cursor = HistoryCursor::new(
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryNavigationQuery::SubstringSearch(String::from(*c)),
|
HistoryNavigationQuery::SubstringSearch(String::from(*c)),
|
||||||
|
self.get_history_session_id(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.history_cursor
|
self.history_cursor
|
||||||
@ -1152,6 +1211,7 @@ impl Reedline {
|
|||||||
|
|
||||||
self.history_cursor = HistoryCursor::new(
|
self.history_cursor = HistoryCursor::new(
|
||||||
HistoryNavigationQuery::SubstringSearch(new_substring.to_string()),
|
HistoryNavigationQuery::SubstringSearch(new_substring.to_string()),
|
||||||
|
self.get_history_session_id(),
|
||||||
);
|
);
|
||||||
self.history_cursor
|
self.history_cursor
|
||||||
.back(self.history.as_mut())
|
.back(self.history.as_mut())
|
||||||
@ -1275,7 +1335,7 @@ impl Reedline {
|
|||||||
start_id: None,
|
start_id: None,
|
||||||
end_id: None,
|
end_id: None,
|
||||||
limit: Some(1), // fetch the latest one entries
|
limit: Some(1), // fetch the latest one entries
|
||||||
filter: SearchFilter::anything(),
|
filter: SearchFilter::anything(self.get_history_session_id()),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| Vec::new())
|
.unwrap_or_else(|_| Vec::new())
|
||||||
.get(index.saturating_sub(1))
|
.get(index.saturating_sub(1))
|
||||||
@ -1295,7 +1355,7 @@ impl Reedline {
|
|||||||
start_id: None,
|
start_id: None,
|
||||||
end_id: None,
|
end_id: None,
|
||||||
limit: Some(index as i64), // fetch the latest n entries
|
limit: Some(index as i64), // fetch the latest n entries
|
||||||
filter: SearchFilter::anything(),
|
filter: SearchFilter::anything(self.get_history_session_id()),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| Vec::new())
|
.unwrap_or_else(|_| Vec::new())
|
||||||
.get(index.saturating_sub(1))
|
.get(index.saturating_sub(1))
|
||||||
@ -1315,7 +1375,7 @@ impl Reedline {
|
|||||||
start_id: None,
|
start_id: None,
|
||||||
end_id: None,
|
end_id: None,
|
||||||
limit: Some((index + 1) as i64), // fetch the oldest n entries
|
limit: Some((index + 1) as i64), // fetch the oldest n entries
|
||||||
filter: SearchFilter::anything(),
|
filter: SearchFilter::anything(self.get_history_session_id()),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|_| Vec::new())
|
.unwrap_or_else(|_| Vec::new())
|
||||||
.get(index)
|
.get(index)
|
||||||
@ -1328,7 +1388,9 @@ impl Reedline {
|
|||||||
}),
|
}),
|
||||||
ParseAction::LastToken => self
|
ParseAction::LastToken => self
|
||||||
.history
|
.history
|
||||||
.search(SearchQuery::last_with_search(SearchFilter::anything()))
|
.search(SearchQuery::last_with_search(SearchFilter::anything(
|
||||||
|
self.get_history_session_id(),
|
||||||
|
)))
|
||||||
.unwrap_or_else(|_| Vec::new())
|
.unwrap_or_else(|_| Vec::new())
|
||||||
.get(0)
|
.get(0)
|
||||||
.and_then(|history| history.command_line.split_whitespace().rev().next())
|
.and_then(|history| history.command_line.split_whitespace().rev().next())
|
||||||
@ -1533,7 +1595,7 @@ impl Reedline {
|
|||||||
self.repaint(prompt)?;
|
self.repaint(prompt)?;
|
||||||
if !buffer.is_empty() {
|
if !buffer.is_empty() {
|
||||||
let mut entry = HistoryItem::from_command_line(&buffer);
|
let mut entry = HistoryItem::from_command_line(&buffer);
|
||||||
entry.session_id = self.history_session_id;
|
entry.session_id = self.get_history_session_id();
|
||||||
let entry = self.history.save(entry).expect("todo: error handling");
|
let entry = self.history.save(entry).expect("todo: error handling");
|
||||||
self.history_last_run_id = entry.id;
|
self.history_last_run_id = entry.id;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,10 @@ impl Hinter for DefaultHinter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
self.current_hint = if line.chars().count() >= self.min_chars {
|
self.current_hint = if line.chars().count() >= self.min_chars {
|
||||||
history
|
history
|
||||||
.search(SearchQuery::last_with_prefix(line.to_string()))
|
.search(SearchQuery::last_with_prefix(
|
||||||
|
line.to_string(),
|
||||||
|
history.session(),
|
||||||
|
))
|
||||||
.expect("todo: error handling")
|
.expect("todo: error handling")
|
||||||
.get(0)
|
.get(0)
|
||||||
.map_or_else(String::new, |entry| {
|
.map_or_else(String::new, |entry| {
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use chrono::Utc;
|
|
||||||
|
|
||||||
use crate::{core_editor::LineBuffer, HistoryItem, Result};
|
|
||||||
|
|
||||||
use super::HistoryItemId;
|
use super::HistoryItemId;
|
||||||
|
use crate::{core_editor::LineBuffer, HistoryItem, HistorySessionId, Result};
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
/// Browsing modes for a [`History`]
|
/// Browsing modes for a [`History`]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@ -53,18 +51,23 @@ pub struct SearchFilter {
|
|||||||
pub cwd_prefix: Option<String>,
|
pub cwd_prefix: Option<String>,
|
||||||
/// Filter whether the command completed
|
/// Filter whether the command completed
|
||||||
pub exit_successful: Option<bool>,
|
pub exit_successful: Option<bool>,
|
||||||
|
/// Filter on the session id
|
||||||
|
pub session: Option<HistorySessionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchFilter {
|
impl SearchFilter {
|
||||||
/// Create a search filter with a [`CommandLineSearch`]
|
/// Create a search filter with a [`CommandLineSearch`]
|
||||||
pub fn from_text_search(cmd: CommandLineSearch) -> SearchFilter {
|
pub fn from_text_search(
|
||||||
let mut s = SearchFilter::anything();
|
cmd: CommandLineSearch,
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
|
) -> SearchFilter {
|
||||||
|
let mut s = SearchFilter::anything(session);
|
||||||
s.command_line = Some(cmd);
|
s.command_line = Some(cmd);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
/// No filter constraint
|
/// anything within this session
|
||||||
pub const fn anything() -> SearchFilter {
|
pub fn anything(session: Option<HistorySessionId>) -> SearchFilter {
|
||||||
SearchFilter {
|
SearchFilter {
|
||||||
command_line: None,
|
command_line: None,
|
||||||
not_command_line: None,
|
not_command_line: None,
|
||||||
@ -72,6 +75,7 @@ impl SearchFilter {
|
|||||||
cwd_exact: None,
|
cwd_exact: None,
|
||||||
cwd_prefix: None,
|
cwd_prefix: None,
|
||||||
exit_successful: None,
|
exit_successful: None,
|
||||||
|
session,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +109,7 @@ impl SearchQuery {
|
|||||||
start_id: None,
|
start_id: None,
|
||||||
end_id: None,
|
end_id: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(contains)),
|
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(contains), None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,14 +127,18 @@ impl SearchQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the most recent entry starting with the `prefix`
|
/// Get the most recent entry starting with the `prefix`
|
||||||
pub fn last_with_prefix(prefix: String) -> SearchQuery {
|
pub fn last_with_prefix(prefix: String, session: Option<HistorySessionId>) -> SearchQuery {
|
||||||
SearchQuery::last_with_search(SearchFilter::from_text_search(CommandLineSearch::Prefix(
|
SearchQuery::last_with_search(SearchFilter::from_text_search(
|
||||||
prefix,
|
CommandLineSearch::Prefix(prefix),
|
||||||
)))
|
session,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Query to get all entries in the given [`SearchDirection`]
|
/// Query to get all entries in the given [`SearchDirection`]
|
||||||
pub const fn everything(direction: SearchDirection) -> SearchQuery {
|
pub fn everything(
|
||||||
|
direction: SearchDirection,
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
|
) -> SearchQuery {
|
||||||
SearchQuery {
|
SearchQuery {
|
||||||
direction,
|
direction,
|
||||||
start_time: None,
|
start_time: None,
|
||||||
@ -138,7 +146,7 @@ impl SearchQuery {
|
|||||||
start_id: None,
|
start_id: None,
|
||||||
end_id: None,
|
end_id: None,
|
||||||
limit: None,
|
limit: None,
|
||||||
filter: SearchFilter::anything(),
|
filter: SearchFilter::anything(session),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -159,7 +167,7 @@ pub trait History: Send {
|
|||||||
fn count(&self, query: SearchQuery) -> Result<i64>;
|
fn count(&self, query: SearchQuery) -> Result<i64>;
|
||||||
/// return the total number of history items
|
/// return the total number of history items
|
||||||
fn count_all(&self) -> Result<i64> {
|
fn count_all(&self) -> Result<i64> {
|
||||||
self.count(SearchQuery::everything(SearchDirection::Forward))
|
self.count(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
}
|
}
|
||||||
/// return the results of a query
|
/// return the results of a query
|
||||||
fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>>;
|
fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>>;
|
||||||
@ -176,6 +184,8 @@ pub trait History: Send {
|
|||||||
fn delete(&mut self, h: HistoryItemId) -> Result<()>;
|
fn delete(&mut self, h: HistoryItemId) -> Result<()>;
|
||||||
/// ensure that this history is written to disk
|
/// ensure that this history is written to disk
|
||||||
fn sync(&mut self) -> std::io::Result<()>;
|
fn sync(&mut self) -> std::io::Result<()>;
|
||||||
|
/// get the history session id
|
||||||
|
fn session(&self) -> Option<HistorySessionId>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -265,7 +275,7 @@ mod test {
|
|||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
println!(
|
println!(
|
||||||
"{:#?}",
|
"{:#?}",
|
||||||
history.search(SearchQuery::everything(SearchDirection::Forward))
|
history.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(history.count_all()?, if IS_FILE_BASED { 13 } else { 12 });
|
assert_eq!(history.count_all()?, if IS_FILE_BASED { 13 } else { 12 });
|
||||||
@ -275,7 +285,7 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn get_latest() -> Result<()> {
|
fn get_latest() -> Result<()> {
|
||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
let res = history.search(SearchQuery::last_with_search(SearchFilter::anything()))?;
|
let res = history.search(SearchQuery::last_with_search(SearchFilter::anything(None)))?;
|
||||||
|
|
||||||
search_returned(&*history, res, vec![12])?;
|
search_returned(&*history, res, vec![12])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -286,7 +296,7 @@ mod test {
|
|||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
let res = history.search(SearchQuery {
|
let res = history.search(SearchQuery {
|
||||||
limit: Some(1),
|
limit: Some(1),
|
||||||
..SearchQuery::everything(SearchDirection::Forward)
|
..SearchQuery::everything(SearchDirection::Forward, None)
|
||||||
})?;
|
})?;
|
||||||
search_returned(&*history, res, vec![if IS_FILE_BASED { 0 } else { 1 }])?;
|
search_returned(&*history, res, vec![if IS_FILE_BASED { 0 } else { 1 }])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -296,8 +306,11 @@ mod test {
|
|||||||
fn search_prefix() -> Result<()> {
|
fn search_prefix() -> Result<()> {
|
||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
let res = history.search(SearchQuery {
|
let res = history.search(SearchQuery {
|
||||||
filter: SearchFilter::from_text_search(CommandLineSearch::Prefix("ls ".to_string())),
|
filter: SearchFilter::from_text_search(
|
||||||
..SearchQuery::everything(SearchDirection::Backward)
|
CommandLineSearch::Prefix("ls ".to_string()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
..SearchQuery::everything(SearchDirection::Backward, None)
|
||||||
})?;
|
})?;
|
||||||
search_returned(&*history, res, vec![9, 6])?;
|
search_returned(&*history, res, vec![9, 6])?;
|
||||||
|
|
||||||
@ -308,10 +321,11 @@ mod test {
|
|||||||
fn search_includes() -> Result<()> {
|
fn search_includes() -> Result<()> {
|
||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
let res = history.search(SearchQuery {
|
let res = history.search(SearchQuery {
|
||||||
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(
|
filter: SearchFilter::from_text_search(
|
||||||
"foo.zip".to_string(),
|
CommandLineSearch::Substring("foo.zip".to_string()),
|
||||||
)),
|
None,
|
||||||
..SearchQuery::everything(SearchDirection::Forward)
|
),
|
||||||
|
..SearchQuery::everything(SearchDirection::Forward, None)
|
||||||
})?;
|
})?;
|
||||||
search_returned(&*history, res, vec![2, 3])?;
|
search_returned(&*history, res, vec![2, 3])?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -321,9 +335,12 @@ mod test {
|
|||||||
fn search_includes_limit() -> Result<()> {
|
fn search_includes_limit() -> Result<()> {
|
||||||
let history = create_filled_example_history()?;
|
let history = create_filled_example_history()?;
|
||||||
let res = history.search(SearchQuery {
|
let res = history.search(SearchQuery {
|
||||||
filter: SearchFilter::from_text_search(CommandLineSearch::Substring("c".to_string())),
|
filter: SearchFilter::from_text_search(
|
||||||
|
CommandLineSearch::Substring("c".to_string()),
|
||||||
|
None,
|
||||||
|
),
|
||||||
limit: Some(2),
|
limit: Some(2),
|
||||||
..SearchQuery::everything(SearchDirection::Forward)
|
..SearchQuery::everything(SearchDirection::Forward, None)
|
||||||
})?;
|
})?;
|
||||||
search_returned(&*history, res, vec![1, 4])?;
|
search_returned(&*history, res, vec![1, 4])?;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::{History, HistoryNavigationQuery};
|
use crate::{History, HistoryNavigationQuery, HistorySessionId};
|
||||||
|
|
||||||
use super::base::CommandLineSearch;
|
use super::base::CommandLineSearch;
|
||||||
use super::base::SearchDirection;
|
use super::base::SearchDirection;
|
||||||
@ -13,14 +13,16 @@ pub struct HistoryCursor {
|
|||||||
query: HistoryNavigationQuery,
|
query: HistoryNavigationQuery,
|
||||||
current: Option<HistoryItem>,
|
current: Option<HistoryItem>,
|
||||||
skip_dupes: bool,
|
skip_dupes: bool,
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HistoryCursor {
|
impl HistoryCursor {
|
||||||
pub const fn new(query: HistoryNavigationQuery) -> HistoryCursor {
|
pub fn new(query: HistoryNavigationQuery, session: Option<HistorySessionId>) -> HistoryCursor {
|
||||||
HistoryCursor {
|
HistoryCursor {
|
||||||
query,
|
query,
|
||||||
current: None,
|
current: None,
|
||||||
skip_dupes: true,
|
skip_dupes: true,
|
||||||
|
session,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,13 +40,14 @@ impl HistoryCursor {
|
|||||||
|
|
||||||
fn get_search_filter(&self) -> SearchFilter {
|
fn get_search_filter(&self) -> SearchFilter {
|
||||||
let filter = match self.query.clone() {
|
let filter = match self.query.clone() {
|
||||||
HistoryNavigationQuery::Normal(_) => SearchFilter::anything(),
|
HistoryNavigationQuery::Normal(_) => SearchFilter::anything(self.session),
|
||||||
HistoryNavigationQuery::PrefixSearch(prefix) => {
|
HistoryNavigationQuery::PrefixSearch(prefix) => {
|
||||||
SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix))
|
SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix), self.session)
|
||||||
}
|
|
||||||
HistoryNavigationQuery::SubstringSearch(substring) => {
|
|
||||||
SearchFilter::from_text_search(CommandLineSearch::Substring(substring))
|
|
||||||
}
|
}
|
||||||
|
HistoryNavigationQuery::SubstringSearch(substring) => SearchFilter::from_text_search(
|
||||||
|
CommandLineSearch::Substring(substring),
|
||||||
|
self.session,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
if let (true, Some(current)) = (self.skip_dupes, &self.current) {
|
if let (true, Some(current)) = (self.skip_dupes, &self.current) {
|
||||||
SearchFilter {
|
SearchFilter {
|
||||||
@ -112,20 +115,20 @@ mod tests {
|
|||||||
let hist = Box::<FileBackedHistory>::default();
|
let hist = Box::<FileBackedHistory>::default();
|
||||||
(
|
(
|
||||||
hist,
|
hist,
|
||||||
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default())),
|
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn create_history_at(cap: usize, path: &Path) -> (Box<dyn History>, HistoryCursor) {
|
fn create_history_at(cap: usize, path: &Path) -> (Box<dyn History>, HistoryCursor) {
|
||||||
let hist = Box::new(FileBackedHistory::with_file(cap, path.to_owned()).unwrap());
|
let hist = Box::new(FileBackedHistory::with_file(cap, path.to_owned()).unwrap());
|
||||||
(
|
(
|
||||||
hist,
|
hist,
|
||||||
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default())),
|
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_all_entry_texts(hist: &dyn History) -> Vec<String> {
|
fn get_all_entry_texts(hist: &dyn History) -> Vec<String> {
|
||||||
let res = hist
|
let res = hist
|
||||||
.search(SearchQuery::everything(SearchDirection::Forward))
|
.search(SearchQuery::everything(SearchDirection::Forward, None))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let actual: Vec<_> = res.iter().map(|e| e.command_line.to_string()).collect();
|
let actual: Vec<_> = res.iter().map(|e| e.command_line.to_string()).collect();
|
||||||
actual
|
actual
|
||||||
@ -207,8 +210,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("test"))?;
|
hist.save(HistoryItem::from_command_line("test"))?;
|
||||||
hist.save(HistoryItem::from_command_line("find me"))?;
|
hist.save(HistoryItem::from_command_line("find me"))?;
|
||||||
|
|
||||||
let mut cursor =
|
let mut cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
|
HistoryNavigationQuery::PrefixSearch("find".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
||||||
@ -227,8 +232,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("test"))?;
|
hist.save(HistoryItem::from_command_line("test"))?;
|
||||||
hist.save(HistoryItem::from_command_line("find me"))?;
|
hist.save(HistoryItem::from_command_line("find me"))?;
|
||||||
|
|
||||||
let mut cursor =
|
let mut cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
|
HistoryNavigationQuery::PrefixSearch("find".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
@ -253,8 +260,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("test"))?;
|
hist.save(HistoryItem::from_command_line("test"))?;
|
||||||
hist.save(HistoryItem::from_command_line("find me"))?;
|
hist.save(HistoryItem::from_command_line("find me"))?;
|
||||||
|
|
||||||
let mut cursor =
|
let mut cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
|
HistoryNavigationQuery::PrefixSearch("find".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
@ -279,8 +288,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("test"))?;
|
hist.save(HistoryItem::from_command_line("test"))?;
|
||||||
hist.save(HistoryItem::from_command_line("find me once"))?;
|
hist.save(HistoryItem::from_command_line("find me once"))?;
|
||||||
|
|
||||||
let mut cursor =
|
let mut cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
|
HistoryNavigationQuery::PrefixSearch("find".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
|
assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
@ -299,8 +310,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("find me once"))?;
|
hist.save(HistoryItem::from_command_line("find me once"))?;
|
||||||
hist.save(HistoryItem::from_command_line("find me as well"))?;
|
hist.save(HistoryItem::from_command_line("find me as well"))?;
|
||||||
|
|
||||||
let mut cursor =
|
let mut cursor = HistoryCursor::new(
|
||||||
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
|
HistoryNavigationQuery::PrefixSearch("find".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cursor.string_at_cursor(),
|
cursor.string_at_cursor(),
|
||||||
@ -328,9 +341,10 @@ mod tests {
|
|||||||
hist.save(HistoryItem::from_command_line("don't find me"))?;
|
hist.save(HistoryItem::from_command_line("don't find me"))?;
|
||||||
hist.save(HistoryItem::from_command_line("prefix substring suffix"))?;
|
hist.save(HistoryItem::from_command_line("prefix substring suffix"))?;
|
||||||
|
|
||||||
let mut cursor = HistoryCursor::new(HistoryNavigationQuery::SubstringSearch(
|
let mut cursor = HistoryCursor::new(
|
||||||
"substring".to_string(),
|
HistoryNavigationQuery::SubstringSearch("substring".to_string()),
|
||||||
));
|
None,
|
||||||
|
);
|
||||||
cursor.back(&*hist)?;
|
cursor.back(&*hist)?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cursor.string_at_cursor(),
|
cursor.string_at_cursor(),
|
||||||
@ -351,7 +365,10 @@ mod tests {
|
|||||||
let (mut hist, _) = create_history();
|
let (mut hist, _) = create_history();
|
||||||
hist.save(HistoryItem::from_command_line("substring"))?;
|
hist.save(HistoryItem::from_command_line("substring"))?;
|
||||||
|
|
||||||
let cursor = HistoryCursor::new(HistoryNavigationQuery::SubstringSearch("".to_string()));
|
let cursor = HistoryCursor::new(
|
||||||
|
HistoryNavigationQuery::SubstringSearch("".to_string()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(cursor.string_at_cursor(), None);
|
assert_eq!(cursor.string_at_cursor(), None);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -3,7 +3,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
result::{ReedlineError, ReedlineErrorVariants},
|
result::{ReedlineError, ReedlineErrorVariants},
|
||||||
Result,
|
HistorySessionId, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@ -30,6 +30,7 @@ pub struct FileBackedHistory {
|
|||||||
entries: VecDeque<String>,
|
entries: VecDeque<String>,
|
||||||
file: Option<PathBuf>,
|
file: Option<PathBuf>,
|
||||||
len_on_disk: usize, // Keep track what was previously written to disk
|
len_on_disk: usize, // Keep track what was previously written to disk
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FileBackedHistory {
|
impl Default for FileBackedHistory {
|
||||||
@ -268,6 +269,10 @@ impl History for FileBackedHistory {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn session(&self) -> Option<HistorySessionId> {
|
||||||
|
self.session
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileBackedHistory {
|
impl FileBackedHistory {
|
||||||
@ -285,6 +290,7 @@ impl FileBackedHistory {
|
|||||||
entries: VecDeque::new(),
|
entries: VecDeque::new(),
|
||||||
file: None,
|
file: None,
|
||||||
len_on_disk: 0,
|
len_on_disk: 0,
|
||||||
|
session: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
|
||||||
|
use rusqlite::ToSql;
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::{fmt::Display, time::Duration};
|
use std::{fmt::Display, time::Duration};
|
||||||
|
|
||||||
@ -32,6 +34,15 @@ impl Display for HistorySessionId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
|
||||||
|
impl ToSql for HistorySessionId {
|
||||||
|
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||||
|
Ok(rusqlite::types::ToSqlOutput::Owned(
|
||||||
|
rusqlite::types::Value::Integer(self.0),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<HistorySessionId> for i64 {
|
impl From<HistorySessionId> for i64 {
|
||||||
fn from(id: HistorySessionId) -> Self {
|
fn from(id: HistorySessionId) -> Self {
|
||||||
id.0
|
id.0
|
||||||
|
@ -16,6 +16,7 @@ const SQLITE_APPLICATION_ID: i32 = 1151497937;
|
|||||||
/// to add information such as a timestamp, running directory, result...
|
/// to add information such as a timestamp, running directory, result...
|
||||||
pub struct SqliteBackedHistory {
|
pub struct SqliteBackedHistory {
|
||||||
db: rusqlite::Connection,
|
db: rusqlite::Connection,
|
||||||
|
session: Option<HistorySessionId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_history_item(row: &rusqlite::Row) -> rusqlite::Result<HistoryItem> {
|
fn deserialize_history_item(row: &rusqlite::Row) -> rusqlite::Result<HistoryItem> {
|
||||||
@ -167,6 +168,10 @@ impl History for SqliteBackedHistory {
|
|||||||
// no-op (todo?)
|
// no-op (todo?)
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn session(&self) -> Option<HistorySessionId> {
|
||||||
|
self.session
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn map_sqlite_err(err: rusqlite::Error) -> ReedlineError {
|
fn map_sqlite_err(err: rusqlite::Error) -> ReedlineError {
|
||||||
// TODO: better error mapping
|
// TODO: better error mapping
|
||||||
@ -243,8 +248,9 @@ impl SqliteBackedHistory {
|
|||||||
",
|
",
|
||||||
)
|
)
|
||||||
.map_err(map_sqlite_err)?;
|
.map_err(map_sqlite_err)?;
|
||||||
Ok(SqliteBackedHistory { db })
|
Ok(SqliteBackedHistory { db, session: None })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_query<'a>(
|
fn construct_query<'a>(
|
||||||
&self,
|
&self,
|
||||||
query: &'a SearchQuery,
|
query: &'a SearchQuery,
|
||||||
@ -331,6 +337,10 @@ impl SqliteBackedHistory {
|
|||||||
wheres.push("exit_status != 0");
|
wheres.push("exit_status != 0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(session_id) = query.filter.session {
|
||||||
|
wheres.push("session_id = :session_id");
|
||||||
|
params.push((":session_id", Box::new(session_id)));
|
||||||
|
}
|
||||||
let mut wheres = wheres.join(" and ");
|
let mut wheres = wheres.join(" and ");
|
||||||
if wheres.is_empty() {
|
if wheres.is_empty() {
|
||||||
wheres = "true".to_string();
|
wheres = "true".to_string();
|
||||||
|
Loading…
Reference in New Issue
Block a user