diff --git a/TODO.md b/TODO.md index 67b4df97..72ceaadc 100644 --- a/TODO.md +++ b/TODO.md @@ -8,7 +8,7 @@ ------ 1 -- [ ] selection mode +- [x] selection mode - [x] % for whole doc selection - [x] vertical splits - [x] input counts (30j) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 855a12eb..28caa6d5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -673,6 +673,14 @@ pub fn goto_mode(cx: &mut Context) { cx.doc().mode = Mode::Goto; } +pub fn select_mode(cx: &mut Context) { + cx.doc().mode = Mode::Select; +} + +pub fn exit_select_mode(cx: &mut Context) { + cx.doc().mode = Mode::Normal; +} + // NOTE: Transactions in this module get appended to history when we switch back to normal mode. pub mod insert { use super::*; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e684a9ff..24c5eee8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -91,7 +91,7 @@ use std::collections::HashMap; pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers}; // TODO: could be trie based -pub type Keymap = HashMap, Command>; +pub type Keymap = HashMap; pub type Keymaps = HashMap; macro_rules! key { @@ -131,125 +131,180 @@ 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!('t') => commands::till_next_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!('0') => commands::move_line_start, + key!('$') => 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, + shift!('I') => commands::prepend_to_line, + key!('a') => commands::append_mode, + shift!('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, + shift!('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::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::search, + key!('n') => commands::search_next, + key!('*') => commands::search_selection, + + key!('u') => commands::undo, + shift!('U') => commands::redo, + + key!('y') => commands::yank, + // yank_all + key!('p') => commands::paste, + // paste_all + + key!('>') => commands::indent, + key!('<') => commands::unindent, + key!('=') => commands::format_selections, + shift!('J') => commands::join_selections, + shift!('K') => commands::keep_selections, + + // 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 { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + } => commands::normal_mode, + Key { + code: KeyCode::PageUp, + modifiers: Modifiers::NONE + } => commands::page_up, + Key { + code: KeyCode::PageDown, + modifiers: Modifiers::NONE + } => commands::page_down, + ctrl!('u') => commands::half_page_up, + ctrl!('d') => commands::half_page_down, + + ctrl!('p') => commands::file_picker, + ctrl!('b') => commands::buffer_picker, + Key { + code: KeyCode::Tab, + modifiers: Modifiers::NONE + } => commands::next_view, + + // move under c + ctrl!('c') => commands::toggle_comments, + ctrl!('K') => commands::hover, + + // z family for save/restore/combine from/to sels from 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!('w') => commands::extend_next_word_start, + key!('b') => commands::extend_prev_word_start, + key!('e') => commands::extend_next_word_end, + + Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + } => commands::exit_select_mode as Command, + ) + .into_iter(), + ); + hashmap!( - Mode::Normal => - // as long as you cast the first item, rust is able to infer the other cases - hashmap!( - vec![key!('h')] => commands::move_char_left as Command, - vec![key!('j')] => commands::move_line_down, - vec![key!('k')] => commands::move_line_up, - vec![key!('l')] => commands::move_char_right, - - vec![key!('0')] => commands::move_line_start, - vec![key!('$')] => commands::move_line_end, - - vec![shift!('H')] => commands::extend_char_left, - vec![shift!('J')] => commands::extend_line_down, - vec![shift!('K')] => commands::extend_line_up, - vec![shift!('L')] => commands::extend_char_right, - - vec![key!('w')] => commands::move_next_word_start, - vec![shift!('W')] => commands::extend_next_word_start, - vec![key!('b')] => commands::move_prev_word_start, - vec![shift!('B')] => commands::extend_prev_word_start, - vec![key!('e')] => commands::move_next_word_end, - vec![key!('E')] => commands::extend_next_word_end, - - vec![key!('g')] => commands::goto_mode, - vec![key!(':')] => commands::command_mode, - - vec![key!('i')] => commands::insert_mode, - vec![shift!('I')] => commands::prepend_to_line, - vec![key!('a')] => commands::append_mode, - vec![shift!('A')] => commands::append_to_line, - vec![key!('o')] => commands::open_below, - - vec![key!('d')] => commands::delete_selection, - vec![key!('c')] => commands::change_selection, - - vec![key!('s')] => commands::select_regex, - vec![alt!('s')] => commands::split_selection_on_newline, - vec![shift!('S')] => commands::split_selection, - vec![key!(';')] => commands::collapse_selection, - vec![alt!(';')] => commands::flip_selections, - vec![key!('%')] => commands::select_all, - vec![key!('x')] => commands::select_line, - - // TODO: figure out what key to use - vec![key!('[')] => commands::expand_selection, - - vec![key!('/')] => commands::search, - vec![key!('n')] => commands::search_next, - vec![key!('*')] => commands::search_selection, - - vec![key!('u')] => commands::undo, - vec![shift!('U')] => commands::redo, - - vec![key!('y')] => commands::yank, - vec![key!('p')] => commands::paste, - - vec![key!('>')] => commands::indent, - vec![key!('<')] => commands::unindent, - vec![key!('=')] => commands::format_selections, - vec![ctrl!('j')] => commands::join_selections, - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode, - vec![Key { - code: KeyCode::PageUp, - modifiers: Modifiers::NONE - }] => commands::page_up, - vec![Key { - code: KeyCode::PageDown, - modifiers: Modifiers::NONE - }] => commands::page_down, - vec![ctrl!('u')] => commands::half_page_up, - vec![ctrl!('d')] => commands::half_page_down, - - vec![ctrl!('p')] => commands::file_picker, - vec![ctrl!('b')] => commands::buffer_picker, - vec![Key { - code: KeyCode::Tab, - modifiers: Modifiers::NONE - }] => commands::next_view, - - // move under c - vec![ctrl!('c')] => commands::toggle_comments, - // was K, figure out a key - vec![ctrl!('k')] => commands::hover, - ), - Mode::Insert => hashmap!( - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode as Command, - vec![Key { - code: KeyCode::Backspace, - modifiers: Modifiers::NONE - }] => commands::insert::delete_char_backward, - vec![Key { - code: KeyCode::Delete, - modifiers: Modifiers::NONE - }] => commands::insert::delete_char_forward, - vec![Key { - code: KeyCode::Enter, - modifiers: Modifiers::NONE - }] => commands::insert::insert_newline, - vec![Key { - code: KeyCode::Tab, - modifiers: Modifiers::NONE - }] => commands::insert::insert_tab, - - vec![ctrl!('x')] => commands::completion, - ), - Mode::Goto => hashmap!( - vec![Key { - code: KeyCode::Esc, - modifiers: Modifiers::NONE - }] => commands::normal_mode as Command, - vec![key!('g')] => commands::move_file_start as Command, - vec![key!('e')] => commands::move_file_end as Command, - ), + // 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 { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + } => commands::normal_mode as Command, + Key { + code: KeyCode::Backspace, + modifiers: Modifiers::NONE + } => commands::insert::delete_char_backward, + Key { + code: KeyCode::Delete, + modifiers: Modifiers::NONE + } => commands::insert::delete_char_forward, + Key { + code: KeyCode::Enter, + modifiers: Modifiers::NONE + } => commands::insert::insert_newline, + Key { + code: KeyCode::Tab, + modifiers: Modifiers::NONE + } => commands::insert::insert_tab, + + ctrl!('x') => commands::completion, + ), + Mode::Goto => hashmap!( + Key { + code: KeyCode::Esc, + modifiers: Modifiers::NONE + } => commands::normal_mode as Command, + key!('g') => commands::move_file_start as Command, + key!('e') => commands::move_file_end as Command, + ), ) } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 499d8021..2aa1d469 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -305,6 +305,7 @@ impl EditorView { ) { let mode = match doc.mode() { Mode::Insert => "INS", + Mode::Select => "SEL", Mode::Normal => "NOR", Mode::Goto => "GOTO", }; @@ -355,7 +356,6 @@ impl Component for EditorView { Event::Key(event) => { let view = cx.editor.view_mut(); - let keys = vec![event]; // TODO: sequences (`gg`) let mode = view.doc.mode(); // TODO: handle count other than 1 @@ -368,7 +368,7 @@ impl Component for EditorView { match mode { Mode::Insert => { - if let Some(command) = self.keymap[&Mode::Insert].get(&keys) { + if let Some(command) = self.keymap[&Mode::Insert].get(&event) { command(&mut cxt); } else if let KeyEvent { code: KeyCode::Char(c), @@ -379,11 +379,11 @@ impl Component for EditorView { } } mode => { - match *keys.as_slice() { - [KeyEvent { + match event { + KeyEvent { code: KeyCode::Char(i @ '0'..='9'), modifiers: KeyModifiers::NONE, - }] => { + } => { let i = i.to_digit(10).unwrap() as usize; cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i)); } @@ -394,7 +394,7 @@ impl Component for EditorView { // if this fails, count was Some(0) // debug_assert!(cxt.count != 0); - if let Some(command) = self.keymap[&mode].get(&keys) { + if let Some(command) = self.keymap[&mode].get(&event) { command(&mut cxt); // TODO: simplistic ensure cursor in view for now diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 3f3f620f..b79e201d 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -10,6 +10,7 @@ use helix_core::{ #[derive(Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, + Select, Insert, Goto, }