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
This commit is contained in:
PabloMansanet 2021-06-17 13:08:05 +02:00 committed by GitHub
parent 47d2e3aefa
commit f7e00cf720
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 894 additions and 306 deletions

View File

@ -4,4 +4,5 @@ # Summary
- [Usage](./usage.md)
- [Configuration](./configuration.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)

48
book/src/remapping.md Normal file
View File

@ -0,0 +1,48 @@
# Key Remapping
One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be
available in the feature).
To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like
this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
C-S-esc = "select_line" # Maps Control-Shift-Escape to select_line
[keys.insert]
A-x = "normal_mode" # Maps Alt-X to enter normal mode
```
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
* Backspace => "backspace"
* Space => "space"
* Return/Enter => "ret"
* < => "lt"
* > => "gt"
* + => "plus"
* - => "minus"
* ; => "semicolon"
* % => "percent"
* Left => "left"
* Right => "right"
* Up => "up"
* Home => "home"
* End => "end"
* Page Up => "pageup"
* Page Down => "pagedown"
* Tab => "tab"
* Back Tab => "backtab"
* Delete => "del"
* Insert => "ins"
* Null => "null"
* Escape => "esc"
Commands can be found in the source code at `../../helix-term/src/commands.rs`

View File

@ -1,7 +1,7 @@
use helix_lsp::lsp;
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{args::Args, compositor::Compositor, ui};
use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui};
use log::{error, info};
@ -40,13 +40,14 @@ pub struct Application {
}
impl Application {
pub fn new(mut args: Args) -> Result<Self, Error> {
pub fn new(mut args: Args, config: Config) -> Result<Self, Error> {
use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();
let mut editor = Editor::new(size);
compositor.push(Box::new(ui::EditorView::new()));
let mut editor_view = Box::new(ui::EditorView::new(config.keymaps));
compositor.push(editor_view);
if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty

File diff suppressed because it is too large Load Diff

33
helix-term/src/config.rs Normal file
View File

@ -0,0 +1,33 @@
use anyhow::{Error, Result};
use std::{collections::HashMap, str::FromStr};
use serde::{de::Error as SerdeError, Deserialize, Serialize};
use crate::keymap::{parse_keymaps, Keymaps};
#[derive(Default)]
pub struct Config {
pub keymaps: Keymaps,
}
#[derive(Serialize, Deserialize)]
struct TomlConfig {
keys: Option<HashMap<String, HashMap<String, String>>>,
}
impl<'de> Deserialize<'de> for Config {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let config = TomlConfig::deserialize(deserializer)?;
Ok(Self {
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),
})
}
}

View File

@ -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 @@
// #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub type Keymap = HashMap<KeyEvent, Command>;
pub type Keymaps = HashMap<Mode, Keymap>;
#[derive(Clone, Debug)]
pub struct Keymap(pub HashMap<KeyEvent, Command>);
#[derive(Clone, Debug)]
pub struct Keymaps(pub HashMap<Mode, Keymap>);
#[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,
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) => commands::move_char_left,
key!(Down) => commands::move_line_down,
key!(Up) => commands::move_line_up,
key!(Right) => commands::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') => 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!('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) => commands::move_line_start,
key!(End) => commands::move_line_end,
key!(Home) => Command::move_line_start,
key!(End) => Command::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!('w') => Command::move_next_word_start,
key!('b') => Command::move_prev_word_start,
key!('e') => Command::move_next_word_end,
key!('v') => commands::select_mode,
key!('g') => commands::goto_mode,
key!(':') => commands::command_mode,
key!('v') => Command::select_mode,
key!('g') => Command::goto_mode,
key!(':') => Command::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,
// [<space> ]<space> equivalents too (add blank new line, no edit)
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,
// [<space> ]<space> 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!('d') => Command::delete_selection,
// TODO: also delete without yanking
key!('c') => Command::change_selection,
// TODO: also change delete without yanking
// key!('r') => commands::replace_with_char,
// key!('r') => Command::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!('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') => commands::match_brackets,
// TODO: refactor into
// key!('m') => commands::select_to_matching,
// key!('M') => commands::back_select_to_matching,
// select mode extend equivalents
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
// 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,
// TODO: figure out what key to use
// key!('[') => Command::expand_selection, ??
key!('[') => Command::left_bracket_mode,
key!(']') => Command::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!('/') => 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') => commands::undo,
key!('U') => commands::redo,
key!('u') => Command::undo,
key!('U') => Command::redo,
key!('y') => commands::yank,
// yank_all
key!('p') => commands::paste_after,
// paste_all
key!('P') => commands::paste_before,
key!('y') => Command::yank,
// yank_all
key!('p') => Command::paste_after,
// paste_all
key!('P') => Command::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
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!(' ') => commands::keep_primary_selection,
// TODO: clashes with space mode
key!(' ') => Command::keep_primary_selection,
// key!('q') => commands::record_macro,
// key!('Q') => commands::replay_macro,
// key!('q') => Command::record_macro,
// key!('Q') => Command::replay_macro,
// ~ / apostrophe => change case
// & align selections
// _ trim selections
// ~ / apostrophe => change case
// & align selections
// _ trim selections
// C / altC = copy (repeat) selections on prev/next lines
// 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,
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') => commands::window_mode,
ctrl!('w') => Command::window_mode,
// move under <space>c
ctrl!('c') => commands::toggle_comments,
key!('K') => commands::hover,
// move under <space>c
ctrl!('c') => Command::toggle_comments,
key!('K') => Command::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
key!(Tab) => commands::jump_forward,
ctrl!('o') => commands::jump_backward,
// ctrl!('s') => commands::save_selection,
// supposedly ctrl!('i') but did not work
key!(Tab) => Command::jump_forward,
ctrl!('o') => Command::jump_backward,
// ctrl!('s') => Command::save_selection,
key!(' ') => commands::space_mode,
key!('z') => commands::view_mode,
key!(' ') => Command::space_mode,
key!('z') => Command::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!('"') => 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) => commands::extend_char_left,
key!(Down) => commands::extend_line_down,
key!(Up) => commands::extend_line_up,
key!(Right) => commands::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') => commands::extend_next_word_start,
key!('b') => commands::extend_prev_word_start,
key!('e') => commands::extend_next_word_end,
key!('w') => Command::extend_next_word_start,
key!('b') => Command::extend_prev_word_start,
key!('e') => Command::extend_next_word_end,
key!('t') => commands::extend_till_char,
key!('f') => commands::extend_next_char,
key!('t') => Command::extend_till_char,
key!('f') => Command::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(),
);
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(),
);
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,
),
)
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<Self, Self::Err> {
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::<u8>(&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<String, HashMap<String, String>>) -> Result<Keymaps> {
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::<RepresentableKeyEvent>(&key)?;
let command = str::parse::<Command>(&command)?;
keymaps.0.get_mut(&mode).unwrap().0.insert(key.0, command);
}
}
Ok(keymaps)
}
impl Deref for Keymap {
type Target = HashMap<KeyEvent, Command>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Deref for Keymaps {
type Target = HashMap<Mode, Keymap>;
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::<RepresentableKeyEvent>("backspace").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("left").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>(",").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char(','),
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("w").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::NONE
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("F12").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::NONE
})
);
}
fn parsing_modified_keys() {
assert_eq!(
str::parse::<RepresentableKeyEvent>("S-minus").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::SHIFT
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("C-A-S-F12").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
})
);
assert_eq!(
str::parse::<RepresentableKeyEvent>("S-C-2").unwrap(),
RepresentableKeyEvent(KeyEvent {
code: KeyCode::F(2),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
})
);
}
#[test]
fn parsing_nonsensical_keys_fails() {
assert!(str::parse::<RepresentableKeyEvent>("F13").is_err());
assert!(str::parse::<RepresentableKeyEvent>("F0").is_err());
assert!(str::parse::<RepresentableKeyEvent>("aaa").is_err());
assert!(str::parse::<RepresentableKeyEvent>("S-S-a").is_err());
assert!(str::parse::<RepresentableKeyEvent>("C-A-S-C-1").is_err());
assert!(str::parse::<RepresentableKeyEvent>("FU").is_err());
assert!(str::parse::<RepresentableKeyEvent>("123").is_err());
assert!(str::parse::<RepresentableKeyEvent>("S--").is_err());
}
}

View File

@ -4,5 +4,6 @@
pub mod args;
pub mod commands;
pub mod compositor;
pub mod config;
pub mod keymap;
pub mod ui;

View File

@ -1,6 +1,6 @@
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use std::path::PathBuf;
use anyhow::{Context, Result};
@ -89,10 +89,17 @@ async fn main() -> Result<()> {
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();
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args).context("unable to create new appliction")?;
let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await.unwrap();
Ok(())

View File

@ -11,10 +11,7 @@
syntax::{self, HighlightEvent},
Position, Range,
};
use helix_view::{
document::{IndentStyle, Mode},
Document, Editor, Theme, View,
};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow;
use crossterm::{
@ -30,7 +27,7 @@
};
pub struct EditorView {
keymap: Keymaps,
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>,
@ -40,16 +37,16 @@ pub struct EditorView {
impl Default for EditorView {
fn default() -> Self {
Self::new()
Self::new(Keymaps::default())
}
}
impl EditorView {
pub fn new() -> Self {
pub fn new(keymaps: Keymaps) -> Self {
Self {
keymap: keymap::default(),
keymaps,
on_next_key: None,
last_insert: (commands::normal_mode, Vec::new()),
last_insert: (commands::Command::normal_mode, Vec::new()),
completion: None,
}
}
@ -546,8 +543,8 @@ pub fn render_statusline(
}
fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
command(cx);
if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
command.execute(cx);
} else if let KeyEvent {
code: KeyCode::Char(ch),
..
@ -568,7 +565,7 @@ fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent)
// special handling for repeat operator
key!('.') => {
// first execute whatever put us into insert mode
(self.last_insert.0)(cxt);
self.last_insert.0.execute(cxt);
// then replay the inputs
for key in &self.last_insert.1 {
self.insert_mode(cxt, *key)
@ -584,8 +581,8 @@ fn command_mode(&self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent)
// set the register
cxt.selected_register = cxt.editor.selected_register.take();
if let Some(command) = self.keymap[&mode].get(&event) {
command(cxt);
if let Some(command) = self.keymaps[&mode].get(&event) {
command.execute(cxt);
}
}
}
@ -699,7 +696,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.0 = self.keymaps[&mode][&key];
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {

View File

@ -1,7 +1,9 @@
use anyhow::{Context, Error};
use anyhow::{anyhow, Context, Error};
use std::cell::Cell;
use std::fmt::Display;
use std::future::Future;
use std::path::{Component, Path, PathBuf};
use std::str::FromStr;
use std::sync::Arc;
use helix_core::{
@ -86,6 +88,29 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
}
}
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<Self, Self::Err> {
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<T, F>(mut_ref: &mut T, closure: F)