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
pull/511/head
Gokul Soumya 3 years ago committed by GitHub
parent a630fb5d20
commit 88d6f65239
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -16,7 +16,6 @@ use helix_core::{
use helix_view::{ use helix_view::{
document::{IndentStyle, Mode}, document::{IndentStyle, Mode},
editor::Action, editor::Action,
info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::KeyCode, keyboard::KeyCode,
view::{View, PADDING}, view::{View, PADDING},
@ -39,7 +38,6 @@ use crate::{
use crate::job::{self, Job, Jobs}; use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, TryFutureExt}; use futures_util::{FutureExt, TryFutureExt};
use std::collections::HashMap;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::{fmt, future::Future}; use std::{fmt, future::Future};
@ -48,7 +46,7 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> { pub struct Context<'a> {
@ -77,18 +75,6 @@ impl<'a> Context<'a> {
self.on_next_key_callback = Some(Box::new(on_next_key_callback)); self.on_next_key_callback = Some(Box::new(on_next_key_callback));
} }
#[inline]
pub fn on_next_key_mode(&mut self, map: HashMap<KeyEvent, fn(&mut Context)>) {
let count = self.count;
self.on_next_key(move |cx, event| {
cx.count = count;
cx.editor.autoinfo = None;
if let Some(func) = map.get(&event) {
func(cx);
}
});
}
#[inline] #[inline]
pub fn callback<T, F>( pub fn callback<T, F>(
&mut self, &mut self,
@ -139,13 +125,21 @@ fn align_view(doc: &Document, view: &mut View, align: Align) {
/// A command is composed of a static name, and a function that takes the current state plus a count, /// A command is composed of a static name, and a function that takes the current state plus a count,
/// and does a side-effect on the state (usually by creating and applying a transaction). /// and does a side-effect on the state (usually by creating and applying a transaction).
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Command(&'static str, fn(cx: &mut Context)); pub struct Command {
name: &'static str,
fun: fn(cx: &mut Context),
doc: &'static str,
}
macro_rules! commands { macro_rules! commands {
( $($name:ident),* ) => { ( $($name:ident, $doc:literal),* ) => {
$( $(
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
pub const $name: Self = Self(stringify!($name), $name); pub const $name: Self = Self {
name: stringify!($name),
fun: $name,
doc: $doc
};
)* )*
pub const COMMAND_LIST: &'static [Self] = &[ pub const COMMAND_LIST: &'static [Self] = &[
@ -156,145 +150,159 @@ macro_rules! commands {
impl Command { impl Command {
pub fn execute(&self, cx: &mut Context) { pub fn execute(&self, cx: &mut Context) {
(self.1)(cx); (self.fun)(cx);
} }
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
self.0 self.name
}
pub fn doc(&self) -> &'static str {
self.doc
} }
#[rustfmt::skip]
commands!( commands!(
move_char_left, move_char_left, "Move left",
move_char_right, move_char_right, "Move right",
move_line_up, move_line_up, "Move up",
move_line_down, move_line_down, "Move down",
move_next_word_start, extend_char_left, "Extend left",
move_prev_word_start, extend_char_right, "Extend right",
move_next_word_end, extend_line_up, "Extend up",
move_next_long_word_start, extend_line_down, "Extend down",
move_prev_long_word_start, move_next_word_start, "Move to beginning of next word",
move_next_long_word_end, move_prev_word_start, "Move to beginning of previous word",
extend_next_word_start, move_next_word_end, "Move to end of next word",
extend_prev_word_start, move_next_long_word_start, "Move to beginning of next long word",
extend_next_word_end, move_prev_long_word_start, "Move to beginning of previous long word",
find_till_char, move_next_long_word_end, "Move to end of next long word",
find_next_char, extend_next_word_start, "Extend to beginning of next word",
extend_till_char, extend_prev_word_start, "Extend to beginning of previous word",
extend_next_char, extend_next_word_end, "Extend to end of next word",
till_prev_char, find_till_char, "Move till next occurance of char",
find_prev_char, find_next_char, "Move to next occurance of char",
extend_till_prev_char, extend_till_char, "Extend till next occurance of char",
extend_prev_char, extend_next_char, "Extend to next occurance of char",
replace, till_prev_char, "Move till previous occurance of char",
switch_case, find_prev_char, "Move to previous occurance of char",
switch_to_uppercase, extend_till_prev_char, "Extend till previous occurance of char",
switch_to_lowercase, extend_prev_char, "Extend to previous occurance of char",
page_up, replace, "Replace with new char",
page_down, switch_case, "Switch (toggle) case",
half_page_up, switch_to_uppercase, "Switch to uppercase",
half_page_down, switch_to_lowercase, "Switch to lowercase",
extend_char_left, page_up, "Move page up",
extend_char_right, page_down, "Move page down",
extend_line_up, half_page_up, "Move half page up",
extend_line_down, half_page_down, "Move half page down",
select_all, select_all, "Select whole document",
select_regex, select_regex, "Select all regex matches inside selections",
split_selection, split_selection, "Split selection into subselections on regex matches",
split_selection_on_newline, split_selection_on_newline, "Split selection on newlines",
search, search, "Search for regex pattern",
search_next, search_next, "Select next search match",
extend_search_next, extend_search_next, "Add next search match to selection",
search_selection, search_selection, "Use current selection as search pattern",
extend_line, extend_line, "Select current line, if already selected, extend to next line",
extend_to_line_bounds, extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
delete_selection, delete_selection, "Delete selection",
change_selection, change_selection, "Change selection (delete and enter insert mode)",
collapse_selection, collapse_selection, "Collapse selection onto a single cursor",
flip_selections, flip_selections, "Flip selection cursor and anchor",
insert_mode, insert_mode, "Insert before selection",
append_mode, append_mode, "Insert after selection (append)",
command_mode, command_mode, "Enter command mode",
file_picker, file_picker, "Open file picker",
code_action, code_action, "Perform code action",
buffer_picker, buffer_picker, "Open buffer picker",
symbol_picker, symbol_picker, "Open symbol picker",
last_picker, last_picker, "Open last picker",
prepend_to_line, prepend_to_line, "Insert at start of line",
append_to_line, append_to_line, "Insert at end of line",
open_below, open_below, "Open new line below selection",
open_above, open_above, "Open new line above selection",
normal_mode, normal_mode, "Enter normal mode",
goto_mode, select_mode, "Enter selection extend mode",
select_mode, exit_select_mode, "Exit selection mode",
exit_select_mode, goto_definition, "Goto definition",
goto_definition, goto_type_definition, "Goto type definition",
goto_type_definition, goto_implementation, "Goto implementation",
goto_implementation, goto_file_start, "Goto file start",
goto_file_start, goto_file_end, "Goto file end",
goto_file_end, goto_reference, "Goto references",
goto_reference, goto_window_top, "Goto window top",
goto_first_diag, goto_window_middle, "Goto window middle",
goto_last_diag, goto_window_bottom, "Goto window bottom",
goto_next_diag, goto_last_accessed_file, "Goto last accessed file",
goto_prev_diag, goto_first_diag, "Goto first diagnostic",
goto_line_start, goto_last_diag, "Goto last diagnostic",
goto_line_end, goto_next_diag, "Goto next diagnostic",
goto_line_end_newline, goto_prev_diag, "Goto previous diagnostic",
goto_first_nonwhitespace, goto_line_start, "Goto line start",
signature_help, goto_line_end, "Goto line end",
insert_tab, // TODO: different description ?
insert_newline, goto_line_end_newline, "Goto line end",
delete_char_backward, goto_first_nonwhitespace, "Goto first non-blank in line",
delete_char_forward, signature_help, "Show signature help",
delete_word_backward, insert_tab, "Insert tab char",
undo, insert_newline, "Insert newline char",
redo, delete_char_backward, "Delete previous char",
yank, delete_char_forward, "Delete next char",
yank_joined_to_clipboard, delete_word_backward, "Delete previous word",
yank_main_selection_to_clipboard, undo, "Undo change",
replace_with_yanked, redo, "Redo change",
replace_selections_with_clipboard, yank, "Yank selection",
paste_after, yank_joined_to_clipboard, "Join and yank selections to clipboard",
paste_before, yank_main_selection_to_clipboard, "Yank main selection to clipboard",
paste_clipboard_after, replace_with_yanked, "Replace with yanked text",
paste_clipboard_before, replace_selections_with_clipboard, "Replace selections by clipboard content",
indent, paste_after, "Paste after selection",
unindent, paste_before, "Paste before selection",
format_selections, paste_clipboard_after, "Paste clipboard after selections",
join_selections, paste_clipboard_before, "Paste clipboard before selections",
keep_selections, indent, "Indent selection",
keep_primary_selection, unindent, "Unindent selection",
completion, format_selections, "Format selection",
hover, join_selections, "Join lines inside selection",
toggle_comments, keep_selections, "Keep selections matching regex",
expand_selection, keep_primary_selection, "Keep primary selection",
match_brackets, completion, "Invoke completion popup",
jump_forward, hover, "Show docs for item under cursor",
jump_backward, toggle_comments, "Comment/uncomment selections",
window_mode, expand_selection, "Expand selection to parent syntax node",
rotate_view, jump_forward, "Jump forward on jumplist",
hsplit, jump_backward, "Jump backward on jumplist",
vsplit, rotate_view, "Goto next window",
wclose, hsplit, "Horizontal bottom split",
select_register, vsplit, "Vertical right split",
space_mode, wclose, "Close window",
view_mode, select_register, "Select register",
left_bracket_mode, align_view_middle, "Align view middle",
right_bracket_mode, align_view_top, "Align view top",
match_mode align_view_center, "Align view center",
align_view_bottom, "Align view bottom",
scroll_up, "Scroll view up",
scroll_down, "Scroll view down",
match_brackets, "Goto matching bracket",
surround_add, "Surround add",
surround_replace, "Surround replace",
surround_delete, "Surround delete",
select_textobject_around, "Select around object",
select_textobject_inner, "Select inside object"
); );
} }
impl fmt::Debug for Command { impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self; let Command { name, .. } = self;
f.debug_tuple("Command").field(name).finish() f.debug_tuple("Command").field(name).finish()
} }
} }
impl fmt::Display for Command { impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self; let Command { name, .. } = self;
f.write_str(name) f.write_str(name)
} }
} }
@ -306,7 +314,7 @@ impl std::str::FromStr for Command {
Command::COMMAND_LIST Command::COMMAND_LIST
.iter() .iter()
.copied() .copied()
.find(|cmd| cmd.0 == s) .find(|cmd| cmd.name == s)
.ok_or_else(|| anyhow!("No command named '{}'", s)) .ok_or_else(|| anyhow!("No command named '{}'", s))
} }
} }
@ -2396,20 +2404,6 @@ fn exit_select_mode(cx: &mut Context) {
doc_mut!(cx.editor).mode = Mode::Normal; doc_mut!(cx.editor).mode = Mode::Normal;
} }
fn goto_prehook(cx: &mut Context) -> bool {
if let Some(count) = cx.count {
push_jump(cx.editor);
let (view, doc) = current!(cx.editor);
let line_idx = std::cmp::min(count.get() - 1, doc.text().len_lines().saturating_sub(2));
let pos = doc.text().line_to_char(line_idx);
doc.set_selection(view.id, Selection::point(pos));
true
} else {
false
}
}
fn goto_impl( fn goto_impl(
editor: &mut Editor, editor: &mut Editor,
compositor: &mut Compositor, compositor: &mut Compositor,
@ -3794,201 +3788,3 @@ fn surround_delete(cx: &mut Context) {
} }
}) })
} }
/// Do nothing, just for modeinfo.
fn noop(_cx: &mut Context) -> bool {
false
}
/// Generate modeinfo.
///
/// If prehook returns true then it will stop the rest.
macro_rules! mode_info {
// TODO: reuse $mode for $stat
(@join $first:expr $(,$rest:expr)*) => {
concat!($first, $(", ", $rest),*)
};
(@name #[doc = $name:literal] $(#[$rest:meta])*) => {
$name
};
{
#[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident,
$(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
} => {
mode_info! {
#[doc = $name]
$(#[$doc])*
$mode, $stat, noop,
$(
#[doc = $desc]
$($key)|+ => $func
),+,
}
};
{
#[doc = $name:literal] $(#[$doc:meta])* $mode:ident, $stat:ident, $prehook:expr,
$(#[doc = $desc:literal] $($key:tt)|+ => $func:expr),+,
} => {
#[doc = $name]
$(#[$doc])*
#[doc = ""]
#[doc = "<table><tr><th>key</th><th>desc</th></tr><tbody>"]
$(
#[doc = "<tr><td>"]
// TODO switch to this once we use rust 1.54
// right now it will produce multiple rows
// #[doc = mode_info!(@join $($key),+)]
$(
#[doc = $key]
)+
// <-
#[doc = "</td><td>"]
#[doc = $desc]
#[doc = "</td></tr>"]
)+
#[doc = "</tbody></table>"]
pub fn $mode(cx: &mut Context) {
if $prehook(cx) {
return;
}
static $stat: OnceCell<Info> = OnceCell::new();
cx.editor.autoinfo = Some($stat.get_or_init(|| Info::key(
$name.trim(),
vec![$((&[$($key.parse().unwrap()),+], $desc)),+],
)));
use helix_core::hashmap;
// TODO: try and convert this to match later
let map = hashmap! {
$($($key.parse::<KeyEvent>().unwrap() => $func as for<'r, 's> fn(&'r mut Context<'s>)),+),*
};
cx.on_next_key_mode(map);
}
};
}
mode_info! {
/// space mode
space_mode, SPACE_MODE,
/// resume last picker
"'" => last_picker,
/// file picker
"f" => file_picker,
/// buffer picker
"b" => buffer_picker,
/// symbol picker
"s" => symbol_picker,
/// window mode
"w" => window_mode,
/// yank joined to clipboard
"y" => yank_joined_to_clipboard,
/// yank main selection to clipboard
"Y" => yank_main_selection_to_clipboard,
/// paste system clipboard after selections
"p" => paste_clipboard_after,
/// paste system clipboard before selections
"P" => paste_clipboard_before,
/// replace selections with clipboard
"R" => replace_selections_with_clipboard,
/// perform code action
"a" => code_action,
/// keep primary selection
"space" => keep_primary_selection,
}
mode_info! {
/// goto
///
/// When specified with a count, it will go to that line without entering the mode.
goto_mode, GOTO_MODE, goto_prehook,
/// file start
"g" => goto_file_start,
/// file end
"e" => goto_file_end,
/// line start
"h" => goto_line_start,
/// line end
"l" => goto_line_end,
/// line first non blank
"s" => goto_first_nonwhitespace,
/// definition
"d" => goto_definition,
/// type references
"y" => goto_type_definition,
/// references
"r" => goto_reference,
/// implementation
"i" => goto_implementation,
/// window top
"t" => goto_window_top,
/// window middle
"m" => goto_window_middle,
/// window bottom
"b" => goto_window_bottom,
/// last accessed file
"a" => goto_last_accessed_file,
}
mode_info! {
/// window
window_mode, WINDOW_MODE,
/// rotate
"w" | "C-w" => rotate_view,
/// horizontal split
"h" => hsplit,
/// vertical split
"v" => vsplit,
/// close
"q" => wclose,
}
mode_info! {
/// match
match_mode, MATCH_MODE,
/// matching character
"m" => match_brackets,
/// surround add
"s" => surround_add,
/// surround replace
"r" => surround_replace,
/// surround delete
"d" => surround_delete,
/// around object
"a" => select_textobject_around,
/// inside object
"i" => select_textobject_inner,
}
mode_info! {
/// select to previous
left_bracket_mode, LEFT_BRACKET_MODE,
/// previous diagnostic
"d" => goto_prev_diag,
/// diagnostic (first)
"D" => goto_first_diag,
}
mode_info! {
/// select to next
right_bracket_mode, RIGHT_BRACKET_MODE,
/// diagnostic
"d" => goto_next_diag,
/// diagnostic (last)
"D" => goto_last_diag,
}
mode_info! {
/// view
view_mode, VIEW_MODE,
/// align view top
"t" => align_view_top,
/// align view center
"z" | "c" => align_view_center,
/// align view bottom
"b" => align_view_bottom,
/// align view middle
"m" => align_view_middle,
/// scroll up
"k" => scroll_up,
/// scroll down
"j" => scroll_down,
}

@ -2,9 +2,6 @@ use serde::Deserialize;
use crate::keymap::Keymaps; use crate::keymap::Keymaps;
#[cfg(test)]
use crate::commands::Command;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
pub struct Config { pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
@ -22,12 +19,10 @@ pub struct LspConfig {
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap;
use crate::keymap::Keymap;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::{ use helix_view::document::Mode;
document::Mode,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
};
let sample_keymaps = r#" let sample_keymaps = r#"
[keys.insert] [keys.insert]
@ -42,22 +37,13 @@ fn parsing_keymaps_config_file() {
toml::from_str::<Config>(sample_keymaps).unwrap(), toml::from_str::<Config>(sample_keymaps).unwrap(),
Config { Config {
keys: Keymaps(hashmap! { keys: Keymaps(hashmap! {
Mode::Insert => hashmap! { Mode::Insert => Keymap::new(keymap!({ "Insert mode"
KeyEvent { "y" => move_line_down,
code: KeyCode::Char('y'), "S-C-a" => delete_selection,
modifiers: KeyModifiers::NONE, })),
} => Command::move_line_down, Mode::Normal => Keymap::new(keymap!({ "Normal mode"
KeyEvent { "A-F12" => move_next_word_end,
code: KeyCode::Char('a'), })),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
} => Command::delete_selection,
},
Mode::Normal => hashmap! {
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::ALT,
} => Command::move_next_word_end,
},
}), }),
..Default::default() ..Default::default()
} }

@ -1,7 +1,7 @@
pub use crate::commands::Command; pub use crate::commands::Command;
use crate::config::Config; use crate::config::Config;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::{document::Mode, input::KeyEvent}; use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -24,30 +24,276 @@ macro_rules! key {
}; };
} }
macro_rules! ctrl { /// Macro for defining the root of a `Keymap` object. Example:
($($ch:tt)*) => { ///
KeyEvent { /// ```
code: ::helix_view::keyboard::KeyCode::Char($($ch)*), /// # use helix_core::hashmap;
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, /// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
/// "g" => goto_file_start,
/// "e" => goto_file_end,
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
};
(@trie
{ $label:literal $($($key:literal)|+ => $value:tt,)+ }
) => {
keymap!({ $label $($($key)|+ => $value,)+ })
};
(
{ $label:literal $($($key:literal)|+ => $value:tt,)+ }
) => {
// modified from the hashmap! macro
{
let _cap = hashmap!(@count $($($key),+),*);
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
let mut _order = ::std::vec::Vec::with_capacity(_cap);
$(
$(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
_map.insert(
_key,
keymap!(@trie $value)
);
_order.push(_key);
)+
)*
$crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
} }
}; };
} }
macro_rules! alt { #[derive(Debug, Clone, Deserialize)]
($($ch:tt)*) => { pub struct KeyTrieNode {
KeyEvent { /// A label for keys coming under this node, like "Goto mode"
code: ::helix_view::keyboard::KeyCode::Char($($ch)*), #[serde(skip)]
modifiers: ::helix_view::keyboard::KeyModifiers::ALT, name: String,
#[serde(flatten)]
map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)]
order: Vec<KeyEvent>,
}
impl KeyTrieNode {
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
Self {
name: name.to_string(),
map,
order,
}
} }
pub fn name(&self) -> &str {
&self.name
}
/// 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.
pub fn merge(&mut self, mut other: Self) {
for (key, trie) in std::mem::take(&mut other.map) {
if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) {
if let KeyTrie::Node(other_node) = trie {
node.merge(other_node);
continue;
}
}
self.map.insert(key, trie);
}
for &key in self.map.keys() {
if !self.order.contains(&key) {
self.order.push(key);
}
}
}
}
impl From<KeyTrieNode> for Info {
fn from(node: KeyTrieNode) -> Self {
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
for (&key, trie) in node.iter() {
let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Node(n) => n.name(),
}; };
match body.iter().position(|(d, _)| d == &desc) {
// FIXME: multiple keys are ordered randomly (use BTreeSet)
Some(pos) => body[pos].1.push(key),
None => body.push((desc, vec![key])),
}
}
body.sort_unstable_by_key(|(_, keys)| {
node.order.iter().position(|&k| k == keys[0]).unwrap()
});
let prefix = format!("{} ", node.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
Info::key(node.name(), body)
}
}
impl Default for KeyTrieNode {
fn default() -> Self {
Self::new("", HashMap::new(), Vec::new())
}
}
impl PartialEq for KeyTrieNode {
fn eq(&self, other: &Self) -> bool {
self.map == other.map
}
}
impl Deref for KeyTrieNode {
type Target = HashMap<KeyEvent, KeyTrie>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl DerefMut for KeyTrieNode {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
Leaf(Command),
Node(KeyTrieNode),
}
impl KeyTrie {
pub fn node(&self) -> Option<&KeyTrieNode> {
match *self {
KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) => None,
}
}
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self {
KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) => None,
}
}
/// Merge another KeyTrie in, assuming that this KeyTrie and the other
/// are both Nodes. Panics otherwise.
pub fn merge_nodes(&mut self, mut other: Self) {
let node = std::mem::take(other.node_mut().unwrap());
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(_) => None,
}?
}
Some(trie)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KeymapResult {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
/// and including current (invalid) key.
Cancelled(Vec<KeyEvent>),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Keymap {
/// Always a Node
#[serde(flatten)]
root: KeyTrie,
#[serde(skip)]
state: Vec<KeyEvent>,
}
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Keymap {
root,
state: Vec::new(),
}
}
pub fn root(&self) -> &KeyTrie {
&self.root
}
/// Lookup `key` in the keymap to try and find a command to execute
pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
let &first = self.state.get(0).unwrap_or(&key);
let trie = match self.root.search(&[first]) {
Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
None => return KeymapResult::NotFound,
Some(t) => t,
};
self.state.push(key);
match trie.search(&self.state[1..]) {
Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
Some(&KeyTrie::Leaf(command)) => {
self.state.clear();
KeymapResult::Matched(command)
}
None => KeymapResult::Cancelled(self.state.drain(..).collect()),
}
}
pub fn merge(&mut self, other: Self) {
self.root.merge_nodes(other.root);
}
}
impl Deref for Keymap {
type Target = KeyTrieNode;
fn deref(&self) -> &Self::Target {
&self.root.node().unwrap()
}
}
impl Default for Keymap {
fn default() -> Self {
Self::new(KeyTrie::Node(KeyTrieNode::default()))
}
} }
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>); pub struct Keymaps(pub HashMap<Mode, Keymap>);
impl Deref for Keymaps { impl Deref for Keymaps {
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>; type Target = HashMap<Mode, Keymap>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -62,252 +308,298 @@ impl DerefMut for Keymaps {
impl Default for Keymaps { impl Default for Keymaps {
fn default() -> Keymaps { fn default() -> Keymaps {
let normal = hashmap!( let normal = keymap!({ "Normal mode"
key!('h') => Command::move_char_left, "h" | "left" => move_char_left,
key!('j') => Command::move_line_down, "j" | "down" => move_line_down,
key!('k') => Command::move_line_up, "k" | "up" => move_line_up,
key!('l') => Command::move_char_right, "l" | "right" => move_char_right,
key!(Left) => Command::move_char_left, "t" => find_till_char,
key!(Down) => Command::move_line_down, "f" => find_next_char,
key!(Up) => Command::move_line_up, "T" => till_prev_char,
key!(Right) => Command::move_char_right, "F" => find_prev_char,
"r" => replace,
key!('t') => Command::find_till_char, "R" => replace_with_yanked,
key!('f') => Command::find_next_char,
key!('T') => Command::till_prev_char, "~" => switch_case,
key!('F') => Command::find_prev_char, "`" => switch_to_lowercase,
// and matching set for select mode (extend) "A-`" => switch_to_uppercase,
//
key!('r') => Command::replace, "home" => goto_line_start,
key!('R') => Command::replace_with_yanked, "end" => goto_line_end,
key!('~') => Command::switch_case, "w" => move_next_word_start,
alt!('`') => Command::switch_to_uppercase, "b" => move_prev_word_start,
key!('`') => Command::switch_to_lowercase, "e" => move_next_word_end,
key!(Home) => Command::goto_line_start, "W" => move_next_long_word_start,
key!(End) => Command::goto_line_end, "B" => move_prev_long_word_start,
"E" => move_next_long_word_end,
key!('w') => Command::move_next_word_start,
key!('b') => Command::move_prev_word_start, "v" => select_mode,
key!('e') => Command::move_next_word_end, "g" => { "Goto"
"g" => goto_file_start,
key!('W') => Command::move_next_long_word_start, "e" => goto_file_end,
key!('B') => Command::move_prev_long_word_start, "h" => goto_line_start,
key!('E') => Command::move_next_long_word_end, "l" => goto_line_end,
"s" => goto_first_nonwhitespace,
key!('v') => Command::select_mode, "d" => goto_definition,
key!('g') => Command::goto_mode, "y" => goto_type_definition,
key!(':') => Command::command_mode, "r" => goto_reference,
"i" => goto_implementation,
key!('i') => Command::insert_mode, "t" => goto_window_top,
key!('I') => Command::prepend_to_line, "m" => goto_window_middle,
key!('a') => Command::append_mode, "b" => goto_window_bottom,
key!('A') => Command::append_to_line, "a" => goto_last_accessed_file,
key!('o') => Command::open_below, },
key!('O') => Command::open_above, ":" => command_mode,
"i" => insert_mode,
"I" => prepend_to_line,
"a" => append_mode,
"A" => append_to_line,
"o" => open_below,
"O" => open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit) // [<space> ]<space> equivalents too (add blank new line, no edit)
"d" => delete_selection,
key!('d') => Command::delete_selection,
// TODO: also delete without yanking // TODO: also delete without yanking
key!('c') => Command::change_selection, "c" => change_selection,
// TODO: also change delete without yanking // TODO: also change delete without yanking
// key!('r') => Command::replace_with_char, "s" => select_regex,
"A-s" => split_selection_on_newline,
key!('s') => Command::select_regex, "S" => split_selection,
alt!('s') => Command::split_selection_on_newline, ";" => collapse_selection,
key!('S') => Command::split_selection, "A-;" => flip_selections,
key!(';') => Command::collapse_selection, "%" => select_all,
alt!(';') => Command::flip_selections, "x" => extend_line,
key!('%') => Command::select_all, "X" => extend_to_line_bounds,
key!('x') => Command::extend_line,
key!('X') => Command::extend_to_line_bounds,
// crop_to_whole_line // crop_to_whole_line
"m" => { "Match"
"m" => match_brackets,
"s" => surround_add,
"r" => surround_replace,
"d" => surround_delete,
"a" => select_textobject_around,
"i" => select_textobject_inner,
},
"[" => { "Left bracket"
"d" => goto_prev_diag,
"D" => goto_first_diag,
},
"]" => { "Right bracket"
"d" => goto_next_diag,
"D" => goto_last_diag,
},
key!('m') => Command::match_mode, "/" => search,
key!('[') => Command::left_bracket_mode,
key!(']') => Command::right_bracket_mode,
key!('/') => Command::search,
// ? for search_reverse // ? for search_reverse
key!('n') => Command::search_next, "n" => search_next,
key!('N') => Command::extend_search_next, "N" => extend_search_next,
// N for search_prev // N for search_prev
key!('*') => Command::search_selection, "*" => search_selection,
key!('u') => Command::undo, "u" => undo,
key!('U') => Command::redo, "U" => redo,
key!('y') => Command::yank, "y" => yank,
// yank_all // yank_all
key!('p') => Command::paste_after, "p" => paste_after,
// paste_all // paste_all
key!('P') => Command::paste_before, "P" => paste_before,
key!('>') => Command::indent, ">" => indent,
key!('<') => Command::unindent, "<" => unindent,
key!('=') => Command::format_selections, "=" => format_selections,
key!('J') => Command::join_selections, "J" => join_selections,
// TODO: conflicts hover/doc // TODO: conflicts hover/doc
key!('K') => Command::keep_selections, "K" => keep_selections,
// TODO: and another method for inverse // TODO: and another method for inverse
// TODO: clashes with space mode // TODO: clashes with space mode
key!(' ') => Command::keep_primary_selection, "space" => keep_primary_selection,
// key!('q') => Command::record_macro, // "q" => record_macro,
// key!('Q') => Command::replay_macro, // "Q" => replay_macro,
// ~ / apostrophe => change case
// & align selections // & align selections
// _ trim selections // _ trim selections
// C / altC = copy (repeat) selections on prev/next lines // C / altC = copy (repeat) selections on prev/next lines
key!(Esc) => Command::normal_mode, "esc" => normal_mode,
key!(PageUp) => Command::page_up, "C-b" | "pageup" => page_up,
key!(PageDown) => Command::page_down, "C-f" | "pagedown" => page_down,
ctrl!('b') => Command::page_up, "C-u" => half_page_up,
ctrl!('f') => Command::page_down, "C-d" => half_page_down,
ctrl!('u') => Command::half_page_up,
ctrl!('d') => Command::half_page_down, "C-w" => { "Window"
"C-w" | "w" => rotate_view,
ctrl!('w') => Command::window_mode, "C-h" | "h" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
},
// move under <space>c // move under <space>c
ctrl!('c') => Command::toggle_comments, "C-c" => toggle_comments,
key!('K') => Command::hover, "K" => hover,
// z family for save/restore/combine from/to sels from register // z family for save/restore/combine from/to sels from register
// supposedly ctrl!('i') but did not work // supposedly "C-i" but did not work
key!(Tab) => Command::jump_forward, "tab" => jump_forward,
ctrl!('o') => Command::jump_backward, "C-o" => jump_backward,
// ctrl!('s') => Command::save_selection, // "C-s" => save_selection,
key!(' ') => Command::space_mode, "space" => { "Space"
key!('z') => Command::view_mode, "f" => file_picker,
"b" => buffer_picker,
"s" => symbol_picker,
"a" => code_action,
"'" => last_picker,
"w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
},
"y" => yank_joined_to_clipboard,
"Y" => yank_main_selection_to_clipboard,
"p" => paste_clipboard_after,
"P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard,
"space" => keep_primary_selection,
},
"z" => { "View"
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
"k" => scroll_up,
"j" => scroll_down,
},
key!('"') => Command::select_register, "\"" => select_register,
); });
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether // TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird // we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode. // because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone(); let mut select = normal.clone();
select.extend( select.merge_nodes(keymap!({ "Select mode"
hashmap!( "h" | "left" => extend_char_left,
key!('h') => Command::extend_char_left, "j" | "down" => extend_line_down,
key!('j') => Command::extend_line_down, "k" | "up" => extend_line_up,
key!('k') => Command::extend_line_up, "l" | "right" => extend_char_right,
key!('l') => Command::extend_char_right,
"w" => extend_next_word_start,
key!(Left) => Command::extend_char_left, "b" => extend_prev_word_start,
key!(Down) => Command::extend_line_down, "e" => extend_next_word_end,
key!(Up) => Command::extend_line_up,
key!(Right) => Command::extend_char_right, "t" => extend_till_char,
"f" => extend_next_char,
key!('w') => Command::extend_next_word_start, "T" => extend_till_prev_char,
key!('b') => Command::extend_prev_word_start, "F" => extend_prev_char,
key!('e') => Command::extend_next_word_end,
"home" => goto_line_start,
key!('t') => Command::extend_till_char, "end" => goto_line_end,
key!('f') => Command::extend_next_char, "esc" => exit_select_mode,
}));
key!('T') => Command::extend_till_prev_char, let insert = keymap!({ "Insert mode"
key!('F') => Command::extend_prev_char, "esc" => normal_mode,
key!(Home) => Command::goto_line_start,
key!(End) => Command::goto_line_end, "backspace" => delete_char_backward,
key!(Esc) => Command::exit_select_mode, "del" => delete_char_forward,
) "ret" => insert_newline,
.into_iter(), "tab" => insert_tab,
); "C-w" => delete_word_backward,
"left" => move_char_left,
"down" => move_line_down,
"up" => move_line_up,
"right" => move_char_right,
"pageup" => page_up,
"pagedown" => page_down,
"home" => goto_line_start,
"end" => goto_line_end_newline,
"C-x" => completion,
});
Keymaps(hashmap!( Keymaps(hashmap!(
// as long as you cast the first item, rust is able to infer the other cases Mode::Normal => Keymap::new(normal),
// TODO: select could be normal mode with some bindings merged over Mode::Select => Keymap::new(select),
Mode::Normal => normal, Mode::Insert => Keymap::new(insert),
Mode::Select => select,
Mode::Insert => hashmap!(
key!(Esc) => Command::normal_mode as Command,
key!(Backspace) => Command::delete_char_backward,
key!(Delete) => Command::delete_char_forward,
key!(Enter) => Command::insert_newline,
key!(Tab) => Command::insert_tab,
key!(Left) => Command::move_char_left,
key!(Down) => Command::move_line_down,
key!(Up) => Command::move_line_up,
key!(Right) => Command::move_char_right,
key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down,
key!(Home) => Command::goto_line_start,
key!(End) => Command::goto_line_end_newline,
ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward,
),
)) ))
} }
} }
/// Merge default config keys with user overwritten keys for custom /// Merge default config keys with user overwritten keys for custom user config.
/// user config.
pub fn merge_keys(mut config: Config) -> Config { pub fn merge_keys(mut config: Config) -> Config {
let mut delta = std::mem::take(&mut config.keys); let mut delta = std::mem::take(&mut config.keys);
for (mode, keys) in &mut *config.keys { for (mode, keys) in &mut *config.keys {
keys.extend(delta.remove(mode).unwrap_or_default()); keys.merge(delta.remove(mode).unwrap_or_default())
} }
config config
} }
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
use helix_view::keyboard::{KeyCode, KeyModifiers};
let config = Config { let config = Config {
keys: Keymaps(hashmap! { keys: Keymaps(hashmap! {
Mode::Normal => hashmap! { Mode::Normal => Keymap::new(
KeyEvent { keymap!({ "Normal mode"
code: KeyCode::Char('i'), "i" => normal_mode,
modifiers: KeyModifiers::NONE, "无" => insert_mode,
} => Command::normal_mode, "z" => jump_backward,
KeyEvent { // key that does not exist "g" => { "Merge into goto mode"
code: KeyCode::Char('无'), "$" => goto_line_end,
modifiers: KeyModifiers::NONE, "g" => delete_char_forward,
} => Command::insert_mode,
}, },
})
)
}), }),
..Default::default() ..Default::default()
}; };
let merged_config = merge_keys(config.clone()); let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config); assert_ne!(config, merged_config);
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!( assert_eq!(
*merged_config keymap.get(key!('i')),
.keys KeymapResult::Matched(Command::normal_mode),
.0 "Leaf should replace leaf"
.get(&Mode::Normal)
.unwrap()
.get(&KeyEvent {
code: KeyCode::Char('i'),
modifiers: KeyModifiers::NONE
})
.unwrap(),
Command::normal_mode
); );
assert_eq!( assert_eq!(
*merged_config keymap.get(key!('无')),
.keys KeymapResult::Matched(Command::insert_mode),
.0 "New leaf should be present in merged keymap"
.get(&Mode::Normal) );
.unwrap() // Assumes that z is a node in the default keymap
.get(&KeyEvent { assert_eq!(
code: KeyCode::Char('无'), keymap.get(key!('z')),
modifiers: KeyModifiers::NONE KeymapResult::Matched(Command::jump_backward),
}) "Leaf should replace node"
.unwrap(),
Command::insert_mode
); );
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(Command::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(Command::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(Command::goto_file_end),
"Old leaves in subnode should be present in merged node"
);
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
} }

