Refactor key into helix-view

Now also make use of Deserialize for Config.
pull/314/head
Ivan Tham 3 years ago committed by Blaž Hrastnik
parent 1c25852021
commit ca806d4f85

@ -1,7 +1,7 @@
use helix_lsp::{lsp, LspProgressMap}; use helix_lsp::{lsp, LspProgressMap};
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui}; use crate::{args::Args, compositor::Compositor, config::Config, ui};
use log::{error, info}; use log::{error, info};
@ -49,7 +49,7 @@ impl Application {
let size = compositor.size(); let size = compositor.size();
let mut editor = Editor::new(size); let mut editor = Editor::new(size);
let mut editor_view = Box::new(ui::EditorView::new(config.keymaps)); let mut editor_view = Box::new(ui::EditorView::new(config.keys));
compositor.push(editor_view); compositor.push(editor_view);
if !args.files.is_empty() { if !args.files.is_empty() {

@ -11,6 +11,7 @@ use helix_core::{
use helix_view::{ use helix_view::{
document::{IndentStyle, Mode}, document::{IndentStyle, Mode},
input::{KeyCode, KeyEvent},
view::{View, PADDING}, view::{View, PADDING},
Document, DocumentId, Editor, ViewId, Document, DocumentId, Editor, ViewId,
}; };
@ -38,8 +39,8 @@ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crossterm::event::{KeyCode, KeyEvent};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer};
pub struct Context<'a> { pub struct Context<'a> {
pub selected_register: helix_view::RegisterSelection, pub selected_register: helix_view::RegisterSelection,
@ -252,6 +253,48 @@ impl Command {
); );
} }
impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.debug_tuple("Command").field(name).finish()
}
}
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.write_str(name)
}
}
impl std::str::FromStr for Command {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Command::COMMAND_LIST
.iter()
.copied()
.find(|cmd| cmd.0 == s)
.ok_or_else(|| anyhow!("No command named '{}'", s))
}
}
impl<'de> Deserialize<'de> for Command {
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 PartialEq for Command {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
fn move_char_left(cx: &mut Context) { fn move_char_left(cx: &mut Context) {
let count = cx.count(); let count = cx.count();
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -3042,29 +3085,3 @@ fn right_bracket_mode(cx: &mut Context) {
} }
}) })
} }
impl fmt::Display for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.write_str(name)
}
}
impl std::str::FromStr for Command {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Command::COMMAND_LIST
.iter()
.copied()
.find(|cmd| cmd.0 == s)
.ok_or_else(|| anyhow!("No command named '{}'", s))
}
}
impl fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Command(name, _) = self;
f.debug_tuple("Command").field(name).finish()
}
}

@ -1,10 +1,9 @@
use anyhow::{Error, Result}; use serde::Deserialize;
use std::{collections::HashMap, str::FromStr};
use serde::{de::Error as SerdeError, Deserialize, Serialize}; use crate::commands::Command;
use crate::keymap::Keymaps;
use crate::keymap::{parse_keymaps, Keymaps};
#[derive(Debug, PartialEq, Deserialize)]
pub struct GlobalConfig { pub struct GlobalConfig {
pub lsp_progress: bool, pub lsp_progress: bool,
} }
@ -15,35 +14,50 @@ impl Default for GlobalConfig {
} }
} }
#[derive(Default)] #[derive(Debug, Default, PartialEq, Deserialize)]
#[serde(default)]
pub struct Config { pub struct Config {
pub global: GlobalConfig, pub global: GlobalConfig,
pub keymaps: Keymaps, pub keys: Keymaps,
} }
#[derive(Serialize, Deserialize)] #[test]
#[serde(rename_all = "kebab-case")] fn parsing_keymaps_config_file() {
struct TomlConfig { use helix_core::hashmap;
lsp_progress: Option<bool>, use helix_view::document::Mode;
keys: Option<HashMap<String, HashMap<String, String>>>, use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
}
let sample_keymaps = r#"
[keys.insert]
y = "move_line_down"
S-C-a = "delete_selection"
impl<'de> Deserialize<'de> for Config { [keys.normal]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> A-F12 = "move_next_word_end"
where "#;
D: serde::Deserializer<'de>,
{ assert_eq!(
let config = TomlConfig::deserialize(deserializer)?; toml::from_str::<Config>(sample_keymaps).unwrap(),
Ok(Self { Config {
global: GlobalConfig { global: Default::default(),
lsp_progress: config.lsp_progress.unwrap_or(true), 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,
}, },
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),
}) })
} }
);
} }

