Refactor key into helix-view
Now also make use of Deserialize for Config.
This commit is contained in:
parent
1c25852021
commit
ca806d4f85
9 changed files with 388 additions and 414 deletions
|
@ -1,7 +1,7 @@
|
|||
use helix_lsp::{lsp, LspProgressMap};
|
||||
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};
|
||||
|
||||
|
@ -49,7 +49,7 @@ impl Application {
|
|||
let size = compositor.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);
|
||||
|
||||
if !args.files.is_empty() {
|
||||
|
|
|
@ -11,6 +11,7 @@ use helix_core::{
|
|||
|
||||
use helix_view::{
|
||||
document::{IndentStyle, Mode},
|
||||
input::{KeyCode, KeyEvent},
|
||||
view::{View, PADDING},
|
||||
Document, DocumentId, Editor, ViewId,
|
||||
};
|
||||
|
@ -38,8 +39,8 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
|
||||
pub struct Context<'a> {
|
||||
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) {
|
||||
let count = cx.count();
|
||||
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 std::{collections::HashMap, str::FromStr};
|
||||
use serde::Deserialize;
|
||||
|
||||
use serde::{de::Error as SerdeError, Deserialize, Serialize};
|
||||
|
||||
use crate::keymap::{parse_keymaps, Keymaps};
|
||||
use crate::commands::Command;
|
||||
use crate::keymap::Keymaps;
|
||||
|
||||
#[derive(Debug, PartialEq, Deserialize)]
|
||||
pub struct GlobalConfig {
|
||||
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 global: GlobalConfig,
|
||||
pub keymaps: Keymaps,
|
||||
pub keys: Keymaps,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct TomlConfig {
|
||||
lsp_progress: Option<bool>,
|
||||
keys: Option<HashMap<String, HashMap<String, String>>>,
|
||||
}
|
||||
#[test]
|
||||
fn parsing_keymaps_config_file() {
|
||||
use helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
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 {
|
||||
global: GlobalConfig {
|
||||
lsp_progress: config.lsp_progress.unwrap_or(true),
|
||||
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,
|
||||
},
|
||||
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 helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
|
@ -99,14 +101,6 @@ use std::{
|
|||
// 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_rules! key {
|
||||
($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 {
|
||||
fn default() -> Self {
|
||||
let normal = Keymap(hashmap!(
|
||||
fn default() -> Keymaps {
|
||||
let normal = hashmap!(
|
||||
key!('h') => Command::move_char_left,
|
||||
key!('j') => Command::move_line_down,
|
||||
key!('k') => Command::move_line_up,
|
||||
|
@ -277,12 +283,12 @@ impl Default for Keymaps {
|
|||
key!('z') => Command::view_mode,
|
||||
|
||||
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(
|
||||
select.extend(
|
||||
hashmap!(
|
||||
key!('h') => Command::extend_char_left,
|
||||
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
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => Keymap(hashmap!(
|
||||
Mode::Insert => hashmap!(
|
||||
key!(Esc) => Command::normal_mode as Command,
|
||||
key!(Backspace) => Command::delete_char_backward,
|
||||
key!(Delete) => Command::delete_char_forward,
|
||||
|
@ -329,309 +335,7 @@ impl Default for Keymaps {
|
|||
key!(PageDown) => Command::page_down,
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use anyhow::{Context, Error, Result};
|
||||
use helix_term::application::Application;
|
||||
use helix_term::args::Args;
|
||||
use helix_term::config::Config;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
let mut base_config = fern::Dispatch::new();
|
||||
|
||||
|
@ -89,12 +88,11 @@ FLAGS:
|
|||
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();
|
||||
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
|
||||
Ok(config) => toml::from_str(&config)?,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
|
||||
Err(err) => return Err(Error::new(err)),
|
||||
};
|
||||
|
||||
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
|
||||
|
||||
|
|
|
@ -11,12 +11,13 @@ use helix_core::{
|
|||
syntax::{self, HighlightEvent},
|
||||
Position, Range,
|
||||
};
|
||||
use helix_view::input::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use helix_view::{document::Mode, Document, Editor, Theme, View};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crossterm::{
|
||||
cursor,
|
||||
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
|
||||
event::{read, Event, EventStream},
|
||||
};
|
||||
use tui::{
|
||||
backend::CrosstermBackend,
|
||||
|
@ -607,7 +608,8 @@ impl Component for EditorView {
|
|||
cx.editor.resize(Rect::new(0, 0, width, height - 1));
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(mut key) => {
|
||||
Event::Key(key) => {
|
||||
let mut key = KeyEvent::from(key);
|
||||
canonicalize_key(&mut key);
|
||||
// clear status
|
||||
cx.editor.status_msg = None;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use anyhow::{anyhow, Context, Error};
|
||||
use serde::de::{self, Deserialize, Deserializer};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::future::Future;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
@ -15,8 +17,6 @@ use helix_core::{
|
|||
|
||||
use crate::{DocumentId, ViewId};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
|
@ -24,6 +24,40 @@ pub enum Mode {
|
|||
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)]
|
||||
pub enum IndentStyle {
|
||||
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
|
||||
/// original value.
|
||||
fn take_with<T, F>(mut_ref: &mut T, closure: F)
|
||||
|
|
226
helix-view/src/input.rs
Normal file
226
helix-view/src/input.rs
Normal file
|
@ -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 editor;
|
||||
pub mod input;
|
||||
pub mod register_selection;
|
||||
pub mod theme;
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
|
||||
use slotmap::new_key_type;
|
||||
new_key_type! { pub struct DocumentId; }
|
||||
new_key_type! { pub struct ViewId; }
|
||||
slotmap::new_key_type! {
|
||||
pub struct DocumentId;
|
||||
pub struct ViewId;
|
||||
}
|
||||
|
||||
pub use document::Document;
|
||||
pub use editor::Editor;
|
||||
|
|
Loading…
Add table
Reference in a new issue