Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,41 @@ let edit_mode = Box::new(Emacs::new(keybindings));
let mut line_editor = Reedline::create().with_edit_mode(edit_mode);
```

### Integrate with key sequences

```rust
// Configure reedline with key sequence bindings (like "jj" in vi insert mode)

use {
crossterm::event::{KeyCode, KeyModifiers},
reedline::{
default_vi_insert_keybindings, default_vi_normal_keybindings, KeyCombination, Reedline,
ReedlineEvent, Vi,
},
};

let mut insert_keybindings = default_vi_insert_keybindings();
insert_keybindings.add_sequence_binding(
vec![
KeyCombination {
modifier: KeyModifiers::NONE,
key_code: KeyCode::Char('j'),
},
KeyCombination {
modifier: KeyModifiers::NONE,
key_code: KeyCode::Char('j'),
},
],
ReedlineEvent::ViExitToNormalMode,
);

let edit_mode = Box::new(Vi::new(
insert_keybindings,
default_vi_normal_keybindings(),
));
let mut line_editor = Reedline::create().with_edit_mode(edit_mode);
```

### Integrate with `History`

```rust,no_run
Expand Down
3 changes: 3 additions & 0 deletions src/core_editor/line_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ mod test {
}

#[rstest]
#[case("abc def", 0, ' ', true, 3)]
#[case("abc def ghi", 0, 'c', true, 2)]
#[case("abc def ghi", 0, 'a', true, 0)]
#[case("abc def ghi", 0, 'z', true, 0)]
Expand All @@ -1471,6 +1472,7 @@ mod test {
}

#[rstest]
#[case("abc def", 0, ' ', true, 2)]
#[case("abc def ghi", 0, 'd', true, 3)]
#[case("abc def ghi", 3, 'd', true, 3)]
#[case("a😇c", 0, 'c', true, 1)]
Expand Down Expand Up @@ -1513,6 +1515,7 @@ mod test {
}

#[rstest]
#[case("abc def", 0, ' ', true, " def")]
#[case("abc def ghi", 0, 'b', true, "bc def ghi")]
#[case("abc def ghi", 0, 'i', true, "i")]
#[case("abc def ghi", 0, 'z', true, "abc def ghi")]
Expand Down
105 changes: 103 additions & 2 deletions src/edit_mode/base.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
use crate::{
enums::{EventStatus, ReedlineEvent, ReedlineRawEvent},
PromptEditMode,
EditCommand, PromptEditMode,
};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};

use super::{keybindings, KeyCombination};

