After trying part 8c, the user found p and P doing the same thing on the canvas: both pasted a sibling below the node (P only differed in the outline's order). Their pick: p pastes under the node, as its last children, the way a adds a child; P pastes the next siblings, below it, where o adds one. ]p went away, since p does what it did. Inside a title p and P still paste after and before the text cursor, as in vim: they're their own commands (paste-after, paste-before), and the node ones are paste-as-child and paste-as-sibling. Co-Authored-By: Claude Opus 5.5 <[email protected]>
490 lines
20 KiB
Rust
490 lines
20 KiB
Rust
//! The key engine reads key presses the way vim does. It collects a count
|
|
//! (`5j`), a register (`"a`), a sequence (`gg`), an operator and its target
|
|
//! (`dab`), or a key and the character after it (`ma`), and hands over a
|
|
//! complete [`Typed`] command. It also says what can come next, for the
|
|
//! which-key popup ([`KeyEngine::hints`]).
|
|
|
|
use std::time::Instant;
|
|
|
|
use super::command::{Bound, CharKey, Command, Operator, Register, Target, TargetBound};
|
|
use super::key::{Key, notation};
|
|
use super::keymap::{Binding, Tables};
|
|
|
|
/// The biggest count. `99999j` would only go somewhere silly.
|
|
const MAX_COUNT: u32 = 9999;
|
|
|
|
/// A complete command, with what was typed around it.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Typed {
|
|
pub command: Command,
|
|
/// The number typed first, like the 5 in `5j`. Around an operator, both
|
|
/// counts multiply: `2d3w` is 6.
|
|
pub count: Option<u32>,
|
|
/// The register named with `"`, like `a` in `"ayy`.
|
|
pub register: Option<Register>,
|
|
}
|
|
|
|
impl Typed {
|
|
/// How many times to do it: the count, or once.
|
|
pub fn times(&self) -> u32 {
|
|
self.count.unwrap_or(1)
|
|
}
|
|
}
|
|
|
|
/// What a key press led to.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Outcome {
|
|
/// A complete command.
|
|
Run(Typed),
|
|
/// The keys so far start something: waiting for the next key.
|
|
Waiting,
|
|
/// Esc dropped what was being typed.
|
|
Cancelled,
|
|
/// Nothing starts like this. The keys typed (in notation) were dropped.
|
|
Unknown(String),
|
|
}
|
|
|
|
/// What the engine waits for next.
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
enum Stage {
|
|
/// A command's keys: none yet, or the start of a sequence like `g`.
|
|
#[default]
|
|
Command,
|
|
/// A register's name, after `"`.
|
|
Register,
|
|
/// The character after a key like `m`.
|
|
Char(CharKey),
|
|
/// An operator's target, like `ab` after `d`. `count` is the one typed
|
|
/// before the operator.
|
|
Target { operator: Operator, operator_keys: Vec<Key>, count: Option<u32> },
|
|
/// The character of a target like `f` (`dfx`), with the counts so far.
|
|
TargetChar { operator: Operator, kind: CharKey, count: Option<u32> },
|
|
}
|
|
|
|
/// Reads key presses into commands. It remembers what's half typed in
|
|
/// between.
|
|
#[derive(Debug, Default)]
|
|
pub struct KeyEngine {
|
|
stage: Stage,
|
|
/// The count being typed: before the command, or before an operator's
|
|
/// target.
|
|
count: Option<u32>,
|
|
register: Option<Register>,
|
|
/// The keys typed so far in this stage.
|
|
keys: Vec<Key>,
|
|
/// Everything typed so far, as vim's `showcmd` shows it: `2"a`, `dg`.
|
|
shown: String,
|
|
/// When the first key of the sequence was pressed (for which-key's delay).
|
|
since: Option<Instant>,
|
|
}
|
|
|
|
/// What can follow the keys typed so far, for the which-key popup.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Hints {
|
|
/// The keys typed and what they lead to: `g · go to`.
|
|
pub title: String,
|
|
pub entries: Vec<Hint>,
|
|
}
|
|
|
|
/// One possible next key.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Hint {
|
|
pub keys: String,
|
|
pub help: String,
|
|
/// Whether the key opens a group of more keys (shown as `+name`).
|
|
pub group: bool,
|
|
}
|
|
|
|
impl KeyEngine {
|
|
/// Reads one key press, using the tables in effect.
|
|
pub fn press(&mut self, key: Key, tables: &Tables<'_>) -> Outcome {
|
|
if key.is_escape() && self.is_pending() {
|
|
self.reset();
|
|
return Outcome::Cancelled;
|
|
}
|
|
self.shown.push_str(&key.to_string());
|
|
match std::mem::take(&mut self.stage) {
|
|
Stage::Command => self.command_key(key, tables),
|
|
Stage::Register => match key.as_char().and_then(Register::named) {
|
|
Some(register) => {
|
|
self.register = Some(register);
|
|
Outcome::Waiting
|
|
}
|
|
None => self.unknown(),
|
|
},
|
|
Stage::Char(kind) => match key.as_char() {
|
|
Some(c) => self.run(kind.command(c)),
|
|
None => self.unknown(),
|
|
},
|
|
Stage::Target { operator, operator_keys, count } => {
|
|
self.target_key(key, tables, operator, operator_keys, count)
|
|
}
|
|
Stage::TargetChar { operator, kind, count } => match key.as_char().and_then(|c| kind.target(c)) {
|
|
Some(target) => {
|
|
self.count = count;
|
|
self.run(Command::Operate(operator, target))
|
|
}
|
|
None => self.unknown(),
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Forgets anything half typed.
|
|
pub fn reset(&mut self) {
|
|
*self = KeyEngine::default();
|
|
}
|
|
|
|
/// Whether something is half typed: a count, a register, a sequence…
|
|
pub fn is_pending(&self) -> bool {
|
|
self.stage != Stage::Command
|
|
|| !self.keys.is_empty()
|
|
|| self.count.is_some()
|
|
|| self.register.is_some()
|
|
}
|
|
|
|
/// The keys typed so far, like vim's `showcmd`: `2"ad`.
|
|
pub fn shown(&self) -> &str {
|
|
&self.shown
|
|
}
|
|
|
|
/// When the sequence being typed started (counts don't count), for
|
|
/// which-key's delay.
|
|
pub fn pending_since(&self) -> Option<Instant> {
|
|
self.since
|
|
}
|
|
|
|
/// The first key of the sequence being typed (to show the leader menu
|
|
/// right away).
|
|
pub fn first_key(&self) -> Option<Key> {
|
|
match &self.stage {
|
|
Stage::Target { operator_keys, .. } => operator_keys.first().copied(),
|
|
_ => self.keys.first().copied(),
|
|
}
|
|
}
|
|
|
|
/// What can be typed next, for the which-key popup. `None` while nothing
|
|
/// that has follow-ups is half typed.
|
|
pub fn hints(&self, tables: &Tables<'_>) -> Option<Hints> {
|
|
let fixed = |what: &str, choices: &[(&str, &str)]| Hints {
|
|
title: format!("{} · {what}", self.shown),
|
|
entries: choices
|
|
.iter()
|
|
.map(|(keys, help)| Hint { keys: keys.to_string(), help: help.to_string(), group: false })
|
|
.collect(),
|
|
};
|
|
match &self.stage {
|
|
Stage::Register => Some(fixed("register", Register::HINTS)),
|
|
Stage::Char(kind) | Stage::TargetChar { kind, .. } => {
|
|
Some(fixed("then a character", kind.hints()))
|
|
}
|
|
Stage::Command if self.keys.is_empty() => None,
|
|
Stage::Command => {
|
|
let title = match tables.group_name(&self.keys) {
|
|
Some(name) => format!("{} · {name}", self.shown),
|
|
None => self.shown.clone(),
|
|
};
|
|
let entries = next_keys(&self.keys, &tables.commands_starting_with(&self.keys), tables);
|
|
Some(Hints { title, entries })
|
|
}
|
|
Stage::Target { operator, operator_keys, count } => {
|
|
let mut entries = Vec::new();
|
|
if self.keys.is_empty() {
|
|
// A count takes in the next siblings, as `3dd` takes three
|
|
// lines in vim: say exactly how many.
|
|
let help = match (tables.in_text(), multiply(*count, self.count).unwrap_or(1)) {
|
|
(true, 1) => "the line".to_string(),
|
|
(true, many) => format!("{many} lines"),
|
|
(false, 1) => "this node, with its branch".to_string(),
|
|
(false, 2) => {
|
|
"2 nodes: this one and the next sibling, with their branches".to_string()
|
|
}
|
|
(false, many) => format!(
|
|
"{many} nodes: this one and the next {} siblings, with their branches",
|
|
many - 1
|
|
),
|
|
};
|
|
entries.push(Hint { keys: notation(operator_keys), help, group: false });
|
|
}
|
|
let targets = tables.targets_starting_with(&self.keys);
|
|
for target in targets {
|
|
let next = &target.keys[..=self.keys.len()];
|
|
let keys = target.keys[self.keys.len()].to_string();
|
|
if entries.iter().any(|hint| hint.keys == keys) {
|
|
continue;
|
|
}
|
|
let hint = if target.keys.len() == next.len() {
|
|
Hint { keys, help: target.help.clone(), group: false }
|
|
} else {
|
|
let name = tables.target_group_name(next).unwrap_or("more");
|
|
Hint { keys, help: format!("+{name}"), group: true }
|
|
};
|
|
entries.push(hint);
|
|
}
|
|
let group =
|
|
tables.target_group_name(&self.keys).map(|name| format!(" {name}")).unwrap_or_default();
|
|
Some(Hints { title: format!("{} · {}{group}", self.shown, operator.name()), entries })
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A key while waiting for a command.
|
|
fn command_key(&mut self, key: Key, tables: &Tables<'_>) -> Outcome {
|
|
if self.keys.is_empty() {
|
|
if let Some(count) = with_digit(self.count, key) {
|
|
self.count = Some(count);
|
|
return Outcome::Waiting;
|
|
}
|
|
self.since.get_or_insert_with(Instant::now);
|
|
if key.as_char() == Some('"') && self.register.is_none() {
|
|
self.stage = Stage::Register;
|
|
return Outcome::Waiting;
|
|
}
|
|
}
|
|
self.keys.push(key);
|
|
let candidates = tables.commands_starting_with(&self.keys);
|
|
let Some(exact) = candidates.iter().find(|binding| binding.keys == self.keys) else {
|
|
return if candidates.is_empty() { self.unknown() } else { Outcome::Waiting };
|
|
};
|
|
match exact.bound {
|
|
Bound::Command(command) => self.run(command),
|
|
Bound::Operator(operator) => {
|
|
let operator_keys = std::mem::take(&mut self.keys);
|
|
self.stage = Stage::Target { operator, operator_keys, count: self.count.take() };
|
|
Outcome::Waiting
|
|
}
|
|
Bound::WithChar(kind) => {
|
|
self.keys.clear();
|
|
self.stage = Stage::Char(kind);
|
|
Outcome::Waiting
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A key while an operator waits for its target.
|
|
fn target_key(
|
|
&mut self,
|
|
key: Key,
|
|
tables: &Tables<'_>,
|
|
operator: Operator,
|
|
operator_keys: Vec<Key>,
|
|
count: Option<u32>,
|
|
) -> Outcome {
|
|
if self.keys.is_empty()
|
|
&& let Some(target_count) = with_digit(self.count, key)
|
|
{
|
|
self.count = Some(target_count);
|
|
self.stage = Stage::Target { operator, operator_keys, count };
|
|
return Outcome::Waiting;
|
|
}
|
|
self.keys.push(key);
|
|
// The operator's own keys again: this node (or line), as in `dd`.
|
|
if self.keys == operator_keys {
|
|
self.count = multiply(count, self.count);
|
|
return self.run(Command::Operate(operator, Target::Current));
|
|
}
|
|
let candidates = tables.targets_starting_with(&self.keys);
|
|
if let Some(exact) = candidates.iter().find(|target| target.keys == self.keys) {
|
|
return match exact.bound {
|
|
TargetBound::Target(target) => {
|
|
self.count = multiply(count, self.count);
|
|
self.run(Command::Operate(operator, target))
|
|
}
|
|
TargetBound::WithChar(kind) => {
|
|
self.keys.clear();
|
|
let count = multiply(count, self.count.take());
|
|
self.stage = Stage::TargetChar { operator, kind, count };
|
|
Outcome::Waiting
|
|
}
|
|
};
|
|
}
|
|
if candidates.is_empty() && !operator_keys.starts_with(&self.keys) {
|
|
return self.unknown();
|
|
}
|
|
self.stage = Stage::Target { operator, operator_keys, count };
|
|
Outcome::Waiting
|
|
}
|
|
|
|
fn run(&mut self, command: Command) -> Outcome {
|
|
let typed = Typed { command, count: self.count, register: self.register };
|
|
self.reset();
|
|
Outcome::Run(typed)
|
|
}
|
|
|
|
fn unknown(&mut self) -> Outcome {
|
|
let shown = std::mem::take(&mut self.shown);
|
|
self.reset();
|
|
Outcome::Unknown(shown)
|
|
}
|
|
}
|
|
|
|
/// The count with one more digit typed, if `key` is a digit that continues
|
|
/// or starts a count (`0` only continues one: alone, it's a key of its own).
|
|
fn with_digit(count: Option<u32>, key: Key) -> Option<u32> {
|
|
let digit = key.as_char()?.to_digit(10)?;
|
|
if digit == 0 && count.is_none() {
|
|
return None;
|
|
}
|
|
Some((count.unwrap_or(0) * 10 + digit).min(MAX_COUNT))
|
|
}
|
|
|
|
/// Two counts typed around an operator multiply (`2d3w` deletes 6 words).
|
|
fn multiply(before: Option<u32>, after: Option<u32>) -> Option<u32> {
|
|
match (before, after) {
|
|
(None, None) => None,
|
|
_ => Some((before.unwrap_or(1) * after.unwrap_or(1)).min(MAX_COUNT)),
|
|
}
|
|
}
|
|
|
|
/// The keys that can follow `typed`, one entry per next key: what it does,
|
|
/// or `+group` when more keys follow.
|
|
fn next_keys(typed: &[Key], candidates: &[&Binding], tables: &Tables<'_>) -> Vec<Hint> {
|
|
let mut hints: Vec<Hint> = Vec::new();
|
|
for binding in candidates {
|
|
let Some(next) = binding.keys.get(typed.len()) else { continue };
|
|
let keys = next.to_string();
|
|
if hints.iter().any(|hint| hint.keys == keys) {
|
|
continue;
|
|
}
|
|
let hint = if binding.keys.len() == typed.len() + 1 {
|
|
Hint { keys, help: binding.help.clone(), group: false }
|
|
} else {
|
|
let name = tables.group_name(&binding.keys[..=typed.len()]).unwrap_or("more");
|
|
Hint { keys, help: format!("+{name}"), group: true }
|
|
};
|
|
hints.push(hint);
|
|
}
|
|
hints
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::input::bindings::Context;
|
|
use crate::input::command::{Motion, TextObject};
|
|
use crate::input::key::parse_keys;
|
|
use crate::input::keymap::{DEFAULT_LEADER, Keymap};
|
|
|
|
const NORMAL: &[Context] = &[Context::Canvas, Context::Normal, Context::Motions];
|
|
|
|
/// Types `text` (vim notation) and returns what the last key led to.
|
|
fn type_keys(engine: &mut KeyEngine, keymap: &Keymap, text: &str) -> Outcome {
|
|
let tables = keymap.tables(NORMAL);
|
|
let mut last = Outcome::Waiting;
|
|
for key in parse_keys(text, keymap.leader()).unwrap() {
|
|
last = engine.press(key, &tables);
|
|
}
|
|
last
|
|
}
|
|
|
|
fn run(command: Command, count: Option<u32>, register: Option<Register>) -> Outcome {
|
|
Outcome::Run(Typed { command, count, register })
|
|
}
|
|
|
|
#[test]
|
|
fn sequences_counts_and_registers() {
|
|
let keymap = Keymap::new(DEFAULT_LEADER);
|
|
let mut engine = KeyEngine::default();
|
|
assert_eq!(type_keys(&mut engine, &keymap, "g"), Outcome::Waiting);
|
|
assert_eq!(type_keys(&mut engine, &keymap, "g"), run(Command::Move(Motion::First), None, None));
|
|
assert_eq!(type_keys(&mut engine, &keymap, "12j"), run(Command::Move(Motion::Down), Some(12), None));
|
|
assert_eq!(
|
|
type_keys(&mut engine, &keymap, "\"+p"),
|
|
run(Command::PasteAsChild, None, Some(Register::Clipboard))
|
|
);
|
|
assert_eq!(
|
|
type_keys(&mut engine, &keymap, "3\"ap"),
|
|
run(Command::PasteAsChild, Some(3), Some(Register::Named('a')))
|
|
);
|
|
assert_eq!(type_keys(&mut engine, &keymap, "gq"), Outcome::Unknown("gq".into()));
|
|
assert_eq!(type_keys(&mut engine, &keymap, "z<Esc>"), Outcome::Cancelled);
|
|
assert!(!engine.is_pending());
|
|
}
|
|
|
|
#[test]
|
|
fn operators_wait_for_their_target() {
|
|
let keymap = Keymap::new(DEFAULT_LEADER);
|
|
let mut engine = KeyEngine::default();
|
|
let delete = |target, count| run(Command::Operate(Operator::Delete, target), count, None);
|
|
assert_eq!(type_keys(&mut engine, &keymap, "dd"), delete(Target::Current, None));
|
|
assert_eq!(type_keys(&mut engine, &keymap, "dab"), delete(Target::Branch, None));
|
|
assert_eq!(
|
|
type_keys(&mut engine, &keymap, "2d3d"),
|
|
delete(Target::Current, Some(6)),
|
|
"counts multiply"
|
|
);
|
|
assert_eq!(
|
|
type_keys(&mut engine, &keymap, ">>"),
|
|
run(Command::Operate(Operator::Indent, Target::Current), None, None)
|
|
);
|
|
assert_eq!(type_keys(&mut engine, &keymap, "dx"), Outcome::Unknown("dx".into()));
|
|
assert_eq!(type_keys(&mut engine, &keymap, "ma"), run(Command::SetMark('a'), None, None));
|
|
assert_eq!(type_keys(&mut engine, &keymap, "'A"), run(Command::JumpToMark('A'), None, None));
|
|
}
|
|
|
|
#[test]
|
|
fn inside_text_motions_are_targets_too() {
|
|
let keymap = Keymap::new(DEFAULT_LEADER);
|
|
let tables = keymap.tables(&[Context::Text]);
|
|
let mut engine = KeyEngine::default();
|
|
let mut type_text = |text: &str| {
|
|
let keys = parse_keys(text, keymap.leader()).unwrap();
|
|
keys.into_iter().map(|key| engine.press(key, &tables)).last().unwrap()
|
|
};
|
|
let operate = |operator, target, count| run(Command::Operate(operator, target), count, None);
|
|
assert_eq!(type_text("dw"), operate(Operator::Delete, Target::Motion(Motion::Next), None));
|
|
assert_eq!(type_text("d0"), operate(Operator::Delete, Target::Motion(Motion::LineStart), None));
|
|
assert_eq!(type_text("ci\""), operate(Operator::Change, Target::Inside(TextObject::Quotes), None));
|
|
assert_eq!(type_text("ya("), operate(Operator::Yank, Target::Around(TextObject::Parens), None));
|
|
assert_eq!(
|
|
type_text("2d3fx"),
|
|
operate(Operator::Delete, Target::Motion(Motion::FindForward('x')), Some(6))
|
|
);
|
|
assert_eq!(type_text("cc"), operate(Operator::Change, Target::Current, None));
|
|
assert_eq!(type_text("x"), operate(Operator::Delete, Target::Motion(Motion::Right), None));
|
|
assert_eq!(type_text("rq"), run(Command::ReplaceChar('q'), None, None));
|
|
|
|
type_text("d");
|
|
let hints = engine.hints(&tables).unwrap();
|
|
assert_eq!(hints.entries[0], Hint { keys: "d".into(), help: "the line".into(), group: false });
|
|
assert!(hints.entries.contains(&Hint { keys: "i".into(), help: "+inside".into(), group: true }));
|
|
assert!(hints.entries.contains(&Hint {
|
|
keys: "w".into(),
|
|
help: "the next word".into(),
|
|
group: false
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn which_key_lists_what_can_follow() {
|
|
let keymap = Keymap::new(DEFAULT_LEADER);
|
|
let tables = keymap.tables(NORMAL);
|
|
let mut engine = KeyEngine::default();
|
|
assert_eq!(engine.hints(&tables), None);
|
|
|
|
type_keys(&mut engine, &keymap, "<Space>");
|
|
let hints = engine.hints(&tables).unwrap();
|
|
assert_eq!(hints.title, "<Space> · actions");
|
|
assert!(hints.entries.contains(&Hint { keys: "c".into(), help: "color…".into(), group: false }));
|
|
assert!(hints.entries.contains(&Hint { keys: "v".into(), help: "+show a view".into(), group: true }));
|
|
assert_eq!(engine.first_key(), Some(DEFAULT_LEADER));
|
|
|
|
engine.reset();
|
|
type_keys(&mut engine, &keymap, "d");
|
|
let hints = engine.hints(&tables).unwrap();
|
|
assert_eq!(hints.title, "d · delete");
|
|
let keys: Vec<&str> = hints.entries.iter().map(|hint| hint.keys.as_str()).collect();
|
|
assert_eq!(keys, ["d", "a", "i"]);
|
|
assert!(hints.entries[1].group);
|
|
|
|
engine.reset();
|
|
type_keys(&mut engine, &keymap, "2d3");
|
|
let hints = engine.hints(&tables).unwrap();
|
|
assert_eq!(hints.title, "2d3 · delete");
|
|
assert_eq!(hints.entries[0].help, "6 nodes: this one and the next 5 siblings, with their branches");
|
|
|
|
engine.reset();
|
|
type_keys(&mut engine, &keymap, "\"");
|
|
assert_eq!(engine.hints(&tables).unwrap().title, "\" · register");
|
|
}
|
|
}
|