diff --git a/examples/example.rs b/examples/example.rs index 2b3363fcd3..b629c94563 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::{OutputStreamType, HistorySearchBehaviour}; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; use rustyline::hint::{Hinter, HistoryHinter}; @@ -85,6 +85,7 @@ fn main() -> rustyline::Result<()> { .completion_type(CompletionType::List) .edit_mode(EditMode::Emacs) .output_stream(OutputStreamType::Stdout) + .history_search_behaviour(HistorySearchBehaviour::HistoryLineByLine) .build(); let h = MyHelper { completer: FilenameCompleter::new(), diff --git a/src/config.rs b/src/config.rs index 08aaa9882f..b90e5d6fbb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,6 +28,9 @@ pub struct Config { output_stream: OutputStreamType, /// Horizontal space taken by a tab. tab_stop: usize, + /// 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 { @@ -145,6 +148,18 @@ impl Config { pub(crate) fn set_tab_stop(&mut self, tab_stop: usize) { self.tab_stop = tab_stop; } + + /// 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 { @@ -162,6 +177,7 @@ impl Default for Config { color_mode: ColorMode::Enabled, output_stream: OutputStreamType::Stdout, tab_stop: 8, + history_search_behaviour: HistorySearchBehaviour::HistoryLineByLine, } } } @@ -252,6 +268,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. + HistoryLineByLine, + /// Implicit reverse search: Only show history entries which start with the current prefix + /// before the cursor. + ReverseSearchPrefix, +} + /// Configuration builder #[derive(Clone, Debug, Default)] pub struct Builder { @@ -357,6 +384,15 @@ 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 @@ -451,4 +487,12 @@ pub trait Configurer { fn set_tab_stop(&mut self, tab_stop: usize) { self.config_mut().set_tab_stop(tab_stop); } + + /// 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 0285c8ecb3..eec7ea470f 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::history::Direction; use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word}; @@ -33,6 +34,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> { @@ -47,6 +49,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 { @@ -62,6 +65,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { ctx, hint: None, highlight_char: false, + history_search_behaviour, } } @@ -229,6 +233,12 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> { Ok(true) } } + + 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> { @@ -574,25 +584,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::ReverseSearchPrefix 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::HistoryLineByLine | HistorySearchBehaviour::ReverseSearchPrefix => { + 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::HistoryLineByLine => buf.len(), + HistorySearchBehaviour::ReverseSearchPrefix => 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(); @@ -614,13 +641,17 @@ 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() + let pos = if keep_cursor_pos { + self.line.pos() + } else { + buf.len() + }; + self.update_line(buf, pos); } else { - self.out.beep() + self.restore(); + self.out.beep()?; } + self.refresh_line() } /// Substitute the currently edited line with the first/last history entry. @@ -642,9 +673,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 diff --git a/src/lib.rs b/src/lib.rs index 9caa9559c4..fa79c8b26b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -426,7 +426,7 @@ 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, helper, ctx); + let mut s = State::new(&mut stdout, prompt, helper, ctx, editor.config.history_search_behaviour()); let mut input_state = InputState::new(&editor.config, Arc::clone(&editor.custom_bindings)); s.line.set_delete_listener(editor.kill_ring.clone()); @@ -555,8 +555,8 @@ fn readline_edit( 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()?