mod helix_keybindings;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
pub use helix_keybindings::{
default_helix_insert_keybindings, default_helix_normal_keybindings,
default_helix_select_keybindings,
};
use super::{is_plain_char, is_text_char, parse_non_key_event};
use crate::{
Direction, EditCommand, EditMode, FindStop, Granularity, Keybindings, MotionTarget,
PromptEditMode, PromptHelixMode, ReedlineEvent, WordEdge, WordKind,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HelixMode {
Normal,
Insert,
Select,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pending {
Find {
direction: Direction,
stop: FindStop,
},
Replace,
Goto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Outcome {
Absorb(Pending),
Execute(Action),
Reject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Verb {
SelectingMotion(MotionTarget),
CollapsingMotion(MotionTarget),
Collapse(Direction),
Deselect,
OnSelection(Op),
Submit,
ChangeMode,
Undo,
Redo,
Paste(Direction),
OpenLine(Direction),
SelectAll,
SelectLine,
LineOrHistory(Direction),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Op {
Cut,
Change,
Yank,
Replace(char),
Switchcase,
Lowercase,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Action {
count: usize,
verb: Verb,
next_mode: Option<HelixMode>,
}
impl Action {
fn repeated(self, cmd: EditCommand) -> ReedlineEvent {
ReedlineEvent::Edit(vec![cmd; self.count])
}
}
fn exec(count: usize, verb: Verb, next_mode: Option<HelixMode>) -> Outcome {
Outcome::Execute(Action {
count,
verb,
next_mode,
})
}
fn word(kind: WordKind, edge: WordEdge, direction: Direction) -> MotionTarget {
MotionTarget::Word {
kind,
edge,
direction,
}
}
#[derive(Debug, Clone)]
pub struct Helix {
insert_keybindings: Keybindings,
normal_keybindings: Keybindings,
select_keybindings: Keybindings,
mode: HelixMode,
count: Option<usize>,
pending: Option<Pending>,
}
impl EditMode for Helix {
fn parse_event(&mut self, event: crate::ReedlineRawEvent) -> crate::ReedlineEvent {
match event.into() {
Event::Key(key) => match self.mode {
HelixMode::Insert => self.dispatch_insert(key),
_ => self.dispatch(key),
},
event => parse_non_key_event(event),
}
}
fn edit_mode(&self) -> crate::PromptEditMode {
match self.mode {
HelixMode::Insert => PromptEditMode::Helix(PromptHelixMode::Insert),
HelixMode::Normal => PromptEditMode::Helix(PromptHelixMode::Normal),
HelixMode::Select => PromptEditMode::Helix(PromptHelixMode::Select),
}
}
}
impl Helix {
#[must_use]
pub fn with_insert_keybindings(mut self, keybindings: Keybindings) -> Self {
self.insert_keybindings = keybindings;
self
}
#[must_use]
pub fn with_normal_keybindings(mut self, keybindings: Keybindings) -> Self {
self.normal_keybindings = keybindings;
self
}
#[must_use]
pub fn with_select_keybindings(mut self, keybindings: Keybindings) -> Self {
self.select_keybindings = keybindings;
self
}
fn dispatch(&mut self, key: KeyEvent) -> ReedlineEvent {
debug_assert!(self.mode != HelixMode::Insert);
let outcome = match (self.pending.take(), key.code) {
(Some(pending), _) => complete_pending(pending, self.count.unwrap_or(1), key),
(None, KeyCode::Char(c @ '0'..='9'))
if key.modifiers == KeyModifiers::NONE && (c != '0' || self.count.is_some()) =>
{
self.count = Some(
self.count
.unwrap_or(0)
.saturating_mul(10)
.saturating_add(c.to_digit(10).unwrap_or(0) as usize)
.min(u16::MAX as usize),
);
return ReedlineEvent::None;
}
(None, code) => {
if self.count.is_none() && code != KeyCode::Esc {
let table = match self.mode {
HelixMode::Select => &self.select_keybindings,
_ => &self.normal_keybindings,
};
if let Some(event) = table.find_binding(key.modifiers, code) {
return event;
}
}
interpret(self.mode, self.count, key)
}
};
match outcome {
Outcome::Absorb(pending) => {
self.pending = Some(pending);
ReedlineEvent::None
}
Outcome::Execute(action) => {
self.count = None;
let event = lower(action, self.mode);
if let Some(next_mode) = action.next_mode {
self.mode = next_mode;
}
event
}
Outcome::Reject => {
self.count = None;
ReedlineEvent::None
}
}
}
fn dispatch_insert(&mut self, key: KeyEvent) -> ReedlineEvent {
if matches!(key.code, KeyCode::Esc) {
self.mode = HelixMode::Normal;
return ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]);
}
if let Some(event) = self
.insert_keybindings
.find_binding(key.modifiers, key.code)
{
return event;
}
match key.code {
KeyCode::Enter if key.modifiers == KeyModifiers::NONE => ReedlineEvent::Enter,
KeyCode::Char(ch) if is_text_char(key.modifiers) => {
ReedlineEvent::Edit(vec![EditCommand::InsertChar(ch)])
}
_ => ReedlineEvent::None,
}
}
}
impl Default for Helix {
fn default() -> Self {
Self {
insert_keybindings: default_helix_insert_keybindings(),
normal_keybindings: default_helix_normal_keybindings(),
select_keybindings: default_helix_select_keybindings(),
mode: HelixMode::Insert,
count: None,
pending: None,
}
}
}
fn complete_pending(pending: Pending, count: usize, key: KeyEvent) -> Outcome {
let ch = match key.code {
KeyCode::Char(ch) if is_text_char(key.modifiers) => ch,
_ => return Outcome::Reject,
};
match pending {
Pending::Find { direction, stop } => exec(
count,
Verb::SelectingMotion(MotionTarget::Find {
ch,
direction,
stop,
}),
None,
),
Pending::Replace => exec(count, Verb::OnSelection(Op::Replace(ch)), None),
Pending::Goto => {
let target = match ch {
'h' => MotionTarget::LineEdge(Direction::Backward),
'l' => MotionTarget::LineEdge(Direction::Forward),
'g' => MotionTarget::BufferEdge(Direction::Backward),
'e' => MotionTarget::BufferEdge(Direction::Forward),
's' => MotionTarget::LineStartNonBlank,
_ => return Outcome::Reject,
};
exec(count, Verb::CollapsingMotion(target), None)
}
}
}
fn interpret(mode: HelixMode, count: Option<usize>, key: KeyEvent) -> Outcome {
if let KeyCode::Char(_) = key.code {
if !is_plain_char(key.modifiers) {
return Outcome::Reject;
}
}
if key.code == KeyCode::Char('g') && count.is_none() {
return Outcome::Absorb(Pending::Goto);
}
let count = count.unwrap_or(1);
match key.code {
KeyCode::Char(ch) => match ch {
'f' => Outcome::Absorb(Pending::Find {
direction: Direction::Forward,
stop: FindStop::On,
}),
'F' => Outcome::Absorb(Pending::Find {
direction: Direction::Backward,
stop: FindStop::On,
}),
't' => Outcome::Absorb(Pending::Find {
direction: Direction::Forward,
stop: FindStop::Before,
}),
'T' => Outcome::Absorb(Pending::Find {
direction: Direction::Backward,
stop: FindStop::Before,
}),
'r' => Outcome::Absorb(Pending::Replace),
'w' => exec(
count,
Verb::SelectingMotion(word(WordKind::Word, WordEdge::Start, Direction::Forward)),
None,
),
'b' => exec(
count,
Verb::SelectingMotion(word(WordKind::Word, WordEdge::Start, Direction::Backward)),
None,
),
'e' => exec(
count,
Verb::SelectingMotion(word(WordKind::Word, WordEdge::End, Direction::Forward)),
None,
),
'W' => exec(
count,
Verb::SelectingMotion(word(
WordKind::LongWord,
WordEdge::Start,
Direction::Forward,
)),
None,
),
'B' => exec(
count,
Verb::SelectingMotion(word(
WordKind::LongWord,
WordEdge::Start,
Direction::Backward,
)),
None,
),
'E' => exec(
count,
Verb::SelectingMotion(word(WordKind::LongWord, WordEdge::End, Direction::Forward)),
None,
),
'l' => exec(
count,
Verb::CollapsingMotion(MotionTarget::Grapheme(Direction::Forward)),
None,
),
'h' => exec(
count,
Verb::CollapsingMotion(MotionTarget::Grapheme(Direction::Backward)),
None,
),
'j' => exec(count, Verb::LineOrHistory(Direction::Forward), None),
'k' => exec(count, Verb::LineOrHistory(Direction::Backward), None),
'x' => exec(count, Verb::SelectLine, None),
'%' => exec(count, Verb::SelectAll, None),
'~' => exec(count, Verb::OnSelection(Op::Switchcase), None),
'`' => exec(count, Verb::OnSelection(Op::Lowercase), None),
'I' => exec(
count,
Verb::CollapsingMotion(MotionTarget::LineStartNonBlank),
Some(HelixMode::Insert),
),
'A' => exec(
count,
Verb::CollapsingMotion(MotionTarget::LineEdge(Direction::Forward)),
Some(HelixMode::Insert),
),
'v' => match mode {
HelixMode::Normal => exec(count, Verb::ChangeMode, Some(HelixMode::Select)),
HelixMode::Select => exec(count, Verb::ChangeMode, Some(HelixMode::Normal)),
_ => Outcome::Reject,
},
'i' => exec(
count,
Verb::Collapse(Direction::Backward),
Some(HelixMode::Insert),
),
'a' => exec(
count,
Verb::Collapse(Direction::Forward),
Some(HelixMode::Insert),
),
'd' => exec(count, Verb::OnSelection(Op::Cut), Some(HelixMode::Normal)),
'c' => exec(
count,
Verb::OnSelection(Op::Change),
Some(HelixMode::Insert),
),
'y' => exec(count, Verb::OnSelection(Op::Yank), Some(HelixMode::Normal)),
'o' => exec(
count,
Verb::OpenLine(Direction::Forward),
Some(HelixMode::Insert),
),
'O' => exec(
count,
Verb::OpenLine(Direction::Backward),
Some(HelixMode::Insert),
),
'u' => exec(count, Verb::Undo, None),
'U' => exec(count, Verb::Redo, None),
'p' => exec(
count,
Verb::Paste(Direction::Forward),
Some(HelixMode::Normal),
),
'P' => exec(
count,
Verb::Paste(Direction::Backward),
Some(HelixMode::Normal),
),
_ => Outcome::Reject,
},
KeyCode::Enter => exec(count, Verb::Submit, Some(HelixMode::Insert)),
KeyCode::Esc => match mode {
HelixMode::Normal => exec(count, Verb::Deselect, None),
HelixMode::Select => exec(count, Verb::ChangeMode, Some(HelixMode::Normal)),
HelixMode::Insert => Outcome::Reject,
},
_ => Outcome::Reject,
}
}
fn lower(action: Action, mode: HelixMode) -> ReedlineEvent {
let event = match action.verb {
Verb::SelectingMotion(target) | Verb::CollapsingMotion(target) => match mode {
HelixMode::Normal => action.repeated(match action.verb {
Verb::SelectingMotion(_) => EditCommand::Select(target),
_ => EditCommand::Move(target),
}),
HelixMode::Select => action.repeated(EditCommand::Extend(target)),
HelixMode::Insert => {
ReedlineEvent::None
}
},
Verb::OnSelection(op) => match op {
Op::Cut | Op::Change => ReedlineEvent::Edit(vec![EditCommand::CutSelection {
granularity: Granularity::CharWise,
}]),
Op::Yank => ReedlineEvent::Edit(vec![EditCommand::CopySelection]),
Op::Replace(ch) => ReedlineEvent::Edit(vec![EditCommand::ReplaceChar(ch)]),
Op::Switchcase => ReedlineEvent::Edit(vec![EditCommand::SwitchcaseSelection]),
Op::Lowercase => ReedlineEvent::Edit(vec![EditCommand::LowercaseSelection]),
},
Verb::Collapse(dir) => ReedlineEvent::Edit(vec![EditCommand::CollapseSelection(dir)]),
Verb::OpenLine(direction) => {
let first = match direction {
Direction::Forward => EditCommand::InsertNewlineBelow,
Direction::Backward => EditCommand::InsertNewlineAbove,
};
let mut cmds = vec![first];
cmds.resize(action.count, EditCommand::InsertNewlineAbove);
ReedlineEvent::Edit(cmds)
}
Verb::Undo => action.repeated(EditCommand::Undo),
Verb::Redo => action.repeated(EditCommand::Redo),
Verb::Paste(direction) => ReedlineEvent::Edit(vec![EditCommand::PasteAtSelectionEdge {
direction,
count: action.count,
}]),
Verb::SelectAll => ReedlineEvent::Edit(vec![EditCommand::SelectAll]),
Verb::SelectLine => action.repeated(EditCommand::SelectLine),
Verb::Deselect => ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]),
Verb::ChangeMode => ReedlineEvent::None,
Verb::LineOrHistory(direction) => {
let event = match mode {
HelixMode::Select => ReedlineEvent::Edit(vec![match direction {
Direction::Forward => EditCommand::MoveLineDown { select: true },
Direction::Backward => EditCommand::MoveLineUp { select: true },
}]),
_ => ReedlineEvent::UntilFound(match direction {
Direction::Forward => vec![ReedlineEvent::MenuDown, ReedlineEvent::Down],
Direction::Backward => vec![ReedlineEvent::MenuUp, ReedlineEvent::Up],
}),
};
ReedlineEvent::Multiple(vec![event; action.count])
}
Verb::Submit => {
return ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::CollapseSelection(Direction::Forward)]),
ReedlineEvent::Enter,
]);
}
};
if action.next_mode.is_some() {
ReedlineEvent::Multiple(vec![event, ReedlineEvent::Repaint])
} else {
event
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::ReedlineRawEvent;
use pretty_assertions::assert_eq;
use rstest::rstest;
fn key(code: KeyCode, modifiers: KeyModifiers) -> ReedlineRawEvent {
ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(code, modifiers))).unwrap()
}
fn chr(c: char) -> ReedlineRawEvent {
let modifiers = if c.is_ascii_uppercase() {
KeyModifiers::SHIFT
} else {
KeyModifiers::NONE
};
key(KeyCode::Char(c), modifiers)
}
fn kev(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent::new(code, modifiers)
}
fn normal() -> Helix {
Helix {
mode: HelixMode::Normal,
..Default::default()
}
}
fn w() -> MotionTarget {
word(WordKind::Word, WordEdge::Start, Direction::Forward)
}
#[test]
fn defaults_to_insert_and_inserts_chars() {
let mut helix = Helix::default();
assert_eq!(
helix.edit_mode(),
PromptEditMode::Helix(PromptHelixMode::Insert)
);
assert_eq!(
helix.parse_event(chr('a')),
ReedlineEvent::Edit(vec![EditCommand::InsertChar('a')])
);
}
#[test]
fn insert_accepts_altgr_chars() {
let mut helix = Helix::default();
assert_eq!(
helix.parse_event(key(
KeyCode::Char('µ'),
KeyModifiers::CONTROL | KeyModifiers::ALT
)),
ReedlineEvent::Edit(vec![EditCommand::InsertChar('µ')])
);
}
#[test]
fn insert_esc_enters_normal() {
let mut helix = Helix::default();
assert_eq!(
helix.parse_event(key(KeyCode::Esc, KeyModifiers::NONE)),
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
);
assert_eq!(helix.mode, HelixMode::Normal);
}
#[test]
fn insert_enter_submits_only_without_modifiers() {
let mut helix = Helix::default();
assert_eq!(
helix.parse_event(key(KeyCode::Enter, KeyModifiers::NONE)),
ReedlineEvent::Enter
);
assert_eq!(
helix.parse_event(key(KeyCode::Enter, KeyModifiers::CONTROL)),
ReedlineEvent::None
);
}
#[rstest]
#[case('w', WordKind::Word, WordEdge::Start, Direction::Forward)]
#[case('W', WordKind::LongWord, WordEdge::Start, Direction::Forward)]
#[case('e', WordKind::Word, WordEdge::End, Direction::Forward)]
#[case('E', WordKind::LongWord, WordEdge::End, Direction::Forward)]
#[case('b', WordKind::Word, WordEdge::Start, Direction::Backward)]
#[case('B', WordKind::LongWord, WordEdge::Start, Direction::Backward)]
fn word_motions_select_in_normal_mode(
#[case] c: char,
#[case] kind: WordKind,
#[case] edge: WordEdge,
#[case] direction: Direction,
) {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![EditCommand::Select(word(kind, edge, direction))])
);
}
#[test]
fn word_motion_extends_in_select_mode() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Extend(w())])
);
}
#[test]
fn h_and_l_collapse_in_normal_extend_in_select() {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr('l')),
ReedlineEvent::Edit(vec![EditCommand::Move(MotionTarget::Grapheme(
Direction::Forward
))])
);
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(chr('h')),
ReedlineEvent::Edit(vec![EditCommand::Extend(MotionTarget::Grapheme(
Direction::Backward
))])
);
}
#[test]
fn count_repeats_motion() {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('3')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w()); 3])
);
assert_eq!(helix.count, None);
}
#[test]
fn x_selects_a_line_once_per_count() {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr('x')),
ReedlineEvent::Edit(vec![EditCommand::SelectLine])
);
assert_eq!(helix.parse_event(chr('3')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr('x')),
ReedlineEvent::Edit(vec![EditCommand::SelectLine; 3])
);
}
#[test]
fn count_accumulates_digits() {
let mut helix = normal();
let _ = helix.parse_event(chr('1'));
let _ = helix.parse_event(chr('2'));
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w()); 12])
);
}
#[test]
fn leading_zero_is_not_a_count() {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('0')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w())])
);
}
#[test]
fn live_count_suppresses_table_bindings() {
let mut helix = normal();
let _ = helix.parse_event(chr('3'));
assert_eq!(
helix.parse_event(key(KeyCode::Char('c'), KeyModifiers::CONTROL)),
ReedlineEvent::None
);
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w())])
);
}
#[test]
fn ctrl_c_uses_common_control_binding() {
let mut helix = normal();
assert_eq!(
helix.parse_event(key(KeyCode::Char('c'), KeyModifiers::CONTROL)),
ReedlineEvent::CtrlC
);
}
#[test]
fn select_mode_consults_its_own_table() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(key(KeyCode::Right, KeyModifiers::NONE)),
ReedlineEvent::Edit(vec![EditCommand::Extend(MotionTarget::Grapheme(
Direction::Forward
))])
);
assert_eq!(
helix.parse_event(key(KeyCode::Up, KeyModifiers::NONE)),
ReedlineEvent::Edit(vec![EditCommand::MoveLineUp { select: true }])
);
}
#[test]
fn normal_mode_arrows_keep_menu_navigation() {
let mut helix = normal();
assert_eq!(
helix.parse_event(key(KeyCode::Left, KeyModifiers::NONE)),
ReedlineEvent::UntilFound(vec![ReedlineEvent::MenuLeft, ReedlineEvent::Left])
);
}
#[test]
fn select_mode_backspace_extends() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(key(KeyCode::Backspace, KeyModifiers::NONE)),
ReedlineEvent::Edit(vec![EditCommand::Extend(MotionTarget::Grapheme(
Direction::Backward
))])
);
}
#[test]
fn select_mode_home_and_end_extend_to_the_line_edges() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(key(KeyCode::Home, KeyModifiers::NONE)),
ReedlineEvent::Edit(vec![EditCommand::Extend(MotionTarget::LineEdge(
Direction::Backward
))])
);
assert_eq!(
helix.parse_event(key(KeyCode::End, KeyModifiers::NONE)),
ReedlineEvent::Edit(vec![EditCommand::Extend(MotionTarget::LineEdge(
Direction::Forward
))])
);
}
#[test]
fn custom_select_table_shadows_the_default() {
let mut bindings = default_helix_select_keybindings();
bindings.add_binding(
KeyModifiers::NONE,
KeyCode::Right,
ReedlineEvent::ClearScreen,
);
let mut helix = Helix {
mode: HelixMode::Normal,
..Default::default()
}
.with_select_keybindings(bindings);
assert_eq!(
helix.parse_event(key(KeyCode::Right, KeyModifiers::NONE)),
ReedlineEvent::UntilFound(vec![
ReedlineEvent::HistoryHintComplete,
ReedlineEvent::MenuRight,
ReedlineEvent::Right,
])
);
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(key(KeyCode::Right, KeyModifiers::NONE)),
ReedlineEvent::ClearScreen
);
}
#[test]
fn find_waits_for_char_then_selects() {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('f')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr('x')),
ReedlineEvent::Edit(vec![EditCommand::Select(MotionTarget::Find {
ch: 'x',
direction: Direction::Forward,
stop: FindStop::On,
})])
);
}
#[test]
fn till_backward_uses_stop_before() {
let mut helix = normal();
let _ = helix.parse_event(chr('T'));
assert_eq!(
helix.parse_event(chr('a')),
ReedlineEvent::Edit(vec![EditCommand::Select(MotionTarget::Find {
ch: 'a',
direction: Direction::Backward,
stop: FindStop::Before,
})])
);
}
#[test]
fn count_survives_into_pending() {
let mut helix = normal();
let _ = helix.parse_event(chr('2'));
let _ = helix.parse_event(chr('f'));
let target = MotionTarget::Find {
ch: 'x',
direction: Direction::Forward,
stop: FindStop::On,
};
assert_eq!(
helix.parse_event(chr('x')),
ReedlineEvent::Edit(vec![EditCommand::Select(target); 2])
);
}
#[test]
fn find_accepts_altgr_argument() {
let mut helix = normal();
let _ = helix.parse_event(chr('f'));
assert_eq!(
helix.parse_event(key(
KeyCode::Char('@'),
KeyModifiers::CONTROL | KeyModifiers::ALT
)),
ReedlineEvent::Edit(vec![EditCommand::Select(MotionTarget::Find {
ch: '@',
direction: Direction::Forward,
stop: FindStop::On,
})])
);
}
#[test]
fn altgr_char_is_not_a_command() {
let mut helix = normal();
assert_eq!(
helix.parse_event(key(
KeyCode::Char('w'),
KeyModifiers::CONTROL | KeyModifiers::ALT
)),
ReedlineEvent::None
);
assert_eq!(helix.pending, None);
}
#[test]
fn replace_waits_for_char() {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('r')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr('z')),
ReedlineEvent::Edit(vec![EditCommand::ReplaceChar('z')])
);
}
#[rstest]
#[case('h', MotionTarget::LineEdge(Direction::Backward))]
#[case('l', MotionTarget::LineEdge(Direction::Forward))]
#[case('g', MotionTarget::BufferEdge(Direction::Backward))]
#[case('e', MotionTarget::BufferEdge(Direction::Forward))]
fn goto_moves_in_normal_and_extends_in_select(#[case] c: char, #[case] target: MotionTarget) {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('g')), ReedlineEvent::None);
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![EditCommand::Move(target)])
);
let _ = helix.parse_event(chr('v'));
let _ = helix.parse_event(chr('g'));
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![EditCommand::Extend(target)])
);
}
#[test]
fn g_absorbs_without_emitting() {
let mut helix = normal();
assert_eq!(helix.parse_event(chr('g')), ReedlineEvent::None);
assert_eq!(helix.pending, Some(Pending::Goto));
}
#[rstest]
#[case('h', MotionTarget::LineEdge(Direction::Backward))]
#[case('l', MotionTarget::LineEdge(Direction::Forward))]
#[case('e', MotionTarget::BufferEdge(Direction::Forward))]
fn goto_shadows_the_bare_binding_for_the_same_key(
#[case] c: char,
#[case] target: MotionTarget,
) {
let mut helix = normal();
let bare = helix.parse_event(chr(c));
assert_ne!(bare, ReedlineEvent::Edit(vec![EditCommand::Move(target)]));
let _ = helix.parse_event(chr('g'));
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![EditCommand::Move(target)])
);
}
#[test]
fn goto_keeps_select_mode() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
let _ = helix.parse_event(chr('g'));
let _ = helix.parse_event(chr('h'));
assert_eq!(helix.mode, HelixMode::Select);
}
#[test]
fn unbound_goto_target_rejects_and_clears() {
let mut helix = normal();
let _ = helix.parse_event(chr('g'));
assert_eq!(helix.parse_event(chr('z')), ReedlineEvent::None);
assert_eq!(helix.pending, None);
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w())])
);
}
#[test]
fn esc_cancels_pending_goto() {
let mut helix = normal();
let _ = helix.parse_event(chr('g'));
let _ = helix.parse_event(key(KeyCode::Esc, KeyModifiers::NONE));
assert_eq!(helix.pending, None);
assert_eq!(
helix.parse_event(chr('h')),
ReedlineEvent::Edit(vec![EditCommand::Move(MotionTarget::Grapheme(
Direction::Backward
))])
);
}
#[test]
fn a_live_count_rejects_the_goto_prefix() {
let mut helix = normal();
let _ = helix.parse_event(chr('3'));
assert_eq!(helix.parse_event(chr('g')), ReedlineEvent::None);
assert_eq!(helix.pending, None);
assert_eq!(helix.count, None);
assert_eq!(helix.parse_event(chr('g')), ReedlineEvent::None);
assert_eq!(helix.pending, Some(Pending::Goto));
}
#[test]
fn a_typed_one_is_still_a_count() {
let mut helix = normal();
let _ = helix.parse_event(chr('1'));
assert_eq!(helix.count, Some(1));
assert_eq!(helix.parse_event(chr('g')), ReedlineEvent::None);
assert_eq!(helix.pending, None);
}
#[test]
fn esc_cancels_pending_sequence() {
let mut helix = normal();
let _ = helix.parse_event(chr('2'));
let _ = helix.parse_event(chr('f'));
let _ = helix.parse_event(key(KeyCode::Esc, KeyModifiers::NONE));
assert_eq!(helix.count, None);
assert_eq!(helix.pending, None);
assert_eq!(
helix.parse_event(chr('w')),
ReedlineEvent::Edit(vec![EditCommand::Select(w())])
);
}
#[rstest]
#[case('d', Op::Cut, Some(HelixMode::Normal))]
#[case('c', Op::Change, Some(HelixMode::Insert))]
#[case('y', Op::Yank, Some(HelixMode::Normal))]
fn operator_next_mode_is_mode_independent(
#[case] c: char,
#[case] op: Op,
#[case] next_mode: Option<HelixMode>,
) {
for mode in [HelixMode::Normal, HelixMode::Select] {
assert_eq!(
interpret(mode, None, kev(KeyCode::Char(c), KeyModifiers::NONE)),
Outcome::Execute(Action {
count: 1,
verb: Verb::OnSelection(op),
next_mode,
})
);
}
}
#[rstest]
#[case('i', Direction::Backward)]
#[case('a', Direction::Forward)]
fn insert_entries_collapse_to_an_edge(#[case] c: char, #[case] direction: Direction) {
for mode in [HelixMode::Normal, HelixMode::Select] {
assert_eq!(
interpret(mode, None, kev(KeyCode::Char(c), KeyModifiers::NONE)),
Outcome::Execute(Action {
count: 1,
verb: Verb::Collapse(direction),
next_mode: Some(HelixMode::Insert),
})
);
}
}
#[test]
fn v_toggles_between_normal_and_select() {
assert_eq!(
interpret(
HelixMode::Normal,
None,
kev(KeyCode::Char('v'), KeyModifiers::NONE)
),
Outcome::Execute(Action {
count: 1,
verb: Verb::ChangeMode,
next_mode: Some(HelixMode::Select),
})
);
assert_eq!(
interpret(
HelixMode::Select,
None,
kev(KeyCode::Char('v'), KeyModifiers::NONE)
),
Outcome::Execute(Action {
count: 1,
verb: Verb::ChangeMode,
next_mode: Some(HelixMode::Normal),
})
);
}
#[test]
fn esc_deselects_in_normal_and_leaves_select() {
assert_eq!(
interpret(
HelixMode::Normal,
None,
kev(KeyCode::Esc, KeyModifiers::NONE)
),
Outcome::Execute(Action {
count: 1,
verb: Verb::Deselect,
next_mode: None,
})
);
assert_eq!(
interpret(
HelixMode::Select,
None,
kev(KeyCode::Esc, KeyModifiers::NONE)
),
Outcome::Execute(Action {
count: 1,
verb: Verb::ChangeMode,
next_mode: Some(HelixMode::Normal),
})
);
}
#[test]
fn operators_leave_select_mode() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(chr('d')),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::CutSelection {
granularity: Granularity::CharWise,
}]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.mode, HelixMode::Normal);
}
#[test]
fn c_cuts_and_enters_insert() {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr('c')),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::CutSelection {
granularity: Granularity::CharWise,
}]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.mode, HelixMode::Insert);
}
#[test]
fn y_copies_and_stays_normal() {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr('y')),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::CopySelection]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.mode, HelixMode::Normal);
}
#[test]
fn esc_in_normal_deselects() {
let mut helix = normal();
assert_eq!(
helix.parse_event(key(KeyCode::Esc, KeyModifiers::NONE)),
ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])
);
assert_eq!(helix.mode, HelixMode::Normal);
}
#[test]
fn esc_returns_from_select_despite_table_binding() {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
let _ = helix.parse_event(key(KeyCode::Esc, KeyModifiers::NONE));
assert_eq!(helix.mode, HelixMode::Normal);
}
#[test]
fn enter_collapses_then_submits_and_enters_insert() {
let mut helix = normal();
assert_eq!(
helix.parse_event(key(KeyCode::Enter, KeyModifiers::NONE)),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::CollapseSelection(Direction::Forward)]),
ReedlineEvent::Enter,
])
);
assert_eq!(helix.mode, HelixMode::Insert);
}
#[rstest]
#[case('u', EditCommand::Undo)]
#[case('U', EditCommand::Redo)]
fn undo_redo_lower_to_bare_edits(#[case] c: char, #[case] expected: EditCommand) {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![expected])
);
}
#[test]
fn count_repeats_undo() {
let mut helix = normal();
let _ = helix.parse_event(chr('3'));
assert_eq!(
helix.parse_event(chr('u')),
ReedlineEvent::Edit(vec![EditCommand::Undo; 3])
);
assert_eq!(helix.count, None);
}
#[rstest]
#[case('u', EditCommand::Undo)]
#[case('U', EditCommand::Redo)]
fn undo_redo_keep_select_mode(#[case] c: char, #[case] expected: EditCommand) {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Edit(vec![expected])
);
assert_eq!(helix.mode, HelixMode::Select);
}
#[rstest]
#[case('p', Direction::Forward)]
#[case('P', Direction::Backward)]
fn paste_carries_the_edge_direction(#[case] c: char, #[case] direction: Direction) {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::PasteAtSelectionEdge {
direction,
count: 1
}]),
ReedlineEvent::Repaint,
])
);
}
#[test]
fn paste_carries_the_count_in_the_command() {
let mut helix = normal();
let _ = helix.parse_event(chr('3'));
assert_eq!(
helix.parse_event(chr('p')),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![EditCommand::PasteAtSelectionEdge {
direction: Direction::Forward,
count: 3,
}]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.count, None);
}
#[rstest]
#[case('p')]
#[case('P')]
fn paste_leaves_select_mode(#[case] c: char) {
let mut helix = normal();
let _ = helix.parse_event(chr('v'));
let _ = helix.parse_event(chr(c));
assert_eq!(helix.mode, HelixMode::Normal);
}
#[rstest]
#[case('p', KeyModifiers::NONE, Direction::Forward)]
#[case('P', KeyModifiers::SHIFT, Direction::Backward)]
fn paste_next_mode_is_mode_independent(
#[case] c: char,
#[case] modifiers: KeyModifiers,
#[case] direction: Direction,
) {
for mode in [HelixMode::Normal, HelixMode::Select] {
assert_eq!(
interpret(mode, None, kev(KeyCode::Char(c), modifiers)),
Outcome::Execute(Action {
count: 1,
verb: Verb::Paste(direction),
next_mode: Some(HelixMode::Normal),
})
);
}
}
#[test]
fn paste_event_produces_insert_string() {
let mut helix = Helix::default();
let paste = ReedlineRawEvent::try_from(Event::Paste("hello".to_string())).unwrap();
assert_eq!(
helix.parse_event(paste),
ReedlineEvent::Edit(vec![EditCommand::InsertString("hello".to_string())])
);
}
#[rstest]
#[case('o', EditCommand::InsertNewlineBelow)]
#[case('O', EditCommand::InsertNewlineAbove)]
fn open_line_enters_insert(#[case] c: char, #[case] expected: EditCommand) {
let mut helix = normal();
assert_eq!(
helix.parse_event(chr(c)),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![expected]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.mode, HelixMode::Insert);
}
#[rstest]
#[case('o', KeyModifiers::NONE, Direction::Forward)]
#[case('O', KeyModifiers::SHIFT, Direction::Backward)]
fn open_line_next_mode_is_mode_independent(
#[case] c: char,
#[case] modifiers: KeyModifiers,
#[case] direction: Direction,
) {
for mode in [HelixMode::Normal, HelixMode::Select] {
assert_eq!(
interpret(mode, None, kev(KeyCode::Char(c), modifiers)),
Outcome::Execute(Action {
count: 1,
verb: Verb::OpenLine(direction),
next_mode: Some(HelixMode::Insert),
})
);
}
}
#[test]
fn count_seeks_once_then_opens_above() {
let mut helix = normal();
let _ = helix.parse_event(chr('3'));
assert_eq!(
helix.parse_event(chr('o')),
ReedlineEvent::Multiple(vec![
ReedlineEvent::Edit(vec![
EditCommand::InsertNewlineBelow,
EditCommand::InsertNewlineAbove,
EditCommand::InsertNewlineAbove,
]),
ReedlineEvent::Repaint,
])
);
assert_eq!(helix.count, None);
}
}