use escriba_core::{Action, Mode, Motion, TextObject};
use escriba_mode::{ModalState, OpState, OperatorPending};
use crate::{Key, Keymap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FindSpec {
pub ch: char,
pub backward: bool,
pub till: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MarkKey {
Set,
GotoExact,
GotoLine,
}
enum Claim {
Consumed,
Compose { action: Action, times: u32 },
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum OperandCount {
SelfCounted,
Drained,
}
struct OperandCapture {
name: &'static str,
claim: fn(&mut KeyPipeline, &ModalState, Key) -> Option<Claim>,
count: OperandCount,
}
static OPERAND_CHAIN: &[OperandCapture] = &[
OperandCapture {
name: "mark",
claim: KeyPipeline::claim_mark,
count: OperandCount::Drained,
},
OperandCapture {
name: "object",
claim: KeyPipeline::claim_object,
count: OperandCount::SelfCounted,
},
OperandCapture {
name: "find",
claim: KeyPipeline::claim_find,
count: OperandCount::Drained,
},
OperandCapture {
name: "replace",
claim: KeyPipeline::claim_replace,
count: OperandCount::Drained,
},
];
#[must_use]
pub fn operand_capture_order() -> Vec<&'static str> {
OPERAND_CHAIN.iter().map(|c| c.name).collect()
}
enum SeqStep {
Pending,
Resolved(Action),
Passthrough,
}
#[derive(Debug, Clone)]
pub struct KeyPipeline {
keymap: Keymap,
pending_keys: Vec<Key>,
op: zenmai::Stateful<OperatorPending>,
pending_object: Option<bool>,
pending_find: Option<FindSpec>,
pending_replace: bool,
pending_mark: Option<MarkKey>,
last_find: Option<FindSpec>,
}
impl Default for KeyPipeline {
fn default() -> Self {
Self::default_vim()
}
}
impl KeyPipeline {
#[must_use]
pub fn new(keymap: Keymap) -> Self {
Self {
keymap,
pending_keys: Vec::new(),
op: zenmai::Stateful::new(OpState::Resting),
pending_object: None,
pending_find: None,
pending_replace: false,
pending_mark: None,
last_find: None,
}
}
#[must_use]
pub fn default_vim() -> Self {
Self::new(Keymap::default_vim())
}
#[must_use]
pub const fn keymap(&self) -> &Keymap {
&self.keymap
}
pub const fn keymap_mut(&mut self) -> &mut Keymap {
&mut self.keymap
}
#[must_use]
pub fn pending_keys(&self) -> &[Key] {
&self.pending_keys
}
#[must_use]
pub fn op_state(&self) -> &OpState {
self.op.state()
}
#[must_use]
pub const fn last_find(&self) -> Option<FindSpec> {
self.last_find
}
#[must_use]
pub fn is_pending(&self) -> bool {
!self.pending_keys.is_empty()
|| self.pending_object.is_some()
|| self.pending_find.is_some()
|| self.pending_replace
|| self.pending_mark.is_some()
|| !matches!(self.op.state(), OpState::Resting)
}
pub fn reset(&mut self) {
self.pending_keys.clear();
self.op = zenmai::Stateful::new(OpState::Resting);
self.pending_object = None;
self.pending_find = None;
self.pending_replace = false;
self.pending_mark = None;
}
pub fn feed(&mut self, modal: &mut ModalState, key: &Key) -> Vec<(Action, u32)> {
let units = self.resolve_key(modal, key);
let mut out = Vec::with_capacity(units.len());
for (action, count) in units {
out.extend(self.compose(&action, count));
}
out
}
pub fn compose(&mut self, action: &Action, count: u32) -> Vec<(Action, u32)> {
let (action, count) = match action {
Action::Move(Motion::Column(_)) => (Action::Move(Motion::Column(count)), 1),
a => (a.clone(), count),
};
self.op.dispatch((action, count))
}
pub fn resolve_key(&mut self, modal: &mut ModalState, key: &Key) -> Vec<(Action, u32)> {
for cap in OPERAND_CHAIN {
let Some(claim) = (cap.claim)(self, modal, *key) else {
continue;
};
return match claim {
Claim::Consumed => Vec::new(),
Claim::Compose { action, times } => match cap.count {
OperandCount::SelfCounted => vec![(action, 1); times.max(1) as usize],
OperandCount::Drained => {
let n = modal.pending_count().unwrap_or(1);
modal.clear_count();
vec![(action, n)]
}
},
};
}
match self.step_sequence(modal.mode(), *key) {
SeqStep::Pending => return Vec::new(),
SeqStep::Resolved(action) => {
let count = modal.pending_count().unwrap_or(1);
modal.clear_count();
return vec![(action, 1); count as usize];
}
SeqStep::Passthrough => {}
}
let counted = self.keymap.dispatch(modal, key);
if matches!(counted.action, Action::Pending) {
match key {
Key::Char(c) => {
if let Some(d) = c.to_digit(10) {
modal.append_count(d);
}
}
Key::Esc => {
if self.operator_armed() {
self.disarm();
}
modal.clear_count();
}
_ => {}
}
return Vec::new();
}
modal.clear_count();
vec![(counted.action, counted.count)]
}
fn step_sequence(&mut self, mode: Mode, key: Key) -> SeqStep {
if !matches!(mode, Mode::Normal | Mode::Visual | Mode::VisualLine) {
return SeqStep::Passthrough;
}
if !self.pending_keys.is_empty() {
let mut seq = self.pending_keys.clone();
seq.push(key);
if let Some(b) = self.keymap.lookup_sequence(mode, &seq) {
let action = b.action.clone();
self.pending_keys.clear();
return SeqStep::Resolved(action);
}
if self.keymap.is_sequence_prefix(mode, &seq) {
self.pending_keys = seq;
return SeqStep::Pending;
}
self.pending_keys.clear();
}
let start = [key];
if self.keymap.is_sequence_prefix(mode, &start) && self.keymap.lookup(mode, &key).is_none()
{
self.pending_keys = start.to_vec();
return SeqStep::Pending;
}
SeqStep::Passthrough
}
fn operator_armed(&self) -> bool {
matches!(self.op.state(), OpState::Awaiting { .. })
}
fn disarm(&mut self) {
self.op.dispatch((Action::ChangeMode(Mode::Normal), 1));
}
fn claim_mark(&mut self, modal: &ModalState, key: Key) -> Option<Claim> {
if let Some(kind) = self.pending_mark.take() {
let Key::Char(name) = key else {
if self.operator_armed() {
self.disarm();
}
return Some(Claim::Consumed);
};
let action = match kind {
MarkKey::Set => Action::SetMark(name),
MarkKey::GotoExact => Action::Move(Motion::MarkExact(name)),
MarkKey::GotoLine => Action::Move(Motion::MarkLine(name)),
};
return Some(Claim::Compose { action, times: 1 });
}
if !matches!(modal.mode(), Mode::Normal | Mode::Visual) {
return None;
}
if self.pending_object.is_some() || !self.pending_keys.is_empty() {
return None;
}
let Key::Char(c) = key else { return None };
let kind = match c {
'm' => MarkKey::Set,
'`' => MarkKey::GotoExact,
'\'' => MarkKey::GotoLine,
_ => return None,
};
self.pending_mark = Some(kind);
Some(Claim::Consumed)
}
fn claim_object(&mut self, _modal: &ModalState, key: Key) -> Option<Claim> {
let Key::Char(c) = key else {
if self.pending_object.take().is_some() {
self.disarm();
return Some(Claim::Consumed);
}
return None;
};
if let Some(around) = self.pending_object.take() {
let object = object_for(c, around);
let OpState::Awaiting { op, count } = *self.op.state() else {
return Some(Claim::Consumed);
};
self.disarm();
let Some(object) = object else {
return Some(Claim::Consumed);
};
return Some(Claim::Compose {
action: Action::ApplyOperatorObject { op, object },
times: count,
});
}
if matches!(c, 'i' | 'a') && self.operator_armed() {
self.pending_object = Some(c == 'a');
return Some(Claim::Consumed);
}
None
}
fn claim_find(&mut self, modal: &ModalState, key: Key) -> Option<Claim> {
if let Some(spec) = self.pending_find.take() {
let Key::Char(ch) = key else {
if self.operator_armed() {
self.disarm();
}
return Some(Claim::Consumed);
};
let spec = FindSpec { ch, ..spec };
self.last_find = Some(spec);
return Some(Claim::Compose {
action: Action::Move(Motion::FindChar {
ch,
backward: spec.backward,
till: spec.till,
}),
times: 1,
});
}
if modal.mode() != Mode::Normal && modal.mode() != Mode::Visual {
return None;
}
if !self.pending_keys.is_empty() {
return None;
}
let Key::Char(c) = key else { return None };
let (backward, till) = match c {
'f' => (false, false),
'F' => (true, false),
't' => (false, true),
'T' => (true, true),
_ => return None,
};
self.pending_find = Some(FindSpec {
ch: '\0',
backward,
till,
});
Some(Claim::Consumed)
}
fn claim_replace(&mut self, modal: &ModalState, key: Key) -> Option<Claim> {
if self.pending_replace {
self.pending_replace = false;
let Key::Char(ch) = key else {
return Some(Claim::Consumed);
};
return Some(Claim::Compose {
action: Action::ReplaceChar(ch),
times: 1,
});
}
if modal.mode() != Mode::Normal && modal.mode() != Mode::Visual {
return None;
}
if !self.pending_keys.is_empty() {
return None;
}
if key != Key::Char('r') {
return None;
}
if self.operator_armed() {
self.disarm();
return Some(Claim::Consumed);
}
self.pending_replace = true;
Some(Claim::Consumed)
}
}
fn object_for(c: char, around: bool) -> Option<TextObject> {
let delimited = |open, close| {
Some(TextObject::Delimited {
open,
close,
around,
})
};
match c {
'w' => Some(TextObject::Word { around }),
'(' | ')' | 'b' => delimited('(', ')'),
'{' | '}' | 'B' => delimited('{', '}'),
'[' | ']' => delimited('[', ']'),
'<' | '>' => delimited('<', '>'),
'"' => delimited('"', '"'),
'\'' => delimited('\'', '\''),
'`' => delimited('`', '`'),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use escriba_core::{InsertAt, Operator};
fn normal() -> ModalState {
ModalState::new()
}
fn type_keys(p: &mut KeyPipeline, m: &mut ModalState, keys: &str) -> Vec<(Action, u32)> {
keys.chars()
.flat_map(|c| p.feed(m, &Key::Char(c)))
.collect()
}
#[test]
fn dw_composes_one_apply_operator() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert!(p.feed(&mut m, &Key::Char('d')).is_empty(), "d waits");
assert!(p.is_pending());
assert_eq!(
p.feed(&mut m, &Key::Char('w')),
vec![(
Action::ApplyOperator {
op: Operator::Delete,
motion: Motion::WordStartNext
},
1
)]
);
assert!(!p.is_pending());
}
#[test]
fn three_d_two_w_is_one_operation_at_count_six() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "3d2w"),
vec![(
Action::ApplyOperator {
op: Operator::Delete,
motion: Motion::WordStartNext
},
6
)]
);
assert_eq!(m.pending_count(), None, "the count was drained");
}
#[test]
fn ciw_composes_one_object_operation() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "ciw"),
vec![(
Action::ApplyOperatorObject {
op: Operator::Change,
object: TextObject::Word { around: false }
},
1
)]
);
assert!(!p.is_pending());
}
#[test]
fn di_paren_is_an_object_not_insert() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "di("),
vec![(
Action::ApplyOperatorObject {
op: Operator::Delete,
object: TextObject::Delimited {
open: '(',
close: ')',
around: false
}
},
1
)]
);
}
#[test]
fn a_counted_object_emits_one_unit_per_repeat() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
let steps = type_keys(&mut p, &mut m, "2daw");
let obj = Action::ApplyOperatorObject {
op: Operator::Delete,
object: TextObject::Word { around: true },
};
assert_eq!(steps, vec![(obj.clone(), 1), (obj, 1)]);
}
#[test]
fn fx_is_a_find_motion_and_is_remembered() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
let find = Action::Move(Motion::FindChar {
ch: 'x',
backward: false,
till: false,
});
assert_eq!(type_keys(&mut p, &mut m, "fx"), vec![(find, 1)]);
assert_eq!(
p.last_find(),
Some(FindSpec {
ch: 'x',
backward: false,
till: false
})
);
}
#[test]
fn a_counted_find_carries_the_count_once() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
let steps = type_keys(&mut p, &mut m, "3f.");
assert_eq!(steps.len(), 1);
assert_eq!(steps[0].1, 3, "3f. is the third dot, not the ninth");
}
#[test]
fn dtx_composes_a_till_find() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "dtx"),
vec![(
Action::ApplyOperator {
op: Operator::Delete,
motion: Motion::FindChar {
ch: 'x',
backward: false,
till: true
}
},
1
)]
);
}
#[test]
fn r_takes_its_operand_even_when_bound() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "rw"),
vec![(Action::ReplaceChar('w'), 1)]
);
}
#[test]
fn dr_cancels_the_operator() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert!(type_keys(&mut p, &mut m, "dr").is_empty());
assert!(!p.is_pending(), "the typo disarmed rather than arming r");
}
#[test]
fn gg_resolves_the_sequence() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert!(p.feed(&mut m, &Key::Char('g')).is_empty());
assert_eq!(p.pending_keys(), &[Key::Char('g')]);
assert!(p.is_pending());
assert_eq!(
p.feed(&mut m, &Key::Char('g')),
vec![(Action::Move(Motion::DocStart), 1)]
);
assert!(p.pending_keys().is_empty());
}
#[test]
fn dgg_composes_through_the_sequence() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "dgg"),
vec![(
Action::ApplyOperator {
op: Operator::Delete,
motion: Motion::DocStart
},
1
)]
);
}
#[test]
fn capital_g_is_a_single_key() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
p.feed(&mut m, &Key::Char('G')),
vec![(Action::Move(Motion::DocEnd), 1)]
);
}
#[test]
fn digit_counts_accumulate() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert!(p.feed(&mut m, &Key::Char('1')).is_empty());
assert!(p.feed(&mut m, &Key::Char('5')).is_empty());
assert_eq!(m.pending_count(), Some(15));
assert_eq!(
p.feed(&mut m, &Key::Char('j')),
vec![(Action::Move(Motion::Down), 15)]
);
assert_eq!(m.pending_count(), None);
}
#[test]
fn five_j() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "5j"),
vec![(Action::Move(Motion::Down), 5)]
);
}
#[test]
fn zero_is_a_motion_alone_and_a_digit_mid_count() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
p.feed(&mut m, &Key::Char('0')),
vec![(Action::Move(Motion::LineStart), 1)]
);
assert_eq!(
type_keys(&mut p, &mut m, "10j"),
vec![(Action::Move(Motion::Down), 10)]
);
}
#[test]
fn esc_cancels_a_half_typed_operator_and_its_count() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
type_keys(&mut p, &mut m, "3d");
assert!(p.is_pending());
assert!(p.feed(&mut m, &Key::Esc).is_empty(), "Esc is dropped");
assert!(!p.is_pending());
assert_eq!(
p.feed(&mut m, &Key::Char('w')),
vec![(Action::Move(Motion::WordStartNext), 1)]
);
type_keys(&mut p, &mut m, "5");
p.feed(&mut m, &Key::Esc);
assert_eq!(m.pending_count(), None, "a bare count dies with Esc too");
}
#[test]
fn a_sequence_key_after_an_operator_keeps_it_armed() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
type_keys(&mut p, &mut m, "dg");
assert!(matches!(p.op_state(), OpState::Awaiting { .. }));
}
#[test]
fn esc_cancels_a_half_typed_object() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
type_keys(&mut p, &mut m, "di");
assert!(p.is_pending());
assert!(p.feed(&mut m, &Key::Esc).is_empty());
assert!(!p.is_pending(), "object AND operator disarmed");
assert_eq!(
p.feed(&mut m, &Key::Char('w')),
vec![(Action::Move(Motion::WordStartNext), 1)]
);
}
#[test]
fn is_pending_truth_table() {
let cases: &[(&str, bool)] = &[
("", false),
("d", true),
("di", true),
("f", true),
("df", true),
("r", true),
("m", true),
("`", true),
("g", true),
("5", false), ("dw", false),
("fx", false),
("rx", false),
("ma", false),
("gg", false),
("j", false),
];
for (keys, want) in cases {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
type_keys(&mut p, &mut m, keys);
assert_eq!(p.is_pending(), *want, "after {keys:?}");
}
}
#[test]
fn reset_abandons_everything_half_typed() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
type_keys(&mut p, &mut m, "fx");
type_keys(&mut p, &mut m, "dg");
assert!(p.is_pending());
p.reset();
assert!(!p.is_pending());
assert!(p.last_find().is_some(), "memory survives a reset");
}
#[test]
fn insert_mode_keys_pass_through_as_text() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
p.feed(&mut m, &Key::Char('i')),
vec![(Action::EnterInsert(InsertAt::Caret), 1)]
);
m.enter_insert();
for c in ['g', 'g', 'f', 'd', 'r', 'm', '5', 'i'] {
assert_eq!(
p.feed(&mut m, &Key::Char(c)),
vec![(Action::InsertChar(c), 1)],
"{c:?} in Insert is text"
);
assert!(!p.is_pending(), "{c:?} armed nothing in Insert");
}
}
#[test]
fn marks_capture_their_letter() {
let (mut p, mut m) = (KeyPipeline::default_vim(), normal());
assert_eq!(
type_keys(&mut p, &mut m, "ma"),
vec![(Action::SetMark('a'), 1)]
);
assert_eq!(
type_keys(&mut p, &mut m, "d`a"),
vec![(
Action::ApplyOperator {
op: Operator::Delete,
motion: Motion::MarkExact('a')
},
1
)]
);
}
#[test]
fn a_column_count_is_an_argument() {
let mut p = KeyPipeline::default_vim();
assert_eq!(
p.compose(&Action::Move(Motion::Column(0)), 40),
vec![(Action::Move(Motion::Column(40)), 1)]
);
}
#[test]
fn the_chain_order_is_mark_object_find_replace() {
assert_eq!(
operand_capture_order(),
vec!["mark", "object", "find", "replace"]
);
}
}