use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use crate::action::{DirectKind, Expr, MotionExpr, MotionKind, Operator, Target, Token};
use crate::config::{
BRACKET_NEXT_BINDINGS, BRACKET_PREV_BINDINGS, CTRL_W_BINDINGS, GOTO_BINDINGS, KeySig, Keymap,
OBJECT_BINDINGS, OP_PENDING_BINDINGS, WINDOW_BINDINGS, Z_BINDINGS,
};
use crate::mode::Mode;
#[derive(Debug)]
pub(in crate::app) enum Parse {
Complete(Expr),
Incomplete,
Invalid,
}
#[derive(Debug, Clone, Copy)]
enum ParseCtx {
Initial,
LeaderPending,
OpPending,
ObjectExpected,
GotoPending,
CharArgPending,
ZPending,
WindowPending,
CtrlWPending,
BracketPending,
SurroundCharPending,
}
fn context_of(prev: &[Token]) -> ParseCtx {
use Token::*;
if is_surround_char_pending(prev) {
return ParseCtx::SurroundCharPending;
}
let mut last: Option<&Token> = None;
for t in prev.iter().rev() {
if !matches!(t, Count(_)) {
last = Some(t);
break;
}
}
match last {
None => ParseCtx::Initial,
Some(LeaderPrefix) => ParseCtx::LeaderPending,
Some(Op(_) | SurroundAddPrefix) => ParseCtx::OpPending,
Some(Scope(_)) => ParseCtx::ObjectExpected,
Some(GotoPrefix) => ParseCtx::GotoPending,
Some(FindCharPrefix { .. } | ReplaceCharPrefix) => ParseCtx::CharArgPending,
Some(ZPrefix) => ParseCtx::ZPending,
Some(WindowPrefix) => ParseCtx::WindowPending,
Some(CtrlWPrefix) => ParseCtx::CtrlWPending,
Some(BracketPrefix { .. }) => ParseCtx::BracketPending,
_ => ParseCtx::Initial,
}
}
fn is_surround_char_pending(prev: &[Token]) -> bool {
use Token::*;
if matches!(
prev,
[.., SurroundDeletePrefix]
| [.., SurroundChangePrefix]
| [.., SurroundChangePrefix, SurroundChar(_)]
) {
return true;
}
for (i, t) in prev.iter().enumerate().rev() {
if matches!(t, SurroundAddPrefix) {
return surround_add_target_complete(&prev[i + 1..]);
}
}
false
}
fn surround_add_target_complete(tail: &[Token]) -> bool {
use Token::*;
let (_, after) = take_count(tail);
matches!(
after,
[Motion(_)]
| [FindCharPrefix { .. }, Motion(_)]
| [GotoPrefix, Motion(_)]
| [Scope(_), Object(_)]
| [SelfDouble(_)]
)
}
pub(in crate::app) fn tokenize(
km: &Keymap,
prev: &[Token],
mode: Mode,
key: KeyEvent,
) -> Option<Token> {
debug_assert_eq!(mode, Mode::Normal);
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('r') {
return Some(Token::Direct(DirectKind::Redo));
}
if prev.is_empty()
&& key.modifiers.contains(KeyModifiers::CONTROL)
&& key.code == KeyCode::Char('w')
{
return Some(Token::CtrlWPrefix);
}
let ctx = context_of(prev);
let code = key.code;
if let Some(c) = ascii_digit(code) {
let already_counting = matches!(prev.last(), Some(Token::Count(_)));
let d = c.to_digit(10).unwrap();
return match (ctx, c, already_counting) {
(ParseCtx::Initial, '0', false) => Some(Token::Motion(MotionKind::LineStart)),
(_, '0', true) => Some(Token::Count(0)),
(ParseCtx::Initial | ParseCtx::OpPending, '1'..='9', _) => Some(Token::Count(d)),
_ => None,
};
}
let sig = KeySig::from_event(key);
match ctx {
ParseCtx::Initial => km.initial.get(&sig).copied(),
ParseCtx::LeaderPending => km.leader.get(&sig).copied(),
ParseCtx::OpPending => op_pending_token(code, prev),
ParseCtx::ObjectExpected => object_token(code),
ParseCtx::GotoPending => goto_pending_token(code),
ParseCtx::CharArgPending => char_arg_token(code, prev),
ParseCtx::ZPending => z_pending_token(code),
ParseCtx::WindowPending => window_pending_token(code),
ParseCtx::CtrlWPending => ctrl_w_pending_token(code),
ParseCtx::BracketPending => bracket_pending_token(code, prev),
ParseCtx::SurroundCharPending => surround_char_token(code),
}
}
fn surround_char_token(code: KeyCode) -> Option<Token> {
match code {
KeyCode::Char(c) => Some(Token::SurroundChar(c)),
_ => None,
}
}
fn bracket_pending_token(code: KeyCode, prev: &[Token]) -> Option<Token> {
let forward = prev.iter().rev().find_map(|t| match t {
Token::BracketPrefix { forward } => Some(*forward),
_ => None,
})?;
let table = if forward {
BRACKET_NEXT_BINDINGS
} else {
BRACKET_PREV_BINDINGS
};
table.iter().find(|b| b.matches(code)).map(|b| b.token)
}
fn window_pending_token(code: crossterm::event::KeyCode) -> Option<Token> {
WINDOW_BINDINGS
.iter()
.find(|b| b.matches(code))
.map(|b| b.token)
}
fn ctrl_w_pending_token(code: crossterm::event::KeyCode) -> Option<Token> {
CTRL_W_BINDINGS
.iter()
.find(|b| b.matches(code))
.map(|b| b.token)
}
fn char_arg_token(code: KeyCode, prev: &[Token]) -> Option<Token> {
let prefix = prev
.iter()
.rev()
.find(|t| matches!(t, Token::FindCharPrefix { .. } | Token::ReplaceCharPrefix))?;
let KeyCode::Char(ch) = code else {
return None;
};
match prefix {
Token::FindCharPrefix { forward, till } => Some(Token::Motion(MotionKind::FindChar {
ch,
forward: *forward,
till: *till,
})),
Token::ReplaceCharPrefix => Some(Token::Direct(DirectKind::ReplaceChar { ch })),
_ => None,
}
}
fn z_pending_token(code: KeyCode) -> Option<Token> {
Z_BINDINGS.iter().find(|b| b.matches(code)).map(|b| b.token)
}
fn goto_pending_token(code: KeyCode) -> Option<Token> {
GOTO_BINDINGS
.iter()
.find(|b| b.matches(code))
.map(|b| b.token)
}
fn op_pending_token(code: KeyCode, prev: &[Token]) -> Option<Token> {
let pending_op = prev.iter().rev().find_map(|t| match t {
Token::Op(o) => Some(*o),
_ => None,
})?;
if matches!(code, KeyCode::Char('s'))
&& matches!(prev.last(), Some(Token::Op(_)))
&& matches!(
pending_op,
Operator::Yank | Operator::Change | Operator::Delete
)
{
return Some(match pending_op {
Operator::Yank => Token::SurroundAddPrefix,
Operator::Change => Token::SurroundChangePrefix,
Operator::Delete => Token::SurroundDeletePrefix,
_ => unreachable!(),
});
}
if matches!(code, KeyCode::Char('s')) && matches!(prev.last(), Some(Token::SurroundAddPrefix)) {
return Some(Token::SelfDouble(pending_op));
}
let same_key = matches!(
(pending_op, code),
(Operator::Delete, KeyCode::Char('d'))
| (Operator::Yank, KeyCode::Char('y'))
| (Operator::Change, KeyCode::Char('c'))
| (Operator::Indent, KeyCode::Char('>'))
| (Operator::Dedent, KeyCode::Char('<'))
| (Operator::Comment, KeyCode::Char('c'))
| (Operator::BlockComment, KeyCode::Char('c'))
);
if same_key {
return Some(Token::SelfDouble(pending_op));
}
OP_PENDING_BINDINGS
.iter()
.find(|b| b.matches(code))
.map(|b| b.token)
}
fn ascii_digit(code: KeyCode) -> Option<char> {
match code {
KeyCode::Char(c) if c.is_ascii_digit() => Some(c),
_ => None,
}
}
fn object_token(code: KeyCode) -> Option<Token> {
OBJECT_BINDINGS
.iter()
.find(|b| b.matches(code))
.map(|b| b.token)
}
fn take_count(tokens: &[Token]) -> (u32, &[Token]) {
let mut count: u32 = 0;
let mut i = 0;
while let Some(Token::Count(d)) = tokens.get(i) {
count = count.saturating_mul(10).saturating_add(*d);
i += 1;
}
if i == 0 {
(1, tokens)
} else {
(count.max(1), &tokens[i..])
}
}
pub(in crate::app) fn classify(tokens: &[Token]) -> Parse {
if let Some(expr) = build_expr(tokens) {
return Parse::Complete(expr);
}
if is_valid_prefix(tokens) {
return Parse::Incomplete;
}
Parse::Invalid
}
fn build_expr(tokens: &[Token]) -> Option<Expr> {
use Token::*;
let (outer_count, rest) = take_count(tokens);
match rest {
[Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[Motion(m)] => Some(Expr::Motion(MotionExpr {
motion: *m,
count: outer_count,
})),
[FindCharPrefix { .. }, Motion(m)] => Some(Expr::Motion(MotionExpr {
motion: *m,
count: outer_count,
})),
[LeaderPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[LeaderPrefix, SelfDouble(op)] => Some(Expr::Op {
op: *op,
target: Target::LineWise,
outer_count,
}),
[LeaderPrefix, WindowPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[CtrlWPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[BracketPrefix { .. }, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[GotoPrefix, GotoPrefix] => Some(Expr::Motion(MotionExpr {
motion: MotionKind::FileStart,
count: outer_count,
})),
[GotoPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[GotoPrefix, Motion(m)] => Some(Expr::Motion(MotionExpr {
motion: *m,
count: outer_count,
})),
[GotoPrefix, Op(op), inner @ ..] => build_op_expr(*op, inner, outer_count),
[ZPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[ReplaceCharPrefix, Direct(d)] => Some(Expr::Direct {
kind: *d,
count: outer_count,
}),
[Op(Operator::Yank), SurroundAddPrefix, inner @ ..] => build_surround_add(inner),
[
Op(Operator::Change),
SurroundChangePrefix,
SurroundChar(from),
SurroundChar(to),
] => Some(Expr::SurroundChange {
from: *from,
to: *to,
}),
[Op(Operator::Delete), SurroundDeletePrefix, SurroundChar(ch)] => {
Some(Expr::SurroundDelete { ch: *ch })
}
[Op(op), inner @ ..] => build_op_expr(*op, inner, outer_count),
_ => None,
}
}
fn build_surround_add(inner: &[Token]) -> Option<Expr> {
use Token::*;
let last = inner.last()?;
let SurroundChar(ch) = last else {
return None;
};
let target_tokens = &inner[..inner.len() - 1];
let (motion_count, body) = take_count(target_tokens);
let target = match body {
[SelfDouble(_)] => Target::LineWise,
[Motion(m)] | [FindCharPrefix { .. }, Motion(m)] | [GotoPrefix, Motion(m)] => {
Target::Motion(MotionExpr {
motion: *m,
count: motion_count,
})
}
[Scope(s), Object(o)] if motion_count == 1 => Target::TextObject {
scope: *s,
object: *o,
},
_ => return None,
};
Some(Expr::SurroundAdd { target, ch: *ch })
}
fn build_op_expr(op: Operator, after_op: &[Token], outer_count: u32) -> Option<Expr> {
use Token::*;
let (motion_count, body) = take_count(after_op);
match body {
[SelfDouble(_)] => Some(Expr::Op {
op,
target: Target::LineWise,
outer_count: outer_count.saturating_mul(motion_count),
}),
[Motion(m)] => Some(Expr::Op {
op,
target: Target::Motion(MotionExpr {
motion: *m,
count: motion_count,
}),
outer_count,
}),
[FindCharPrefix { .. }, Motion(m)] => Some(Expr::Op {
op,
target: Target::Motion(MotionExpr {
motion: *m,
count: motion_count,
}),
outer_count,
}),
[GotoPrefix, Motion(m)] => Some(Expr::Op {
op,
target: Target::Motion(MotionExpr {
motion: *m,
count: motion_count,
}),
outer_count,
}),
[GotoPrefix, Direct(DirectKind::SearchSelectNext { reverse })] => Some(Expr::Op {
op,
target: Target::SearchMatch { reverse: *reverse },
outer_count: outer_count.saturating_mul(motion_count),
}),
[Scope(s), Object(o)] if motion_count == 1 => Some(Expr::Op {
op,
target: Target::TextObject {
scope: *s,
object: *o,
},
outer_count,
}),
_ => None,
}
}
fn is_valid_prefix(tokens: &[Token]) -> bool {
use Token::*;
let (_, rest) = take_count(tokens);
match rest {
[] => true, [LeaderPrefix] => true, [LeaderPrefix, WindowPrefix] => true, [CtrlWPrefix] => true, [GotoPrefix] => true, [BracketPrefix { .. }] => true, [ZPrefix] => true, [FindCharPrefix { .. }] => true, [ReplaceCharPrefix] => true, [Op(_)] => true, [Op(_), Scope(_)] => true, [Op(_), FindCharPrefix { .. }] => true, [Op(_), GotoPrefix] => true, [Op(_), Count(_), ..] => {
let after_op = &rest[1..];
let (_, after_inner_count) = take_count(after_op);
matches!(after_inner_count, [] | [Scope(_)] | [FindCharPrefix { .. }])
}
[GotoPrefix, after @ ..] if matches!(after.first(), Some(Op(_))) => is_valid_prefix(after),
[Op(Operator::Delete), SurroundDeletePrefix] => true,
[Op(Operator::Change), SurroundChangePrefix] => true,
[Op(Operator::Change), SurroundChangePrefix, SurroundChar(_)] => true,
[Op(Operator::Yank), SurroundAddPrefix, after @ ..] => is_valid_ys_tail(after),
_ => false,
}
}
fn is_valid_ys_tail(tail: &[Token]) -> bool {
use Token::*;
let (_, after) = take_count(tail);
matches!(
after,
[] | [Scope(_)]
| [FindCharPrefix { .. }]
| [GotoPrefix]
| [Motion(_)]
| [FindCharPrefix { .. }, Motion(_)]
| [GotoPrefix, Motion(_)]
| [Scope(_), Object(_)]
| [SelfDouble(_)]
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::{Object, Operator, Scope as ScopeKind};
fn complete(toks: &[Token]) -> Option<Expr> {
match classify(toks) {
Parse::Complete(e) => Some(e),
_ => None,
}
}
fn incomplete(toks: &[Token]) -> bool {
matches!(classify(toks), Parse::Incomplete)
}
fn invalid(toks: &[Token]) -> bool {
matches!(classify(toks), Parse::Invalid)
}
#[test]
fn ds_complete_with_one_char() {
let toks = [
Token::Op(Operator::Delete),
Token::SurroundDeletePrefix,
Token::SurroundChar('"'),
];
assert_eq!(complete(&toks), Some(Expr::SurroundDelete { ch: '"' }));
}
#[test]
fn ds_prefix_alone_is_incomplete() {
assert!(incomplete(&[
Token::Op(Operator::Delete),
Token::SurroundDeletePrefix
]));
}
#[test]
fn cs_needs_two_chars() {
let prefix = [Token::Op(Operator::Change), Token::SurroundChangePrefix];
assert!(incomplete(&prefix));
let one_char = [
Token::Op(Operator::Change),
Token::SurroundChangePrefix,
Token::SurroundChar('"'),
];
assert!(incomplete(&one_char));
let full = [
Token::Op(Operator::Change),
Token::SurroundChangePrefix,
Token::SurroundChar('"'),
Token::SurroundChar('\''),
];
assert_eq!(
complete(&full),
Some(Expr::SurroundChange {
from: '"',
to: '\''
})
);
}
#[test]
fn ys_with_text_object() {
let toks = [
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::Scope(ScopeKind::Inner),
Token::Object(Object::Word),
Token::SurroundChar('"'),
];
let expected = Expr::SurroundAdd {
target: Target::TextObject {
scope: ScopeKind::Inner,
object: Object::Word,
},
ch: '"',
};
assert_eq!(complete(&toks), Some(expected));
}
#[test]
fn ys_with_motion() {
let toks = [
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::Motion(MotionKind::WordForward),
Token::SurroundChar(')'),
];
let expected = Expr::SurroundAdd {
target: Target::Motion(MotionExpr {
motion: MotionKind::WordForward,
count: 1,
}),
ch: ')',
};
assert_eq!(complete(&toks), Some(expected));
}
#[test]
fn yss_is_line_wise() {
let toks = [
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::SelfDouble(Operator::Yank),
Token::SurroundChar('"'),
];
let expected = Expr::SurroundAdd {
target: Target::LineWise,
ch: '"',
};
assert_eq!(complete(&toks), Some(expected));
}
#[test]
fn ys_intermediate_states_are_incomplete() {
let cases: Vec<Vec<Token>> = vec![
vec![Token::Op(Operator::Yank), Token::SurroundAddPrefix],
vec![
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::Scope(ScopeKind::Inner),
],
vec![
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::Scope(ScopeKind::Inner),
Token::Object(Object::Word),
],
vec![
Token::Op(Operator::Yank),
Token::SurroundAddPrefix,
Token::Motion(MotionKind::WordForward),
],
];
for c in cases {
assert!(incomplete(&c), "expected incomplete: {:?}", c);
}
}
#[test]
fn plain_yank_still_parses() {
let toks = [
Token::Op(Operator::Yank),
Token::Motion(MotionKind::WordForward),
];
let expected = Expr::Op {
op: Operator::Yank,
target: Target::Motion(MotionExpr {
motion: MotionKind::WordForward,
count: 1,
}),
outer_count: 1,
};
assert_eq!(complete(&toks), Some(expected));
}
#[test]
fn s_after_indent_op_is_invalid() {
let toks = [
Token::Op(Operator::Indent),
Token::SurroundAddPrefix,
Token::SurroundChar('"'),
];
assert!(invalid(&toks));
}
}