feat: add configurable Space leader bindings

This commit is contained in:
Valdemar Lorenzen
2026-10-08 13:54:48 +02:00
parent 5d2d67ab09
commit 5a67bf64a3
8 changed files with 958 additions and 28 deletions
+614 -19
View File
@@ -438,21 +438,28 @@ fn control_key(ch: char) -> Input {
}
fn display_key(input: &Input) -> String {
let mut display = String::new();
append_key(&mut display, input);
display
}
fn append_key(display: &mut String, input: &Input) {
use std::fmt::Write as _;
match input {
Input::Char('\t') => "Tab".into(),
Input::Char(' ') => "Space".into(),
Input::Char(ch) => ch.to_string(),
Input::Ctrl(ch) => format!("Ctrl+{ch}"),
Input::Escape => "Esc".into(),
Input::Enter => "Enter".into(),
Input::Backspace => "Backspace".into(),
Input::Delete => "Delete".into(),
Input::Left => "Left".into(),
Input::Right => "Right".into(),
Input::Up => "Up".into(),
Input::Down => "Down".into(),
Input::Home => "Home".into(),
Input::End => "End".into(),
Input::Char('\t') => display.push_str("Tab"),
Input::Char(' ') => display.push_str("Space"),
Input::Char(ch) => display.push(*ch),
Input::Ctrl(ch) => write!(display, "Ctrl+{ch}").expect("writing to a String"),
Input::Escape => display.push_str("Esc"),
Input::Enter => display.push_str("Enter"),
Input::Backspace => display.push_str("Backspace"),
Input::Delete => display.push_str("Delete"),
Input::Left => display.push_str("Left"),
Input::Right => display.push_str("Right"),
Input::Up => display.push_str("Up"),
Input::Down => display.push_str("Down"),
Input::Home => display.push_str("Home"),
Input::End => display.push_str("End"),
Input::Paste(_) => unreachable!("paste cannot be bound"),
}
}
@@ -671,6 +678,10 @@ pub struct Editor {
capabilities: Vec<String>,
features: HashMap<String, Feature>,
bindings: HashMap<Mode, HashMap<Input, Binding>>,
leader: Input,
leader_bindings: HashMap<Mode, HashMap<Vec<Input>, Binding>>,
leader_suffix: Vec<Input>,
leader_display: String,
palette: Option<Palette>,
count: Option<usize>,
}
@@ -689,6 +700,10 @@ impl Editor {
capabilities,
features: HashMap::new(),
bindings: default_bindings(),
leader: Input::Char(' '),
leader_bindings: HashMap::new(),
leader_suffix: Vec::new(),
leader_display: String::new(),
palette: None,
count: None,
};
@@ -709,6 +724,19 @@ impl Editor {
true,
)
.map_err(io::Error::other)?;
for (mode, key, target) in [
("normal", "leader space", "core.palette"),
("visual", "leader space", "core.palette"),
("normal", "leader w", "w"),
("normal", "leader b n", "bnext"),
("normal", "leader b p", "bprev"),
("normal", "leader b l", "buffers"),
("normal", "leader b d", "bdelete"),
] {
editor
.bind(mode, key, Some(target.into()))
.map_err(io::Error::other)?;
}
Ok(editor)
}
@@ -755,6 +783,46 @@ impl Editor {
pub fn count(&self) -> Option<usize> {
self.count
}
/// Actual pending leader prefix, cached until the next input.
pub fn leader_pending(&self) -> Option<&str> {
(!self.leader_display.is_empty()).then_some(self.leader_display.as_str())
}
fn clear_leader(&mut self) {
self.leader_suffix.clear();
self.leader_display.clear();
}
fn cancel_leader(&mut self) {
if self.leader_pending().is_some() {
self.count = None;
self.clear_leader();
}
}
/// Changes the symbolic leader without moving its bindings.
pub fn set_leader(&mut self, key: &str) -> Result<(), String> {
let leader = parse_key(key)?;
if leader == Input::Escape || matches!(leader, Input::Char(ch) if ch.is_ascii_digit()) {
return Err("Leader cannot be Escape or an ASCII digit".into());
}
if leader != self.leader
&& [Mode::Normal, Mode::Visual].into_iter().any(|mode| {
self.bindings
.get(&mode)
.is_some_and(|keys| keys.contains_key(&leader))
})
{
return Err(format!(
"Leader key already bound: {}",
display_key(&leader)
));
}
self.cancel_leader();
self.leader = leader;
self.refresh_palette();
Ok(())
}
/// Editable Ex/search prompt, without its `:` or `/` prefix.
pub fn command_line(&self) -> &str {
&self.command_line
@@ -867,6 +935,7 @@ impl Editor {
}
pub fn disable_plugin(&mut self, name: &str, reason: &str) {
self.cancel_leader();
if self.features.get(name).is_some_and(|feature| feature.core) {
self.set_message("Core features cannot be disabled");
return;
@@ -877,13 +946,18 @@ impl Editor {
!matches!(binding, Binding::Command(command) if feature.commands.contains(command))
});
}
for bindings in self.leader_bindings.values_mut() {
bindings.retain(|_, binding| {
!matches!(binding, Binding::Command(command) if feature.commands.contains(command))
});
}
}
self.refresh_palette();
self.set_message(format!("Plugin {name} disabled: {reason}"));
}
/// Overrides one mode's binding. `None` disables the key, including literal
/// typing in Insert/Command mode. Invalid overrides leave the binding intact.
/// Overrides one mode's single key or symbolic `leader` sequence. `None`
/// disables it. Invalid overrides leave existing bindings intact.
pub fn bind(&mut self, mode: &str, key: &str, target: Option<String>) -> Result<(), String> {
let mode = match mode {
"normal" => Mode::Normal,
@@ -893,7 +967,35 @@ impl Editor {
"visual" => Mode::Visual,
_ => return Err(format!("Unknown binding mode: {mode}")),
};
let key = parse_key(key)?;
let mut tokens = key.split_whitespace();
let suffix = if tokens
.next()
.is_some_and(|token| token.eq_ignore_ascii_case("leader"))
{
if !matches!(mode, Mode::Normal | Mode::Visual) {
return Err("Leader sequences can only be bound in Normal or Visual mode".into());
}
let suffix: Vec<_> = tokens.map(parse_key).collect::<Result<_, _>>()?;
if suffix.is_empty() {
return Err("Leader binding requires at least one suffix key".into());
}
if self.leader_bindings.get(&mode).is_some_and(|bindings| {
bindings.keys().any(|existing| {
*existing != suffix
&& (existing.starts_with(&suffix) || suffix.starts_with(existing))
})
}) {
return Err(format!("Ambiguous leader binding: {key:?}"));
}
Some(suffix)
} else {
None
};
let input = if suffix.is_none() {
Some(parse_key(key)?)
} else {
None
};
let binding = match target {
None => Binding::Disabled,
Some(target) if target.starts_with("core.") => Binding::Action(
@@ -910,7 +1012,18 @@ impl Editor {
Binding::Command(command)
}
};
self.bindings.entry(mode).or_default().insert(key, binding);
self.cancel_leader();
if let Some(suffix) = suffix {
self.leader_bindings
.entry(mode)
.or_default()
.insert(suffix, binding);
} else {
self.bindings
.entry(mode)
.or_default()
.insert(input.expect("single-key binding"), binding);
}
self.refresh_palette();
Ok(())
}
@@ -929,6 +1042,31 @@ impl Editor {
})
.map(|(input, _)| display_key(input))
.collect();
if !self
.bindings
.get(&self.active.mode)
.is_some_and(|keys| keys.contains_key(&self.leader))
{
for (suffix, binding) in self
.leader_bindings
.get(&self.active.mode)
.into_iter()
.flat_map(|keys| keys.iter())
{
if match (action, binding) {
(Some(action), Binding::Action(bound)) => action == *bound,
(None, Binding::Command(bound)) => command == bound,
_ => false,
} {
let mut display = display_key(&self.leader);
for input in suffix {
display.push(' ');
append_key(&mut display, input);
}
bindings.push(display);
}
}
}
bindings.sort();
bindings
}
@@ -1042,6 +1180,7 @@ impl Editor {
// Paste is literal input, never a configurable key or command.
if let Input::Paste(text) = input {
self.count = None;
self.clear_leader();
if self.active.mode == Mode::Insert {
self.paste(&text);
}
@@ -1052,6 +1191,13 @@ impl Editor {
Input::Ctrl(ch) => control_key(ch),
input => input,
};
if self.leader_pending().is_some() {
if input == Input::Escape {
self.cancel_leader();
return None;
}
return self.handle_leader(input);
}
if input == Input::Escape && self.count.take().is_some() {
return None;
}
@@ -1092,6 +1238,10 @@ impl Editor {
}
None => {}
}
if matches!(self.active.mode, Mode::Normal | Mode::Visual) && input == self.leader {
append_key(&mut self.leader_display, &input);
return None;
}
self.count = None;
match (self.active.mode, input) {
(Mode::Insert, Input::Char(ch)) if !ch.is_control() || ch == '\t' => {
@@ -1108,8 +1258,38 @@ impl Editor {
None
}
fn handle_leader(&mut self, input: Input) -> Option<Invocation> {
self.leader_display.push(' ');
append_key(&mut self.leader_display, &input);
self.leader_suffix.push(input);
let bindings = self.leader_bindings.get(&self.active.mode);
match bindings.and_then(|keys| keys.get(self.leader_suffix.as_slice())) {
Some(Binding::Action(action)) => return self.act(*action),
Some(Binding::Command(command)) => {
let command = command.clone();
return self.execute(&command);
}
Some(Binding::Disabled) => {
self.cancel_leader();
return None;
}
None => {}
}
// ponytail: scan finite leader routes; use a trie if catalogs grow large.
if bindings.is_some_and(|keys| {
keys.keys()
.any(|suffix| suffix.starts_with(&self.leader_suffix))
}) {
return None;
}
self.set_message(format!("Unknown leader sequence: {}", self.leader_display));
self.cancel_leader();
None
}
fn act(&mut self, action: Action) -> Option<Invocation> {
let count = self.count.take().unwrap_or(1);
self.clear_leader();
match action {
Action::Palette => {
self.palette = Some(Palette {
@@ -1322,6 +1502,7 @@ impl Editor {
}
self.command_line.clear();
self.count = None;
self.clear_leader();
self.palette = None;
}
@@ -1487,6 +1668,7 @@ impl Editor {
pub fn execute(&mut self, command: &str) -> Option<Invocation> {
self.count = None;
self.clear_leader();
self.palette = None;
let command = command.trim().strip_prefix(':').unwrap_or(command.trim());
let (name, args) = command
@@ -1592,6 +1774,7 @@ impl Editor {
.then(|| self.snapshot());
if before.is_some() {
self.count = None;
self.clear_leader();
self.clear_selection();
self.collapse_primary();
// Plugin inserts use one primary insertion point, not the selection set.
@@ -2796,6 +2979,415 @@ mod tests {
}
}
#[test]
fn default_leader_routes_save_switch_list_delete_and_open_palette() {
let directory = Directory::new();
let path = directory.path("first");
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
type_text(&mut editor, "ihello");
editor.handle(Input::Escape);
type_text(&mut editor, " w");
assert_eq!(fs::read_to_string(&path).unwrap(), "hello");
assert!(!editor.dirty());
editor.execute(&format!("edit {}", directory.path("second").display()));
type_text(&mut editor, " b");
assert_eq!(editor.leader_pending(), Some("Space b"));
editor.handle(Input::Char('n'));
assert_eq!(editor.buffer_id(), 1);
assert_eq!(editor.leader_pending(), None);
type_text(&mut editor, " bp");
assert_eq!(editor.buffer_id(), 2);
type_text(&mut editor, " bl");
assert!(editor.message().contains("first"));
assert!(editor.message().contains("second"));
type_text(&mut editor, " bd");
assert!(!editor.has_buffer(2));
assert_eq!(editor.buffer_id(), 1);
for mode in [Mode::Normal, Mode::Visual] {
if mode == Mode::Visual {
editor.handle(Input::Char('v'));
}
editor.handle(Input::Char(' '));
assert_eq!(editor.leader_pending(), Some("Space"));
editor.handle(Input::Char(' '));
assert!(editor.palette().is_some());
assert_eq!(editor.mode(), mode);
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Escape);
}
}
#[test]
fn nested_leader_dispatches_plugin_once_with_count_and_multiple_carets() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("aa\nbb".into())])
.unwrap();
set_carets(&mut editor, &[(0, 0), (1, 0)], 1);
editor.register(plugin("fixture", &["greet"])).unwrap();
editor
.bind("normal", "LeAdEr g ctrl+r 7 shift+i", Some("greet".into()))
.unwrap();
type_text(&mut editor, "8 g");
editor.handle(Input::Ctrl('R'));
assert_eq!(editor.leader_pending(), Some("Space g Ctrl+r"));
editor.handle(Input::Char('7'));
assert_eq!(editor.count(), Some(8));
assert_eq!(editor.leader_pending(), Some("Space g Ctrl+r 7"));
assert_eq!(
editor.handle(Input::Char('I')),
Some(Invocation {
plugin: "fixture".into(),
command: "greet".into(),
args: String::new(),
})
);
assert_eq!(editor.selection_count(), 2);
assert_eq!(editor.context().text, "aa\nbb");
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Ctrl('p'));
assert_eq!(
palette_entry(&editor, "greet").bindings,
vec!["Space g Ctrl+r 7 I"]
);
}
#[test]
fn custom_leader_is_symbolic_and_ordinary_trigger_bindings_preempt_by_mode() {
let mut editor = editor();
type_text(&mut editor, "3 b");
editor.set_leader("comma").unwrap();
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
editor.handle(Input::Ctrl('p'));
assert_eq!(palette_entry(&editor, "bnext").bindings, vec![", b n"]);
editor
.bind("normal", ",", Some("core.command".into()))
.unwrap();
assert!(palette_entry(&editor, "bnext").bindings.is_empty());
editor.set_leader(",").unwrap();
editor.handle(Input::Escape);
editor.handle(Input::Char(','));
assert_eq!(editor.mode(), Mode::Command);
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Escape);
editor.handle(Input::Char('v'));
type_text(&mut editor, ", ");
assert!(editor.palette().is_some());
assert_eq!(editor.mode(), Mode::Visual);
editor.handle(Input::Escape);
editor.handle(Input::Escape);
editor.bind("normal", ",", None).unwrap();
type_text(&mut editor, "4,");
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
editor.set_leader("\\").unwrap();
type_text(&mut editor, "\\b");
assert_eq!(editor.leader_pending(), Some("\\ b"));
editor.handle(Input::Char('n'));
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Ctrl('p'));
assert_eq!(palette_entry(&editor, "bnext").bindings, vec!["\\ b n"]);
}
#[test]
fn leader_prefix_conflicts_and_invalid_replacements_are_atomic() {
let mut editor = editor();
assert!(editor.bind("normal", "leader b", None).is_err());
editor
.bind("normal", "leader z", Some("core.down".into()))
.unwrap();
assert!(editor.bind("normal", "leader z n", None).is_err());
assert!(
editor
.bind("normal", "leader z", Some("missing".into()))
.is_err()
);
editor
.apply(vec![Effect::Replace("a\nb\nc".into())])
.unwrap();
type_text(&mut editor, " z");
assert_eq!(editor.cursor().row, 1);
editor.bind("normal", "leader z", None).unwrap();
assert!(
editor
.bind("normal", "leader z n", Some("core.down".into()))
.is_err()
);
type_text(&mut editor, "2 z");
assert_eq!(editor.cursor().row, 1);
assert_eq!(editor.count(), None);
editor.bind("normal", "leader g n", None).unwrap();
assert!(editor.bind("normal", "leader g", None).is_err());
for key in ["leader", "leader ctrl+", "leader b ctrl++r", "leader <Esc>"] {
assert!(editor.bind("normal", key, None).is_err(), "{key}");
}
editor
.bind("normal", " LEADER\tz ", Some("core.up".into()))
.unwrap();
type_text(&mut editor, " z");
assert_eq!(editor.cursor().row, 0);
assert!(
editor
.leader_bindings
.get(&Mode::Normal)
.unwrap()
.contains_key(&vec![Input::Char('b'), Input::Char('n')])
);
}
#[test]
fn leader_validation_preserves_choice_and_reuses_action_mode_eligibility() {
let mut editor = editor();
editor.register(plugin("fixture", &["greet"])).unwrap();
for mode in ["insert", "command", "search", "unknown"] {
assert!(editor.bind(mode, "leader z", None).is_err(), "{mode}");
}
assert!(
editor
.bind("visual", "leader z", Some("core.insert".into()))
.is_err()
);
assert!(
editor
.bind("visual", "leader z", Some("greet".into()))
.is_err()
);
editor
.bind("visual", "leader z", Some("core.right".into()))
.unwrap();
editor.bind("normal", "comma", None).unwrap();
editor.bind("visual", "\\", None).unwrap();
for key in [
"", "esc", "ESC", "0", "9", "i", "ctrl+p", ",", "\\", "leader b", "alt+j",
] {
assert!(editor.set_leader(key).is_err(), "{key}");
assert_eq!(editor.leader, Input::Char(' '));
}
editor.bind("insert", "界", None).unwrap();
editor.set_leader("界").unwrap();
editor.handle(Input::Char('界'));
assert_eq!(editor.leader_pending(), Some("界"));
editor.handle(Input::Escape);
editor.set_leader("space").unwrap();
editor.bind("normal", "space", None).unwrap();
editor.set_leader("SPACE").unwrap();
assert_eq!(editor.leader, Input::Char(' '));
}
#[test]
fn leader_escape_unknown_and_paste_cancel_count_without_changing_selections() {
for visual in [false, true] {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
if visual {
editor
.bind("visual", "leader b n", Some("core.down".into()))
.unwrap();
type_text(&mut editor, "vl");
}
let mode = editor.mode();
let carets = editor.carets();
let selections = editor.selections();
for prefix in [" b", "3 b"] {
type_text(&mut editor, prefix);
assert_eq!(editor.leader_pending(), Some("Space b"));
editor.handle(Input::Escape);
assert_eq!(editor.mode(), mode);
assert_eq!(editor.carets(), carets);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
}
type_text(&mut editor, "4 b");
editor.handle(Input::Char('x'));
assert!(
editor
.message()
.contains("Unknown leader sequence: Space b x")
);
assert_eq!(editor.context().text, "abcd\nabcd");
assert_eq!(editor.carets(), carets);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.mode(), mode);
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
type_text(&mut editor, "5 b");
editor.handle(Input::Paste("n".into()));
assert_eq!(editor.context().text, "abcd\nabcd");
assert_eq!(editor.carets(), carets);
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.selection_count(), if visual { 2 } else { 1 });
}
}
#[test]
fn leader_digit_suffix_uses_pending_count_for_remapped_motion_not_new_count() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace(["abcd"; 12].join("\n"))])
.unwrap();
set_carets(&mut editor, &[(0, 0), (1, 0)], 1);
editor
.bind("normal", "leader g 0 2", Some("core.down".into()))
.unwrap();
type_text(&mut editor, "3 g0");
assert_eq!(editor.count(), Some(3));
assert_eq!(editor.leader_pending(), Some("Space g 0"));
editor.handle(Input::Char('2'));
assert_eq!(
editor.carets(),
vec![Cursor { row: 3, column: 0 }, Cursor { row: 4, column: 0 }]
);
assert_eq!(editor.count(), None);
assert_eq!(editor.leader_pending(), None);
editor
.bind("visual", "leader 7", Some("core.right".into()))
.unwrap();
editor.handle(Input::Char('v'));
type_text(&mut editor, "2 7");
assert_eq!(editor.cursor(), Cursor { row: 4, column: 2 });
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selection_count(), 2);
assert_eq!(editor.count(), None);
}
#[test]
fn leader_keys_are_literal_in_insert_prompts_and_palette_queries() {
for (key, ch) in [("space", ' '), ("comma", ',')] {
for mode_key in ['i', ':', '/'] {
let mut editor = editor();
editor.set_leader(key).unwrap();
editor.handle(Input::Char(mode_key));
let text = format!("{ch}b n{ch}");
type_text(&mut editor, &text);
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
if mode_key == 'i' {
assert_eq!(editor.context().text, text);
assert_eq!(editor.mode(), Mode::Insert);
} else {
assert_eq!(editor.command_line(), text);
assert_eq!(
editor.mode(),
if mode_key == ':' {
Mode::Command
} else {
Mode::Search
}
);
}
let mode = editor.mode();
editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "core word next ");
assert_eq!(editor.palette().unwrap().query, "core word next ");
assert_eq!(editor.leader_pending(), None);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), mode);
}
}
}
#[test]
fn leader_routes_retire_with_plugins_and_binding_changes_cancel_pending() {
let mut editor = editor();
editor.register(plugin("fixture", &["greet"])).unwrap();
editor.bind("normal", "g", Some("greet".into())).unwrap();
editor
.bind("normal", "leader g n", Some("greet".into()))
.unwrap();
type_text(&mut editor, "3 g");
editor
.bind("normal", "leader g n", Some("core.down".into()))
.unwrap();
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
type_text(&mut editor, "2 g");
editor.bind("normal", "z", None).unwrap();
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
editor
.bind("normal", "leader g n", Some("greet".into()))
.unwrap();
type_text(&mut editor, "4 g");
editor.disable_plugin("fixture", "retired");
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
assert_eq!(editor.handle(Input::Char('g')), None);
type_text(&mut editor, " g");
assert_eq!(editor.leader_pending(), None);
assert!(editor.message().contains("Unknown leader sequence"));
assert!(
editor
.bind("normal", "leader g n", Some("greet".into()))
.is_err()
);
editor.handle(Input::Ctrl('p'));
assert!(
!editor
.palette()
.unwrap()
.entries
.iter()
.any(|entry| entry.name == "greet")
);
}
#[test]
fn pending_leader_never_survives_external_commands_or_buffer_transitions() {
let directory = Directory::new();
let mut editor = editor();
editor.apply(vec![Effect::Replace("abcd".into())]).unwrap();
editor.execute(&format!("edit {}", directory.path("other").display()));
editor.set_leader(",").unwrap();
for command in [
"buffer 1",
"buffer 1",
"bnext",
"bprev",
"buffers",
"buffer 999",
] {
type_text(&mut editor, "3,b");
assert_eq!(editor.leader_pending(), Some(", b"));
editor.execute(command);
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
editor.handle(Input::Char(','));
assert_eq!(editor.leader_pending(), Some(","));
editor.handle(Input::Escape);
}
editor.execute("buffer 1");
type_text(&mut editor, "vl");
let selections = editor.selections();
type_text(&mut editor, "2,");
editor.execute("buffer 2");
editor.execute("buffer 1");
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.leader_pending(), None);
assert_eq!(editor.count(), None);
editor.handle(Input::Char(','));
editor
.apply(vec![Effect::Replace("replacement".into())])
.unwrap();
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.leader_pending(), None);
editor
.bind("normal", "leader i", Some("core.insert".into()))
.unwrap();
type_text(&mut editor, ",i");
assert_eq!(editor.mode(), Mode::Insert);
assert_eq!(editor.leader_pending(), None);
}
#[test]
fn counted_cursor_motions_apply_to_all_carets_and_clamp_through_short_rows() {
let mut editor = editor();
@@ -3331,7 +3923,10 @@ mod tests {
assert_eq!(entry.owner, "core");
assert!(entry.enabled);
}
assert_eq!(palette_entry(&editor, "bnext").bindings, vec!["z"]);
assert_eq!(
palette_entry(&editor, "bnext").bindings,
vec!["Space b n", "z"]
);
editor.disable_plugin("fixture", "failure");
assert!(
!editor