feat(commands): command palette (#1400)

* feat(commands): command palette

Add new command to display command pallete that can be used
to discover and execute available commands.

Fixes: https://github.com/helix-editor/helix/issues/559

* Make picker take the whole context, not just editor

* Bind command pallete

* Typable commands also in the palette

* Show key bindings for commands

* Fix tests, small refactor

* Refactor keymap mapping, fix typo

* Ignore sequence key bindings for now

* Apply suggestions

* Fix lint issues in tests

* Fix after rebase

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
imgbot
Matouš Dzivjak 2 years ago committed by GitHub
parent 24f90ba8d8
commit afec54485a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,7 +44,7 @@ use movement::Movement;
use crate::{
args,
compositor::{self, Component, Compositor},
ui::{self, overlay::overlayed, FilePicker, Popup, Prompt, PromptEvent},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Job, Jobs};
@ -430,6 +430,7 @@ impl MappableCommand {
decrement, "Decrement",
record_macro, "Record macro",
replay_macro, "Replay macro",
command_palette, "Open command pallete",
);
}
@ -3692,6 +3693,69 @@ pub fn code_action(cx: &mut Context) {
)
}
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::<ui::EditorView>().unwrap().keymaps[&doc.mode].reverse_map();
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
commands.extend(
cmd::TYPABLE_COMMAND_LIST
.iter()
.map(|cmd| MappableCommand::Typable {
name: cmd.name.to_owned(),
doc: cmd.doc.to_owned(),
args: Vec::new(),
}),
);
// formats key bindings, multiple bindings are comma separated,
// individual key presses are joined with `+`
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
bindings
.iter()
.map(|bind| {
bind.iter()
.map(|key| key.to_string())
.collect::<Vec<String>>()
.join("+")
})
.collect::<Vec<String>>()
.join(", ")
};
let picker = Picker::new(
commands,
move |command| match command {
MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String)
{
Some(bindings) => format!("{} ({})", doc, fmt_binding(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 {
register: None,
count: std::num::NonZeroUsize::new(1),
editor: cx.editor,
callback: None,
on_next_key_callback: None,
jobs: cx.jobs,
};
command.execute(&mut ctx);
},
);
compositor.push(Box::new(picker));
},
));
}
pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
let doc = doc!(editor);
let language_server = match doc.language_server() {

@ -343,13 +343,46 @@ pub struct Keymap {
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Self {
Keymap {
root,
state: Vec::new(),
sticky: None,
}
}
pub fn reverse_map(&self) -> HashMap<String, Vec<Vec<KeyEvent>>> {
// recursively visit all nodes in keymap
fn map_node(
cmd_map: &mut HashMap<String, Vec<Vec<KeyEvent>>>,
node: &KeyTrie,
keys: &mut Vec<KeyEvent>,
) {
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::Node(next) => {
for (key, trie) in &next.map {
keys.push(*key);
map_node(cmd_map, trie, keys);
keys.pop();
}
}
KeyTrie::Sequence(_) => {}
};
}
let mut res = HashMap::new();
map_node(&mut res, &self.root, &mut Vec::new());
res
}
pub fn root(&self) -> &KeyTrie {
&self.root
}
@ -706,6 +739,7 @@ impl Default for Keymaps {
"/" => global_search,
"k" => hover,
"r" => rename_symbol,
"?" => command_palette,
},
"z" => { "View"
"z" | "c" => align_view_center,
@ -958,4 +992,45 @@ mod tests {
"Mismatch for view mode on `z` and `Z`"
);
}
#[test]
fn reverse_map() {
let normal_mode = keymap!({ "Normal mode"
"i" => insert_mode,
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_file_end,
},
"j" | "k" => move_line_down,
});
let keymap = Keymap::new(normal_mode);
let mut reverse_map = keymap.reverse_map();
// sort keybindings in order to have consistent 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()
}
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"
)
}
}

@ -31,7 +31,7 @@ use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>,

Loading…
Cancel
Save