diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a071bfaa8..f913e4a56 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -907,28 +907,37 @@ impl EditorView { } fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) { - if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { - match keyresult { - KeymapResult::NotFound => { - if let Some(ch) = event.char() { - commands::insert::insert_char(cx, ch) + let mut stack = vec![event]; + + while let Some(event) = stack.pop() { + if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) { + match keyresult { + KeymapResult::NotFound => { + if let Some(ch) = event.char() { + commands::insert::insert_char(cx, ch) + } } - } - KeymapResult::Cancelled(pending) => { - for ev in pending { - match ev.char() { - Some(ch) => commands::insert::insert_char(cx, ch), - None => { - if let KeymapResult::Matched(command) = - self.keymaps.get(Mode::Insert, ev) - { - command.execute(cx); - } + KeymapResult::Cancelled(pending) => { + let mut pending = pending.into_iter(); + if let Some(first) = pending.next() { + // Note that since this is the first pending key, we know + // it can't map to a command by itself. + match first.char() { + // The first key is both the start of a menu and a regular + // insert key. The user may have intended to type it as an + // insert, and then execute the remaining suffix of keys. + Some(ch) => commands::insert::insert_char(cx, ch), + // If the first key is not a character to insert, then we + // assume the user intended to enter a command menu, so we + // should just discard pending keys if they don't match. + None => continue, } } + + stack.extend(pending.rev()); } + _ => unreachable!(), } - _ => unreachable!(), } } } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 35214bcb8..6240e6988 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -18,6 +18,7 @@ mod test { mod auto_indent; mod auto_pairs; mod commands; + mod insert_keymap_suffix; mod languages; mod movement; mod prompt; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 70b3f4022..bbecc3477 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -349,8 +349,6 @@ impl AppBuilder { self } - // Remove this attribute once `with_config` is used in a test: - #[allow(dead_code)] pub fn with_config(mut self, mut config: Config) -> Self { let keys = replace(&mut config.keys, helix_term::keymap::default()); merge_keys(&mut config.keys, keys); diff --git a/helix-term/tests/test/insert_keymap_suffix.rs b/helix-term/tests/test/insert_keymap_suffix.rs new file mode 100644 index 000000000..8315d5677 --- /dev/null +++ b/helix-term/tests/test/insert_keymap_suffix.rs @@ -0,0 +1,36 @@ +use super::*; + +#[tokio::test(flavor = "multi_thread")] +async fn insert_keymap_suffix() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_config(config()), + ("#[|]#", "iselffd", "self#[|]#"), + ) + .await?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn insert_keymap_suffix_non_char() -> anyhow::Result<()> { + test_with_config( + AppBuilder::new().with_config(config()), + ("#[|]#", "iua", "a#[|]#"), + ) + .await?; + + Ok(()) +} + +fn config() -> Config { + let config = r#" + [keys.insert] + f.d = "normal_mode" + F1.j = "insert_newline" + "#; + Config::load( + Ok(config.to_owned()), + Err(helix_term::config::ConfigLoadError::default()), + ) + .unwrap() +}