on master help sign in
feat(commands): command palette (#1400)
afec544Matouš Dzivjak
helix-term/src/commands.rs+65−1
@@ -44,7 +44,7 @@ use movement::Movement;
44use crate::{
44use crate::{
45 args,
45 args,
46 compositor::{self, Component, Compositor},
46 compositor::{self, Component, Compositor},
47− ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
47+ ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
48};
48};
49
49
50use crate::job::{self, Job, Jobs};
50use crate::job::{self, Job, Jobs};
@@ -430,6 +430,7 @@ impl MappableCommand {
430 decrement, "Decrement",
430 decrement, "Decrement",
431 record_macro, "Record macro",
431 record_macro, "Record macro",
432 replay_macro, "Replay macro",
432 replay_macro, "Replay macro",
433+ command_palette, "Open command pallete",
433 );
434 );
434}
435}
435
436
@@ -3692,6 +3693,69 @@ pub fn code_action(cx: &mut Context) {
3692 )
3693 )
3693}
3694}
3694
3695
3696+pub fn command_palette(cx: &mut Context) {
3697+ cx.callback = Some(Box::new(
3698+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
3699+ let doc = doc_mut!(cx.editor);
3700+ let keymap =
3701+ compositor.find::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
3702+
3703+ let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
3704+ commands.extend(
3705+ cmd::TYPABLE_COMMAND_LIST
3706+ .iter()
3707+ .map(|cmd| MappableCommand::Typable {
3708+ name: cmd.name.to_owned(),
3709+ doc: cmd.doc.to_owned(),
3710+ args: Vec::new(),
3711+ }),
3712+ );
3713+
3714+ // formats key bindings, multiple bindings are comma separated,
3715+ // individual key presses are joined with `+`
3716+ let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
3717+ bindings
3718+ .iter()
3719+ .map(|bind| {
3720+ bind.iter()
3721+ .map(|key| key.to_string())
3722+ .collect::<Vec<String>>()
3723+ .join("+")
3724+ })
3725+ .collect::<Vec<String>>()
3726+ .join(", ")
3727+ };
3728+
3729+ let picker = Picker::new(
3730+ commands,
3731+ move |command| match command {
3732+ MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String)
3733+ {
3734+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
3735+ None => doc.into(),
3736+ },
3737+ MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
3738+ Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
3739+ None => (*doc).into(),
3740+ },
3741+ },
3742+ move |cx, command, _action| {
3743+ let mut ctx = Context {
3744+ register: None,
3745+ count: std::num::NonZeroUsize::new(1),
3746+ editor: cx.editor,
3747+ callback: None,
3748+ on_next_key_callback: None,
3749+ jobs: cx.jobs,
3750+ };
3751+ command.execute(&mut ctx);
3752+ },
3753+ );
3754+ compositor.push(Box::new(picker));
3755+ },
3756+ ));
3757+}
3758+
3695pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
3759pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
3696 let doc = doc!(editor);
3760 let doc = doc!(editor);
3697 let language_server = match doc.language_server() {
3761 let language_server = match doc.language_server() {
helix-term/src/keymap.rs+76−1
@@ -343,13 +343,46 @@ pub struct Keymap {
343
343
344impl Keymap {
344impl Keymap {
345 pub fn new(root: KeyTrie) -> Self {
345 pub fn new(root: KeyTrie) -> Self {
346− Self {
346+ Keymap {
347 root,
347 root,
348 state: Vec::new(),
348 state: Vec::new(),
349 sticky: None,
349 sticky: None,
350 }
350 }
351 }
351 }
352
352
353+ pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
354+ // recursively visit all nodes in keymap
355+ fn map_node(
356+ cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
357+ node: &KeyTrie,
358+ keys: &mut Vec<KeyEvent>,
359+ ) {
360+ match node {
361+ KeyTrie::Leaf(cmd) => match cmd {
362+ MappableCommand::Typable { name, .. } => {
363+ cmd_map.entry(name.into()).or_default().push(keys.clone())
364+ }
365+ MappableCommand::Static { name, .. } => cmd_map
366+ .entry(name.to_string())
367+ .or_default()
368+ .push(keys.clone()),
369+ },
370+ KeyTrie::Node(next) => {
371+ for (key, trie) in &next.map {
372+ keys.push(*key);
373+ map_node(cmd_map, trie, keys);
374+ keys.pop();
375+ }
376+ }
377+ KeyTrie::Sequence(_) => {}
378+ };
379+ }
380+
381+ let mut res = HashMap::new();
382+ map_node(&mut res, &self.root, &mut Vec::new());
383+ res
384+ }
385+
353 pub fn root(&self) -> &KeyTrie {
386 pub fn root(&self) -> &KeyTrie {
354 &self.root
387 &self.root
355 }
388 }
@@ -706,6 +739,7 @@ impl Default for Keymaps {
706 "/" => global_search,
739 "/" => global_search,
707 "k" => hover,
740 "k" => hover,
708 "r" => rename_symbol,
741 "r" => rename_symbol,
742+ "?" => command_palette,
709 },
743 },
710 "z" => { "View"
744 "z" => { "View"
711 "z" | "c" => align_view_center,
745 "z" | "c" => align_view_center,
@@ -958,4 +992,45 @@ mod tests {
958 "Mismatch for view mode on `z` and `Z`"
992 "Mismatch for view mode on `z` and `Z`"
959 );
993 );
960 }
994 }
995+
996+ #[test]
997+ fn reverse_map() {
998+ let normal_mode = keymap!({ "Normal mode"
999+ "i" => insert_mode,
1000+ "g" => { "Goto"
1001+ "g" => goto_file_start,
1002+ "e" => goto_file_end,
1003+ },
1004+ "j" | "k" => move_line_down,
1005+ });
1006+ let keymap = Keymap::new(normal_mode);
1007+ let mut reverse_map = keymap.reverse_map();
1008+
1009+ // sort keybindings in order to have consistent tests
1010+ // HashMaps can be compared but we can still get different ordering of bindings
1011+ // for commands that have multiple bindings assigned
1012+ for v in reverse_map.values_mut() {
1013+ v.sort()
1014+ }
1015+
1016+ assert_eq!(
1017+ reverse_map,
1018+ HashMap::from([
1019+ ("insert_mode".to_string(), vec![vec![key!('i')]]),
1020+ (
1021+ "goto_file_start".to_string(),
1022+ vec![vec![key!('g'), key!('g')]]
1023+ ),
1024+ (
1025+ "goto_file_end".to_string(),
1026+ vec![vec![key!('g'), key!('e')]]
1027+ ),
1028+ (
1029+ "move_line_down".to_string(),
1030+ vec![vec![key!('j')], vec![key!('k')]]
1031+ ),
1032+ ]),
1033+ "Mistmatch"
1034+ )
1035+ }
961}
1036}
helix-term/src/ui/editor.rs+1−1
@@ -31,7 +31,7 @@ use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
31use tui::buffer::Buffer as Surface;
31use tui::buffer::Buffer as Surface;
32
32
33pub struct EditorView {
33pub struct EditorView {
34− keymaps: Keymaps,
34+ pub keymaps: Keymaps,
35 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
35 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
36 last_insert: (commands::MappableCommand, Vec<KeyEvent>),
36 last_insert: (commands::MappableCommand, Vec<KeyEvent>),
37 pub(crate) completion: Option<Completion>,
37 pub(crate) completion: Option<Completion>,