Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split KeyPress into separate Key and Modifier types. #435

Merged
merged 16 commits into from
Oct 11, 2020
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ maintenance = { status = "actively-developed" }
members = ["rustyline-derive"]

[dependencies]
bitflags = "1.2"
cfg-if = "0.1.6"
dirs-next = { version = "1.0", optional = true }
fs2 = "0.4"
Expand Down
6 changes: 3 additions & 3 deletions examples/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rustyline::error::ReadlineError;
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::validate::{self, MatchingBracketValidator, Validator};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyPress};
use rustyline::{Cmd, CompletionType, Config, Context, EditMode, Editor, KeyEvent};
use rustyline_derive::Helper;

#[derive(Helper)]
Expand Down Expand Up @@ -97,8 +97,8 @@ fn main() -> rustyline::Result<()> {
};
let mut rl = Editor::with_config(config);
rl.set_helper(Some(h));
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
if rl.load_history("history.txt").is_err() {
println!("No previous history.");
}
Expand Down
293 changes: 150 additions & 143 deletions src/keymap.rs

Large diffs are not rendered by default.

213 changes: 144 additions & 69 deletions src/keys.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,119 @@
//! Key constants

/// Input key pressed and modifiers
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyEvent(pub KeyCode, pub Modifiers);

impl KeyEvent {
/// Constant value representing an unmodified press of `KeyCode::Backspace`.
pub(crate) const BACKSPACE: Self = Self(KeyCode::Backspace, Modifiers::NONE);
/// Constant value representing an unmodified press of `KeyCode::Enter`.
pub(crate) const ENTER: Self = Self(KeyCode::Enter, Modifiers::NONE);
/// Constant value representing an unmodified press of `KeyCode::Esc`.
pub(crate) const ESC: Self = Self(KeyCode::Esc, Modifiers::NONE);

/// Constructor from `char` and modifiers
pub fn new(c: char, mut mods: Modifiers) -> Self {
use {KeyCode as K, KeyEvent as E, Modifiers as M};

if !c.is_control() {
if !mods.is_empty() {
mods.remove(M::SHIFT); // TODO Validate: no SHIFT even if
// `c` is uppercase
}
return E(K::Char(c), mods);
}
#[allow(clippy::match_same_arms)]
match c {
'\x00' => E(K::Char(' '), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL),
'\x03' => E(K::Char('C'), mods | M::CTRL),
'\x04' => E(K::Char('D'), mods | M::CTRL),
'\x05' => E(K::Char('E'), mods | M::CTRL),
'\x06' => E(K::Char('F'), mods | M::CTRL),
'\x07' => E(K::Char('G'), mods | M::CTRL),
'\x08' => E(K::Backspace, mods), // '\b'
'\x09' => {
// '\t'
if mods.contains(M::SHIFT) {
mods.remove(M::SHIFT);
E(K::BackTab, mods)
} else {
E(K::Tab, mods)
}
}
'\x0a' => E(K::Char('J'), mods | M::CTRL), // '\n' (10)
'\x0b' => E(K::Char('K'), mods | M::CTRL),
'\x0c' => E(K::Char('L'), mods | M::CTRL),
'\x0d' => E(K::Enter, mods), // '\r' (13)
'\x0e' => E(K::Char('N'), mods | M::CTRL),
'\x0f' => E(K::Char('O'), mods | M::CTRL),
'\x10' => E(K::Char('P'), mods | M::CTRL),
'\x11' => E(K::Char('Q'), mods | M::CTRL),
'\x12' => E(K::Char('R'), mods | M::CTRL),
'\x13' => E(K::Char('S'), mods | M::CTRL),
'\x14' => E(K::Char('T'), mods | M::CTRL),
'\x15' => E(K::Char('U'), mods | M::CTRL),
'\x16' => E(K::Char('V'), mods | M::CTRL),
'\x17' => E(K::Char('W'), mods | M::CTRL),
'\x18' => E(K::Char('X'), mods | M::CTRL),
'\x19' => E(K::Char('Y'), mods | M::CTRL),
'\x1a' => E(K::Char('Z'), mods | M::CTRL),
'\x1b' => E(K::Esc, mods), // Ctrl-[
'\x1c' => E(K::Char('\\'), mods | M::CTRL),
'\x1d' => E(K::Char(']'), mods | M::CTRL),
'\x1e' => E(K::Char('^'), mods | M::CTRL),
'\x1f' => E(K::Char('_'), mods | M::CTRL),
'\x7f' => E(K::Backspace, mods), // Rubout
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
_ => E(K::Null, mods),
}
}

/// Constructor from `char` with Ctrl modifier
pub fn ctrl(c: char) -> Self {
Self::new(c, Modifiers::CTRL)
}

/// Constructor from `char` with Alt modifier
pub fn alt(c: char) -> Self {
Self::new(c, Modifiers::ALT)
}

/// ctrl-a => ctrl-A (uppercase)
/// shift-A => A (no SHIFT modifier)
/// shift-Tab => BackTab
pub fn normalize(e: Self) -> Self {
use {KeyCode as K, KeyEvent as E, Modifiers as M};

match e {
E(K::Char(c), m) if c.is_ascii_control() => Self::new(c, m),
E(K::Char(c), m) if c.is_ascii_lowercase() && m.contains(M::CTRL) => {
E(K::Char(c.to_ascii_uppercase()), m)
}
E(K::Char(c), m) if c.is_ascii_uppercase() && m.contains(M::SHIFT) => {
E(K::Char(c), m ^ M::SHIFT)
}
E(K::Tab, m) if m.contains(M::SHIFT) => E(K::BackTab, m ^ M::SHIFT),
_ => e,
}
}
}

