diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 269ce13d15a8..e1e7daddc082 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -272,10 +272,7 @@ impl Application { pub fn handle_idle_timeout(&mut self) { use crate::compositor::EventResult; - let editor_view = self - .compositor - .find::() - .expect("expected at least one EditorView"); + let editor_view = self.compositor.editor_view(); let mut cx = crate::compositor::Context { editor: &mut self.editor, @@ -622,14 +619,9 @@ impl Application { log::info!("window/logMessage: {:?}", params); } Notification::ProgressMessage(params) - if !self - .compositor - .has_component(std::any::type_name::()) => + if !self.compositor.has_component::() => { - let editor_view = self - .compositor - .find::() - .expect("expected at least one EditorView"); + let editor_view = self.compositor.editor_view(); let lsp::ProgressParams { token, value } = params; let lsp::ProgressParamsValue::WorkDone(work) = value; @@ -724,10 +716,7 @@ impl Application { MethodCall::WorkDoneProgressCreate(params) => { self.lsp_progress.create(server_id, params.token); - let editor_view = self - .compositor - .find::() - .expect("expected at least one EditorView"); + let editor_view = self.compositor.editor_view(); let spinner = editor_view.spinners_mut().get_or_create(server_id); if spinner.is_stopped() { spinner.start(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c74898106576..c3b54da73b41 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -397,8 +397,20 @@ impl MappableCommand { surround_add, "Surround add", surround_replace, "Surround replace", surround_delete, "Surround delete", - select_textobject_around, "Select around object", - select_textobject_inner, "Select inside object", + select_around_word, "Select around current word", + select_inside_word, "Select inside current word", + select_around_long_word, "Select around current long word", + select_inside_long_word, "Select inside current long word", + select_around_class, "Select around current class", + select_inside_class, "Select inside current class", + select_around_function, "Select around current function", + select_inside_function, "Select inside current function", + select_around_parameter, "Select around current argument/parameter", + select_inside_parameter, "Select inside current argument/parameter", + select_around_cursor_pair, "Select around matching delimiter under cursor", + select_inside_cursor_pair, "Select inside matching delimiter under cursor", + prompt_and_select_around_pair, "Select around matching delimiter", + prompt_and_select_inside_pair, "Select inside matching delimiter", goto_next_function, "Goto next function", goto_prev_function, "Goto previous function", goto_next_class, "Goto next class", @@ -482,11 +494,8 @@ impl std::str::FromStr for MappableCommand { } impl<'de> Deserialize<'de> for MappableCommand { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; + fn deserialize>(deserializer: D) -> Result { + let s: &'de str = Deserialize::deserialize(deserializer)?; s.parse().map_err(de::Error::custom) } } @@ -515,6 +524,90 @@ impl PartialEq for MappableCommand { } } +#[derive(Clone)] +pub struct FallbackCommand { + pub name: &'static str, + pub fun: fn(cx: &mut Context, event: KeyEvent), + pub with_prompt: MappableCommand, + pub doc: &'static str, +} + +macro_rules! fallback_commands { + ( $($name:ident, $prompting_name:ident, $doc:literal,)* ) => { + $( + #[allow(non_upper_case_globals)] + pub const $name: Self = Self { + name: stringify!($name), + fun: $name, + with_prompt: MappableCommand::$prompting_name, + doc: $doc + }; + )* + + pub const FALLBACK_COMMAND_LIST: &'static [Self] = &[ + $( Self::$name, )* + ]; + } +} +impl FallbackCommand { + pub fn execute(&self, cx: &mut Context, event: KeyEvent) { + (self.fun)(cx, event) + } + + pub fn name(&self) -> &str { + self.name + } + + pub fn doc(&self) -> &str { + self.doc + } + + #[rustfmt::skip] + fallback_commands!( + select_around_pair, prompt_and_select_around_pair, "Select around matching delimiter", + select_inside_pair, prompt_and_select_inside_pair, "Select inside matching delimiter", + ); +} + +impl fmt::Debug for FallbackCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("FallbackCommand") + .field(&self.name()) + .finish() + } +} + +impl fmt::Display for FallbackCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.name()) + } +} + +impl std::str::FromStr for FallbackCommand { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + FallbackCommand::FALLBACK_COMMAND_LIST + .iter() + .find(|cmd| cmd.name() == s) + .cloned() + .ok_or_else(|| anyhow!("No command named '{}'", s)) + } +} + +impl<'de> Deserialize<'de> for FallbackCommand { + fn deserialize>(deserializer: D) -> Result { + let s: &'de str = Deserialize::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + +impl PartialEq for FallbackCommand { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + fn no_op(_cx: &mut Context) {} fn move_impl(cx: &mut Context, move_fn: F, dir: Direction, behaviour: Movement) @@ -2104,8 +2197,7 @@ pub fn command_palette(cx: &mut Context) { cx.callback = Some(Box::new( move |compositor: &mut Compositor, cx: &mut compositor::Context| { let doc = doc_mut!(cx.editor); - let keymap = - compositor.find::().unwrap().keymaps.map[&doc.mode].reverse_map(); + let keymap = compositor.editor_view().keymaps.map[&doc.mode].reverse_map(); let mut commands: Vec = MappableCommand::STATIC_COMMAND_LIST.into(); commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| { @@ -2116,33 +2208,14 @@ pub fn command_palette(cx: &mut Context) { } })); - // formats key bindings, multiple bindings are comma separated, - // individual key presses are joined with `+` - let fmt_binding = |bindings: &Vec>| -> String { - bindings - .iter() - .map(|bind| { - bind.iter() - .map(|key| key.to_string()) - .collect::>() - .join("+") - }) - .collect::>() - .join(", ") - }; - - let picker = Picker::new( + let picker = Picker::new_with_compositor_callback( commands, - move |command| match command { - MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) - { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + move |command| { + let doc = command.doc(); + match keymap.get(command.name()) { + Some(bindings) => format!("{doc} ({bindings})").into(), None => doc.into(), - }, - MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), - None => (*doc).into(), - }, + } }, move |cx, command, _action| { let mut ctx = Context { @@ -2154,6 +2227,13 @@ pub fn command_palette(cx: &mut Context) { jobs: cx.jobs, }; command.execute(&mut ctx); + if let Some(cb) = ctx.on_next_key_callback { + Some(Box::new(|compositor, _cx| { + compositor.editor_view().on_next_key(cb) + })) + } else { + None + } }, ); compositor.push(Box::new(picker)); @@ -3537,8 +3617,7 @@ pub fn completion(cx: &mut Context) { return; } let size = compositor.size(); - let ui = compositor.find::().unwrap(); - ui.set_completion( + compositor.editor_view().set_completion( editor, items, offset_encoding, @@ -3943,96 +4022,111 @@ fn goto_prev_comment(cx: &mut Context) { goto_ts_object_impl(cx, "comment", Direction::Backward) } -fn select_textobject_around(cx: &mut Context) { - select_textobject(cx, textobject::TextObject::Around); +#[derive(Copy, Clone, Debug)] +enum TextObjectSelector { + Word(bool), + Treesitter(&'static str), + Matching(Option), } -fn select_textobject_inner(cx: &mut Context) { - select_textobject(cx, textobject::TextObject::Inside); +fn select_textobject_impl( + editor: &mut Editor, + obj_type: textobject::TextObject, + sel: TextObjectSelector, + count: usize, +) { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| match sel { + TextObjectSelector::Word(long) => { + textobject::textobject_word(text, range, obj_type, count, long) + } + TextObjectSelector::Treesitter(obj_name) => { + if let Some((lang_config, syntax)) = doc.language_config().zip(doc.syntax()) { + textobject::textobject_treesitter( + text, + range, + obj_type, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + } else { + range + } + } + TextObjectSelector::Matching(ch) => { + let ch = ch.unwrap_or_else(|| text.char(range.cursor(text))); + if !ch.is_ascii_alphanumeric() { + // TODO: cancel new ranges if inconsistent surround matches across lines + textobject::textobject_surround(text, range, obj_type, ch, count) + } else { + range + } + } + }); + doc.set_selection(view.id, selection); } -fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { +fn select_textobject(cx: &mut Context, obj_type: textobject::TextObject, sel: TextObjectSelector) { let count = cx.count(); + let motion = move |editor: &mut Editor| select_textobject_impl(editor, obj_type, sel, count); + motion(cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); +} - cx.on_next_key(move |cx, event| { - cx.editor.autoinfo = None; - cx.editor.pseudo_pending = None; - if let Some(ch) = event.char() { - let textobject = move |editor: &mut Editor| { - let (view, doc) = current!(editor); - let text = doc.text().slice(..); - - let textobject_treesitter = |obj_name: &str, range: Range| -> Range { - let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { - Some(t) => t, - None => return range, - }; - textobject::textobject_treesitter( - text, - range, - objtype, - obj_name, - syntax.tree().root_node(), - lang_config, - count, - ) - }; +macro_rules! select_textobject_commands { + ( $($name:ident($objtype:ident, $objsel:ident($val:expr));)* ) => { + $( + fn $name(cx: &mut Context) { + select_textobject( + cx, + textobject::TextObject::$objtype, + TextObjectSelector::$objsel($val), + ) + } + )* + }; +} - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count, false), - 'W' => textobject::textobject_word(text, range, objtype, count, true), - 'c' => textobject_treesitter("class", range), - 'f' => textobject_treesitter("function", range), - 'a' => textobject_treesitter("parameter", range), - 'o' => textobject_treesitter("comment", range), - 'm' => { - let ch = text.char(range.cursor(text)); - if !ch.is_ascii_alphanumeric() { - textobject::textobject_surround(text, range, objtype, ch, count) - } else { - range - } - } - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) - } - _ => range, - } - }); - doc.set_selection(view.id, selection); - }; - textobject(cx.editor); - cx.editor.last_motion = Some(Motion(Box::new(textobject))); - } - }); +select_textobject_commands! { + select_around_word(Around, Word(false)); + select_inside_word(Inside, Word(false)); + select_around_long_word(Around, Word(true)); + select_inside_long_word(Inside, Word(true)); + select_around_class(Around, Treesitter("class")); + select_inside_class(Inside, Treesitter("class")); + select_around_function(Around, Treesitter("function")); + select_inside_function(Inside, Treesitter("function")); + select_around_parameter(Around, Treesitter("parameter")); + select_inside_parameter(Inside, Treesitter("parameter")); + select_around_cursor_pair(Around, Matching(None)); + select_inside_cursor_pair(Inside, Matching(None)); +} - if let Some((title, abbrev)) = match objtype { - textobject::TextObject::Inside => Some(("Match inside", "mi")), - textobject::TextObject::Around => Some(("Match around", "ma")), - _ => return, - } { - let help_text = [ - ("w", "Word"), - ("W", "WORD"), - ("c", "Class (tree-sitter)"), - ("f", "Function (tree-sitter)"), - ("a", "Argument/parameter (tree-sitter)"), - ("o", "Comment (tree-sitter)"), - ("m", "Matching delimiter under cursor"), - (" ", "... or any character acting as a pair"), - ]; +fn prompt_and_select_around_pair(cx: &mut Context) { + cx.editor.set_status("Select a delimiter..."); + cx.on_next_key(select_around_pair); +} - cx.editor.autoinfo = Some(Info::new( - title, - help_text - .into_iter() - .map(|(col1, col2)| (col1.to_string(), col2.to_string())) - .collect(), - )); - cx.editor.pseudo_pending = Some(abbrev.to_string()); - }; +fn prompt_and_select_inside_pair(cx: &mut Context) { + cx.editor.set_status("Select a delimiter..."); + cx.on_next_key(select_inside_pair); +} + +fn select_around_pair(cx: &mut Context, event: KeyEvent) { + select_pair(cx, textobject::TextObject::Around, event); +} + +fn select_inside_pair(cx: &mut Context, event: KeyEvent) { + select_pair(cx, textobject::TextObject::Inside, event); +} + +fn select_pair(cx: &mut Context, objtype: textobject::TextObject, event: KeyEvent) { + if let Some(ch) = event.char() { + select_textobject(cx, objtype, TextObjectSelector::Matching(Some(ch))); + } } fn surround_add(cx: &mut Context) { diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 4f988aceed38..8831b8091818 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -26,6 +26,7 @@ pub enum EventResult { use helix_view::Editor; use crate::job::Jobs; +use crate::ui; pub struct Context<'a> { pub editor: &'a mut Editor, @@ -206,25 +207,28 @@ impl Compositor { (None, CursorKind::Hidden) } - pub fn has_component(&self, type_name: &str) -> bool { + pub fn has_component(&self) -> bool { self.layers .iter() - .any(|component| component.type_name() == type_name) + .any(|component| component.as_any().is::()) } pub fn find(&mut self) -> Option<&mut T> { - let type_name = std::any::type_name::(); self.layers .iter_mut() - .find(|component| component.type_name() == type_name) - .and_then(|component| component.as_any_mut().downcast_mut()) + .find_map(|component| component.as_any_mut().downcast_mut::()) } pub fn find_id(&mut self, id: &'static str) -> Option<&mut T> { self.layers .iter_mut() - .find(|component| component.id() == Some(id)) - .and_then(|component| component.as_any_mut().downcast_mut()) + .filter(|component| component.id() == Some(id)) + .find_map(|component| component.as_any_mut().downcast_mut::()) + } + + pub fn editor_view(&mut self) -> &mut ui::EditorView { + self.find::() + .expect("Expected at least one EditorView") } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 992a0cb8ba85..377b5b9bf462 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,4 +1,4 @@ -pub use crate::commands::MappableCommand; +pub use crate::commands::{FallbackCommand, MappableCommand}; use crate::config::Config; use helix_core::hashmap; use helix_view::{document::Mode, info::Info, input::KeyEvent}; @@ -6,7 +6,7 @@ use serde::Deserialize; use std::{ borrow::Cow, collections::{BTreeSet, HashMap}, - ops::{Deref, DerefMut}, + ops::Deref, }; #[macro_export] @@ -96,9 +96,19 @@ macro_rules! keymap { }; (@trie - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } + { + $label:literal + $(sticky=$sticky:literal)? + $($($key:literal)|+ => $value:tt,)+ + $(_ => $fallback:ident,)? + } ) => { - keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) + keymap!({ + $label + $(sticky=$sticky)? + $($($key)|+ => $value,)+ + $(_ => $fallback,)? + }) }; (@trie [$($cmd:ident),* $(,)?]) => { @@ -106,7 +116,12 @@ macro_rules! keymap { }; ( - { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } + { + $label:literal + $(sticky=$sticky:literal)? + $($($key:literal)|+ => $value:tt,)+ + $(_ => $fallback:ident,)? + } ) => { // modified from the hashmap! macro { @@ -126,6 +141,7 @@ macro_rules! keymap { )* let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order); $( _node.is_sticky = $sticky; )? + $( _node.fallback = Some($crate::commands::FallbackCommand::$fallback); )? $crate::keymap::KeyTrie::Node(_node) } }; @@ -136,6 +152,8 @@ pub struct KeyTrieNode { /// A label for keys coming under this node, like "Goto mode" name: String, map: HashMap, + // TODO: Come up with a serialized representation of this + fallback: Option, order: Vec, pub is_sticky: bool, } @@ -160,6 +178,7 @@ impl KeyTrieNode { Self { name: name.to_string(), map, + fallback: None, order, is_sticky: false, } @@ -169,6 +188,14 @@ impl KeyTrieNode { &self.name } + pub fn len(&self) -> usize { + self.map.len() + self.fallback.is_some() as usize + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + /// Merge another Node in. Leaves and subnodes from the other node replace /// corresponding keyevent in self, except when both other and self have /// subnodes for same key. In that case the merge is recursive. @@ -187,11 +214,14 @@ impl KeyTrieNode { self.order.push(key); } } + if other.fallback.is_some() { + self.fallback = other.fallback; + } } pub fn infobox(&self) -> Info { - let mut body: Vec<(&str, BTreeSet)> = Vec::with_capacity(self.len()); - for (&key, trie) in self.iter() { + let mut body: Vec<(&str, BTreeSet>)> = Vec::with_capacity(self.len()); + for (&key, trie) in &self.map { let desc = match trie { KeyTrie::Leaf(cmd) => { if cmd.name() == "no_op" { @@ -204,23 +234,31 @@ impl KeyTrieNode { }; match body.iter().position(|(d, _)| d == &desc) { Some(pos) => { - body[pos].1.insert(key); + body[pos].1.insert(Some(key)); } - None => body.push((desc, BTreeSet::from([key]))), + None => body.push((desc, [Some(key)].into())), + } + } + if let Some(cmd) = &self.fallback { + let desc = cmd.doc(); + match body.iter().position(|(d, _)| d == &desc) { + Some(pos) => { + body[pos].1.insert(None); + } + None => body.push((desc, [None].into())), } } body.sort_unstable_by_key(|(_, keys)| { - self.order - .iter() - .position(|&k| k == *keys.iter().next().unwrap()) - .unwrap() + match keys.iter().next().unwrap() { + Some(key) => self.order.iter().position(|k| k == key).unwrap(), + None => usize::MAX, // fallback goes at the end + } }); let prefix = format!("{} ", self.name()); if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { - body = body - .into_iter() - .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys)) - .collect(); + for (desc, _) in &mut body { + *desc = desc.strip_prefix(&prefix).unwrap(); + } } Info::from_keymap(self.name(), body) } @@ -228,6 +266,21 @@ impl KeyTrieNode { pub fn order(&self) -> &[KeyEvent] { self.order.as_slice() } + + fn search(&self, keys: &'_ [KeyEvent]) -> KeyTrieResult<'_> { + use KeyTrieResult::*; + match keys { + [] => Pending(self), + [k] => match self.map.get(k) { + Some(trie) => trie.search(&[]), + None => self.fallback.as_ref().map_or(NotFound, MatchedFallback), + }, + [k, rest @ ..] => match self.map.get(k) { + Some(trie) => trie.search(rest), + None => NotFound, + }, + } + } } impl Default for KeyTrieNode { @@ -238,21 +291,7 @@ impl Default for KeyTrieNode { impl PartialEq for KeyTrieNode { fn eq(&self, other: &Self) -> bool { - self.map == other.map - } -} - -impl Deref for KeyTrieNode { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl DerefMut for KeyTrieNode { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map + self.map == other.map && self.fallback == other.fallback } } @@ -286,16 +325,34 @@ impl KeyTrie { self.node_mut().unwrap().merge(node); } - pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> { - let mut trie = self; - for key in keys { - trie = match trie { - KeyTrie::Node(map) => map.get(key), - // leaf encountered while keys left to process - KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None, - }? + fn search(&self, keys: &'_ [KeyEvent]) -> KeyTrieResult<'_> { + match self { + KeyTrie::Leaf(cmd) if keys.is_empty() => KeyTrieResult::Matched(cmd), + KeyTrie::Sequence(cmds) if keys.is_empty() => KeyTrieResult::MatchedSequence(cmds), + KeyTrie::Node(node) => node.search(keys), + _ => KeyTrieResult::NotFound, + } + } +} + +#[derive(Debug, PartialEq)] +enum KeyTrieResult<'a> { + Pending(&'a KeyTrieNode), + Matched(&'a MappableCommand), + MatchedFallback(&'a FallbackCommand), + MatchedSequence(&'a [MappableCommand]), + NotFound, +} + +impl From> for KeymapResult { + fn from(r: KeyTrieResult<'_>) -> Self { + match r { + KeyTrieResult::Pending(node) => Self::Pending(node.clone()), + KeyTrieResult::Matched(cmd) => Self::Matched(cmd.clone()), + KeyTrieResult::MatchedFallback(cmd) => Self::MatchedFallback(cmd.clone()), + KeyTrieResult::MatchedSequence(cmds) => Self::MatchedSequence(cmds.to_vec()), + KeyTrieResult::NotFound => Self::NotFound, } - Some(trie) } } @@ -304,6 +361,8 @@ pub enum KeymapResult { /// Needs more keys to execute a command. Contains valid keys for next keystroke. Pending(KeyTrieNode), Matched(MappableCommand), + /// Matched a fallback command, which uses the last key event. + MatchedFallback(FallbackCommand), /// Matched a sequence of commands to execute. MatchedSequence(Vec), /// Key was not found in the root keymap @@ -313,6 +372,70 @@ pub enum KeymapResult { Cancelled(Vec), } +#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord)] +pub struct Binding { + pub keys: Vec, + pub is_fallback: bool, +} + +impl Binding { + pub fn new(keys: &[KeyEvent]) -> Self { + Self { + keys: keys.to_vec(), + is_fallback: false, + } + } + + pub fn fallback(keys: &[KeyEvent]) -> Self { + Self { + keys: keys.to_vec(), + is_fallback: true, + } + } +} + +impl std::fmt::Display for Binding { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + debug_assert!(!self.keys.is_empty()); + self.keys[0].fmt(f)?; + for key in &self.keys[1..] { + write!(f, "+{key}")?; + } + if self.is_fallback { + write!(f, "+…")?; + } + Ok(()) + } +} + +#[derive(Debug, Clone, Default, PartialEq)] +pub struct Bindings { + pub bindings: Vec, +} + +impl Bindings { + pub fn add_binding(&mut self, keys: &[KeyEvent]) { + self.bindings.push(Binding::new(keys)); + } + + pub fn add_fallback(&mut self, keys: &[KeyEvent]) { + self.bindings.push(Binding::fallback(keys)); + } +} + +impl std::fmt::Display for Bindings { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.bindings.is_empty() { + return Ok(()); + } + self.bindings[0].fmt(f)?; + for binding in &self.bindings[1..] { + write!(f, ", {binding}")?; + } + Ok(()) + } +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] pub struct Keymap { @@ -325,29 +448,30 @@ impl Keymap { Keymap { root } } - pub fn reverse_map(&self) -> HashMap>> { + pub fn reverse_map(&self) -> HashMap { // recursively visit all nodes in keymap fn map_node( - cmd_map: &mut HashMap>>, + cmd_map: &mut HashMap, node: &KeyTrie, keys: &mut Vec, ) { match node { - KeyTrie::Leaf(cmd) => match cmd { - MappableCommand::Typable { name, .. } => { - cmd_map.entry(name.into()).or_default().push(keys.clone()) - } - MappableCommand::Static { name, .. } => cmd_map - .entry(name.to_string()) - .or_default() - .push(keys.clone()), - }, + KeyTrie::Leaf(cmd) => cmd_map + .entry(cmd.name().into()) + .or_default() + .add_binding(keys), KeyTrie::Node(next) => { for (key, trie) in &next.map { keys.push(*key); map_node(cmd_map, trie, keys); keys.pop(); } + if let Some(fallback) = &next.fallback { + cmd_map + .entry(fallback.with_prompt.name().into()) + .or_default() + .add_fallback(keys) + } } KeyTrie::Sequence(_) => {} }; @@ -436,35 +560,26 @@ impl Keymaps { }; let trie = match trie_node.search(&[*first]) { - Some(KeyTrie::Leaf(ref cmd)) => { - return KeymapResult::Matched(cmd.clone()); - } - Some(KeyTrie::Sequence(ref cmds)) => { - return KeymapResult::MatchedSequence(cmds.clone()); - } - None => return KeymapResult::NotFound, - Some(t) => t, + KeyTrieResult::Pending(node) => node, + res => return res.into(), }; self.state.push(key); - match trie.search(&self.state[1..]) { - Some(&KeyTrie::Node(ref map)) => { - if map.is_sticky { + let res = trie.search(&self.state[1..]); + match res { + KeyTrieResult::Pending(node) => { + if node.is_sticky { self.state.clear(); - self.sticky = Some(map.clone()); + self.sticky = Some(node.clone()); } - KeymapResult::Pending(map.clone()) - } - Some(&KeyTrie::Leaf(ref cmd)) => { - self.state.clear(); - KeymapResult::Matched(cmd.clone()) } - Some(&KeyTrie::Sequence(ref cmds)) => { - self.state.clear(); - KeymapResult::MatchedSequence(cmds.clone()) + KeyTrieResult::NotFound => { + let keys = std::mem::take(&mut self.state); + return KeymapResult::Cancelled(keys); } - None => KeymapResult::Cancelled(self.state.drain(..).collect()), + _ => self.state.clear(), } + res.into() } } @@ -559,8 +674,24 @@ impl Default for Keymaps { "s" => surround_add, "r" => surround_replace, "d" => surround_delete, - "a" => select_textobject_around, - "i" => select_textobject_inner, + "a" => { "Select around" + "w" => select_around_word, + "W" => select_around_long_word, + "c" => select_around_class, + "f" => select_around_function, + "a" => select_around_parameter, + "m" => select_around_cursor_pair, + _ => select_around_pair, + }, + "i" => { "Select inside" + "w" => select_inside_word, + "W" => select_inside_long_word, + "c" => select_inside_class, + "f" => select_inside_function, + "a" => select_inside_parameter, + "m" => select_inside_cursor_pair, + _ => select_inside_pair, + }, }, "[" => { "Left bracket" "d" => goto_prev_diag, @@ -894,20 +1025,20 @@ mod tests { let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap(); // Assumes that `g` is a node in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_line_end), + keymap.root().search(&[key!('g'), key!('$')]), + KeyTrieResult::Matched(&MappableCommand::goto_line_end), "Leaf should be present in merged subnode" ); // Assumes that `gg` is in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::delete_char_forward), + keymap.root().search(&[key!('g'), key!('g')]), + KeyTrieResult::Matched(&MappableCommand::delete_char_forward), "Leaf should replace old leaf in merged subnode" ); // Assumes that `ge` is in default keymap assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(MappableCommand::goto_last_line), + keymap.root().search(&[key!('g'), key!('e')]), + KeyTrieResult::Matched(&MappableCommand::goto_last_line), "Old leaves in subnode should be present in merged node" ); @@ -937,16 +1068,16 @@ mod tests { let keymap = merged_config.keys.map.get_mut(&Mode::Normal).unwrap(); // Make sure mapping works assert_eq!( - keymap - .root() - .search(&[key!(' '), key!('s'), key!('v')]) - .unwrap(), - &KeyTrie::Leaf(MappableCommand::vsplit), + keymap.root().search(&[key!(' '), key!('s'), key!('v')]), + KeyTrieResult::Matched(&MappableCommand::vsplit), "Leaf should be present in merged subnode" ); + let node = match keymap.root().search(&[key!(' ')]) { + KeyTrieResult::Pending(n) => n, + _ => panic!("Expected node"), + }; // Make sure an order was set during merge - let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); - assert!(!node.node().unwrap().order().is_empty()) + assert!(!node.order().is_empty()) } #[test] @@ -954,13 +1085,13 @@ mod tests { let keymaps = Keymaps::default(); let root = keymaps.map.get(&Mode::Normal).unwrap().root(); assert_eq!( - root.search(&[key!(' '), key!('w')]).unwrap(), - root.search(&["C-w".parse::().unwrap()]).unwrap(), + root.search(&[key!(' '), key!('w')]), + root.search(&["C-w".parse::().unwrap()]), "Mismatch for window mode on `Space-w` and `Ctrl-w`" ); assert_eq!( - root.search(&[key!('z')]).unwrap(), - root.search(&[key!('Z')]).unwrap(), + root.search(&[key!('z')]), + root.search(&[key!('Z')]), "Mismatch for view mode on `z` and `Z`" ); } @@ -974,6 +1105,14 @@ mod tests { "e" => goto_file_end, }, "j" | "k" => move_line_down, + "m" => { "Match" + "m" => match_brackets, + "a" => { "Select around" + "w" => select_around_word, + "W" => select_around_long_word, + _ => select_around_pair, + }, + }, }); let keymap = Keymap::new(normal_mode); let mut reverse_map = keymap.reverse_map(); @@ -982,27 +1121,34 @@ mod tests { // HashMaps can be compared but we can still get different ordering of bindings // for commands that have multiple bindings assigned for v in reverse_map.values_mut() { - v.sort() + v.bindings.sort() } - assert_eq!( - reverse_map, - HashMap::from([ - ("insert_mode".to_string(), vec![vec![key!('i')]]), - ( - "goto_file_start".to_string(), - vec![vec![key!('g'), key!('g')]] - ), - ( - "goto_file_end".to_string(), - vec![vec![key!('g'), key!('e')]] - ), - ( - "move_line_down".to_string(), - vec![vec![key!('j')], vec![key!('k')]] - ), - ]), - "Mistmatch" - ) + fn make_binding(pair: &(&str, bool)) -> Binding { + Binding { + keys: pair.0.chars().map(|k| key!(k,)).collect(), + is_fallback: pair.1, + } + } + + let expected_map = [ + ("insert_mode", &[("i", false)][..]), + ("goto_file_start", &[("gg", false)]), + ("goto_file_end", &[("ge", false)]), + ("move_line_down", &[("j", false), ("k", false)]), + ("match_brackets", &[("mm", false)]), + ("select_around_word", &[("maw", false)]), + ("select_around_long_word", &[("maW", false)]), + ("prompt_and_select_around_pair", &[("ma", true)]), + ] + .into_iter() + .map(|(command, bindings)| { + let mut bindings = bindings.iter().map(make_binding).collect::>(); + bindings.sort(); + (command.to_owned(), Bindings { bindings }) + }) + .collect::>(); + + assert_eq!(reverse_map, expected_map, "Mismatch"); } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 611d65fb37e3..b1d6f626c7a8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -708,6 +708,7 @@ impl EditorView { match &key_result { KeymapResult::Matched(command) => command.execute(cxt), + KeymapResult::MatchedFallback(command) => command.execute(cxt, event), KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), KeymapResult::MatchedSequence(commands) => { for command in commands { @@ -863,6 +864,10 @@ impl EditorView { EventResult::Consumed(None) } + + pub fn on_next_key(&mut self, cb: Box) { + self.on_next_key = Some(cb); + } } impl EditorView { @@ -1256,9 +1261,6 @@ impl Component for EditorView { disp.push_str(&s); } } - if let Some(pseudo_pending) = &cx.editor.pseudo_pending { - disp.push_str(pseudo_pending.as_str()) - } let style = cx.editor.theme.get("ui.text"); let macro_width = if cx.editor.macro_recording.is_some() { 3 diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 3f2da92fae69..0631c658fe04 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,5 +1,5 @@ use crate::{ - compositor::{Component, Compositor, Context, EventResult}, + compositor::{Callback, Component, Compositor, Context, EventResult}, ctrl, key, shift, ui::{self, EditorView}, }; @@ -293,14 +293,14 @@ pub struct Picker { pub truncate_start: bool, format_fn: Box Cow>, - callback_fn: Box, + callback_fn: Box Option>, } impl Picker { - pub fn new( + pub fn new_with_compositor_callback( options: Vec, format_fn: impl Fn(&T) -> Cow + 'static, - callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + callback_fn: impl Fn(&mut Context, &T, Action) -> Option + 'static, ) -> Self { let prompt = Prompt::new( "".into(), @@ -336,6 +336,18 @@ impl Picker { picker } + pub fn new( + options: Vec, + format_fn: impl Fn(&T) -> Cow + 'static, + callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + ) -> Self { + let callback = move |ctx: &mut Context, option: &T, action: Action| { + callback_fn(ctx, option, action); + None + }; + Self::new_with_compositor_callback(options, format_fn, callback) + } + pub fn score(&mut self) { let now = Instant::now(); @@ -483,10 +495,19 @@ impl Component for Picker { _ => return EventResult::Ignored(None), }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { - // remove the layer - compositor.last_picker = compositor.pop(); - }))); + let close_fn = |compositor_cb: Option| { + let f = |compositor: &mut Compositor, cx: &mut Context| { + // remove the layer + compositor.last_picker = compositor.pop(); + // if the selected option gave back a callback, run it + if let Some(cb) = compositor_cb { + cb(compositor, cx) + } + }; + EventResult::Consumed(Some(Box::new(f))) + }; + + let mut compositor_cb = None; match key_event.into() { shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { @@ -508,25 +529,25 @@ impl Component for Picker { self.to_end(); } key!(Esc) | ctrl!('c') => { - return close_fn; + return close_fn(None); } key!(Enter) => { if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::Replace); + compositor_cb = (self.callback_fn)(cx, option, Action::Replace); } - return close_fn; + return close_fn(compositor_cb); } ctrl!('s') => { if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::HorizontalSplit); + compositor_cb = (self.callback_fn)(cx, option, Action::HorizontalSplit); } - return close_fn; + return close_fn(compositor_cb); } ctrl!('v') => { if let Some(option) = self.selection() { - (self.callback_fn)(cx, option, Action::VerticalSplit); + compositor_cb = (self.callback_fn)(cx, option, Action::VerticalSplit); } - return close_fn; + return close_fn(compositor_cb); } ctrl!(' ') => { self.save_filter(cx); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index adf0cdf335a7..7c82709693c4 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -316,7 +316,6 @@ pub struct Editor { pub idle_timer: Pin>, pub last_motion: Option, - pub pseudo_pending: Option, pub last_completion: Option, @@ -373,7 +372,6 @@ impl Editor { idle_timer: Box::pin(sleep(config.idle_timeout)), last_motion: None, last_completion: None, - pseudo_pending: None, config, auto_pairs, exit_code: 0, diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 5ad6a60c7487..8874fa419e4c 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -41,11 +41,12 @@ impl Info { } } - pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet)>) -> Self { + pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet>)>) -> Self { + let fmt_key = |key: &Option| key.map_or("…".into(), |k| k.to_string()); let body = body .into_iter() .map(|(desc, events)| { - let events = events.iter().map(ToString::to_string).collect::>(); + let events = events.iter().map(fmt_key).collect::>(); (events.join(", "), desc.to_string()) }) .collect();