@ -2,7 +2,7 @@ use crate::{
commands, commands,
compositor::{Component, Context, EventResult}, compositor::{Component, Context, EventResult},
key, key,
keymap::Keymaps, keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{Completion, ProgressSpinners},
}; };
@ -15,6 +15,7 @@ use helix_core::{
use helix_view::{ use helix_view::{
document::Mode, document::Mode,
graphics::{CursorKind, Modifier, Rect, Style}, graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::{KeyCode, KeyModifiers}, keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View, Document, Editor, Theme, View,
@ -30,6 +31,7 @@ pub struct EditorView {
last_insert: (commands::Command, Vec<KeyEvent>), last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>, completion: Option<Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
pub autoinfo: Option<Info>,
} }
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
@ -48,6 +50,7 @@ impl EditorView {
last_insert: (commands::Command::normal_mode, Vec::new()), last_insert: (commands::Command::normal_mode, Vec::new()),
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
autoinfo: None,
} }
} }
@ -559,19 +562,53 @@ impl EditorView {
); );
} }
fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) { /// Handle events by looking them up in `self.keymaps`. Returns None
if let Some(command) = self.keymaps[&Mode::Insert].get(&event) { /// if event was handled (a command was executed or a subkeymap was
command.execute(cx); /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
} else if let KeyEvent { /// otherwise.
code: KeyCode::Char(ch), fn handle_keymap_event(
.. &mut self,
} = event mode: Mode,
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
self.autoinfo = None;
match self.keymaps.get_mut(&mode).unwrap().get(event) {
KeymapResult::Matched(command) => command.execute(cxt),
KeymapResult::Pending(node) => self.autoinfo = Some(node.into()),
k @ KeymapResult::NotFound | k @ KeymapResult::Cancelled(_) => return Some(k),
}
None
}
fn insert_mode(&mut self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(keyresult) = self.handle_keymap_event(Mode::Insert, cx, event) {
match keyresult {
KeymapResult::NotFound => {
if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch)
}
}
KeymapResult::Cancelled(pending) => {
for ev in pending {
match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch),
None => {
if let KeymapResult::Matched(command) =
self.keymaps.get_mut(&Mode::Insert).unwrap().get(ev)
{ {
commands::insert::insert_char(cx, ch); command.execute(cx);
}
}
}
}
}
_ => unreachable!(),
}
} }
} }
fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) { fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
match event { match event {
// count handling // count handling
key!(i @ '0'..='9') => { key!(i @ '0'..='9') => {
@ -584,8 +621,8 @@ impl EditorView {
// first execute whatever put us into insert mode // first execute whatever put us into insert mode
self.last_insert.0.execute(cxt); self.last_insert.0.execute(cxt);
// then replay the inputs // then replay the inputs
for key in &self.last_insert.1 { for &key in &self.last_insert.1.clone() {
self.insert_mode(cxt, *key) self.insert_mode(cxt, key)
} }
} }
_ => { _ => {
@ -598,9 +635,7 @@ impl EditorView {
// set the register // set the register
cxt.selected_register = cxt.editor.selected_register.take(); cxt.selected_register = cxt.editor.selected_register.take();
if let Some(command) = self.keymaps[&mode].get(&event) { self.handle_keymap_event(mode, cxt, event);
command.execute(cxt);
}
} }
} }
} }
@ -714,7 +749,11 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so // how we entered insert mode is important, and we should track that so
// we can repeat the side effect. // we can repeat the side effect.
self.last_insert.0 = self.keymaps[&mode][&key]; self.last_insert.0 = match self.keymaps.get_mut(&mode).unwrap().get(key) {
KeymapResult::Matched(command) => command,
// FIXME: insert mode can only be entered through single KeyCodes
_ => unimplemented!(),
};
self.last_insert.1.clear(); self.last_insert.1.clear();
} }
(Mode::Insert, Mode::Normal) => { (Mode::Insert, Mode::Normal) => {
@ -752,9 +791,8 @@ impl Component for EditorView {
); );
} }
if let Some(info) = std::mem::take(&mut cx.editor.autoinfo) { if let Some(ref info) = self.autoinfo {
info.render(area, surface, cx); info.render(area, surface, cx);
cx.editor.autoinfo = Some(info);
} }
// render status msg // render status msg

