diff --git a/examples/example.rs b/examples/example.rs index 7b3ccc86ba..f9d32bdc69 100644 --- a/examples/example.rs +++ b/examples/example.rs @@ -1,7 +1,7 @@ use std::borrow::Cow::{self, Borrowed, Owned}; use rustyline::completion::{Completer, FilenameCompleter, Pair}; -use rustyline::config::OutputStreamType; +use rustyline::config::{HistorySearchBehaviour, OutputStreamType}; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::{Hinter, HistoryHinter}; @@ -87,6 +87,7 @@ fn main() -> rustyline::Result<()> { .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) + .history_search_behaviour(HistorySearchBehaviour::LineByLine) .build(); let h = MyHelper { completer: FilenameCompleter::new(), diff --git a/src/command.rs b/src/command.rs index ab3c91fdb8..20a33ff521 100644 --- a/src/command.rs +++ b/src/command.rs @@ -104,8 +104,8 @@ pub fn execute( s.edit_history_next(false)? } } - Cmd::HistorySearchBackward => s.edit_history_search(Direction::Reverse)?, - Cmd::HistorySearchForward => s.edit_history_search(Direction::Forward)?, + Cmd::HistorySearchBackward => s.edit_history_search(Direction::Reverse, false)?, + Cmd::HistorySearchForward => s.edit_history_search(Direction::Forward, false)?, Cmd::TransposeChars => { // Exchange the char before cursor with the character at cursor. s.edit_transpose_chars()? diff --git a/src/config.rs b/src/config.rs index ead493dfbe..9c6da167b0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,6 +34,9 @@ pub struct Config { check_cursor_position: bool, /// Bracketed paste on unix platform enable_bracketed_paste: bool, + /// Whether Arrow-Up/Down and C-p/n should go through the history line-by-line or perform a + /// reverse-search with the current prefix before the cursor. + history_search_behaviour: HistorySearchBehaviour, } impl Config { @@ -176,6 +179,21 @@ impl Config { pub fn enable_bracketed_paste(&self) -> bool { self.enable_bracketed_paste } + + /// Whether Arrow-Up/Down and C-p/n should go through the history line-by-line or perform a + /// reverse-search with the current prefix before the cursor. + /// + /// By default, go through the history line-by-line. + pub fn history_search_behaviour(&self) -> HistorySearchBehaviour { + self.history_search_behaviour + } + + pub(crate) fn set_history_search_behaviour( + &mut self, + history_search_behaviour: HistorySearchBehaviour, + ) { + self.history_search_behaviour = history_search_behaviour; + } } impl Default for Config { @@ -196,6 +214,7 @@ impl Default for Config { indent_size: 2, check_cursor_position: false, enable_bracketed_paste: true, + history_search_behaviour: HistorySearchBehaviour::LineByLine, } } } @@ -286,6 +305,17 @@ pub enum OutputStreamType { Stdout, } +/// Control going through the history with Arrow-Up/Down and C-p/n +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum HistorySearchBehaviour { + /// Go backwards through the history line-by-line. + LineByLine, + /// Implicit reverse search: Only show history entries which start with the current prefix + /// before the cursor. + PrefixReverseSearch, +} + /// Configuration builder #[derive(Clone, Debug, Default)] pub struct Builder { @@ -415,6 +445,18 @@ impl Builder { self } + /// Whether Arrow-Up/Down and C-p/n should go through the history line-by-line or perform a + /// reverse-search with the current prefix before the cursor. + /// + /// By default, go through the history line-by-line. + pub fn history_search_behaviour( + mut self, + history_search_behaviour: HistorySearchBehaviour, + ) -> Self { + self.set_history_search_behaviour(history_search_behaviour); + self + } + /// Builds a `Config` with the settings specified so far. pub fn build(self) -> Config { self.p @@ -529,4 +571,13 @@ pub trait Configurer { fn enable_bracketed_paste(&mut self, enabled: bool) { self.config_mut().enable_bracketed_paste = enabled; } + + /// Whether Arrow-Up/Down and C-p/n should go through the history line-by-line or perform a + /// reverse-search with the current prefix before the cursor. + /// + /// By default, go through the history line-by-line. + fn set_history_search_behaviour(&mut self, history_search_behaviour: HistorySearchBehaviour) { + self.config_mut() + .set_history_search_behaviour(history_search_behaviour); + } } diff --git a/src/edit.rs b/src/edit.rs index 540f907095..886d759e2b 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -8,6 +8,7 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthChar; use super::{Context, Helper, Result}; +use crate::config::HistorySearchBehaviour; use crate::highlight::Highlighter; use crate::hint::Hint; use crate::history::Direction; @@ -34,6 +35,7 @@ pub struct State<'out, 'prompt, H: Helper> { pub ctx: Context<'out>, // Give access to history for `hinter` pub hint: Option>, // last hint displayed highlight_char: bool, // `true` if a char has been highlighted + history_search_behaviour: HistorySearchBehaviour, // search history line-by-line or prefix-based } enum Info<'m> { @@ -48,6 +50,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { prompt: &'prompt str, helper: Option<&'out H>, ctx: Context<'out>, + history_search_behaviour: HistorySearchBehaviour, ) -> State<'out, 'prompt, H> { let prompt_size = out.calculate_position(prompt, Position::default()); State { @@ -63,6 +66,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { ctx, hint: None, highlight_char: false, + history_search_behaviour, } } @@ -228,6 +232,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { Ok(ValidationResult::Valid(None)) } } + + fn update_line(&mut self, buf: &str, pos: usize) { + self.changes.borrow_mut().begin(); + self.line.update(buf, pos); + self.changes.borrow_mut().end(); + } } impl<'out, 'prompt, H: Helper> Invoke for State<'out, 'prompt, H> { @@ -581,25 +591,42 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { } else if self.ctx.history_index == 0 && prev { return Ok(()); } - if prev { - self.ctx.history_index -= 1; - } else { - self.ctx.history_index += 1; - } - if self.ctx.history_index < history.len() { - let buf = history.get(self.ctx.history_index).unwrap(); - self.changes.borrow_mut().begin(); - self.line.update(buf, buf.len()); - self.changes.borrow_mut().end(); - } else { - // Restore current edited line - self.restore(); + + match self.history_search_behaviour { + // only if we have a prefix, do a prefix reverse search + HistorySearchBehaviour::PrefixReverseSearch if self.line.pos() != 0 => { + let direction = if prev { + Direction::Reverse + } else { + Direction::Forward + }; + self.edit_history_search(direction, true)?; + } + // if we don't have a prefix, just go forward / backward one history entry + HistorySearchBehaviour::LineByLine | HistorySearchBehaviour::PrefixReverseSearch => { + if prev { + self.ctx.history_index -= 1; + } else { + self.ctx.history_index += 1; + } + if self.ctx.history_index < history.len() { + let buf = history.get(self.ctx.history_index).unwrap(); + let pos = match self.history_search_behaviour { + HistorySearchBehaviour::LineByLine => buf.len(), + HistorySearchBehaviour::PrefixReverseSearch => self.line.pos(), + }; + self.update_line(buf, pos); + } else { + // Restore current edited line + self.restore(); + } + } } self.refresh_line() } // Non-incremental, anchored search - pub fn edit_history_search(&mut self, dir: Direction) -> Result<()> { + pub fn edit_history_search(&mut self, dir: Direction, keep_cursor_pos: bool) -> Result<()> { let history = self.ctx.history; if history.is_empty() { return self.out.beep(); @@ -621,13 +648,28 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { ) { self.ctx.history_index = history_index; let buf = history.get(history_index).unwrap(); - self.changes.borrow_mut().begin(); - self.line.update(buf, buf.len()); - self.changes.borrow_mut().end(); - self.refresh_line() - } else { - self.out.beep() + + // if the history search results in the same command we found last, continue searching + // for a different command + if buf == self.line.as_str() { + return self.edit_history_search(dir, keep_cursor_pos); + } + + let pos = if keep_cursor_pos { + self.line.pos() + } else { + buf.len() + }; + self.update_line(buf, pos); + } else { + self.ctx.history_index = match dir { + Direction::Forward => history.len(), + Direction::Reverse => 0, + }; + self.restore(); + self.out.beep()?; } + self.refresh_line() } /// Substitute the currently edited line with the first/last history entry. @@ -649,9 +691,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { if first { self.ctx.history_index = 0; let buf = history.get(self.ctx.history_index).unwrap(); - self.changes.borrow_mut().begin(); - self.line.update(buf, buf.len()); - self.changes.borrow_mut().end(); + self.update_line(buf, buf.len()); } else { self.ctx.history_index = history.len(); // Restore current edited line @@ -677,6 +717,7 @@ pub fn init_state<'out, H: Helper>( pos: usize, helper: Option<&'out H>, history: &'out crate::history::History, + history_search_behaviour: HistorySearchBehaviour, ) -> State<'out, 'static, H> { State { out, @@ -691,12 +732,14 @@ pub fn init_state<'out, H: Helper>( ctx: Context::new(history), hint: Some(Box::new("hint".to_owned())), highlight_char: false, + history_search_behaviour, } } #[cfg(test)] mod test { use super::init_state; + use crate::config::HistorySearchBehaviour; use crate::history::History; use crate::tty::Sink; @@ -708,7 +751,8 @@ mod test { history.add("line1"); let line = "current edited line"; let helper: Option<()> = None; - let mut s = init_state(&mut out, line, 6, helper.as_ref(), &history); + let hsb = HistorySearchBehaviour::LineByLine; + let mut s = init_state(&mut out, line, 6, helper.as_ref(), &history, hsb); s.ctx.history_index = history.len(); for _ in 0..2 { @@ -738,4 +782,53 @@ mod test { assert_eq!(2, s.ctx.history_index); assert_eq!(line, s.line.as_str()); } + + #[test] + fn edit_history_prefix() { + let mut out = Sink::new(); + let mut history = History::new(); + history.add("foo"); + history.add("bar"); + history.add("cd abcdef"); + history.add("ls"); + history.add("cd abcdef"); + history.add("ls"); + history.add("cd ab123"); + history.add("ls"); + let line = "cd abxyz"; + let helper: Option<()> = None; + let hsb = HistorySearchBehaviour::PrefixReverseSearch; + let mut s = init_state(&mut out, line, 5, helper.as_ref(), &history, hsb); + s.ctx.history_index = history.len(); + + s.edit_history_next(true).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(6, s.ctx.history_index); + assert_eq!("cd ab123", s.line.as_str()); + + s.edit_history_next(true).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(4, s.ctx.history_index); + assert_eq!("cd abcdef", s.line.as_str()); + + s.edit_history_next(true).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(0, s.ctx.history_index); + assert_eq!(line, s.line.as_str()); + + s.edit_history_next(false).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(2, s.ctx.history_index); + assert_eq!("cd abcdef", s.line.as_str()); + + s.edit_history_next(false).unwrap(); + assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(6, s.ctx.history_index); + assert_eq!("cd ab123", s.line.as_str()); + + s.edit_history_next(false).unwrap(); + // assert_eq!(line, s.saved_line_for_history.as_str()); + assert_eq!(8, s.ctx.history.len()); + assert_eq!(line, s.line.as_str()); + } } diff --git a/src/lib.rs b/src/lib.rs index a79802c54d..5a03e228f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -451,7 +451,13 @@ fn readline_edit( editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc> let ctx = Context::new(&editor.history); - let mut s = State::new(&mut stdout, prompt, editor.helper.as_ref(), ctx); + let mut s = State::new( + &mut stdout, + prompt, + editor.helper.as_ref(), + ctx, + editor.config.history_search_behaviour(), + ); let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); diff --git a/src/test/mod.rs b/src/test/mod.rs index 9b66b83f26..ddb67aa789 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -4,7 +4,7 @@ use std::vec::IntoIter; use radix_trie::Trie; use crate::completion::Completer; -use crate::config::{Config, EditMode}; +use crate::config::{Config, EditMode, HistorySearchBehaviour}; use crate::edit::init_state; use crate::highlight::Highlighter; use crate::hint::Hinter; @@ -57,7 +57,8 @@ fn complete_line() { let mut out = Sink::new(); let history = crate::history::History::new(); let helper = Some(SimpleCompleter); - let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history); + let hsb = HistorySearchBehaviour::LineByLine; + let mut s = init_state(&mut out, "rus", 3, helper.as_ref(), &history, hsb); let config = Config::default(); let mut input_state = InputState::new(&config, Arc::new(RwLock::new(Trie::new()))); let keys = vec![E::ENTER];