/// Define the style of parsing for the edit events
/// Available default options:
/// - Emacs
/// - Vi
pub trait EditMode: Send {
/// Translate the given user input event into what the `LineEditor` understands
fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent;
fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent {
match event.into() {
Event::Key(KeyEvent {
code, modifiers, ..
}) => self.parse_key_event(modifiers, code),
other => self.parse_non_key_event(other),
}
}

/// Translate key events into what the `LineEditor` understands
fn parse_key_event(&mut self, modifiers: KeyModifiers, code: KeyCode) -> ReedlineEvent;

/// Resolve a key combination using keybindings with a fallback to insertable characters.
fn default_key_event(
&self,
keybindings: &keybindings::Keybindings,
combo: KeyCombination,
) -> ReedlineEvent {
match combo.key_code {
KeyCode::Char(c) => keybindings
.find_binding(combo.modifier, KeyCode::Char(c))
.unwrap_or_else(|| {
if combo.modifier == KeyModifiers::NONE
|| combo.modifier == KeyModifiers::SHIFT
|| combo.modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|| combo.modifier
== KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
{
ReedlineEvent::Edit(vec![EditCommand::InsertChar(
if combo.modifier == KeyModifiers::SHIFT {
c.to_ascii_uppercase()
} else {
c
},
)])
} else {
ReedlineEvent::None
}
}),
code => keybindings
.find_binding(combo.modifier, code)
.unwrap_or_else(|| {
if combo.modifier == KeyModifiers::NONE && code == KeyCode::Enter {
ReedlineEvent::Enter
} else {
ReedlineEvent::None
}
}),
}
}

/// What to display in the prompt indicator
fn edit_mode(&self) -> PromptEditMode;
Expand All @@ -18,4 +70,53 @@ pub trait EditMode: Send {
fn handle_mode_specific_event(&mut self, _event: ReedlineEvent) -> EventStatus {
EventStatus::Inapplicable
}

/// Translate non-key events into what the `LineEditor` understands
fn parse_non_key_event(&mut self, event: Event) -> ReedlineEvent {
match event {
Event::Key(KeyEvent {
code, modifiers, ..
}) => self.parse_key_event(modifiers, code),
Event::Mouse(_) => self.with_flushed_sequence(ReedlineEvent::Mouse),
Event::Resize(width, height) => {
self.with_flushed_sequence(ReedlineEvent::Resize(width, height))
}
Event::FocusGained => self.with_flushed_sequence(ReedlineEvent::None),
Event::FocusLost => self.with_flushed_sequence(ReedlineEvent::None),
Event::Paste(body) => {
self.with_flushed_sequence(ReedlineEvent::Edit(vec![EditCommand::InsertString(
body.replace("\r\n", "\n").replace('\r', "\n"),
)]))
}
}
}

/// Flush pending sequences and combine them with an incoming event.
fn with_flushed_sequence(&mut self, event: ReedlineEvent) -> ReedlineEvent {
let Some(flush_event) = self.flush_pending_sequence() else {
return event;
};

if matches!(event, ReedlineEvent::None) {
return flush_event;
}

match flush_event {
ReedlineEvent::Multiple(mut events) => {
events.push(event);
ReedlineEvent::Multiple(events)
}
other => ReedlineEvent::Multiple(vec![other, event]),
}
}

/// Whether a key sequence is currently pending
fn has_pending_sequence(&self) -> bool {
false
}

/// Flush any pending key sequence and return the resulting event
fn flush_pending_sequence(&mut self) -> Option<ReedlineEvent> {
None
}
}
89 changes: 29 additions & 60 deletions src/edit_mode/emacs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ use crate::{
edit_mode::{
keybindings::{
add_common_control_bindings, add_common_edit_bindings, add_common_navigation_bindings,
add_common_selection_bindings, edit_bind, Keybindings,
add_common_selection_bindings, edit_bind, KeyCombination, KeySequenceState,
Keybindings,
},
EditMode,
},
enums::{EditCommand, ReedlineEvent, ReedlineRawEvent},
enums::{EditCommand, ReedlineEvent},
PromptEditMode,
};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{KeyCode, KeyModifiers};

/// Returns the current default emacs keybindings
pub fn default_emacs_keybindings() -> Keybindings {
Expand Down Expand Up @@ -105,90 +106,58 @@ pub fn default_emacs_keybindings() -> Keybindings {
/// This parses the incoming Events like a emacs style-editor
pub struct Emacs {
keybindings: Keybindings,
sequence_state: KeySequenceState,
}

impl Default for Emacs {
fn default() -> Self {
Emacs {
keybindings: default_emacs_keybindings(),
sequence_state: KeySequenceState::default(),
}
}
}

impl EditMode for Emacs {
fn parse_event(&mut self, event: ReedlineRawEvent) -> ReedlineEvent {
match event.into() {
Event::Key(KeyEvent {
code, modifiers, ..
}) => match (modifiers, code) {
(modifier, KeyCode::Char(c)) => {
// Note. The modifier can also be a combination of modifiers, for
// example:
// KeyModifiers::CONTROL | KeyModifiers::ALT
// KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT
//
// Mixed modifiers are used by non american keyboards that have extra
// keys like 'alt gr'. Keep this in mind if in the future there are
// cases where an event is not being captured
let c = match modifier {
KeyModifiers::NONE => c,
_ => c.to_ascii_lowercase(),
};

self.keybindings
.find_binding(modifier, KeyCode::Char(c))
.unwrap_or_else(|| {
if modifier == KeyModifiers::NONE
|| modifier == KeyModifiers::SHIFT
|| modifier == KeyModifiers::CONTROL | KeyModifiers::ALT
|| modifier
== KeyModifiers::CONTROL
| KeyModifiers::ALT
| KeyModifiers::SHIFT
{
ReedlineEvent::Edit(vec![EditCommand::InsertChar(
if modifier == KeyModifiers::SHIFT {
c.to_ascii_uppercase()
} else {
c
},
)])
} else {
ReedlineEvent::None
}
})
}
_ => self
.keybindings
.find_binding(modifiers, code)
.unwrap_or(ReedlineEvent::None),
},

Event::Mouse(_) => ReedlineEvent::Mouse,
Event::Resize(width, height) => ReedlineEvent::Resize(width, height),
Event::FocusGained => ReedlineEvent::None,
Event::FocusLost => ReedlineEvent::None,
Event::Paste(body) => ReedlineEvent::Edit(vec![EditCommand::InsertString(
body.replace("\r\n", "\n").replace('\r', "\n"),
)]),
}
fn parse_key_event(&mut self, modifiers: KeyModifiers, code: KeyCode) -> ReedlineEvent {
let combo = KeyCombination::from((modifiers, code));
let keybindings = &self.keybindings;
let resolution = self.sequence_state.process_combo(keybindings, combo);
resolution
.into_event(|combo| self.default_key_event(keybindings, combo))
.unwrap_or(ReedlineEvent::None)
}

fn edit_mode(&self) -> PromptEditMode {
PromptEditMode::Emacs
}

fn has_pending_sequence(&self) -> bool {
self.sequence_state.is_pending()
}

fn flush_pending_sequence(&mut self) -> Option<ReedlineEvent> {
let keybindings = &self.keybindings;
let resolution = self.sequence_state.flush_with_combos();
resolution.into_event(|combo| self.default_key_event(keybindings, combo))
}
}

impl Emacs {
/// Emacs style input parsing constructor if you want to use custom keybindings
pub const fn new(keybindings: Keybindings) -> Self {
Emacs { keybindings }
Emacs {
keybindings,
sequence_state: KeySequenceState::new(),
}
}
}

#[cfg(test)]
mod test {
use super::*;
use crate::enums::ReedlineRawEvent;
use crossterm::event::{Event, KeyEvent};
use pretty_assertions::assert_eq;

#[test]
Expand Down
Loading