@ -8,7 +8,7 @@ impl Component for Info {
fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.popup"); let style = cx.editor.theme.get("ui.popup");
let block = Block::default() let block = Block::default()
.title(self.title) .title(self.title.as_str())
.borders(Borders::ALL) .borders(Borders::ALL)
.border_style(style); .border_style(style);
let Info { width, height, .. } = self; let Info { width, height, .. } = self;

@ -1,7 +1,6 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info,
theme::{self, Theme}, theme::{self, Theme},
tree::Tree, tree::Tree,
Document, DocumentId, RegisterSelection, View, ViewId, Document, DocumentId, RegisterSelection, View, ViewId,
@ -33,7 +32,6 @@ pub struct Editor {
pub syn_loader: Arc<syntax::Loader>, pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>, pub theme_loader: Arc<theme::Loader>,
pub autoinfo: Option<&'static Info>,
pub status_msg: Option<(String, Severity)>, pub status_msg: Option<(String, Severity)>,
} }
@ -67,7 +65,6 @@ impl Editor {
theme_loader: themes, theme_loader: themes,
registers: Registers::default(), registers: Registers::default(),
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
autoinfo: None,
status_msg: None, status_msg: None,
} }
} }

@ -5,9 +5,9 @@ use std::fmt::Write;
#[derive(Debug)] #[derive(Debug)]
/// Info box used in editor. Rendering logic will be in other crate. /// Info box used in editor. Rendering logic will be in other crate.
pub struct Info { pub struct Info {
/// Title kept as static str for now. /// Title shown at top.
pub title: &'static str, pub title: String,
/// Text body, should contains newline. /// Text body, should contain newlines.
pub text: String, pub text: String,
/// Body width. /// Body width.
pub width: u16, pub width: u16,
@ -16,17 +16,20 @@ pub struct Info {
} }
impl Info { impl Info {
pub fn key(title: &'static str, body: Vec<(&[KeyEvent], &'static str)>) -> Info { // body is a BTreeMap instead of a HashMap because keymaps are represented
// with nested hashmaps with no ordering, and each invocation of infobox would
// show different orders of items
pub fn key(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
let (lpad, mpad, rpad) = (1, 2, 1); let (lpad, mpad, rpad) = (1, 2, 1);
let keymaps_width: u16 = body let keymaps_width: u16 = body
.iter() .iter()
.map(|r| r.0.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2) .map(|r| r.1.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
.max() .max()
.unwrap(); .unwrap();
let mut text = String::new(); let mut text = String::new();
let mut width = 0; let mut width = 0;
let height = body.len() as u16; let height = body.len() as u16;
for (keyevents, desc) in body { for (desc, keyevents) in body {
let keyevent = keyevents[0]; let keyevent = keyevents[0];
let mut left = keymaps_width - keyevent.width() as u16; let mut left = keymaps_width - keyevent.width() as u16;
for _ in 0..lpad { for _ in 0..lpad {
@ -48,7 +51,7 @@ impl Info {
writeln!(text, "{}", desc).ok(); writeln!(text, "{}", desc).ok();
} }
Info { Info {
title, title: title.to_string(),
text, text,
width, width,
height, height,

@ -14,6 +14,16 @@ pub struct KeyEvent {
pub modifiers: KeyModifiers, pub modifiers: KeyModifiers,
} }
impl KeyEvent {
/// Get only the character involved in this event
pub fn char(&self) -> Option<char> {
match self.code {
KeyCode::Char(ch) => Some(ch),
_ => None,
}
}
}
pub(crate) mod keys { pub(crate) mod keys {
pub(crate) const BACKSPACE: &str = "backspace"; pub(crate) const BACKSPACE: &str = "backspace";
pub(crate) const ENTER: &str = "ret"; pub(crate) const ENTER: &str = "ret";
@ -168,7 +178,7 @@ impl std::str::FromStr for KeyEvent {
keys::MINUS => KeyCode::Char('-'), keys::MINUS => KeyCode::Char('-'),
keys::SEMICOLON => KeyCode::Char(';'), keys::SEMICOLON => KeyCode::Char(';'),
keys::PERCENT => KeyCode::Char('%'), keys::PERCENT => KeyCode::Char('%'),
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), single if single.chars().count() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => { function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect(); let function: String = function.chars().skip(1).collect();
let function = str::parse::<u8>(&function)?; let function = str::parse::<u8>(&function)?;

Loading…
Cancel
Save