mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-25 19:03:30 +04:00
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:
parent
47d2e3aefa
commit
f7e00cf720
@ -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
48
book/src/remapping.md
Normal 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`
|
@ -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
33
helix-term/src/config.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -4,5 +4,6 @@
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod config;
|
||||
pub mod keymap;
|
||||
pub mod ui;
|
||||
|
@ -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(())
|
||||
|
@ -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) => {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user