on master help sign in
Allow multi key remappings in config file (#454)

* Use tree like structure to store keymaps * Allow multi key keymaps in config file * Allow multi key keymaps in insert mode * Make keymap state self contained * Add keymap! macro for ergonomic declaration * Add descriptions for editor commands * Allow keymap! to take multiple keys * Restore infobox display * Fix keymap merging and add infobox titles * Fix and add tests for keymaps * Clean up comments and apply suggestions * Allow trailing commas in keymap! * Remove mode suffixes from keymaps * Preserve order of keys when showing infobox * Make command descriptions smaller * Strip infobox title prefix from items * Strip infobox title prefix from items

commit88d6f652390922b389667f469b6d308db569bdaf authorGokul Soumya <gokulps15@gmail.com> dateMon 26 Jul 2021, 21:37 (5 years ago) parenta630fb5
8 files changed+730−608
helix-term/src/commands.rs+149−353 helix-term/src/config.rs+10−24 helix-term/src/keymap.rs+492−200 helix-term/src/ui/editor.rs+57−19 helix-term/src/ui/info.rs+1−1 helix-view/src/editor.rs−3 helix-view/src/info.rs+10−7 helix-view/src/input.rs+11−1
helix-term/src/commands.rs+149−353
@@ -16,7 +16,6 @@ use helix_core::{
1616use helix_view::{
1717 document::{IndentStyle, Mode},
1818 editor::Action,
19− info::Info,
2019 input::KeyEvent,
2120 keyboard::KeyCode,
2221 view::{View, PADDING},
@@ -39,7 +38,6 @@ use crate::{
3938
4039use crate::job::{self, Job, Jobs};
4140use futures_util::{FutureExt, TryFutureExt};
42−use std::collections::HashMap;
4341use std::num::NonZeroUsize;
4442use std::{fmt, future::Future};
4543
@@ -48,7 +46,7 @@ use std::{
4846 path::{Path, PathBuf},
4947};
5048
51−use once_cell::sync::{Lazy, OnceCell};
49+use once_cell::sync::Lazy;
5250use serde::de::{self, Deserialize, Deserializer};
5351
5452pub struct Context<'a> {
@@ -77,18 +75,6 @@ impl<'a> Context<'a> {
7775 self.on_next_key_callback = Some(Box::new(on_next_key_callback));
7876 }
7977
80− #[inline]
81− pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
82− let count = self.count;
83− self.on_next_key(move |cx, event| {
84− cx.count = count;
85− cx.editor.autoinfo = None;
86− if let Some(func) = map.get(&event) {
87− func(cx);
88− }
89− });
90− }
91−
9278 #[inline]
9379 pub fn callback<T, F>(
9480 &mut self,
@@ -139,13 +125,21 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
139125/// A command is composed of a static name, and a function that takes the current state plus a count,
140126/// and does a side-effect on the state (usually by creating and applying a transaction).
141127#[derive(Copy, Clone)]
142−pub struct Command(&'static str, fn(cx: &mut Context));
128+pub struct Command {
129+ name: &'static str,
130+ fun: fn(cx: &mut Context),
131+ doc: &'static str,
132+}
143133
144134macro_rules! commands {
145− ( $($name:ident),* ) => {
135+ ( $($name:ident, $doc:literal),* ) => {
146136 $(
147137 #[allow(non_upper_case_globals)]
148− pub const $name: Self = Self(stringify!($name), $name);
138+ pub const $name: Self = Self {
139+ name: stringify!($name),
140+ fun: $name,
141+ doc: $doc
142+ };
149143 )*
150144
151145 pub const COMMAND_LIST: &'static [Self] = &[
@@ -156,145 +150,159 @@ macro_rules! commands {
156150
157151impl Command {
158152 pub fn execute(&self, cx: &mut Context) {
159− (self.1)(cx);
153+ (self.fun)(cx);
160154 }
161155
162156 pub fn name(&self) -> &'static str {
163− self.0
157+ self.name
158+ }
159+
160+ pub fn doc(&self) -> &'static str {
161+ self.doc
164162 }
165163
164+ #[rustfmt::skip]
166165 commands!(
167− move_char_left,
168− move_char_right,
169− move_line_up,
170− move_line_down,
171− move_next_word_start,
172− move_prev_word_start,
173− move_next_word_end,
174− move_next_long_word_start,
175− move_prev_long_word_start,
176− move_next_long_word_end,
177− extend_next_word_start,
178− extend_prev_word_start,
179− extend_next_word_end,
180− find_till_char,
181− find_next_char,
182− extend_till_char,
183− extend_next_char,
184− till_prev_char,
185− find_prev_char,
186− extend_till_prev_char,
187− extend_prev_char,
188− replace,
189− switch_case,
190− switch_to_uppercase,
191− switch_to_lowercase,
192− page_up,
193− page_down,
194− half_page_up,
195− half_page_down,
196− extend_char_left,
197− extend_char_right,
198− extend_line_up,
199− extend_line_down,
200− select_all,
201− select_regex,
202− split_selection,
203− split_selection_on_newline,
204− search,
205− search_next,
206− extend_search_next,
207− search_selection,
208− extend_line,
209− extend_to_line_bounds,
210− delete_selection,
211− change_selection,
212− collapse_selection,
213− flip_selections,
214− insert_mode,
215− append_mode,
216− command_mode,
217− file_picker,
218− code_action,
219− buffer_picker,
220− symbol_picker,
221− last_picker,
222− prepend_to_line,
223− append_to_line,
224− open_below,
225− open_above,
226− normal_mode,
227− goto_mode,
228− select_mode,
229− exit_select_mode,
230− goto_definition,
231− goto_type_definition,
232− goto_implementation,
233− goto_file_start,
234− goto_file_end,
235− goto_reference,
236− goto_first_diag,
237− goto_last_diag,
238− goto_next_diag,
239− goto_prev_diag,
240− goto_line_start,
241− goto_line_end,
242− goto_line_end_newline,
243− goto_first_nonwhitespace,
244− signature_help,
245− insert_tab,
246− insert_newline,
247− delete_char_backward,
248− delete_char_forward,
249− delete_word_backward,
250− undo,
251− redo,
252− yank,
253− yank_joined_to_clipboard,
254− yank_main_selection_to_clipboard,
255− replace_with_yanked,
256− replace_selections_with_clipboard,
257− paste_after,
258− paste_before,
259− paste_clipboard_after,
260− paste_clipboard_before,
261− indent,
262− unindent,
263− format_selections,
264− join_selections,
265− keep_selections,
266− keep_primary_selection,
267− completion,
268− hover,
269− toggle_comments,
270− expand_selection,
271− match_brackets,
272− jump_forward,
273− jump_backward,
274− window_mode,
275− rotate_view,
276− hsplit,
277− vsplit,
278− wclose,
279− select_register,
280− space_mode,
281− view_mode,
282− left_bracket_mode,
283− right_bracket_mode,
284− match_mode
166+ move_char_left, "Move left",
167+ move_char_right, "Move right",
168+ move_line_up, "Move up",
169+ move_line_down, "Move down",
170+ extend_char_left, "Extend left",
171+ extend_char_right, "Extend right",
172+ extend_line_up, "Extend up",
173+ extend_line_down, "Extend down",
174+ move_next_word_start, "Move to beginning of next word",
175+ move_prev_word_start, "Move to beginning of previous word",
176+ move_next_word_end, "Move to end of next word",
177+ move_next_long_word_start, "Move to beginning of next long word",
178+ move_prev_long_word_start, "Move to beginning of previous long word",
179+ move_next_long_word_end, "Move to end of next long word",
180+ extend_next_word_start, "Extend to beginning of next word",
181+ extend_prev_word_start, "Extend to beginning of previous word",
182+ extend_next_word_end, "Extend to end of next word",
183+ find_till_char, "Move till next occurance of char",
184+ find_next_char, "Move to next occurance of char",
185+ extend_till_char, "Extend till next occurance of char",
186+ extend_next_char, "Extend to next occurance of char",
187+ till_prev_char, "Move till previous occurance of char",
188+ find_prev_char, "Move to previous occurance of char",
189+ extend_till_prev_char, "Extend till previous occurance of char",
190+ extend_prev_char, "Extend to previous occurance of char",
191+ replace, "Replace with new char",
192+ switch_case, "Switch (toggle) case",
193+ switch_to_uppercase, "Switch to uppercase",
194+ switch_to_lowercase, "Switch to lowercase",
195+ page_up, "Move page up",
196+ page_down, "Move page down",
197+ half_page_up, "Move half page up",
198+ half_page_down, "Move half page down",
199+ select_all, "Select whole document",
200+ select_regex, "Select all regex matches inside selections",
201+ split_selection, "Split selection into subselections on regex matches",
202+ split_selection_on_newline, "Split selection on newlines",
203+ search, "Search for regex pattern",
204+ search_next, "Select next search match",
205+ extend_search_next, "Add next search match to selection",
206+ search_selection, "Use current selection as search pattern",
207+ extend_line, "Select current line, if already selected, extend to next line",
208+ extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
209+ delete_selection, "Delete selection",
210+ change_selection, "Change selection (delete and enter insert mode)",
211+ collapse_selection, "Collapse selection onto a single cursor",
212+ flip_selections, "Flip selection cursor and anchor",
213+ insert_mode, "Insert before selection",
214+ append_mode, "Insert after selection (append)",
215+ command_mode, "Enter command mode",
216+ file_picker, "Open file picker",
217+ code_action, "Perform code action",
218+ buffer_picker, "Open buffer picker",
219+ symbol_picker, "Open symbol picker",
220+ last_picker, "Open last picker",
221+ prepend_to_line, "Insert at start of line",
222+ append_to_line, "Insert at end of line",
223+ open_below, "Open new line below selection",
224+ open_above, "Open new line above selection",
225+ normal_mode, "Enter normal mode",
226+ select_mode, "Enter selection extend mode",
227+ exit_select_mode, "Exit selection mode",
228+ goto_definition, "Goto definition",
229+ goto_type_definition, "Goto type definition",
230+ goto_implementation, "Goto implementation",
231+ goto_file_start, "Goto file start",
232+ goto_file_end, "Goto file end",
233+ goto_reference, "Goto references",
234+ goto_window_top, "Goto window top",
235+ goto_window_middle, "Goto window middle",
236+ goto_window_bottom, "Goto window bottom",
237+ goto_last_accessed_file, "Goto last accessed file",
238+ goto_first_diag, "Goto first diagnostic",
239+ goto_last_diag, "Goto last diagnostic",
240+ goto_next_diag, "Goto next diagnostic",
241+ goto_prev_diag, "Goto previous diagnostic",
242+ goto_line_start, "Goto line start",
243+ goto_line_end, "Goto line end",
244+ // TODO: different description ?
245+ goto_line_end_newline, "Goto line end",
246+ goto_first_nonwhitespace, "Goto first non-blank in line",
247+ signature_help, "Show signature help",
248+ insert_tab, "Insert tab char",
249+ insert_newline, "Insert newline char",
250+ delete_char_backward, "Delete previous char",
251+ delete_char_forward, "Delete next char",
252+ delete_word_backward, "Delete previous word",
253+ undo, "Undo change",
254+ redo, "Redo change",
255+ yank, "Yank selection",
256+ yank_joined_to_clipboard, "Join and yank selections to clipboard",
257+ yank_main_selection_to_clipboard, "Yank main selection to clipboard",
258+ replace_with_yanked, "Replace with yanked text",
259+ replace_selections_with_clipboard, "Replace selections by clipboard content",
260+ paste_after, "Paste after selection",
261+ paste_before, "Paste before selection",
262+ paste_clipboard_after, "Paste clipboard after selections",
263+ paste_clipboard_before, "Paste clipboard before selections",
264+ indent, "Indent selection",
265+ unindent, "Unindent selection",
266+ format_selections, "Format selection",
267+ join_selections, "Join lines inside selection",
268+ keep_selections, "Keep selections matching regex",
269+ keep_primary_selection, "Keep primary selection",
270+ completion, "Invoke completion popup",
271+ hover, "Show docs for item under cursor",
272+ toggle_comments, "Comment/uncomment selections",
273+ expand_selection, "Expand selection to parent syntax node",
274+ jump_forward, "Jump forward on jumplist",
275+ jump_backward, "Jump backward on jumplist",
276+ rotate_view, "Goto next window",
277+ hsplit, "Horizontal bottom split",
278+ vsplit, "Vertical right split",
279+ wclose, "Close window",
280+ select_register, "Select register",
281+ align_view_middle, "Align view middle",
282+ align_view_top, "Align view top",
283+ align_view_center, "Align view center",
284+ align_view_bottom, "Align view bottom",
285+ scroll_up, "Scroll view up",
286+ scroll_down, "Scroll view down",
287+ match_brackets, "Goto matching bracket",
288+ surround_add, "Surround add",
289+ surround_replace, "Surround replace",
290+ surround_delete, "Surround delete",
291+ select_textobject_around, "Select around object",
292+ select_textobject_inner, "Select inside object"
285293 );
286294}
287295
288296impl fmt::Debug for Command {
289297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290− let Command(name, _) = self;
298+ let Command { name, .. } = self;
291299 f.debug_tuple("Command").field(name).finish()
292300 }
293301}
294302
295303impl fmt::Display for Command {
296304 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297− let Command(name, _) = self;
305+ let Command { name, .. } = self;
298306 f.write_str(name)
299307 }
300308}
@@ -306,7 +314,7 @@ impl std::str::FromStr for Command {
306314 Command::COMMAND_LIST
307315 .iter()
308316 .copied()
309− .find(|cmd| cmd.0 == s)
317+ .find(|cmd| cmd.name == s)
310318 .ok_or_else(|| anyhow!("No command named '{}'", s))
311319 }
312320}
@@ -2396,20 +2404,6 @@ fn exit_select_mode(cx: &mut Context) {
23962404 doc_mut!(cx.editor).mode = Mode::Normal;
23972405}
23982406
2399−fn goto_prehook(cx: &mut Context) -> bool {
2400− if let Some(count) = cx.count {
2401− push_jump(cx.editor);
2402−
2403− let (view, doc) = current!(cx.editor);
2404− let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2));
2405− let pos = doc.text().line_to_char(line_idx);
2406− doc.set_selection(view.id, Selection::point(pos));
2407− true
2408− } else {
2409− false
2410− }
2411−}
2412−
24132407fn goto_impl(
24142408 editor: &mut Editor,
24152409 compositor: &mut Compositor,
@@ -3794,201 +3788,3 @@ fn surround_delete(cx: &mut Context) {
37943788 }
37953789 })
37963790}
3797−
3798−/// Do nothing, just for modeinfo.
3799−fn noop(_cx: &mut Context) -> bool {
3800− false
3801−}
3802−
3803−/// Generate modeinfo.
3804−///
3805−/// If prehook returns true then it will stop the rest.
3806−macro_rules! mode_info {
3807− // TODO: reuse $mode for $stat
3808− (@join $first:expr $(,$rest:expr)*) => {
3809− concat!($first, $(", ", $rest),*)
3810− };
3811− (@name #[doc = $name:literal] $(#[$rest:meta])*) => {
3812− $name
3813− };
3814− {
3815− #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident,
3816− $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3817− } => {
3818− mode_info! {
3819− #[doc = $name]
3820− $(#[$doc])*
3821− $mode, $stat, noop,
3822− $(
3823− #[doc = $desc]
3824− $($key)|+ => $func
3825− ),+,
3826− }
3827− };
3828− {
3829− #[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr,
3830− $(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
3831− } => {
3832− #[doc = $name]
3833− $(#[$doc])*
3834− #[doc = ""]
3835− #[doc = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
3836− $(
3837− #[doc = "<tr><td>"]
3838− // TODO switch to this once we use rust 1.54
3839− // right now it will produce multiple rows
3840− // #[doc = mode_info!(@join $($key),+)]
3841− $(
3842− #[doc = $key]
3843− )+
3844− // <-
3845− #[doc = "</td><td>"]
3846− #[doc = $desc]
3847− #[doc = "</td></tr>"]
3848− )+
3849− #[doc = "</tbody></table>"]
3850− pub fn $mode(cx: &mut Context) {
3851− if $prehook(cx) {
3852− return;
3853− }
3854− static $stat: OnceCell<Info> = OnceCell::new();
3855− cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key(
3856− $name.trim(),
3857− vec![$((&[$($key.parse().unwrap()),+], $desc)),+],
3858− )));
3859− use helix_core::hashmap;
3860− // TODO: try and convert this to match later
3861− let map = hashmap! {
3862− $($($key.parse::<KeyEvent>().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),*
3863− };
3864− cx.on_next_key_mode(map);
3865− }
3866− };
3867−}
3868−
3869−mode_info! {
3870− /// space mode
3871− space_mode, SPACE_MODE,
3872− /// resume last picker
3873− "'" => last_picker,
3874− /// file picker
3875− "f" => file_picker,
3876− /// buffer picker
3877− "b" => buffer_picker,
3878− /// symbol picker
3879− "s" => symbol_picker,
3880− /// window mode
3881− "w" => window_mode,
3882− /// yank joined to clipboard
3883− "y" => yank_joined_to_clipboard,
3884− /// yank main selection to clipboard
3885− "Y" => yank_main_selection_to_clipboard,
3886− /// paste system clipboard after selections
3887− "p" => paste_clipboard_after,
3888− /// paste system clipboard before selections
3889− "P" => paste_clipboard_before,
3890− /// replace selections with clipboard
3891− "R" => replace_selections_with_clipboard,
3892− /// perform code action
3893− "a" => code_action,
3894− /// keep primary selection
3895− "space" => keep_primary_selection,
3896−}
3897−
3898−mode_info! {
3899− /// goto
3900− ///
3901− /// When specified with a count, it will go to that line without entering the mode.
3902− goto_mode, GOTO_MODE, goto_prehook,
3903− /// file start
3904− "g" => goto_file_start,
3905− /// file end
3906− "e" => goto_file_end,
3907− /// line start
3908− "h" => goto_line_start,
3909− /// line end
3910− "l" => goto_line_end,
3911− /// line first non blank
3912− "s" => goto_first_nonwhitespace,
3913− /// definition
3914− "d" => goto_definition,
3915− /// type references
3916− "y" => goto_type_definition,
3917− /// references
3918− "r" => goto_reference,
3919− /// implementation
3920− "i" => goto_implementation,
3921− /// window top
3922− "t" => goto_window_top,
3923− /// window middle
3924− "m" => goto_window_middle,
3925− /// window bottom
3926− "b" => goto_window_bottom,
3927− /// last accessed file
3928− "a" => goto_last_accessed_file,
3929−}
3930−
3931−mode_info! {
3932− /// window
3933− window_mode, WINDOW_MODE,
3934− /// rotate
3935− "w" | "C-w" => rotate_view,
3936− /// horizontal split
3937− "h" => hsplit,
3938− /// vertical split
3939− "v" => vsplit,
3940− /// close
3941− "q" => wclose,
3942−}
3943−
3944−mode_info! {
3945− /// match
3946− match_mode, MATCH_MODE,
3947− /// matching character
3948− "m" => match_brackets,
3949− /// surround add
3950− "s" => surround_add,
3951− /// surround replace
3952− "r" => surround_replace,
3953− /// surround delete
3954− "d" => surround_delete,
3955− /// around object
3956− "a" => select_textobject_around,
3957− /// inside object
3958− "i" => select_textobject_inner,
3959−}
3960−
3961−mode_info! {
3962− /// select to previous
3963− left_bracket_mode, LEFT_BRACKET_MODE,
3964− /// previous diagnostic
3965− "d" => goto_prev_diag,
3966− /// diagnostic (first)
3967− "D" => goto_first_diag,
3968−}
3969−
3970−mode_info! {
3971− /// select to next
3972− right_bracket_mode, RIGHT_BRACKET_MODE,
3973− /// diagnostic
3974− "d" => goto_next_diag,
3975− /// diagnostic (last)
3976− "D" => goto_last_diag,
3977−}
3978−
3979−mode_info! {
3980− /// view
3981− view_mode, VIEW_MODE,
3982− /// align view top
3983− "t" => align_view_top,
3984− /// align view center
3985− "z" | "c" => align_view_center,
3986− /// align view bottom
3987− "b" => align_view_bottom,
3988− /// align view middle
3989− "m" => align_view_middle,
3990− /// scroll up
3991− "k" => scroll_up,
3992− /// scroll down
3993− "j" => scroll_down,
3994−}
helix-term/src/config.rs+10−24
@@ -2,9 +2,6 @@ use serde::Deserialize;
22
33use crate::keymap::Keymaps;
44
5−#[cfg(test)]
6−use crate::commands::Command;
7−
85#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
96pub struct Config {
107 pub theme: Option<String>,
@@ -22,12 +19,10 @@ pub struct LspConfig {
2219
2320#[test]
2421fn parsing_keymaps_config_file() {
22+ use crate::keymap;
23+ use crate::keymap::Keymap;
2524 use helix_core::hashmap;
26− use helix_view::{
27− document::Mode,
28− input::KeyEvent,
29− keyboard::{KeyCode, KeyModifiers},
30− };
25+ use helix_view::document::Mode;
3126
3227 let sample_keymaps = r#"
3328 [keys.insert]
@@ -42,22 +37,13 @@ fn parsing_keymaps_config_file() {
4237 toml::from_str::<Config>(sample_keymaps).unwrap(),
4338 Config {
4439 keys: Keymaps(hashmap! {
45− Mode::Insert => hashmap! {
46− KeyEvent {
47− code: KeyCode::Char('y'),
48− modifiers: KeyModifiers::NONE,
49− } => Command::move_line_down,
50− KeyEvent {
51− code: KeyCode::Char('a'),
52− modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
53− } => Command::delete_selection,
54− },
55− Mode::Normal => hashmap! {
56− KeyEvent {
57− code: KeyCode::F(12),
58− modifiers: KeyModifiers::ALT,
59− } => Command::move_next_word_end,
60− },
40+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
41+ "y" => move_line_down,
42+ "S-C-a" => delete_selection,
43+ })),
44+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
45+ "A-F12" => move_next_word_end,
46+ })),
6147 }),
6248 ..Default::default()
6349 }
helix-term/src/keymap.rs+492−200
@@ -1,7 +1,7 @@
11pub use crate::commands::Command;
22use crate::config::Config;
33use helix_core::hashmap;
4−use helix_view::{document::Mode, input::KeyEvent};
4+use helix_view::{document::Mode, info::Info, input::KeyEvent};
55use serde::Deserialize;
66use std::{
77 collections::HashMap,
@@ -24,30 +24,276 @@ macro_rules! key {
2424 };
2525}
2626
27−macro_rules! ctrl {
28− ($($ch:tt)*) => {
29− KeyEvent {
30− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
31− modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
27+/// Macro for defining the root of a `Keymap` object. Example:
28+///
29+/// ```
30+/// # use helix_core::hashmap;
31+/// # use helix_term::keymap;
32+/// # use helix_term::keymap::Keymap;
33+/// let normal_mode = keymap!({ "Normal mode"
34+/// "i" => insert_mode,
35+/// "g" => { "Goto"
36+/// "g" => goto_file_start,
37+/// "e" => goto_file_end,
38+/// },
39+/// "j" | "down" => move_line_down,
40+/// });
41+/// let keymap = Keymap::new(normal_mode);
42+/// ```
43+#[macro_export]
44+macro_rules! keymap {
45+ (@trie $cmd:ident) => {
46+ $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
47+ };
48+
49+ (@trie
50+ { $label:literal $($($key:literal)|+ => $value:tt,)+ }
51+ ) => {
52+ keymap!({ $label $($($key)|+ => $value,)+ })
53+ };
54+
55+ (
56+ { $label:literal $($($key:literal)|+ => $value:tt,)+ }
57+ ) => {
58+ // modified from the hashmap! macro
59+ {
60+ let _cap = hashmap!(@count $($($key),+),*);
61+ let mut _map = ::std::collections::HashMap::with_capacity(_cap);
62+ let mut _order = ::std::vec::Vec::with_capacity(_cap);
63+ $(
64+ $(
65+ let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
66+ _map.insert(
67+ _key,
68+ keymap!(@trie $value)
69+ );
70+ _order.push(_key);
71+ )+
72+ )*
73+ $crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
3274 }
3375 };
3476}
3577
36−macro_rules! alt {
37− ($($ch:tt)*) => {
38− KeyEvent {
39− code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
40− modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
78+#[derive(Debug, Clone, Deserialize)]
79+pub struct KeyTrieNode {
80+ /// A label for keys coming under this node, like "Goto mode"
81+ #[serde(skip)]
82+ name: String,
83+ #[serde(flatten)]
84+ map: HashMap<KeyEvent, KeyTrie>,
85+ #[serde(skip)]
86+ order: Vec<KeyEvent>,
87+}
88+
89+impl KeyTrieNode {
90+ pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
91+ Self {
92+ name: name.to_string(),
93+ map,
94+ order,
4195 }
42− };
96+ }
97+
98+ pub fn name(&self) -> &str {
99+ &self.name
100+ }
101+
102+ /// Merge another Node in. Leaves and subnodes from the other node replace
103+ /// corresponding keyevent in self, except when both other and self have
104+ /// subnodes for same key. In that case the merge is recursive.
105+ pub fn merge(&mut self, mut other: Self) {
106+ for (key, trie) in std::mem::take(&mut other.map) {
107+ if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) {
108+ if let KeyTrie::Node(other_node) = trie {
109+ node.merge(other_node);
110+ continue;
111+ }
112+ }
113+ self.map.insert(key, trie);
114+ }
115+
116+ for &key in self.map.keys() {
117+ if !self.order.contains(&key) {
118+ self.order.push(key);
119+ }
120+ }
121+ }
122+}
123+
124+impl From<KeyTrieNode> for Info {
125+ fn from(node: KeyTrieNode) -> Self {
126+ let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
127+ for (&key, trie) in node.iter() {
128+ let desc = match trie {
129+ KeyTrie::Leaf(cmd) => cmd.doc(),
130+ KeyTrie::Node(n) => n.name(),
131+ };
132+ match body.iter().position(|(d, _)| d == &desc) {
133+ // FIXME: multiple keys are ordered randomly (use BTreeSet)
134+ Some(pos) => body[pos].1.push(key),
135+ None => body.push((desc, vec![key])),
136+ }
137+ }
138+ body.sort_unstable_by_key(|(_, keys)| {
139+ node.order.iter().position(|&k| k == keys[0]).unwrap()
140+ });
141+ let prefix = format!("{} ", node.name());
142+ if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
143+ body = body
144+ .into_iter()
145+ .map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
146+ .collect();
147+ }
148+ Info::key(node.name(), body)
149+ }
150+}
151+
152+impl Default for KeyTrieNode {
153+ fn default() -> Self {
154+ Self::new("", HashMap::new(), Vec::new())
155+ }
156+}
157+
158+impl PartialEq for KeyTrieNode {
159+ fn eq(&self, other: &Self) -> bool {
160+ self.map == other.map
161+ }
162+}
163+
164+impl Deref for KeyTrieNode {
165+ type Target = HashMap<KeyEvent, KeyTrie>;
166+
167+ fn deref(&self) -> &Self::Target {
168+ &self.map
169+ }
170+}
171+
172+impl DerefMut for KeyTrieNode {
173+ fn deref_mut(&mut self) -> &mut Self::Target {
174+ &mut self.map
175+ }
176+}
177+
178+#[derive(Debug, Clone, PartialEq, Deserialize)]
179+#[serde(untagged)]
180+pub enum KeyTrie {
181+ Leaf(Command),
182+ Node(KeyTrieNode),
183+}
184+
185+impl KeyTrie {
186+ pub fn node(&self) -> Option<&KeyTrieNode> {
187+ match *self {
188+ KeyTrie::Node(ref node) => Some(node),
189+ KeyTrie::Leaf(_) => None,
190+ }
191+ }
192+
193+ pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
194+ match *self {
195+ KeyTrie::Node(ref mut node) => Some(node),
196+ KeyTrie::Leaf(_) => None,
197+ }
198+ }
199+
200+ /// Merge another KeyTrie in, assuming that this KeyTrie and the other
201+ /// are both Nodes. Panics otherwise.
202+ pub fn merge_nodes(&mut self, mut other: Self) {
203+ let node = std::mem::take(other.node_mut().unwrap());
204+ self.node_mut().unwrap().merge(node);
205+ }
206+
207+ pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
208+ let mut trie = self;
209+ for key in keys {
210+ trie = match trie {
211+ KeyTrie::Node(map) => map.get(key),
212+ // leaf encountered while keys left to process
213+ KeyTrie::Leaf(_) => None,
214+ }?
215+ }
216+ Some(trie)
217+ }
218+}
219+
220+#[derive(Debug, Clone, PartialEq)]
221+pub enum KeymapResult {
222+ /// Needs more keys to execute a command. Contains valid keys for next keystroke.
223+ Pending(KeyTrieNode),
224+ Matched(Command),
225+ /// Key was not found in the root keymap
226+ NotFound,
227+ /// Key is invalid in combination with previous keys. Contains keys leading upto
228+ /// and including current (invalid) key.
229+ Cancelled(Vec<KeyEvent>),
230+}
231+
232+#[derive(Debug, Clone, PartialEq, Deserialize)]
233+pub struct Keymap {
234+ /// Always a Node
235+ #[serde(flatten)]
236+ root: KeyTrie,
237+ #[serde(skip)]
238+ state: Vec<KeyEvent>,
239+}
240+
241+impl Keymap {
242+ pub fn new(root: KeyTrie) -> Self {
243+ Keymap {
244+ root,
245+ state: Vec::new(),
246+ }
247+ }
248+
249+ pub fn root(&self) -> &KeyTrie {
250+ &self.root
251+ }
252+
253+ /// Lookup `key` in the keymap to try and find a command to execute
254+ pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
255+ let &first = self.state.get(0).unwrap_or(&key);
256+ let trie = match self.root.search(&[first]) {
257+ Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
258+ None => return KeymapResult::NotFound,
259+ Some(t) => t,
260+ };
261+ self.state.push(key);
262+ match trie.search(&self.state[1..]) {
263+ Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
264+ Some(&KeyTrie::Leaf(command)) => {
265+ self.state.clear();
266+ KeymapResult::Matched(command)
267+ }
268+ None => KeymapResult::Cancelled(self.state.drain(..).collect()),
269+ }
270+ }
271+
272+ pub fn merge(&mut self, other: Self) {
273+ self.root.merge_nodes(other.root);
274+ }
275+}
276+
277+impl Deref for Keymap {
278+ type Target = KeyTrieNode;
279+
280+ fn deref(&self) -> &Self::Target {
281+ &self.root.node().unwrap()
282+ }
283+}
284+
285+impl Default for Keymap {
286+ fn default() -> Self {
287+ Self::new(KeyTrie::Node(KeyTrieNode::default()))
288+ }
43289}
44290
45291#[derive(Debug, Clone, PartialEq, Deserialize)]
46292#[serde(transparent)]
47−pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
293+pub struct Keymaps(pub HashMap<Mode, Keymap>);
48294
49295impl Deref for Keymaps {
50− type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
296+ type Target = HashMap<Mode, Keymap>;
51297
52298 fn deref(&self) -> &Self::Target {
53299 &self.0
@@ -62,252 +308,298 @@ impl DerefMut for Keymaps {
62308
63309impl Default for Keymaps {
64310 fn default() -> Keymaps {
65− let normal = hashmap!(
66− key!('h') => Command::move_char_left,
67− key!('j') => Command::move_line_down,
68− key!('k') => Command::move_line_up,
69− key!('l') => Command::move_char_right,
70−
71− key!(Left) => Command::move_char_left,
72− key!(Down) => Command::move_line_down,
73− key!(Up) => Command::move_line_up,
74− key!(Right) => Command::move_char_right,
75−
76− key!('t') => Command::find_till_char,
77− key!('f') => Command::find_next_char,
78− key!('T') => Command::till_prev_char,
79− key!('F') => Command::find_prev_char,
80− // and matching set for select mode (extend)
81− //
82− key!('r') => Command::replace,
83− key!('R') => Command::replace_with_yanked,
84−
85− key!('~') => Command::switch_case,
86− alt!('`') => Command::switch_to_uppercase,
87− key!('`') => Command::switch_to_lowercase,
88−
89− key!(Home) => Command::goto_line_start,
90− key!(End) => Command::goto_line_end,
91−
92− key!('w') => Command::move_next_word_start,
93− key!('b') => Command::move_prev_word_start,
94− key!('e') => Command::move_next_word_end,
95−
96− key!('W') => Command::move_next_long_word_start,
97− key!('B') => Command::move_prev_long_word_start,
98− key!('E') => Command::move_next_long_word_end,
99−
100− key!('v') => Command::select_mode,
101− key!('g') => Command::goto_mode,
102− key!(':') => Command::command_mode,
103−
104− key!('i') => Command::insert_mode,
105− key!('I') => Command::prepend_to_line,
106− key!('a') => Command::append_mode,
107− key!('A') => Command::append_to_line,
108− key!('o') => Command::open_below,
109− key!('O') => Command::open_above,
311+ let normal = keymap!({ "Normal mode"
312+ "h" | "left" => move_char_left,
313+ "j" | "down" => move_line_down,
314+ "k" | "up" => move_line_up,
315+ "l" | "right" => move_char_right,
316+
317+ "t" => find_till_char,
318+ "f" => find_next_char,
319+ "T" => till_prev_char,
320+ "F" => find_prev_char,
321+ "r" => replace,
322+ "R" => replace_with_yanked,
323+
324+ "~" => switch_case,
325+ "`" => switch_to_lowercase,
326+ "A-`" => switch_to_uppercase,
327+
328+ "home" => goto_line_start,
329+ "end" => goto_line_end,
330+
331+ "w" => move_next_word_start,
332+ "b" => move_prev_word_start,
333+ "e" => move_next_word_end,
334+
335+ "W" => move_next_long_word_start,
336+ "B" => move_prev_long_word_start,
337+ "E" => move_next_long_word_end,
338+
339+ "v" => select_mode,
340+ "g" => { "Goto"
341+ "g" => goto_file_start,
342+ "e" => goto_file_end,
343+ "h" => goto_line_start,
344+ "l" => goto_line_end,
345+ "s" => goto_first_nonwhitespace,
346+ "d" => goto_definition,
347+ "y" => goto_type_definition,
348+ "r" => goto_reference,
349+ "i" => goto_implementation,
350+ "t" => goto_window_top,
351+ "m" => goto_window_middle,
352+ "b" => goto_window_bottom,
353+ "a" => goto_last_accessed_file,
354+ },
355+ ":" => command_mode,
356+
357+ "i" => insert_mode,
358+ "I" => prepend_to_line,
359+ "a" => append_mode,
360+ "A" => append_to_line,
361+ "o" => open_below,
362+ "O" => open_above,
110363 // [<space> ]<space> equivalents too (add blank new line, no edit)
111364
112−
113− key!('d') => Command::delete_selection,
365+ "d" => delete_selection,
114366 // TODO: also delete without yanking
115− key!('c') => Command::change_selection,
367+ "c" => change_selection,
116368 // TODO: also change delete without yanking
117369
118− // key!('r') => Command::replace_with_char,
119−
120− key!('s') => Command::select_regex,
121− alt!('s') => Command::split_selection_on_newline,
122− key!('S') => Command::split_selection,
123− key!(';') => Command::collapse_selection,
124− alt!(';') => Command::flip_selections,
125− key!('%') => Command::select_all,
126− key!('x') => Command::extend_line,
127− key!('X') => Command::extend_to_line_bounds,
370+ "s" => select_regex,
371+ "A-s" => split_selection_on_newline,
372+ "S" => split_selection,
373+ ";" => collapse_selection,
374+ "A-;" => flip_selections,
375+ "%" => select_all,
376+ "x" => extend_line,
377+ "X" => extend_to_line_bounds,
128378 // crop_to_whole_line
129379
380+ "m" => { "Match"
381+ "m" => match_brackets,
382+ "s" => surround_add,
383+ "r" => surround_replace,
384+ "d" => surround_delete,
385+ "a" => select_textobject_around,
386+ "i" => select_textobject_inner,
387+ },
388+ "[" => { "Left bracket"
389+ "d" => goto_prev_diag,
390+ "D" => goto_first_diag,
391+ },
392+ "]" => { "Right bracket"
393+ "d" => goto_next_diag,
394+ "D" => goto_last_diag,
395+ },
130396
131− key!('m') => Command::match_mode,
132− key!('[') => Command::left_bracket_mode,
133− key!(']') => Command::right_bracket_mode,
134−
135− key!('/') => Command::search,
397+ "/" => search,
136398 // ? for search_reverse
137− key!('n') => Command::search_next,
138− key!('N') => Command::extend_search_next,
399+ "n" => search_next,
400+ "N" => extend_search_next,
139401 // N for search_prev
140− key!('*') => Command::search_selection,
402+ "*" => search_selection,
141403
142− key!('u') => Command::undo,
143− key!('U') => Command::redo,
404+ "u" => undo,
405+ "U" => redo,
144406
145− key!('y') => Command::yank,
407+ "y" => yank,
146408 // yank_all
147− key!('p') => Command::paste_after,
409+ "p" => paste_after,
148410 // paste_all
149− key!('P') => Command::paste_before,
411+ "P" => paste_before,
150412
151− key!('>') => Command::indent,
152− key!('<') => Command::unindent,
153− key!('=') => Command::format_selections,
154− key!('J') => Command::join_selections,
413+ ">" => indent,
414+ "<" => unindent,
415+ "=" => format_selections,
416+ "J" => join_selections,
155417 // TODO: conflicts hover/doc
156− key!('K') => Command::keep_selections,
418+ "K" => keep_selections,
157419 // TODO: and another method for inverse
158420
159421 // TODO: clashes with space mode
160− key!(' ') => Command::keep_primary_selection,
422+ "space" => keep_primary_selection,
161423
162− // key!('q') => Command::record_macro,
163− // key!('Q') => Command::replay_macro,
424+ // "q" => record_macro,
425+ // "Q" => replay_macro,
164426
165− // ~ / apostrophe => change case
166427 // & align selections
167428 // _ trim selections
168429
169430 // C / altC = copy (repeat) selections on prev/next lines
170431
171− key!(Esc) => Command::normal_mode,
172− key!(PageUp) => Command::page_up,
173− key!(PageDown) => Command::page_down,
174− ctrl!('b') => Command::page_up,
175− ctrl!('f') => Command::page_down,
176− ctrl!('u') => Command::half_page_up,
177− ctrl!('d') => Command::half_page_down,
178−
179− ctrl!('w') => Command::window_mode,
432+ "esc" => normal_mode,
433+ "C-b" | "pageup" => page_up,
434+ "C-f" | "pagedown" => page_down,
435+ "C-u" => half_page_up,
436+ "C-d" => half_page_down,
437+
438+ "C-w" => { "Window"
439+ "C-w" | "w" => rotate_view,
440+ "C-h" | "h" => hsplit,
441+ "C-v" | "v" => vsplit,
442+ "C-q" | "q" => wclose,
443+ },
180444
181445 // move under <space>c
182− ctrl!('c') => Command::toggle_comments,
183− key!('K') => Command::hover,
446+ "C-c" => toggle_comments,
447+ "K" => hover,
184448
185449 // z family for save/restore/combine from/to sels from register
186450
187− // supposedly ctrl!('i') but did not work
188− key!(Tab) => Command::jump_forward,
189− ctrl!('o') => Command::jump_backward,
190− // ctrl!('s') => Command::save_selection,
191−
192− key!(' ') => Command::space_mode,
193− key!('z') => Command::view_mode,
451+ // supposedly "C-i" but did not work
452+ "tab" => jump_forward,
453+ "C-o" => jump_backward,
454+ // "C-s" => save_selection,
455+
456+ "space" => { "Space"
457+ "f" => file_picker,
458+ "b" => buffer_picker,
459+ "s" => symbol_picker,
460+ "a" => code_action,
461+ "'" => last_picker,
462+ "w" => { "Window"
463+ "C-w" | "w" => rotate_view,
464+ "C-h" | "h" => hsplit,
465+ "C-v" | "v" => vsplit,
466+ "C-q" | "q" => wclose,
467+ },
468+ "y" => yank_joined_to_clipboard,
469+ "Y" => yank_main_selection_to_clipboard,
470+ "p" => paste_clipboard_after,
471+ "P" => paste_clipboard_before,
472+ "R" => replace_selections_with_clipboard,
473+ "space" => keep_primary_selection,
474+ },
475+ "z" => { "View"
476+ "z" | "c" => align_view_center,
477+ "t" => align_view_top,
478+ "b" => align_view_bottom,
479+ "m" => align_view_middle,
480+ "k" => scroll_up,
481+ "j" => scroll_down,
482+ },
194483
195− key!('"') => Command::select_register,
196− );
484+ "\"" => select_register,
485+ });
197486 // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
198487 // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
199488 // because some selection operations can now be done from normal mode, some from select mode.
200489 let mut select = normal.clone();
201− select.extend(
202− hashmap!(
203− key!('h') => Command::extend_char_left,
204− key!('j') => Command::extend_line_down,
205− key!('k') => Command::extend_line_up,
206− key!('l') => Command::extend_char_right,
207−
208− key!(Left) => Command::extend_char_left,
209− key!(Down) => Command::extend_line_down,
210− key!(Up) => Command::extend_line_up,
211− key!(Right) => Command::extend_char_right,
212−
213− key!('w') => Command::extend_next_word_start,
214− key!('b') => Command::extend_prev_word_start,
215− key!('e') => Command::extend_next_word_end,
216−
217− key!('t') => Command::extend_till_char,
218− key!('f') => Command::extend_next_char,
219−
220− key!('T') => Command::extend_till_prev_char,
221− key!('F') => Command::extend_prev_char,
222− key!(Home) => Command::goto_line_start,
223− key!(End) => Command::goto_line_end,
224− key!(Esc) => Command::exit_select_mode,
225− )
226− .into_iter(),
227− );
228−
490+ select.merge_nodes(keymap!({ "Select mode"
491+ "h" | "left" => extend_char_left,
492+ "j" | "down" => extend_line_down,
493+ "k" | "up" => extend_line_up,
494+ "l" | "right" => extend_char_right,
495+
496+ "w" => extend_next_word_start,
497+ "b" => extend_prev_word_start,
498+ "e" => extend_next_word_end,
499+
500+ "t" => extend_till_char,
501+ "f" => extend_next_char,
502+ "T" => extend_till_prev_char,
503+ "F" => extend_prev_char,
504+
505+ "home" => goto_line_start,
506+ "end" => goto_line_end,
507+ "esc" => exit_select_mode,
508+ }));
509+ let insert = keymap!({ "Insert mode"
510+ "esc" => normal_mode,
511+
512+ "backspace" => delete_char_backward,
513+ "del" => delete_char_forward,
514+ "ret" => insert_newline,
515+ "tab" => insert_tab,
516+ "C-w" => delete_word_backward,
517+
518+ "left" => move_char_left,
519+ "down" => move_line_down,
520+ "up" => move_line_up,
521+ "right" => move_char_right,
522+ "pageup" => page_up,
523+ "pagedown" => page_down,
524+ "home" => goto_line_start,
525+ "end" => goto_line_end_newline,
526+
527+ "C-x" => completion,
528+ });
229529 Keymaps(hashmap!(
230− // as long as you cast the first item, rust is able to infer the other cases
231− // TODO: select could be normal mode with some bindings merged over
232− Mode::Normal => normal,
233− Mode::Select => select,
234− Mode::Insert => hashmap!(
235− key!(Esc) => Command::normal_mode as Command,
236− key!(Backspace) => Command::delete_char_backward,
237− key!(Delete) => Command::delete_char_forward,
238− key!(Enter) => Command::insert_newline,
239− key!(Tab) => Command::insert_tab,
240− key!(Left) => Command::move_char_left,
241− key!(Down) => Command::move_line_down,
242− key!(Up) => Command::move_line_up,
243− key!(Right) => Command::move_char_right,
244− key!(PageUp) => Command::page_up,
245− key!(PageDown) => Command::page_down,
246− key!(Home) => Command::goto_line_start,
247− key!(End) => Command::goto_line_end_newline,
248− ctrl!('x') => Command::completion,
249− ctrl!('w') => Command::delete_word_backward,
250− ),
530+ Mode::Normal => Keymap::new(normal),
531+ Mode::Select => Keymap::new(select),
532+ Mode::Insert => Keymap::new(insert),
251533 ))
252534 }
253535}
254536
255−/// Merge default config keys with user overwritten keys for custom
256−/// user config.
537+/// Merge default config keys with user overwritten keys for custom user config.
257538pub fn merge_keys(mut config: Config) -> Config {
258539 let mut delta = std::mem::take(&mut config.keys);
259540 for (mode, keys) in &mut *config.keys {
260− keys.extend(delta.remove(mode).unwrap_or_default());
541+ keys.merge(delta.remove(mode).unwrap_or_default())
261542 }
262543 config
263544}
264545
265546#[test]
266547fn merge_partial_keys() {
267− use helix_view::keyboard::{KeyCode, KeyModifiers};
268548 let config = Config {
269549 keys: Keymaps(hashmap! {
270− Mode::Normal => hashmap! {
271− KeyEvent {
272− code: KeyCode::Char('i'),
273− modifiers: KeyModifiers::NONE,
274− } => Command::normal_mode,
275− KeyEvent { // key that does not exist
276− code: KeyCode::Char('无'),
277− modifiers: KeyModifiers::NONE,
278− } => Command::insert_mode,
279− },
550+ Mode::Normal => Keymap::new(
551+ keymap!({ "Normal mode"
552+ "i" => normal_mode,
553+ "无" => insert_mode,
554+ "z" => jump_backward,
555+ "g" => { "Merge into goto mode"
556+ "$" => goto_line_end,
557+ "g" => delete_char_forward,
558+ },
559+ })
560+ )
280561 }),
281562 ..Default::default()
282563 };
283− let merged_config = merge_keys(config.clone());
564+ let mut merged_config = merge_keys(config.clone());
284565 assert_ne!(config, merged_config);
566+
567+ let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
285568 assert_eq!(
286− *merged_config
287− .keys
288− .0
289− .get(&Mode::Normal)
290− .unwrap()
291− .get(&KeyEvent {
292− code: KeyCode::Char('i'),
293− modifiers: KeyModifiers::NONE
294− })
295− .unwrap(),
296− Command::normal_mode
569+ keymap.get(key!('i')),
570+ KeymapResult::Matched(Command::normal_mode),
571+ "Leaf should replace leaf"
297572 );
298573 assert_eq!(
299− *merged_config
300− .keys
301− .0
302− .get(&Mode::Normal)
303− .unwrap()
304− .get(&KeyEvent {
305− code: KeyCode::Char('无'),
306− modifiers: KeyModifiers::NONE
307− })
308− .unwrap(),
309− Command::insert_mode
574+ keymap.get(key!('无')),
575+ KeymapResult::Matched(Command::insert_mode),
576+ "New leaf should be present in merged keymap"
310577 );
578+ // Assumes that z is a node in the default keymap
579+ assert_eq!(
580+ keymap.get(key!('z')),
581+ KeymapResult::Matched(Command::jump_backward),
582+ "Leaf should replace node"
583+ );
584+ // Assumes that `g` is a node in default keymap
585+ assert_eq!(
586+ keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
587+ &KeyTrie::Leaf(Command::goto_line_end),
588+ "Leaf should be present in merged subnode"
589+ );
590+ // Assumes that `gg` is in default keymap
591+ assert_eq!(
592+ keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
593+ &KeyTrie::Leaf(Command::delete_char_forward),
594+ "Leaf should replace old leaf in merged subnode"
595+ );
596+ // Assumes that `ge` is in default keymap
597+ assert_eq!(
598+ keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
599+ &KeyTrie::Leaf(Command::goto_file_end),
600+ "Old leaves in subnode should be present in merged node"
601+ );
602+
311603 assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
312604 assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
313605}
helix-term/src/ui/editor.rs+57−19
@@ -2,7 +2,7 @@ use crate::{
22 commands,
33 compositor::{Component, Context, EventResult},
44 key,
5− keymap::Keymaps,
5+ keymap::{KeymapResult, Keymaps},
66 ui::{Completion, ProgressSpinners},
77};
88
@@ -15,6 +15,7 @@ use helix_core::{
1515use helix_view::{
1616 document::Mode,
1717 graphics::{CursorKind, Modifier, Rect, Style},
18+ info::Info,
1819 input::KeyEvent,
1920 keyboard::{KeyCode, KeyModifiers},
2021 Document, Editor, Theme, View,
@@ -30,6 +31,7 @@ pub struct EditorView {
3031 last_insert: (commands::Command, Vec<KeyEvent>),
3132 completion: Option<Completion>,
3233 spinners: ProgressSpinners,
34+ pub autoinfo: Option<Info>,
3335}
3436
3537const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@@ -48,6 +50,7 @@ impl EditorView {
4850 last_insert: (commands::Command::normal_mode, Vec::new()),
4951 completion: None,
5052 spinners: ProgressSpinners::default(),
53+ autoinfo: None,
5154 }
5255 }
5356
@@ -559,19 +562,53 @@ impl EditorView {
559562 );
560563 }
561564
562− fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
563− if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
564− command.execute(cx);
565− } else if let KeyEvent {
566− code: KeyCode::Char(ch),
567− ..
568− } = event
569− {
570− commands::insert::insert_char(cx, ch);
565+ /// Handle events by looking them up in `self.keymaps`. Returns None
566+ /// if event was handled (a command was executed or a subkeymap was
567+ /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
568+ /// otherwise.
569+ fn handle_keymap_event(
570+ &mut self,
571+ mode: Mode,
572+ cxt: &mut commands::Context,
573+ event: KeyEvent,
574+ ) -> Option<KeymapResult> {
575+ self.autoinfo = None;
576+ match self.keymaps.get_mut(&mode).unwrap().get(event) {
577+ KeymapResult::Matched(command) => command.execute(cxt),
578+ KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
579+ k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
571580 }
581+ None
572582 }
573583
574− fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
584+ fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
585+ if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
586+ match keyresult {
587+ KeymapResult::NotFound => {
588+ if let Some(ch) = event.char() {
589+ commands::insert::insert_char(cx, ch)
590+ }
591+ }
592+ KeymapResult::Cancelled(pending) => {
593+ for ev in pending {
594+ match ev.char() {
595+ Some(ch) => commands::insert::insert_char(cx, ch),
596+ None => {
597+ if let KeymapResult::Matched(command) =
598+ self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
599+ {
600+ command.execute(cx);
601+ }
602+ }
603+ }
604+ }
605+ }
606+ _ => unreachable!(),
607+ }
608+ }
609+ }
610+
611+ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
575612 match event {
576613 // count handling
577614 key!(i @ '0'..='9') => {
@@ -584,8 +621,8 @@ impl EditorView {
584621 // first execute whatever put us into insert mode
585622 self.last_insert.0.execute(cxt);
586623 // then replay the inputs
587− for key in &self.last_insert.1 {
588− self.insert_mode(cxt, *key)
624+ for &key in &self.last_insert.1.clone() {
625+ self.insert_mode(cxt, key)
589626 }
590627 }
591628 _ => {
@@ -598,9 +635,7 @@ impl EditorView {
598635 // set the register
599636 cxt.selected_register = cxt.editor.selected_register.take();
600637
601− if let Some(command) = self.keymaps[&mode].get(&event) {
602− command.execute(cxt);
603− }
638+ self.handle_keymap_event(mode, cxt, event);
604639 }
605640 }
606641 }
@@ -714,7 +749,11 @@ impl Component for EditorView {
714749 // how we entered insert mode is important, and we should track that so
715750 // we can repeat the side effect.
716751
717− self.last_insert.0 = self.keymaps[&mode][&key];
752+ self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
753+ KeymapResult::Matched(command) => command,
754+ // FIXME: insert mode can only be entered through single KeyCodes
755+ _ => unimplemented!(),
756+ };
718757 self.last_insert.1.clear();
719758 }
720759 (Mode::Insert, Mode::Normal) => {
@@ -752,9 +791,8 @@ impl Component for EditorView {
752791 );
753792 }
754793
755− if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) {
794+ if let Some(ref info) = self.autoinfo {
756795 info.render(area, surface, cx);
757− cx.editor.autoinfo = Some(info);
758796 }
759797
760798 // render status msg
helix-term/src/ui/info.rs+1−1
@@ -8,7 +8,7 @@ impl Component for Info {
88 fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
99 let style = cx.editor.theme.get("ui.popup");
1010 let block = Block::default()
11− .title(self.title)
11+ .title(self.title.as_str())
1212 .borders(Borders::ALL)
1313 .border_style(style);
1414 let Info { width, height, .. } = self;
helix-view/src/editor.rs−3
@@ -1,7 +1,6 @@
11use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
33 graphics::{CursorKind, Rect},
4− info::Info,
54 theme::{self, Theme},
65 tree::Tree,
76 Document, DocumentId, RegisterSelection, View, ViewId,
@@ -33,7 +32,6 @@ pub struct Editor {
3332 pub syn_loader: Arc<syntax::Loader>,
3433 pub theme_loader: Arc<theme::Loader>,
3534
36− pub autoinfo: Option<&'static Info>,
3735 pub status_msg: Option<(String, Severity)>,
3836}
3937
@@ -67,7 +65,6 @@ impl Editor {
6765 theme_loader: themes,
6866 registers: Registers::default(),
6967 clipboard_provider: get_clipboard_provider(),
70− autoinfo: None,
7168 status_msg: None,
7269 }
7370 }
helix-view/src/info.rs+10−7
@@ -5,9 +5,9 @@ use std::fmt::Write;
55#[derive(Debug)]
66/// Info box used in editor. Rendering logic will be in other crate.
77pub struct Info {
8− /// Title kept as static str for now.
9− pub title: &'static str,
10− /// Text body, should contains newline.
8+ /// Title shown at top.
9+ pub title: String,
10+ /// Text body, should contain newlines.
1111 pub text: String,
1212 /// Body width.
1313 pub width: u16,
@@ -16,17 +16,20 @@ pub struct Info {
1616}
1717
1818impl Info {
19− pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info {
19+ // body is a BTreeMap instead of a HashMap because keymaps are represented
20+ // with nested hashmaps with no ordering, and each invocation of infobox would
21+ // show different orders of items
22+ pub fn key(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
2023 let (lpad, mpad, rpad) = (1, 2, 1);
2124 let keymaps_width: u16 = body
2225 .iter()
23− .map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
26+ .map(|r| r.1.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
2427 .max()
2528 .unwrap();
2629 let mut text = String::new();
2730 let mut width = 0;
2831 let height = body.len() as u16;
29− for (keyevents, desc) in body {
32+ for (desc, keyevents) in body {
3033 let keyevent = keyevents[0];
3134 let mut left = keymaps_width - keyevent.width() as u16;
3235 for _ in 0..lpad {
@@ -48,7 +51,7 @@ impl Info {
4851 writeln!(text, "{}", desc).ok();
4952 }
5053 Info {
51− title,
54+ title: title.to_string(),
5255 text,
5356 width,
5457 height,
helix-view/src/input.rs+11−1
@@ -14,6 +14,16 @@ pub struct KeyEvent {
1414 pub modifiers: KeyModifiers,
1515}
1616
17+impl KeyEvent {
18+ /// Get only the character involved in this event
19+ pub fn char(&self) -> Option<char> {
20+ match self.code {
21+ KeyCode::Char(ch) => Some(ch),
22+ _ => None,
23+ }
24+ }
25+}
26+
1727pub(crate) mod keys {
1828 pub(crate) const BACKSPACE: &str = "backspace";
1929 pub(crate) const ENTER: &str = "ret";
@@ -168,7 +178,7 @@ impl std::str::FromStr for KeyEvent {
168178 keys::MINUS => KeyCode::Char('-'),
169179 keys::SEMICOLON => KeyCode::Char(';'),
170180 keys::PERCENT => KeyCode::Char('%'),
171− single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
181+ single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()),
172182 function if function.len() > 1 && function.starts_with('F') => {
173183 let function: String = function.chars().skip(1).collect();
174184 let function = str::parse::<u8>(&function)?;