mirror of https://github.com/helix-editor/helix
Merge remote-tracking branch 'origin/master' into line_ending_detection
commit
cdd9347457
@ -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`
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::commands::Command;
|
||||||
|
use crate::keymap::Keymaps;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Deserialize)]
|
||||||
|
pub struct GlobalConfig {
|
||||||
|
pub lsp_progress: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GlobalConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { lsp_progress: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct Config {
|
||||||
|
pub global: GlobalConfig,
|
||||||
|
pub keys: Keymaps,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_keymaps_config_file() {
|
||||||
|
use helix_core::hashmap;
|
||||||
|
use helix_view::document::Mode;
|
||||||
|
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
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::<Config>(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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate helix_view;
|
||||||
|
|
||||||
pub mod application;
|
pub mod application;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod compositor;
|
pub mod compositor;
|
||||||
|
pub mod config;
|
||||||
pub mod keymap;
|
pub mod keymap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
@ -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<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(KeyEvent { code, modifiers })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for KeyEvent {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s = String::deserialize(deserializer)?;
|
||||||
|
s.parse().map_err(de::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<event::KeyEvent> 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::<KeyEvent>("backspace").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Backspace,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("left").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Left,
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>(",").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char(','),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("w").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('w'),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("F12").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::F(12),
|
||||||
|
modifiers: KeyModifiers::NONE
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_modified_keys() {
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("S-minus").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('-'),
|
||||||
|
modifiers: KeyModifiers::SHIFT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::F(12),
|
||||||
|
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
str::parse::<KeyEvent>("S-C-2").unwrap(),
|
||||||
|
KeyEvent {
|
||||||
|
code: KeyCode::Char('2'),
|
||||||
|
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_nonsensical_keys_fails() {
|
||||||
|
assert!(str::parse::<KeyEvent>("F13").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("F0").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("aaa").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("S-S-a").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("FU").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("123").is_err());
|
||||||
|
assert!(str::parse::<KeyEvent>("S--").is_err());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! current {
|
||||||
|
( $( $editor:ident ).+ ) => {{
|
||||||
|
let view = $crate::view_mut!( $( $editor ).+ );
|
||||||
|
let doc = &mut $( $editor ).+ .documents[view.doc];
|
||||||
|
(view, doc)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! doc_mut {
|
||||||
|
( $( $editor:ident ).+ ) => {{
|
||||||
|
$crate::current!( $( $editor ).+ ).1
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! view_mut {
|
||||||
|
( $( $editor:ident ).+ ) => {{
|
||||||
|
$( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! view {
|
||||||
|
( $( $editor:ident ).+ ) => {{
|
||||||
|
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
|
||||||
|
}};
|
||||||
|
}
|
Loading…
Reference in New Issue