Skip to content

Commit

Permalink
Add History Search Behaviour
Browse files Browse the repository at this point in the history
Allow history search via Arrow-Up/Down or C-p/n to go either line-by-line or based on the prefix before the cursor position

Fix #423
  • Loading branch information
oberien committed Aug 2, 2020
1 parent f537b8f commit 88e669c
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 26 deletions.
3 changes: 2 additions & 1 deletion examples/example.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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(),
Expand Down
44 changes: 44 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -162,6 +177,7 @@ impl Default for Config {
color_mode: ColorMode::Enabled,
output_stream: OutputStreamType::Stdout,
tab_stop: 8,
history_search_behaviour: HistorySearchBehaviour::HistoryLineByLine,
}
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
73 changes: 51 additions & 22 deletions src/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -33,6 +34,7 @@ pub struct State<'out, 'prompt, H: Helper> {
pub ctx: Context<'out>, // Give access to history for `hinter`
pub hint: Option<String>, // 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> {
Expand All @@ -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 {
Expand All @@ -62,6 +65,7 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
ctx,
hint: None,
highlight_char: false,
history_search_behaviour,
}
}

Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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();
Expand All @@ -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.
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ fn readline_edit<H: Helper>(

editor.reset_kill_ring(); // TODO recreate a new kill ring vs Arc<Mutex<KillRing>>
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());
Expand Down Expand Up @@ -555,8 +555,8 @@ fn readline_edit<H: Helper>(
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()?
Expand Down

0 comments on commit 88e669c

Please sign in to comment.