pub use crate::commands::Command; use crate::config::Config; use helix_core::hashmap; use helix_view::{document::Mode, input::KeyEvent}; use serde::Deserialize; use std::{ collections::HashMap, ops::{Deref, DerefMut}, }; // Kakoune-inspired: // mode = { // normal = { // q = record_macro // w = (next) word // W = next WORD // e = end of word // E = end of WORD // r = replace // R = replace with yanked // t = 'till char // y = yank // u = undo // U = redo // i = insert // I = INSERT (start of line) // o = open below (insert on new line below) // O = open above (insert on new line above) // p = paste (before cursor) // P = PASTE (after cursor) // ` = // [ = select to text object start (alt = select whole object) // ] = select to text object end // { = extend to inner object start // } = extend to inner object end // a = append // A = APPEND (end of line) // s = split // S = select // d = delete() // f = find_char() // g = goto (gg, G, gc, gd, etc) // // h = move_char_left(n) || arrow-left = move_char_left(n) // j = move_line_down(n) || arrow-down = move_line_down(n) // k = move_line_up(n) || arrow_up = move_line_up(n) // l = move_char_right(n) || arrow-right = move_char_right(n) // : = command line // ; = collapse selection to cursor // " = use register // ` = convert case? (to lower) (alt = swap case) // ~ = convert to upper case // . = repeat last command // \ = disable hook? // / = search // > = indent // < = deindent // % = select whole buffer (in vim = jump to matching bracket) // * = search pattern in selection // ( = rotate main selection backward // ) = rotate main selection forward // - = trim selections? (alt = merge contiguous sel together) // @ = convert tabs to spaces // & = align cursor // ? = extend to next given regex match (alt = to prev) // // in kakoune these are alt-h alt-l / gh gl // select from curs to begin end / move curs to begin end // 0 = start of line // ^ = start of line(first non blank char) || Home = start of line(first non blank char) // $ = end of line || End = end of line // // z = save selections // Z = restore selections // x = select line // X = extend line // c = change selected text // C = copy selection? // v = view menu (viewport manipulation) // b = select to previous word start // B = select to previous WORD start // // // // // // // = = align? // + = // } // // gd = goto definition // gr = goto reference // [d = previous diagnostic // d] = next diagnostic // [D = first diagnostic // D] = last diagnostic // } #[macro_export] macro_rules! key { ($key:ident) => { KeyEvent { code: ::helix_view::keyboard::KeyCode::$key, modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; ($($ch:tt)*) => { KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::NONE, } }; } macro_rules! ctrl { ($($ch:tt)*) => { KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL, } }; } macro_rules! alt { ($($ch:tt)*) => { KeyEvent { code: ::helix_view::keyboard::KeyCode::Char($($ch)*), modifiers: ::helix_view::keyboard::KeyModifiers::ALT, } }; } #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(transparent)] pub struct Keymaps(pub HashMap>); impl Deref for Keymaps { type Target = HashMap>; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for Keymaps { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl Default for Keymaps { fn default() -> Keymaps { let normal = 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::goto_line_start, key!(End) => Command::goto_line_end, key!('w') => Command::move_next_word_start, key!('b') => Command::move_prev_word_start, key!('e') => Command::move_next_word_end, key!('W') => Command::move_next_long_word_start, key!('B') => Command::move_prev_long_word_start, key!('E') => Command::move_next_long_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::extend_line, key!('x') => Command::extend_line, key!('X') => Command::extend_to_line_bounds, // crop_to_whole_line key!('m') => Command::match_mode, 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.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::goto_line_start, key!(End) => Command::goto_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 => 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 /// user config. pub fn merge_keys(mut config: Config) -> Config { let mut delta = std::mem::take(&mut config.keys); for (mode, keys) in &mut *config.keys { keys.extend(delta.remove(mode).unwrap_or_default()); } config } #[test] fn merge_partial_keys() { use helix_view::keyboard::{KeyCode, KeyModifiers}; let config = Config { keys: Keymaps(hashmap! { Mode::Normal => hashmap! { KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::NONE, } => Command::normal_mode, KeyEvent { // key that does not exist code: KeyCode::Char('无'), modifiers: KeyModifiers::NONE, } => Command::insert_mode, }, }), ..Default::default() }; let merged_config = merge_keys(config.clone()); assert_ne!(config, merged_config); assert_eq!( *merged_config .keys .0 .get(&Mode::Normal) .unwrap() .get(&KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::NONE }) .unwrap(), Command::normal_mode ); assert_eq!( *merged_config .keys .0 .get(&Mode::Normal) .unwrap() .get(&KeyEvent { code: KeyCode::Char('无'), modifiers: KeyModifiers::NONE }) .unwrap(), Command::insert_mode ); assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); }