@ -3,6 +3,8 @@ pub use crate::commands::Command;
use anyhow::{anyhow, Error, Result}; use anyhow::{anyhow, Error, Result};
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
use serde::Deserialize;
use std::{ use std::{
collections::HashMap, collections::HashMap,
fmt::Display, fmt::Display,
@ -99,14 +101,6 @@ use std::{
// D] = last diagnostic // D] = last diagnostic
// } // }
// #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
#[derive(Clone, Debug)]
pub struct Keymap(pub HashMap<KeyEvent, Command>);
#[derive(Clone, Debug)]
pub struct Keymaps(pub HashMap<Mode, Keymap>);
#[macro_export] #[macro_export]
macro_rules! key { macro_rules! key {
($key:ident) => { ($key:ident) => {
@ -141,9 +135,21 @@ macro_rules! alt {
}; };
} }
#[derive(Debug, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
impl Deref for Keymaps {
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Default for Keymaps { impl Default for Keymaps {
fn default() -> Self { fn default() -> Keymaps {
let normal = Keymap(hashmap!( let normal = hashmap!(
key!('h') => Command::move_char_left, key!('h') => Command::move_char_left,
key!('j') => Command::move_line_down, key!('j') => Command::move_line_down,
key!('k') => Command::move_line_up, key!('k') => Command::move_line_up,
@ -277,12 +283,12 @@ impl Default for Keymaps {
key!('z') => Command::view_mode, key!('z') => Command::view_mode,
key!('"') => Command::select_register, key!('"') => Command::select_register,
)); );
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether // 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 // 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. // because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone(); let mut select = normal.clone();
select.0.extend( select.extend(
hashmap!( hashmap!(
key!('h') => Command::extend_char_left, key!('h') => Command::extend_char_left,
key!('j') => Command::extend_line_down, key!('j') => Command::extend_line_down,
@ -315,7 +321,7 @@ impl Default for Keymaps {
// TODO: select could be normal mode with some bindings merged over // TODO: select could be normal mode with some bindings merged over
Mode::Normal => normal, Mode::Normal => normal,
Mode::Select => select, Mode::Select => select,
Mode::Insert => Keymap(hashmap!( Mode::Insert => hashmap!(
key!(Esc) => Command::normal_mode as Command, key!(Esc) => Command::normal_mode as Command,
key!(Backspace) => Command::delete_char_backward, key!(Backspace) => Command::delete_char_backward,
key!(Delete) => Command::delete_char_forward, key!(Delete) => Command::delete_char_forward,
@ -329,309 +335,7 @@ impl Default for Keymaps {
key!(PageDown) => Command::page_down, key!(PageDown) => Command::page_down,
ctrl!('x') => Command::completion, ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward, 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());
}
}

@ -1,10 +1,9 @@
use anyhow::{Context, Error, Result};
use helix_term::application::Application; use helix_term::application::Application;
use helix_term::args::Args; use helix_term::args::Args;
use helix_term::config::Config; use helix_term::config::Config;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{Context, Result};
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new(); let mut base_config = fern::Dispatch::new();
@ -89,12 +88,11 @@ FLAGS:
std::fs::create_dir_all(&conf_dir).ok(); std::fs::create_dir_all(&conf_dir).ok();
} }
let config = std::fs::read_to_string(conf_dir.join("config.toml")) let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
.ok() Ok(config) => toml::from_str(&config)?,
.map(|s| toml::from_str(&s)) Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
.transpose()? Err(err) => return Err(Error::new(err)),
.or_else(|| Some(Config::default())) };
.unwrap();
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;

@ -11,12 +11,13 @@ use helix_core::{
syntax::{self, HighlightEvent}, syntax::{self, HighlightEvent},
Position, Range, Position, Range,
}; };
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow; use std::borrow::Cow;
use crossterm::{ use crossterm::{
cursor, cursor,
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers}, event::{read, Event, EventStream},
}; };
use tui::{ use tui::{
backend::CrosstermBackend, backend::CrosstermBackend,
@ -607,7 +608,8 @@ impl Component for EditorView {
cx.editor.resize(Rect::new(0, 0, width, height - 1)); cx.editor.resize(Rect::new(0, 0, width, height - 1));
EventResult::Consumed(None) EventResult::Consumed(None)
} }
Event::Key(mut key) => { Event::Key(key) => {
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key); canonicalize_key(&mut key);
// clear status // clear status
cx.editor.status_msg = None; cx.editor.status_msg = None;

@ -1,5 +1,7 @@
use anyhow::{anyhow, Context, Error}; use anyhow::{anyhow, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
use std::cell::Cell; use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
use std::future::Future; use std::future::Future;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
@ -15,8 +17,6 @@ use helix_core::{
use crate::{DocumentId, ViewId}; use crate::{DocumentId, ViewId};
use std::collections::HashMap;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode { pub enum Mode {
Normal, Normal,
@ -24,6 +24,40 @@ pub enum Mode {
Insert, Insert,
} }
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)),
}
}
}
// toml deserializer doesn't seem to recognize string as enum
impl<'de> Deserialize<'de> for Mode {
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)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum IndentStyle { pub enum IndentStyle {
Tabs, Tabs,
@ -88,29 +122,6 @@ impl fmt::Debug for Document {
} }
} }
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 /// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value. /// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F) fn take_with<T, F>(mut_ref: &mut T, closure: F)

@ -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());
}
}

@ -3,14 +3,16 @@ pub mod macros;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod input;
pub mod register_selection; pub mod register_selection;
pub mod theme; pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
use slotmap::new_key_type; slotmap::new_key_type! {
new_key_type! { pub struct DocumentId; } pub struct DocumentId;
new_key_type! { pub struct ViewId; } pub struct ViewId;
}
pub use document::Document; pub use document::Document;
pub use editor::Editor; pub use editor::Editor;

Loading…
Cancel
Save