From f7e00cf720f55ea82933ac6625b7511e9d38139e Mon Sep 17 00:00:00 2001 From: PabloMansanet Date: Thu, 17 Jun 2021 13:08:05 +0200 Subject: Configurable keys 2 (Mapping keys to commands) (#268) * Add convenience/clarity wrapper for Range initialization * Add keycode parse and display methods * Add remapping functions and tests * Implement key remapping * Add remapping book entry * Use raw string literal for toml * Add command constants * Make command functions private * Map directly to commands * Match key parsing/displaying to Kakoune * Formatting pass * Update documentation * Formatting * Fix example in the book * Refactor into single config file * Formatting * Refactor configuration and add keymap newtype wrappers * Address first batch of PR comments * Replace FromStr with custom deserialize--- helix-term/src/keymap.rs | 688 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 500 insertions(+), 188 deletions(-) (limited to 'helix-term/src/keymap.rs') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 1402d29d..a2fdbdd1 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -1,7 +1,14 @@ -use crate::commands::{self, Command}; +use crate::commands; +pub use crate::commands::Command; +use anyhow::{anyhow, Error, Result}; use helix_core::hashmap; use helix_view::document::Mode; -use std::collections::HashMap; +use std::{ + collections::HashMap, + fmt::Display, + ops::{Deref, DerefMut}, + str::FromStr, +}; // Kakoune-inspired: // mode = { @@ -95,8 +102,10 @@ use std::collections::HashMap; // #[cfg(feature = "term")] pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -pub type Keymap = HashMap; -pub type Keymaps = HashMap; +#[derive(Clone, Debug)] +pub struct Keymap(pub HashMap); +#[derive(Clone, Debug)] +pub struct Keymaps(pub HashMap); #[macro_export] macro_rules! key { @@ -132,188 +141,491 @@ macro_rules! alt { }; } -pub fn default() -> Keymaps { - let normal = hashmap!( - key!('h') => commands::move_char_left as Command, - key!('j') => commands::move_line_down, - key!('k') => commands::move_line_up, - key!('l') => commands::move_char_right, - - key!(Left) => commands::move_char_left, - key!(Down) => commands::move_line_down, - key!(Up) => commands::move_line_up, - key!(Right) => commands::move_char_right, - - key!('t') => commands::find_till_char, - key!('f') => commands::find_next_char, - key!('T') => commands::till_prev_char, - key!('F') => commands::find_prev_char, - // and matching set for select mode (extend) - // - key!('r') => commands::replace, - key!('R') => commands::replace_with_yanked, - - key!(Home) => commands::move_line_start, - key!(End) => commands::move_line_end, - - key!('w') => commands::move_next_word_start, - key!('b') => commands::move_prev_word_start, - key!('e') => commands::move_next_word_end, - - key!('v') => commands::select_mode, - key!('g') => commands::goto_mode, - key!(':') => commands::command_mode, - - key!('i') => commands::insert_mode, - key!('I') => commands::prepend_to_line, - key!('a') => commands::append_mode, - key!('A') => commands::append_to_line, - key!('o') => commands::open_below, - key!('O') => commands::open_above, - // [ ] equivalents too (add blank new line, no edit) - - - key!('d') => commands::delete_selection, - // TODO: also delete without yanking - key!('c') => commands::change_selection, - // TODO: also change delete without yanking - - // key!('r') => commands::replace_with_char, - - key!('s') => commands::select_regex, - alt!('s') => commands::split_selection_on_newline, - key!('S') => commands::split_selection, - key!(';') => commands::collapse_selection, - alt!(';') => commands::flip_selections, - key!('%') => commands::select_all, - key!('x') => commands::select_line, - key!('X') => commands::extend_line, - // or select mode X? - // extend_to_whole_line, crop_to_whole_line - - - key!('m') => commands::match_brackets, - // TODO: refactor into - // key!('m') => commands::select_to_matching, - // key!('M') => commands::back_select_to_matching, - // select mode extend equivalents - - // key!('.') => commands::repeat_insert, - // repeat_select - - // TODO: figure out what key to use - // key!('[') => commands::expand_selection, ?? - key!('[') => commands::left_bracket_mode, - key!(']') => commands::right_bracket_mode, - - key!('/') => commands::search, - // ? for search_reverse - key!('n') => commands::search_next, - key!('N') => commands::extend_search_next, - // N for search_prev - key!('*') => commands::search_selection, - - key!('u') => commands::undo, - key!('U') => commands::redo, - - key!('y') => commands::yank, - // yank_all - key!('p') => commands::paste_after, - // paste_all - key!('P') => commands::paste_before, - - key!('>') => commands::indent, - key!('<') => commands::unindent, - key!('=') => commands::format_selections, - key!('J') => commands::join_selections, - // TODO: conflicts hover/doc - key!('K') => commands::keep_selections, - // TODO: and another method for inverse - - // TODO: clashes with space mode - key!(' ') => commands::keep_primary_selection, - - // key!('q') => commands::record_macro, - // key!('Q') => commands::replay_macro, - - // ~ / apostrophe => change case - // & align selections - // _ trim selections - - // C / altC = copy (repeat) selections on prev/next lines - - key!(Esc) => commands::normal_mode, - key!(PageUp) => commands::page_up, - key!(PageDown) => commands::page_down, - ctrl!('b') => commands::page_up, - ctrl!('f') => commands::page_down, - ctrl!('u') => commands::half_page_up, - ctrl!('d') => commands::half_page_down, - - ctrl!('w') => commands::window_mode, - - // move under c - ctrl!('c') => commands::toggle_comments, - key!('K') => commands::hover, - - // z family for save/restore/combine from/to sels from register - - // supposedly ctrl!('i') but did not work - key!(Tab) => commands::jump_forward, - ctrl!('o') => commands::jump_backward, - // ctrl!('s') => commands::save_selection, - - key!(' ') => commands::space_mode, - key!('z') => commands::view_mode, - - key!('"') => commands::select_register, - ); - // 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 - // because some selection operations can now be done from normal mode, some from select mode. - let mut select = normal.clone(); - select.extend( - hashmap!( - key!('h') => commands::extend_char_left as Command, - key!('j') => commands::extend_line_down, - key!('k') => commands::extend_line_up, - key!('l') => commands::extend_char_right, - - key!(Left) => commands::extend_char_left, - key!(Down) => commands::extend_line_down, - key!(Up) => commands::extend_line_up, - key!(Right) => commands::extend_char_right, - - key!('w') => commands::extend_next_word_start, - key!('b') => commands::extend_prev_word_start, - key!('e') => commands::extend_next_word_end, - - key!('t') => commands::extend_till_char, - key!('f') => commands::extend_next_char, - - key!('T') => commands::extend_till_prev_char, - key!('F') => commands::extend_prev_char, - key!(Home) => commands::extend_line_start, - key!(End) => commands::extend_line_end, - key!(Esc) => commands::exit_select_mode, - ) - .into_iter(), - ); - - hashmap!( - // as long as you cast the first item, rust is able to infer the other cases - // TODO: select could be normal mode with some bindings merged over - Mode::Normal => normal, - Mode::Select => select, - Mode::Insert => hashmap!( - key!(Esc) => commands::normal_mode as Command, - key!(Backspace) => commands::insert::delete_char_backward, - key!(Delete) => commands::insert::delete_char_forward, - key!(Enter) => commands::insert::insert_newline, - key!(Tab) => commands::insert::insert_tab, - - ctrl!('x') => commands::completion, - ctrl!('w') => commands::insert::delete_word_backward, - ), - ) +impl Default for Keymaps { + fn default() -> Self { + let normal = Keymap(hashmap!( + key!('h') => Command::move_char_left, + key!('j') => Command::move_line_down, + key!('k') => Command::move_line_up, + key!('l') => Command::move_char_right, + + 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!('t') => Command::find_till_char, + key!('f') => Command::find_next_char, + key!('T') => Command::till_prev_char, + key!('F') => Command::find_prev_char, + // and matching set for select mode (extend) + // + key!('r') => Command::replace, + key!('R') => Command::replace_with_yanked, + + key!(Home) => Command::move_line_start, + key!(End) => Command::move_line_end, + + key!('w') => Command::move_next_word_start, + key!('b') => Command::move_prev_word_start, + key!('e') => Command::move_next_word_end, + + key!('v') => Command::select_mode, + key!('g') => Command::goto_mode, + key!(':') => Command::command_mode, + + key!('i') => Command::insert_mode, + key!('I') => Command::prepend_to_line, + key!('a') => Command::append_mode, + key!('A') => Command::append_to_line, + key!('o') => Command::open_below, + key!('O') => Command::open_above, + // [ ] equivalents too (add blank new line, no edit) + + + key!('d') => Command::delete_selection, + // TODO: also delete without yanking + key!('c') => Command::change_selection, + // TODO: also change delete without yanking + + // key!('r') => Command::replace_with_char, + + key!('s') => Command::select_regex, + alt!('s') => Command::split_selection_on_newline, + key!('S') => Command::split_selection, + key!(';') => Command::collapse_selection, + alt!(';') => Command::flip_selections, + key!('%') => Command::select_all, + key!('x') => Command::select_line, + key!('X') => Command::extend_line, + // or select mode X? + // extend_to_whole_line, crop_to_whole_line + + + key!('m') => Command::match_brackets, + // TODO: refactor into + // key!('m') => commands::select_to_matching, + // key!('M') => commands::back_select_to_matching, + // select mode extend equivalents + + // key!('.') => commands::repeat_insert, + // repeat_select + + // TODO: figure out what key to use + // key!('[') => Command::expand_selection, ?? + key!('[') => Command::left_bracket_mode, + key!(']') => Command::right_bracket_mode, + + key!('/') => Command::search, + // ? for search_reverse + key!('n') => Command::search_next, + key!('N') => Command::extend_search_next, + // N for search_prev + key!('*') => Command::search_selection, + + key!('u') => Command::undo, + key!('U') => Command::redo, + + key!('y') => Command::yank, + // yank_all + key!('p') => Command::paste_after, + // paste_all + key!('P') => Command::paste_before, + + key!('>') => Command::indent, + key!('<') => Command::unindent, + key!('=') => Command::format_selections, + key!('J') => Command::join_selections, + // TODO: conflicts hover/doc + key!('K') => Command::keep_selections, + // TODO: and another method for inverse + + // TODO: clashes with space mode + key!(' ') => Command::keep_primary_selection, + + // key!('q') => Command::record_macro, + // key!('Q') => Command::replay_macro, + + // ~ / apostrophe => change case + // & align selections + // _ trim selections + + // C / altC = copy (repeat) selections on prev/next lines + + key!(Esc) => Command::normal_mode, + key!(PageUp) => Command::page_up, + key!(PageDown) => Command::page_down, + ctrl!('b') => Command::page_up, + ctrl!('f') => Command::page_down, + ctrl!('u') => Command::half_page_up, + ctrl!('d') => Command::half_page_down, + + ctrl!('w') => Command::window_mode, + + // move under c + ctrl!('c') => Command::toggle_comments, + key!('K') => Command::hover, + + // z family for save/restore/combine from/to sels from register + + // supposedly ctrl!('i') but did not work + key!(Tab) => Command::jump_forward, + ctrl!('o') => Command::jump_backward, + // ctrl!('s') => Command::save_selection, + + key!(' ') => Command::space_mode, + key!('z') => Command::view_mode, + + key!('"') => Command::select_register, + )); + // 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 + // because some selection operations can now be done from normal mode, some from select mode. + let mut select = normal.clone(); + select.0.extend( + hashmap!( + key!('h') => Command::extend_char_left, + key!('j') => Command::extend_line_down, + key!('k') => Command::extend_line_up, + key!('l') => Command::extend_char_right, + + key!(Left) => Command::extend_char_left, + key!(Down) => Command::extend_line_down, + key!(Up) => Command::extend_line_up, + key!(Right) => Command::extend_char_right, + + key!('w') => Command::extend_next_word_start, + key!('b') => Command::extend_prev_word_start, + key!('e') => Command::extend_next_word_end, + + key!('t') => Command::extend_till_char, + key!('f') => Command::extend_next_char, + + key!('T') => Command::extend_till_prev_char, + key!('F') => Command::extend_prev_char, + key!(Home) => Command::extend_line_start, + key!(End) => Command::extend_line_end, + key!(Esc) => Command::exit_select_mode, + ) + .into_iter(), + ); + + Keymaps(hashmap!( + // as long as you cast the first item, rust is able to infer the other cases + // TODO: select could be normal mode with some bindings merged over + Mode::Normal => normal, + Mode::Select => select, + Mode::Insert => Keymap(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, + ctrl!('x') => Command::completion, + ctrl!('w') => Command::delete_word_backward, + )), + )) + } +} + +// Newtype wrapper over keys to allow toml serialization/parsing +#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] +pub struct RepresentableKeyEvent(pub KeyEvent); +impl Display for RepresentableKeyEvent { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self(key) = self; + f.write_fmt(format_args!( + "{}{}{}", + if key.modifiers.contains(KeyModifiers::SHIFT) { + "S-" + } else { + "" + }, + if key.modifiers.contains(KeyModifiers::ALT) { + "A-" + } else { + "" + }, + if key.modifiers.contains(KeyModifiers::CONTROL) { + "C-" + } else { + "" + }, + ))?; + match key.code { + KeyCode::Backspace => f.write_str("backspace")?, + KeyCode::Enter => f.write_str("ret")?, + KeyCode::Left => f.write_str("left")?, + KeyCode::Right => f.write_str("right")?, + KeyCode::Up => f.write_str("up")?, + KeyCode::Down => f.write_str("down")?, + KeyCode::Home => f.write_str("home")?, + KeyCode::End => f.write_str("end")?, + KeyCode::PageUp => f.write_str("pageup")?, + KeyCode::PageDown => f.write_str("pagedown")?, + KeyCode::Tab => f.write_str("tab")?, + KeyCode::BackTab => f.write_str("backtab")?, + KeyCode::Delete => f.write_str("del")?, + KeyCode::Insert => f.write_str("ins")?, + KeyCode::Null => f.write_str("null")?, + KeyCode::Esc => f.write_str("esc")?, + KeyCode::Char('<') => f.write_str("lt")?, + KeyCode::Char('>') => f.write_str("gt")?, + KeyCode::Char('+') => f.write_str("plus")?, + KeyCode::Char('-') => f.write_str("minus")?, + KeyCode::Char(';') => f.write_str("semicolon")?, + KeyCode::Char('%') => f.write_str("percent")?, + KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, + KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, + }; + Ok(()) + } +} + +impl FromStr for RepresentableKeyEvent { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut tokens: Vec<_> = s.split('-').collect(); + let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { + "backspace" => KeyCode::Backspace, + "space" => KeyCode::Char(' '), + "ret" => KeyCode::Enter, + "lt" => KeyCode::Char('<'), + "gt" => KeyCode::Char('>'), + "plus" => KeyCode::Char('+'), + "minus" => KeyCode::Char('-'), + "semicolon" => KeyCode::Char(';'), + "percent" => KeyCode::Char('%'), + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "tab" => KeyCode::Tab, + "backtab" => KeyCode::BackTab, + "del" => KeyCode::Delete, + "ins" => KeyCode::Insert, + "null" => KeyCode::Null, + "esc" => KeyCode::Esc, + single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), + function if function.len() > 1 && function.starts_with('F') => { + let function: String = function.chars().skip(1).collect(); + let function = str::parse::(&function)?; + (function > 0 && function < 13) + .then(|| KeyCode::F(function)) + .ok_or_else(|| anyhow!("Invalid function key '{}'", function))? + } + invalid => return Err(anyhow!("Invalid key code '{}'", invalid)), + }; + + let mut modifiers = KeyModifiers::empty(); + for token in tokens { + let flag = match token { + "S" => KeyModifiers::SHIFT, + "A" => KeyModifiers::ALT, + "C" => KeyModifiers::CONTROL, + _ => return Err(anyhow!("Invalid key modifier '{}-'", token)), + }; + + if modifiers.contains(flag) { + return Err(anyhow!("Repeated key modifier '{}-'", token)); + } + modifiers.insert(flag); + } + + Ok(RepresentableKeyEvent(KeyEvent { code, modifiers })) + } +} + +pub fn parse_keymaps(toml_keymaps: &HashMap>) -> Result { + let mut keymaps = Keymaps::default(); + + for (mode, map) in toml_keymaps { + let mode = Mode::from_str(&mode)?; + for (key, command) in map { + let key = str::parse::(&key)?; + let command = str::parse::(&command)?; + keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command); + } + } + Ok(keymaps) +} + +impl Deref for Keymap { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Deref for Keymaps { + type Target = HashMap; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Keymap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl DerefMut for Keymaps { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +#[cfg(test)] +mod test { + use crate::config::Config; + + use super::*; + + impl PartialEq for Command { + fn eq(&self, other: &Self) -> bool { + self.name() == other.name() + } + } + + #[test] + fn parsing_keymaps_config_file() { + let sample_keymaps = r#" + [keys.insert] + y = "move_line_down" + S-C-a = "delete_selection" + + [keys.normal] + A-F12 = "move_next_word_end" + "#; + + let config: Config = toml::from_str(sample_keymaps).unwrap(); + assert_eq!( + *config + .keymaps + .0 + .get(&Mode::Insert) + .unwrap() + .0 + .get(&KeyEvent { + code: KeyCode::Char('y'), + modifiers: KeyModifiers::NONE + }) + .unwrap(), + Command::move_line_down + ); + assert_eq!( + *config + .keymaps + .0 + .get(&Mode::Insert) + .unwrap() + .0 + .get(&KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL + }) + .unwrap(), + Command::delete_selection + ); + assert_eq!( + *config + .keymaps + .0 + .get(&Mode::Normal) + .unwrap() + .0 + .get(&KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::ALT + }) + .unwrap(), + Command::move_next_word_end + ); + } + + #[test] + fn parsing_unmodified_keys() { + assert_eq!( + str::parse::("backspace").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE + }) + ); + + assert_eq!( + str::parse::("left").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE + }) + ); + + assert_eq!( + str::parse::(",").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::Char(','), + modifiers: KeyModifiers::NONE + }) + ); + + assert_eq!( + str::parse::("w").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::NONE + }) + ); + + assert_eq!( + str::parse::("F12").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::NONE + }) + ); + } + + fn parsing_modified_keys() { + assert_eq!( + str::parse::("S-minus").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::Char('-'), + modifiers: KeyModifiers::SHIFT + }) + ); + + assert_eq!( + str::parse::("C-A-S-F12").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT + }) + ); + + assert_eq!( + str::parse::("S-C-2").unwrap(), + RepresentableKeyEvent(KeyEvent { + code: KeyCode::F(2), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL + }) + ); + } + + #[test] + fn parsing_nonsensical_keys_fails() { + assert!(str::parse::("F13").is_err()); + assert!(str::parse::("F0").is_err()); + assert!(str::parse::("aaa").is_err()); + assert!(str::parse::("S-S-a").is_err()); + assert!(str::parse::("C-A-S-C-1").is_err()); + assert!(str::parse::("FU").is_err()); + assert!(str::parse::("123").is_err()); + assert!(str::parse::("S--").is_err()); + } } -- cgit v1.2.3-70-g09d2 From 1c2585202145467f0fde7ad9c571e462081c3656 Mon Sep 17 00:00:00 2001 From: wojciechkepka Date: Fri, 18 Jun 2021 19:09:37 +0200 Subject: Make arrow keys and page up/down work in insert mode --- helix-term/src/keymap.rs | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'helix-term/src/keymap.rs') diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index a2fdbdd1..f6bcbcc9 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -321,6 +321,12 @@ impl Default for Keymaps { 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, ctrl!('x') => Command::completion, ctrl!('w') => Command::delete_word_backward, )), -- cgit v1.2.3-70-g09d2 From ca806d4f852e934651132fc9570a6110e30f646d Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Fri, 18 Jun 2021 00:52:41 +0800 Subject: Refactor key into helix-view Now also make use of Deserialize for Config. --- helix-term/src/application.rs | 4 +- helix-term/src/commands.rs | 71 +++++---- helix-term/src/config.rs | 76 ++++++---- helix-term/src/keymap.rs | 336 +++--------------------------------------- helix-term/src/main.rs | 14 +- helix-term/src/ui/editor.rs | 6 +- helix-view/src/document.rs | 61 ++++---- helix-view/src/input.rs | 226 ++++++++++++++++++++++++++++ helix-view/src/lib.rs | 8 +- 9 files changed, 388 insertions(+), 414 deletions(-) create mode 100644 helix-view/src/input.rs (limited to 'helix-term/src/keymap.rs') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2fae467f..ce43808a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,7 +1,7 @@ use helix_lsp::{lsp, LspProgressMap}; use helix_view::{document::Mode, Document, Editor, Theme, View}; -use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui}; +use crate::{args::Args, compositor::Compositor, config::Config, ui}; use log::{error, info}; @@ -49,7 +49,7 @@ impl Application { let size = compositor.size(); let mut editor = Editor::new(size); - let mut editor_view = Box::new(ui::EditorView::new(config.keymaps)); + let mut editor_view = Box::new(ui::EditorView::new(config.keys)); compositor.push(editor_view); if !args.files.is_empty() { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index a8610223..1243a86f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -11,6 +11,7 @@ use helix_core::{ use helix_view::{ document::{IndentStyle, Mode}, + input::{KeyCode, KeyEvent}, view::{View, PADDING}, Document, DocumentId, Editor, ViewId, }; @@ -38,8 +39,8 @@ use std::{ path::{Path, PathBuf}, }; -use crossterm::event::{KeyCode, KeyEvent}; use once_cell::sync::Lazy; +use serde::de::{self, Deserialize, Deserializer}; pub struct Context<'a> { pub selected_register: helix_view::RegisterSelection, @@ -252,6 +253,48 @@ impl Command { ); } +impl fmt::Debug for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Command(name, _) = self; + f.debug_tuple("Command").field(name).finish() + } +} + +impl fmt::Display for Command { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Command(name, _) = self; + f.write_str(name) + } +} + +impl std::str::FromStr for Command { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Command::COMMAND_LIST + .iter() + .copied() + .find(|cmd| cmd.0 == s) + .ok_or_else(|| anyhow!("No command named '{}'", s)) + } +} + +impl<'de> Deserialize<'de> for Command { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + +impl PartialEq for Command { + fn eq(&self, other: &Self) -> bool { + self.name() == other.name() + } +} + fn move_char_left(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); @@ -3042,29 +3085,3 @@ fn right_bracket_mode(cx: &mut Context) { } }) } - -impl fmt::Display for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command(name, _) = self; - f.write_str(name) - } -} - -impl std::str::FromStr for Command { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Command::COMMAND_LIST - .iter() - .copied() - .find(|cmd| cmd.0 == s) - .ok_or_else(|| anyhow!("No command named '{}'", s)) - } -} - -impl fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Command(name, _) = self; - f.debug_tuple("Command").field(name).finish() - } -} diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs index e5e17753..9c962299 100644 --- a/helix-term/src/config.rs +++ b/helix-term/src/config.rs @@ -1,10 +1,9 @@ -use anyhow::{Error, Result}; -use std::{collections::HashMap, str::FromStr}; +use serde::Deserialize; -use serde::{de::Error as SerdeError, Deserialize, Serialize}; - -use crate::keymap::{parse_keymaps, Keymaps}; +use crate::commands::Command; +use crate::keymap::Keymaps; +#[derive(Debug, PartialEq, Deserialize)] pub struct GlobalConfig { pub lsp_progress: bool, } @@ -15,35 +14,50 @@ impl Default for GlobalConfig { } } -#[derive(Default)] +#[derive(Debug, Default, PartialEq, Deserialize)] +#[serde(default)] pub struct Config { pub global: GlobalConfig, - pub keymaps: Keymaps, + pub keys: Keymaps, } -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -struct TomlConfig { - lsp_progress: Option, - keys: Option>>, -} +#[test] +fn parsing_keymaps_config_file() { + use helix_core::hashmap; + use helix_view::document::Mode; + use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; -impl<'de> Deserialize<'de> for Config { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let config = TomlConfig::deserialize(deserializer)?; - Ok(Self { - global: GlobalConfig { - lsp_progress: config.lsp_progress.unwrap_or(true), - }, - keymaps: config - .keys - .map(|r| parse_keymaps(&r)) - .transpose() - .map_err(|e| D::Error::custom(format!("Error deserializing keymap: {}", e)))? - .unwrap_or_else(Keymaps::default), - }) - } + let sample_keymaps = r#" + [keys.insert] + y = "move_line_down" + S-C-a = "delete_selection" + + [keys.normal] + A-F12 = "move_next_word_end" + "#; + + assert_eq!( + toml::from_str::(sample_keymaps).unwrap(), + Config { + global: Default::default(), + keys: Keymaps(hashmap! { + Mode::Insert => hashmap! { + KeyEvent { + code: KeyCode::Char('y'), + modifiers: KeyModifiers::NONE, + } => Command::move_line_down, + KeyEvent { + 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, + }, + }) + } + ); } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index f6bcbcc9..24924832 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -3,6 +3,8 @@ pub use crate::commands::Command; use anyhow::{anyhow, Error, Result}; use helix_core::hashmap; use helix_view::document::Mode; +use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; +use serde::Deserialize; use std::{ collections::HashMap, fmt::Display, @@ -99,14 +101,6 @@ use std::{ // D] = last diagnostic // } -// #[cfg(feature = "term")] -pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; - -#[derive(Clone, Debug)] -pub struct Keymap(pub HashMap); -#[derive(Clone, Debug)] -pub struct Keymaps(pub HashMap); - #[macro_export] macro_rules! key { ($key:ident) => { @@ -141,9 +135,21 @@ macro_rules! alt { }; } +#[derive(Debug, PartialEq, Deserialize)] +#[serde(transparent)] +pub struct Keymaps(pub HashMap>); + +impl Deref for Keymaps { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl Default for Keymaps { - fn default() -> Self { - let normal = Keymap(hashmap!( + fn default() -> Keymaps { + let normal = hashmap!( key!('h') => Command::move_char_left, key!('j') => Command::move_line_down, key!('k') => Command::move_line_up, @@ -277,12 +283,12 @@ impl Default for Keymaps { key!('z') => Command::view_mode, key!('"') => Command::select_register, - )); + ); // 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 // because some selection operations can now be done from normal mode, some from select mode. let mut select = normal.clone(); - select.0.extend( + select.extend( hashmap!( key!('h') => Command::extend_char_left, key!('j') => Command::extend_line_down, @@ -315,7 +321,7 @@ impl Default for Keymaps { // TODO: select could be normal mode with some bindings merged over Mode::Normal => normal, Mode::Select => select, - Mode::Insert => Keymap(hashmap!( + Mode::Insert => hashmap!( key!(Esc) => Command::normal_mode as Command, key!(Backspace) => Command::delete_char_backward, key!(Delete) => Command::delete_char_forward, @@ -329,309 +335,7 @@ impl Default for Keymaps { key!(PageDown) => Command::page_down, ctrl!('x') => Command::completion, ctrl!('w') => Command::delete_word_backward, - )), + ), )) } } - -// Newtype wrapper over keys to allow toml serialization/parsing -#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)] -pub struct RepresentableKeyEvent(pub KeyEvent); -impl Display for RepresentableKeyEvent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self(key) = self; - f.write_fmt(format_args!( - "{}{}{}", - if key.modifiers.contains(KeyModifiers::SHIFT) { - "S-" - } else { - "" - }, - if key.modifiers.contains(KeyModifiers::ALT) { - "A-" - } else { - "" - }, - if key.modifiers.contains(KeyModifiers::CONTROL) { - "C-" - } else { - "" - }, - ))?; - match key.code { - KeyCode::Backspace => f.write_str("backspace")?, - KeyCode::Enter => f.write_str("ret")?, - KeyCode::Left => f.write_str("left")?, - KeyCode::Right => f.write_str("right")?, - KeyCode::Up => f.write_str("up")?, - KeyCode::Down => f.write_str("down")?, - KeyCode::Home => f.write_str("home")?, - KeyCode::End => f.write_str("end")?, - KeyCode::PageUp => f.write_str("pageup")?, - KeyCode::PageDown => f.write_str("pagedown")?, - KeyCode::Tab => f.write_str("tab")?, - KeyCode::BackTab => f.write_str("backtab")?, - KeyCode::Delete => f.write_str("del")?, - KeyCode::Insert => f.write_str("ins")?, - KeyCode::Null => f.write_str("null")?, - KeyCode::Esc => f.write_str("esc")?, - KeyCode::Char('<') => f.write_str("lt")?, - KeyCode::Char('>') => f.write_str("gt")?, - KeyCode::Char('+') => f.write_str("plus")?, - KeyCode::Char('-') => f.write_str("minus")?, - KeyCode::Char(';') => f.write_str("semicolon")?, - KeyCode::Char('%') => f.write_str("percent")?, - KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, - KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, - }; - Ok(()) - } -} - -impl FromStr for RepresentableKeyEvent { - type Err = Error; - - fn from_str(s: &str) -> Result { - let mut tokens: Vec<_> = s.split('-').collect(); - let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { - "backspace" => KeyCode::Backspace, - "space" => KeyCode::Char(' '), - "ret" => KeyCode::Enter, - "lt" => KeyCode::Char('<'), - "gt" => KeyCode::Char('>'), - "plus" => KeyCode::Char('+'), - "minus" => KeyCode::Char('-'), - "semicolon" => KeyCode::Char(';'), - "percent" => KeyCode::Char('%'), - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "up" => KeyCode::Down, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "tab" => KeyCode::Tab, - "backtab" => KeyCode::BackTab, - "del" => KeyCode::Delete, - "ins" => KeyCode::Insert, - "null" => KeyCode::Null, - "esc" => KeyCode::Esc, - single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), - function if function.len() > 1 && function.starts_with('F') => { - let function: String = function.chars().skip(1).collect(); - let function = str::parse::(&function)?; - (function > 0 && function < 13) - .then(|| KeyCode::F(function)) - .ok_or_else(|| anyhow!("Invalid function key '{}'", function))? - } - invalid => return Err(anyhow!("Invalid key code '{}'", invalid)), - }; - - let mut modifiers = KeyModifiers::empty(); - for token in tokens { - let flag = match token { - "S" => KeyModifiers::SHIFT, - "A" => KeyModifiers::ALT, - "C" => KeyModifiers::CONTROL, - _ => return Err(anyhow!("Invalid key modifier '{}-'", token)), - }; - - if modifiers.contains(flag) { - return Err(anyhow!("Repeated key modifier '{}-'", token)); - } - modifiers.insert(flag); - } - - Ok(RepresentableKeyEvent(KeyEvent { code, modifiers })) - } -} - -pub fn parse_keymaps(toml_keymaps: &HashMap>) -> Result { - let mut keymaps = Keymaps::default(); - - for (mode, map) in toml_keymaps { - let mode = Mode::from_str(&mode)?; - for (key, command) in map { - let key = str::parse::(&key)?; - let command = str::parse::(&command)?; - keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command); - } - } - Ok(keymaps) -} - -impl Deref for Keymap { - type Target = HashMap; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Deref for Keymaps { - type Target = HashMap; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Keymap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl DerefMut for Keymaps { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -#[cfg(test)] -mod test { - use crate::config::Config; - - use super::*; - - impl PartialEq for Command { - fn eq(&self, other: &Self) -> bool { - self.name() == other.name() - } - } - - #[test] - fn parsing_keymaps_config_file() { - let sample_keymaps = r#" - [keys.insert] - y = "move_line_down" - S-C-a = "delete_selection" - - [keys.normal] - A-F12 = "move_next_word_end" - "#; - - let config: Config = toml::from_str(sample_keymaps).unwrap(); - assert_eq!( - *config - .keymaps - .0 - .get(&Mode::Insert) - .unwrap() - .0 - .get(&KeyEvent { - code: KeyCode::Char('y'), - modifiers: KeyModifiers::NONE - }) - .unwrap(), - Command::move_line_down - ); - assert_eq!( - *config - .keymaps - .0 - .get(&Mode::Insert) - .unwrap() - .0 - .get(&KeyEvent { - code: KeyCode::Char('a'), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL - }) - .unwrap(), - Command::delete_selection - ); - assert_eq!( - *config - .keymaps - .0 - .get(&Mode::Normal) - .unwrap() - .0 - .get(&KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::ALT - }) - .unwrap(), - Command::move_next_word_end - ); - } - - #[test] - fn parsing_unmodified_keys() { - assert_eq!( - str::parse::("backspace").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE - }) - ); - - assert_eq!( - str::parse::("left").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE - }) - ); - - assert_eq!( - str::parse::(",").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::Char(','), - modifiers: KeyModifiers::NONE - }) - ); - - assert_eq!( - str::parse::("w").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::Char('w'), - modifiers: KeyModifiers::NONE - }) - ); - - assert_eq!( - str::parse::("F12").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::NONE - }) - ); - } - - fn parsing_modified_keys() { - assert_eq!( - str::parse::("S-minus").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::Char('-'), - modifiers: KeyModifiers::SHIFT - }) - ); - - assert_eq!( - str::parse::("C-A-S-F12").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::F(12), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT - }) - ); - - assert_eq!( - str::parse::("S-C-2").unwrap(), - RepresentableKeyEvent(KeyEvent { - code: KeyCode::F(2), - modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL - }) - ); - } - - #[test] - fn parsing_nonsensical_keys_fails() { - assert!(str::parse::("F13").is_err()); - assert!(str::parse::("F0").is_err()); - assert!(str::parse::("aaa").is_err()); - assert!(str::parse::("S-S-a").is_err()); - assert!(str::parse::("C-A-S-C-1").is_err()); - assert!(str::parse::("FU").is_err()); - assert!(str::parse::("123").is_err()); - assert!(str::parse::("S--").is_err()); - } -} diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index ef912480..12176910 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,10 +1,9 @@ +use anyhow::{Context, Error, Result}; use helix_term::application::Application; use helix_term::args::Args; use helix_term::config::Config; use std::path::PathBuf; -use anyhow::{Context, Result}; - fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { let mut base_config = fern::Dispatch::new(); @@ -89,12 +88,11 @@ FLAGS: std::fs::create_dir_all(&conf_dir).ok(); } - let config = std::fs::read_to_string(conf_dir.join("config.toml")) - .ok() - .map(|s| toml::from_str(&s)) - .transpose()? - .or_else(|| Some(Config::default())) - .unwrap(); + let config = match std::fs::read_to_string(conf_dir.join("config.toml")) { + Ok(config) => toml::from_str(&config)?, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), + Err(err) => return Err(Error::new(err)), + }; setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 3dc43d3f..d0eedad6 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -11,12 +11,13 @@ use helix_core::{ syntax::{self, HighlightEvent}, Position, Range, }; +use helix_view::input::{KeyCode, KeyEvent, KeyModifiers}; use helix_view::{document::Mode, Document, Editor, Theme, View}; use std::borrow::Cow; use crossterm::{ cursor, - event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, + event::{read, Event, EventStream}, }; use tui::{ backend::CrosstermBackend, @@ -607,7 +608,8 @@ impl Component for EditorView { cx.editor.resize(Rect::new(0, 0, width, height - 1)); EventResult::Consumed(None) } - Event::Key(mut key) => { + Event::Key(key) => { + let mut key = KeyEvent::from(key); canonicalize_key(&mut key); // clear status cx.editor.status_msg = None; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e9a8097c..8875f70d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,5 +1,7 @@ use anyhow::{anyhow, Context, Error}; +use serde::de::{self, Deserialize, Deserializer}; use std::cell::Cell; +use std::collections::HashMap; use std::fmt::Display; use std::future::Future; use std::path::{Component, Path, PathBuf}; @@ -15,8 +17,6 @@ use helix_core::{ use crate::{DocumentId, ViewId}; -use std::collections::HashMap; - #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, @@ -24,6 +24,40 @@ pub enum Mode { Insert, } +impl Display for Mode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Mode::Normal => f.write_str("normal"), + Mode::Select => f.write_str("select"), + Mode::Insert => f.write_str("insert"), + } + } +} + +impl FromStr for Mode { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "normal" => Ok(Mode::Normal), + "select" => Ok(Mode::Select), + "insert" => Ok(Mode::Insert), + _ => Err(anyhow!("Invalid mode '{}'", s)), + } + } +} + +// toml deserializer doesn't seem to recognize string as enum +impl<'de> Deserialize<'de> for Mode { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum IndentStyle { Tabs, @@ -88,29 +122,6 @@ impl fmt::Debug for Document { } } -impl Display for Mode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Mode::Normal => f.write_str("normal"), - Mode::Select => f.write_str("select"), - Mode::Insert => f.write_str("insert"), - } - } -} - -impl FromStr for Mode { - type Err = Error; - - fn from_str(s: &str) -> Result { - match s { - "normal" => Ok(Mode::Normal), - "select" => Ok(Mode::Select), - "insert" => Ok(Mode::Insert), - _ => Err(anyhow!("Invalid mode '{}'", s)), - } - } -} - /// Like std::mem::replace() except it allows the replacement value to be mapped from the /// original value. fn take_with(mut_ref: &mut T, closure: F) diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs new file mode 100644 index 00000000..ab417819 --- /dev/null +++ b/helix-view/src/input.rs @@ -0,0 +1,226 @@ +//! Input event handling, currently backed by crossterm. +use anyhow::{anyhow, Error}; +use crossterm::event; +use serde::de::{self, Deserialize, Deserializer}; +use std::fmt; + +pub use crossterm::event::{KeyCode, KeyModifiers}; + +/// Represents a key event. +// We use a newtype here because we want to customize Deserialize and Display. +#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)] +pub struct KeyEvent { + pub code: KeyCode, + pub modifiers: KeyModifiers, +} + +impl fmt::Display for KeyEvent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "{}{}{}", + if self.modifiers.contains(KeyModifiers::SHIFT) { + "S-" + } else { + "" + }, + if self.modifiers.contains(KeyModifiers::ALT) { + "A-" + } else { + "" + }, + if self.modifiers.contains(KeyModifiers::CONTROL) { + "C-" + } else { + "" + }, + ))?; + match self.code { + KeyCode::Backspace => f.write_str("backspace")?, + KeyCode::Enter => f.write_str("ret")?, + KeyCode::Left => f.write_str("left")?, + KeyCode::Right => f.write_str("right")?, + KeyCode::Up => f.write_str("up")?, + KeyCode::Down => f.write_str("down")?, + KeyCode::Home => f.write_str("home")?, + KeyCode::End => f.write_str("end")?, + KeyCode::PageUp => f.write_str("pageup")?, + KeyCode::PageDown => f.write_str("pagedown")?, + KeyCode::Tab => f.write_str("tab")?, + KeyCode::BackTab => f.write_str("backtab")?, + KeyCode::Delete => f.write_str("del")?, + KeyCode::Insert => f.write_str("ins")?, + KeyCode::Null => f.write_str("null")?, + KeyCode::Esc => f.write_str("esc")?, + KeyCode::Char('<') => f.write_str("lt")?, + KeyCode::Char('>') => f.write_str("gt")?, + KeyCode::Char('+') => f.write_str("plus")?, + KeyCode::Char('-') => f.write_str("minus")?, + KeyCode::Char(';') => f.write_str("semicolon")?, + KeyCode::Char('%') => f.write_str("percent")?, + KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?, + KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?, + }; + Ok(()) + } +} + +impl std::str::FromStr for KeyEvent { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut tokens: Vec<_> = s.split('-').collect(); + let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? { + "backspace" => KeyCode::Backspace, + "space" => KeyCode::Char(' '), + "ret" => KeyCode::Enter, + "lt" => KeyCode::Char('<'), + "gt" => KeyCode::Char('>'), + "plus" => KeyCode::Char('+'), + "minus" => KeyCode::Char('-'), + "semicolon" => KeyCode::Char(';'), + "percent" => KeyCode::Char('%'), + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "tab" => KeyCode::Tab, + "backtab" => KeyCode::BackTab, + "del" => KeyCode::Delete, + "ins" => KeyCode::Insert, + "null" => KeyCode::Null, + "esc" => KeyCode::Esc, + single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()), + function if function.len() > 1 && function.starts_with('F') => { + let function: String = function.chars().skip(1).collect(); + let function = str::parse::(&function)?; + (function > 0 && function < 13) + .then(|| KeyCode::F(function)) + .ok_or_else(|| anyhow!("Invalid function key '{}'", function))? + } + invalid => return Err(anyhow!("Invalid key code '{}'", invalid)), + }; + + let mut modifiers = KeyModifiers::empty(); + for token in tokens { + let flag = match token { + "S" => KeyModifiers::SHIFT, + "A" => KeyModifiers::ALT, + "C" => KeyModifiers::CONTROL, + _ => return Err(anyhow!("Invalid key modifier '{}-'", token)), + }; + + if modifiers.contains(flag) { + return Err(anyhow!("Repeated key modifier '{}-'", token)); + } + modifiers.insert(flag); + } + + Ok(KeyEvent { code, modifiers }) + } +} + +impl<'de> Deserialize<'de> for KeyEvent { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + s.parse().map_err(de::Error::custom) + } +} + +impl From for KeyEvent { + fn from(event::KeyEvent { code, modifiers }: event::KeyEvent) -> KeyEvent { + KeyEvent { code, modifiers } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parsing_unmodified_keys() { + assert_eq!( + str::parse::("backspace").unwrap(), + KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::("left").unwrap(), + KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::(",").unwrap(), + KeyEvent { + code: KeyCode::Char(','), + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::("w").unwrap(), + KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::NONE + } + ); + + assert_eq!( + str::parse::("F12").unwrap(), + KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::NONE + } + ); + } + + #[test] + fn parsing_modified_keys() { + assert_eq!( + str::parse::("S-minus").unwrap(), + KeyEvent { + code: KeyCode::Char('-'), + modifiers: KeyModifiers::SHIFT + } + ); + + assert_eq!( + str::parse::("C-A-S-F12").unwrap(), + KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT + } + ); + + assert_eq!( + str::parse::("S-C-2").unwrap(), + KeyEvent { + code: KeyCode::Char('2'), + modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL + } + ); + } + + #[test] + fn parsing_nonsensical_keys_fails() { + assert!(str::parse::("F13").is_err()); + assert!(str::parse::("F0").is_err()); + assert!(str::parse::("aaa").is_err()); + assert!(str::parse::("S-S-a").is_err()); + assert!(str::parse::("C-A-S-C-1").is_err()); + assert!(str::parse::("FU").is_err()); + assert!(str::parse::("123").is_err()); + assert!(str::parse::("S--").is_err()); + } +} diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 20613451..8b635700 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -3,14 +3,16 @@ pub mod macros; pub mod document; pub mod editor; +pub mod input; pub mod register_selection; pub mod theme; pub mod tree; pub mod view; -use slotmap::new_key_type; -new_key_type! { pub struct DocumentId; } -new_key_type! { pub struct ViewId; } +slotmap::new_key_type! { + pub struct DocumentId; + pub struct ViewId; +} pub use document::Document; pub use editor::Editor; -- cgit v1.2.3-70-g09d2