on master help sign in
feat: Make it possible to keybind `TypableCommands` (#1169)
a06871aOskar Nehlin
helix-term/src/commands.rs+112−46
@@ -134,47 +134,76 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
134 view.offset.row = line.saturating_sub(relative);
134 view.offset.row = line.saturating_sub(relative);
135}
135}
136
136
137−/// A command is composed of a static name, and a function that takes the current state plus a count,
137+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
138−/// and does a side-effect on the state (usually by creating and applying a transaction).
138+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
139+/// Both of these types of commands can be mapped with keybindings in the config.toml.
139−#[derive(Copy, Clone)]
140+#[derive(Clone)]
140−pub struct Command {
141+pub enum MappableCommand {
142+ Typable {
143+ name: String,
144+ args: Vec<String>,
145+ doc: String,
146+ },
147+ Static {
141− name: &'static str,
148+ name: &'static str,
142− fun: fn(cx: &mut Context),
149+ fun: fn(cx: &mut Context),
143− doc: &'static str,
150+ doc: &'static str,
151+ },
144−}
152+}
145−
153+
146−macro_rules! commands {
154+macro_rules! static_commands {
147 ( $($name:ident, $doc:literal,)* ) => {
155 ( $($name:ident, $doc:literal,)* ) => {
148 $(
156 $(
149 #[allow(non_upper_case_globals)]
157 #[allow(non_upper_case_globals)]
150− pub const $name: Self = Self {
158+ pub const $name: Self = Self::Static {
151 name: stringify!($name),
159 name: stringify!($name),
152 fun: $name,
160 fun: $name,
153 doc: $doc
161 doc: $doc
154 };
162 };
155 )*
163 )*
156
164
157− pub const COMMAND_LIST: &'static [Self] = &[
165+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
158 $( Self::$name, )*
166 $( Self::$name, )*
159 ];
167 ];
160 }
168 }
161}
169}
162
170
163−impl Command {
171+impl MappableCommand {
164 pub fn execute(&self, cx: &mut Context) {
172 pub fn execute(&self, cx: &mut Context) {
165− (self.fun)(cx);
173+ match &self {
174+ MappableCommand::Typable { name, args, doc: _ } => {
175+ let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect();
176+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
177+ let mut cx = compositor::Context {
178+ editor: cx.editor,
179+ jobs: cx.jobs,
180+ scroll: None,
181+ };
182+ if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) {
183+ cx.editor.set_error(format!("{}", e));
184+ }
185+ }
186+ }
187+ MappableCommand::Static { fun, .. } => (fun)(cx),
188+ }
166 }
189 }
167
190
168− pub fn name(&self) -> &'static str {
191+ pub fn name(&self) -> &str {
169− self.name
192+ match &self {
193+ MappableCommand::Typable { name, .. } => name,
194+ MappableCommand::Static { name, .. } => name,
195+ }
170 }
196 }
171
197
172− pub fn doc(&self) -> &'static str {
198+ pub fn doc(&self) -> &str {
173− self.doc
199+ match &self {
200+ MappableCommand::Typable { doc, .. } => doc,
201+ MappableCommand::Static { doc, .. } => doc,
202+ }
174 }
203 }
175
204
176 #[rustfmt::skip]
205 #[rustfmt::skip]
177− commands!(
206+ static_commands!(
178 no_op, "Do nothing",
207 no_op, "Do nothing",
179 move_char_left, "Move left",
208 move_char_left, "Move left",
180 move_char_right, "Move right",
209 move_char_right, "Move right",
@@ -367,33 +396,51 @@ impl Command {
367 );
396 );
368}
397}
369
398
370−impl fmt::Debug for Command {
399+impl fmt::Debug for MappableCommand {
371 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
400 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372− let Command { name, .. } = self;
401+ f.debug_tuple("MappableCommand")
373− f.debug_tuple("Command").field(name).finish()
402+ .field(&self.name())
403+ .finish()
374 }
404 }
375}
405}
376
406
377−impl fmt::Display for Command {
407+impl fmt::Display for MappableCommand {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379− let Command { name, .. } = self;
380− f.write_str(name)
409+ f.write_str(self.name())
381 }
410 }
382}
411}
383
412
384−impl std::str::FromStr for Command {
413+impl std::str::FromStr for MappableCommand {
385 type Err = anyhow::Error;
414 type Err = anyhow::Error;
386
415
387 fn from_str(s: &str) -> Result<Self, Self::Err> {
416 fn from_str(s: &str) -> Result<Self, Self::Err> {
388− Command::COMMAND_LIST
417+ if let Some(suffix) = s.strip_prefix(':') {
418+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
419+ let name = typable_command
420+ .next()
421+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
422+ let args = typable_command
423+ .map(|s| s.to_owned())
424+ .collect::<Vec<String>>();
425+ cmd::TYPABLE_COMMAND_MAP
426+ .get(name)
427+ .map(|cmd| MappableCommand::Typable {
428+ name: cmd.name.to_owned(),
429+ doc: format!(":{} {:?}", cmd.name, args),
430+ args,
431+ })
432+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
433+ } else {
434+ MappableCommand::STATIC_COMMAND_LIST
389− .iter()
435+ .iter()
390− .copied()
436+ .cloned()
391− .find(|cmd| cmd.name == s)
437+ .find(|cmd| cmd.name() == s)
392− .ok_or_else(|| anyhow!("No command named '{}'", s))
438+ .ok_or_else(|| anyhow!("No command named '{}'", s))
439+ }
393 }
440 }
394}
441}
395
442
396−impl<'de> Deserialize<'de> for Command {
443+impl<'de> Deserialize<'de> for MappableCommand {
397 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
444 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
398 where
445 where
399 D: Deserializer<'de>,
446 D: Deserializer<'de>,
@@ -403,9 +450,27 @@ impl<'de> Deserialize<'de> for Command {
403 }
450 }
404}
451}
405
452
406−impl PartialEq for Command {
453+impl PartialEq for MappableCommand {
407 fn eq(&self, other: &Self) -> bool {
454 fn eq(&self, other: &Self) -> bool {
408− self.name() == other.name()
455+ match (self, other) {
456+ (
457+ MappableCommand::Typable {
458+ name: first_name, ..
459+ },
460+ MappableCommand::Typable {
461+ name: second_name, ..
462+ },
463+ ) => first_name == second_name,
464+ (
465+ MappableCommand::Static {
466+ name: first_name, ..
467+ },
468+ MappableCommand::Static {
469+ name: second_name, ..
470+ },
471+ ) => first_name == second_name,
472+ _ => false,
473+ }
409 }
474 }
410}
475}
411
476
@@ -2843,15 +2908,16 @@ mod cmd {
2843 }
2908 }
2844 ];
2909 ];
2845
2910
2846− pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
2911+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
2912+ Lazy::new(|| {
2847− TYPABLE_COMMAND_LIST
2913+ TYPABLE_COMMAND_LIST
2848− .iter()
2914+ .iter()
2849− .flat_map(|cmd| {
2915+ .flat_map(|cmd| {
2850− std::iter::once((cmd.name, cmd))
2916+ std::iter::once((cmd.name, cmd))
2851− .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2917+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
2852− })
2918+ })
2853− .collect()
2919+ .collect()
2854− });
2920+ });
2855}
2921}
2856
2922
2857fn command_mode(cx: &mut Context) {
2923fn command_mode(cx: &mut Context) {
@@ -2877,7 +2943,7 @@ fn command_mode(cx: &mut Context) {
2877 if let Some(cmd::TypableCommand {
2943 if let Some(cmd::TypableCommand {
2878 completer: Some(completer),
2944 completer: Some(completer),
2879 ..
2945 ..
2880− }) = cmd::COMMANDS.get(parts[0])
2946+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
2881 {
2947 {
2882 completer(part)
2948 completer(part)
2883 .into_iter()
2949 .into_iter()
@@ -2912,7 +2978,7 @@ fn command_mode(cx: &mut Context) {
2912 }
2978 }
2913
2979
2914 // Handle typable commands
2980 // Handle typable commands
2915− if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
2981+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
2916 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2982 if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
2917 cx.editor.set_error(format!("{}", e));
2983 cx.editor.set_error(format!("{}", e));
2918 }
2984 }
@@ -2925,7 +2991,7 @@ fn command_mode(cx: &mut Context) {
2925 prompt.doc_fn = Box::new(|input: &str| {
2991 prompt.doc_fn = Box::new(|input: &str| {
2926 let part = input.split(' ').next().unwrap_or_default();
2992 let part = input.split(' ').next().unwrap_or_default();
2927
2993
2928− if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
2994+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
2929 return Some(doc);
2995 return Some(doc);
2930 }
2996 }
2931
2997
helix-term/src/keymap.rs+18−18
@@ -1,4 +1,4 @@
1−pub use crate::commands::Command;
1+pub use crate::commands::MappableCommand;
2use crate::config::Config;
2use crate::config::Config;
3use helix_core::hashmap;
3use helix_core::hashmap;
4use helix_view::{document::Mode, info::Info, input::KeyEvent};
4use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
92#[macro_export]
92#[macro_export]
93macro_rules! keymap {
93macro_rules! keymap {
94 (@trie $cmd:ident) => {
94 (@trie $cmd:ident) => {
95− $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
95+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
96 };
96 };
97
97
98 (@trie
98 (@trie
@@ -260,8 +260,8 @@ impl DerefMut for KeyTrieNode {
260#[derive(Debug, Clone, PartialEq, Deserialize)]
260#[derive(Debug, Clone, PartialEq, Deserialize)]
261#[serde(untagged)]
261#[serde(untagged)]
262pub enum KeyTrie {
262pub enum KeyTrie {
263− Leaf(Command),
263+ Leaf(MappableCommand),
264− Sequence(Vec<Command>),
264+ Sequence(Vec<MappableCommand>),
265 Node(KeyTrieNode),
265 Node(KeyTrieNode),
266}
266}
267
267
@@ -304,9 +304,9 @@ impl KeyTrie {
304pub enum KeymapResultKind {
304pub enum KeymapResultKind {
305 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
305 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
306 Pending(KeyTrieNode),
306 Pending(KeyTrieNode),
307− Matched(Command),
307+ Matched(MappableCommand),
308 /// Matched a sequence of commands to execute.
308 /// Matched a sequence of commands to execute.
309− MatchedSequence(Vec<Command>),
309+ MatchedSequence(Vec<MappableCommand>),
310 /// Key was not found in the root keymap
310 /// Key was not found in the root keymap
311 NotFound,
311 NotFound,
312 /// Key is invalid in combination with previous keys. Contains keys leading upto
312 /// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -386,10 +386,10 @@ impl Keymap {
386 };
386 };
387
387
388 let trie = match trie_node.search(&[*first]) {
388 let trie = match trie_node.search(&[*first]) {
389− Some(&KeyTrie::Leaf(cmd)) => {
389+ Some(KeyTrie::Leaf(ref cmd)) => {
390− return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
390+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
391 }
391 }
392− Some(&KeyTrie::Sequence(ref cmds)) => {
392+ Some(KeyTrie::Sequence(ref cmds)) => {
393 return KeymapResult::new(
393 return KeymapResult::new(
394 KeymapResultKind::MatchedSequence(cmds.clone()),
394 KeymapResultKind::MatchedSequence(cmds.clone()),
395 self.sticky(),
395 self.sticky(),
@@ -408,9 +408,9 @@ impl Keymap {
408 }
408 }
409 KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
409 KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
410 }
410 }
411− Some(&KeyTrie::Leaf(cmd)) => {
411+ Some(&KeyTrie::Leaf(ref cmd)) => {
412 self.state.clear();
412 self.state.clear();
413− return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
413+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
414 }
414 }
415 Some(&KeyTrie::Sequence(ref cmds)) => {
415 Some(&KeyTrie::Sequence(ref cmds)) => {
416 self.state.clear();
416 self.state.clear();
@@ -833,36 +833,36 @@ mod tests {
833 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
833 let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
834 assert_eq!(
834 assert_eq!(
835 keymap.get(key!('i')).kind,
835 keymap.get(key!('i')).kind,
836− KeymapResultKind::Matched(Command::normal_mode),
836+ KeymapResultKind::Matched(MappableCommand::normal_mode),
837 "Leaf should replace leaf"
837 "Leaf should replace leaf"
838 );
838 );
839 assert_eq!(
839 assert_eq!(
840 keymap.get(key!('无')).kind,
840 keymap.get(key!('无')).kind,
841− KeymapResultKind::Matched(Command::insert_mode),
841+ KeymapResultKind::Matched(MappableCommand::insert_mode),
842 "New leaf should be present in merged keymap"
842 "New leaf should be present in merged keymap"
843 );
843 );
844 // Assumes that z is a node in the default keymap
844 // Assumes that z is a node in the default keymap
845 assert_eq!(
845 assert_eq!(
846 keymap.get(key!('z')).kind,
846 keymap.get(key!('z')).kind,
847− KeymapResultKind::Matched(Command::jump_backward),
847+ KeymapResultKind::Matched(MappableCommand::jump_backward),
848 "Leaf should replace node"
848 "Leaf should replace node"
849 );
849 );
850 // Assumes that `g` is a node in default keymap
850 // Assumes that `g` is a node in default keymap
851 assert_eq!(
851 assert_eq!(
852 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
852 keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
853− &KeyTrie::Leaf(Command::goto_line_end),
853+ &KeyTrie::Leaf(MappableCommand::goto_line_end),
854 "Leaf should be present in merged subnode"
854 "Leaf should be present in merged subnode"
855 );
855 );
856 // Assumes that `gg` is in default keymap
856 // Assumes that `gg` is in default keymap
857 assert_eq!(
857 assert_eq!(
858 keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
858 keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
859− &KeyTrie::Leaf(Command::delete_char_forward),
859+ &KeyTrie::Leaf(MappableCommand::delete_char_forward),
860 "Leaf should replace old leaf in merged subnode"
860 "Leaf should replace old leaf in merged subnode"
861 );
861 );
862 // Assumes that `ge` is in default keymap
862 // Assumes that `ge` is in default keymap
863 assert_eq!(
863 assert_eq!(
864 keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
864 keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
865− &KeyTrie::Leaf(Command::goto_last_line),
865+ &KeyTrie::Leaf(MappableCommand::goto_last_line),
866 "Old leaves in subnode should be present in merged node"
866 "Old leaves in subnode should be present in merged node"
867 );
867 );
868
868
@@ -896,7 +896,7 @@ mod tests {
896 .root()
896 .root()
897 .search(&[key!(' '), key!('s'), key!('v')])
897 .search(&[key!(' '), key!('s'), key!('v')])
898 .unwrap(),
898 .unwrap(),
899− &KeyTrie::Leaf(Command::vsplit),
899+ &KeyTrie::Leaf(MappableCommand::vsplit),
900 "Leaf should be present in merged subnode"
900 "Leaf should be present in merged subnode"
901 );
901 );
902 // Make sure an order was set during merge
902 // Make sure an order was set during merge
helix-term/src/ui/editor.rs+6−5
@@ -31,7 +31,7 @@ use tui::buffer::Buffer as Surface;
31pub struct EditorView {
31pub struct EditorView {
32 keymaps: Keymaps,
32 keymaps: Keymaps,
33 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
33 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
34− last_insert: (commands::Command, Vec<KeyEvent>),
34+ last_insert: (commands::MappableCommand, Vec<KeyEvent>),
35 pub(crate) completion: Option<Completion>,
35 pub(crate) completion: Option<Completion>,
36 spinners: ProgressSpinners,
36 spinners: ProgressSpinners,
37 autoinfo: Option<Info>,
37 autoinfo: Option<Info>,
@@ -48,7 +48,7 @@ impl EditorView {
48 Self {
48 Self {
49 keymaps,
49 keymaps,
50 on_next_key: None,
50 on_next_key: None,
51− last_insert: (commands::Command::normal_mode, Vec::new()),
51+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
52 completion: None,
52 completion: None,
53 spinners: ProgressSpinners::default(),
53 spinners: ProgressSpinners::default(),
54 autoinfo: None,
54 autoinfo: None,
@@ -875,7 +875,7 @@ impl EditorView {
875 return EventResult::Ignored;
875 return EventResult::Ignored;
876 }
876 }
877
877
878− commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
878+ commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
879
879
880 EventResult::Consumed(None)
880 EventResult::Consumed(None)
881 }
881 }
@@ -893,7 +893,8 @@ impl EditorView {
893 }
893 }
894
894
895 if modifiers == crossterm::event::KeyModifiers::ALT {
895 if modifiers == crossterm::event::KeyModifiers::ALT {
896− commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
896+ commands::MappableCommand::replace_selections_with_primary_clipboard
897+ .execute(cxt);
897
898
898 return EventResult::Consumed(None);
899 return EventResult::Consumed(None);
899 }
900 }
@@ -907,7 +908,7 @@ impl EditorView {
907 let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
908 let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
908 doc.set_selection(view_id, Selection::point(pos));
909 doc.set_selection(view_id, Selection::point(pos));
909 editor.tree.focus = view_id;
910 editor.tree.focus = view_id;
910− commands::Command::paste_primary_clipboard_before.execute(cxt);
911+ commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
911 return EventResult::Consumed(None);
912 return EventResult::Consumed(None);
912 }
913 }
913
914