impl From<char> for KeyEvent {
fn from(c: char) -> Self {
Self::new(c, Modifiers::NONE)
}
}

/// Input key pressed
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum KeyPress {
pub enum KeyCode {
/// Unsupported escape sequence (on unix platform)
UnknownEscSeq,
/// ⌫ or `KeyPress::Ctrl('H')`
/// ⌫ or Ctrl-H
Backspace,
/// ⇤ (usually Shift-Tab)
BackTab,
Expand All @@ -16,25 +123,15 @@ pub enum KeyPress {
BracketedPasteEnd,
/// Single char
Char(char),
/// Ctrl-↓
ControlDown,
/// Ctrl-←
ControlLeft,
/// Ctrl-→
ControlRight,
/// Ctrl-↑
ControlUp,
/// Ctrl-char
Ctrl(char),
/// ⌦
Delete,
/// ↓ arrow key
Down,
/// ⇲
End,
/// ↵ or `KeyPress::Ctrl('M')`
/// ↵ or Ctrl-M
Enter,
/// Escape or `KeyPress::Ctrl('[')`
/// Escape or Ctrl-[
Esc,
/// Function key
F(u8),
Expand All @@ -44,79 +141,57 @@ pub enum KeyPress {
Insert,
/// ← arrow key
Left,
/// Escape-char or Alt-char
Meta(char),
/// `KeyPress::Char('\0')`
/// \0
Null,
/// ⇟
PageDown,
/// ⇞
PageUp,
/// → arrow key
Right,
/// Shift-↓
ShiftDown,
/// Shift-←
ShiftLeft,
/// Shift-→
ShiftRight,
/// Shift-↑
ShiftUp,
/// ⇥ or `KeyPress::Ctrl('I')`
/// ⇥ or Ctrl-I
Tab,
/// ↑ arrow key
Up,
}

#[cfg(any(windows, unix))]
pub fn char_to_key_press(c: char) -> KeyPress {
if !c.is_control() {
return KeyPress::Char(c);
}
#[allow(clippy::match_same_arms)]
match c {
'\x00' => KeyPress::Ctrl(' '),
'\x01' => KeyPress::Ctrl('A'),
'\x02' => KeyPress::Ctrl('B'),
'\x03' => KeyPress::Ctrl('C'),
'\x04' => KeyPress::Ctrl('D'),
'\x05' => KeyPress::Ctrl('E'),
'\x06' => KeyPress::Ctrl('F'),
'\x07' => KeyPress::Ctrl('G'),
'\x08' => KeyPress::Backspace, // '\b'
'\x09' => KeyPress::Tab, // '\t'
'\x0a' => KeyPress::Ctrl('J'), // '\n' (10)
'\x0b' => KeyPress::Ctrl('K'),
'\x0c' => KeyPress::Ctrl('L'),
'\x0d' => KeyPress::Enter, // '\r' (13)
'\x0e' => KeyPress::Ctrl('N'),
'\x0f' => KeyPress::Ctrl('O'),
'\x10' => KeyPress::Ctrl('P'),
'\x12' => KeyPress::Ctrl('R'),
'\x13' => KeyPress::Ctrl('S'),
'\x14' => KeyPress::Ctrl('T'),
'\x15' => KeyPress::Ctrl('U'),
'\x16' => KeyPress::Ctrl('V'),
'\x17' => KeyPress::Ctrl('W'),
'\x18' => KeyPress::Ctrl('X'),
'\x19' => KeyPress::Ctrl('Y'),
'\x1a' => KeyPress::Ctrl('Z'),
'\x1b' => KeyPress::Esc, // Ctrl-[
'\x1c' => KeyPress::Ctrl('\\'),
'\x1d' => KeyPress::Ctrl(']'),
'\x1e' => KeyPress::Ctrl('^'),
'\x1f' => KeyPress::Ctrl('_'),
'\x7f' => KeyPress::Backspace, // Rubout
_ => KeyPress::Null,
bitflags::bitflags! {
/// The set of modifier keys that were triggered along with a key press.
pub struct Modifiers: u8 {
/// Control modifier
const CTRL = 1<<3;
/// Escape or Alt modifier
const ALT = 1<<2;
/// Shift modifier
const SHIFT = 1<<1;

/// No modifier
const NONE = 0;
/// Ctrl + Shift
const CTRL_SHIFT = Self::CTRL.bits | Self::SHIFT.bits;
/// Alt + Shift
const ALT_SHIFT = Self::ALT.bits | Self::SHIFT.bits;
/// Ctrl + Alt
const CTRL_ALT = Self::CTRL.bits | Self::ALT.bits;
/// Ctrl + Alt + Shift
const CTRL_ALT_SHIFT = Self::CTRL.bits | Self::ALT.bits | Self::SHIFT.bits;
}
}

#[cfg(test)]
mod tests {
use super::{char_to_key_press, KeyPress};
use super::{KeyCode as K, KeyEvent as E, Modifiers as M};

#[test]
fn new() {
assert_eq!(E::ESC, E::new('\x1b', M::NONE));
}

#[test]
fn char_to_key() {
assert_eq!(KeyPress::Esc, char_to_key_press('\x1b'));
fn normalize() {
assert_eq!(E::ctrl('A'), E::normalize(E(K::Char('\x01'), M::NONE)));
assert_eq!(E::ctrl('A'), E::normalize(E::ctrl('a')));
assert_eq!(E::from('A'), E::normalize(E(K::Char('A'), M::SHIFT)));
assert_eq!(E(K::BackTab, M::NONE), E::normalize(E(K::Tab, M::SHIFT)));
}
}
12 changes: 6 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use crate::hint::Hinter;
use crate::history::{Direction, History};
pub use crate::keymap::{Anchor, At, CharSearch, Cmd, Movement, RepeatCount, Word};
use crate::keymap::{InputState, Refresher};
pub use crate::keys::KeyPress;
pub use crate::keys::{KeyCode, KeyEvent, Modifiers};
use crate::kill_ring::{KillRing, Mode};
use crate::line_buffer::WordAction;
use crate::validate::Validator;
Expand Down Expand Up @@ -804,7 +804,7 @@ pub struct Editor<H: Helper> {
helper: Option<H>,
kill_ring: Arc<Mutex<KillRing>>,
config: Config,
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
custom_bindings: Arc<RwLock<HashMap<KeyEvent, Cmd>>>,
}

#[allow(clippy::new_without_default)]
Expand Down Expand Up @@ -923,18 +923,18 @@ impl<H: Helper> Editor<H> {
}

/// Bind a sequence to a command.
pub fn bind_sequence(&mut self, key_seq: KeyPress, cmd: Cmd) -> Option<Cmd> {
pub fn bind_sequence(&mut self, key_seq: KeyEvent, cmd: Cmd) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.insert(key_seq, cmd)
bindings.insert(KeyEvent::normalize(key_seq), cmd)
} else {
None
}
}

/// Remove a binding for the given sequence.
pub fn unbind_sequence(&mut self, key_seq: KeyPress) -> Option<Cmd> {
pub fn unbind_sequence(&mut self, key_seq: KeyEvent) -> Option<Cmd> {
if let Ok(mut bindings) = self.custom_bindings.write() {
bindings.remove(&key_seq)
bindings.remove(&KeyEvent::normalize(key_seq))
} else {
None
}
Expand Down
Loading