diff --git a/helix-term/examples/translate.rs b/helix-term/examples/translate.rs new file mode 100644 index 0000000000000..2e057329265ef --- /dev/null +++ b/helix-term/examples/translate.rs @@ -0,0 +1,68 @@ +use helix_term::keymap::default; +use helix_view::document::Mode; +use helix_view::input::{KeyCode, KeyEvent}; +use std::collections::HashMap; + +const LANGMAP: [(&'static str, &'static str); 6] = [ + (r#"йцукенгшщзхъ"#, r#"qwertyuiop[]"#), + (r#"ЙЦУКЕНГШЩЗХЪ"#, r#"QWERTYUIOP{}"#), + (r#"фывапролджэё"#, r#"asdfghjkl;'\"#), + (r#"ФЫВАПРОЛДЖЭЁ"#, r#"ASDFGHJKL:"|"#), + (r#"]ячсмитьбю/"#, r#"`zxcvbnm,./"#), + (r#"[ЯЧСМИТЬБЮ?"#, r#"~ZXCVBNM<>?"#), +]; + +fn translate(ev: &KeyEvent, f: F) -> Option +where + F: Fn(char) -> Option, +{ + if let KeyCode::Char(c) = ev.code { + Some(KeyEvent { + code: KeyCode::Char(f(c)?), + modifiers: ev.modifiers.clone(), + }) + } else { + None + } +} + +fn main() { + let mut langmap = LANGMAP + .iter() + .map(|(ru, en)| ru.chars().zip(en.chars())) + .flatten() + .filter(|(en, ru)| en != ru) + .map(|(ru, en)| (en, ru)) + .collect::>(); + + langmap + .iter() + .filter_map(|(en, ru)| langmap.contains_key(ru).then(|| *en)) + .collect::>() + .into_iter() + .for_each(|c| { + langmap.remove(&c); + }); + + let keymaps = default::default(); + for mode in [Mode::Normal, Mode::Select] { + println!("[keys.{}]", mode); + keymaps[&mode].traverse_map(|keys, name| { + let tr_keys = keys + .iter() + .filter_map(|ev| translate(ev, |c| langmap.get(&c).map(|c| *c))) + .enumerate() + .map(|(i, ev)| { + if i == 0 { + ev.to_string() + } else { + format!("+{}", ev) + } + }) + .collect::(); + if !tr_keys.is_empty() { + println!(r#"{:?} = "{}""#, tr_keys, name); + } + }); + } +} diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 173c5d49a55b0..3ebcff58b5307 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -22,7 +22,7 @@ use crate::{ compositor::{Compositor, Event}, config::Config, job::Jobs, - keymap::Keymaps, + keymap::{Keymaps, LayoutRemap}, ui::{self, overlay::overlayed}, }; @@ -157,7 +157,13 @@ impl Application { let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { &config.keys })); - let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys))); + let layout_remap = Box::new(Map::new(Arc::clone(&config), |config: &Config| { + &config.editor.layout_remap + })); + let editor_view = Box::new(ui::EditorView::new( + Keymaps::new(keys), + LayoutRemap::new(layout_remap), + )); compositor.push(editor_view); if args.load_tutor { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 088b3b6d2083a..dcc260e0385d5 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -301,6 +301,32 @@ impl Keymap { res } + pub fn traverse_map(&self, f: F) + where + F: Fn(&Vec, String), + { + fn map_node(node: &KeyTrie, keys: &mut Vec, f: &F) + where + F: Fn(&Vec, String), + { + match node { + KeyTrie::Leaf(cmd) => match cmd { + MappableCommand::Typable { name, .. } => f(keys, name.into()), + MappableCommand::Static { name, .. } => f(keys, (*name).to_owned()), + }, + KeyTrie::Node(next) => { + for (key, trie) in &next.map { + keys.push(*key); + map_node(trie, keys, f); + keys.pop(); + } + } + KeyTrie::Sequence(_) => {} + } + } + map_node(&self.root, &mut Vec::new(), &f); + } + pub fn root(&self) -> &KeyTrie { &self.root } @@ -425,6 +451,35 @@ pub fn merge_keys(mut config: Config) -> Config { config } +pub struct LayoutRemap { + pub map: Box>>, +} + +impl LayoutRemap { + pub fn new(map: Box>>) -> Self { + Self { map } + } + + pub fn translate(&self, event: &KeyEvent) -> KeyEvent { + use helix_view::keyboard::KeyCode; + let remap = &*self.map.load(); + if let KeyCode::Char(c) = event.code { + KeyEvent { + code: KeyCode::Char(*remap.get(&c).unwrap_or(&c)), + modifiers: event.modifiers, + } + } else { + *event + } + } +} + +impl Default for LayoutRemap { + fn default() -> Self { + Self::new(Box::new(ArcSwap::new(Arc::new(HashMap::new())))) + } +} + #[cfg(test)] mod tests { use super::macros::keymap; diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 72c9d15e6d0a3..8f582f2ddf758 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -3,7 +3,7 @@ use crate::{ compositor::{Component, Context, Event, EventResult}, job::{self, Callback}, key, - keymap::{KeymapResult, Keymaps}, + keymap::{KeymapResult, Keymaps, LayoutRemap}, ui::{Completion, ProgressSpinners}, }; @@ -34,6 +34,7 @@ use super::statusline; pub struct EditorView { pub keymaps: Keymaps, + layout_remap: LayoutRemap, on_next_key: Option>, pseudo_pending: Vec, last_insert: (commands::MappableCommand, Vec), @@ -50,14 +51,15 @@ pub enum InsertEvent { impl Default for EditorView { fn default() -> Self { - Self::new(Keymaps::default()) + Self::new(Keymaps::default(), LayoutRemap::default()) } } impl EditorView { - pub fn new(keymaps: Keymaps) -> Self { + pub fn new(keymaps: Keymaps, layout_remap: LayoutRemap) -> Self { Self { keymaps, + layout_remap, on_next_key: None, pseudo_pending: Vec::new(), last_insert: (commands::MappableCommand::normal_mode, Vec::new()), @@ -921,6 +923,12 @@ impl EditorView { ) -> Option { let mut last_mode = mode; self.pseudo_pending.extend(self.keymaps.pending()); + // Translate Normal and Select mode keys using layout remap table. + let event = if !matches!(last_mode, Mode::Insert) { + self.layout_remap.translate(&event) + } else { + event + }; let key_result = self.keymaps.get(mode, event); cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index af69ceeae19a6..6df0541badf1a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -13,6 +13,7 @@ use crate::{ use futures_util::stream::select_all::SelectAll; use futures_util::{future, StreamExt}; use helix_lsp::Call; +use log::warn; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ @@ -67,6 +68,62 @@ where ) } +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default, deny_unknown_fields)] +struct LayoutMap { + from: String, + into: String, +} + +impl Default for LayoutMap { + fn default() -> Self { + LayoutMap { + from: String::new(), + into: String::new(), + } + } +} + +fn deserialize_layout_remap<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + // Make (from, to) char pairs from `Vec` of `LayoutMap`s filtering + // cases where `from == to` which we don't need to translate. + let mut pairs = Vec::::deserialize(deserializer)? + .iter() + .map(|layout_map| layout_map.from.chars().zip(layout_map.into.chars())) + .flatten() + .filter(|(from, to)| from != to) + .collect::>(); + pairs + .iter() + .filter_map(|(_, to)| pairs.contains_key(to).then(|| *to)) + .collect::>() + .into_iter() + .for_each(|c| { + warn!( + "Key '{}' removed from layout-remap as it would overwrite Latin layout binding", + c + ); + pairs.remove(&c); + }); + Ok(pairs) +} + +fn serialize_layout_remap(map: &HashMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut from = String::new(); + let mut into = String::new(); + map.iter().for_each(|(f, t)| { + from.push(*f); + into.push(*t); + }); + vec![LayoutMap { from, into }].serialize(serializer) +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct FilePickerConfig { @@ -174,6 +231,12 @@ pub struct Config { pub indent_guides: IndentGuidesConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + /// Translate pressed keys in Normal and Select modes when a keyboard layout different from Latin is chosen in the system. + #[serde( + serialize_with = "serialize_layout_remap", + deserialize_with = "deserialize_layout_remap" + )] + pub layout_remap: HashMap, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -610,6 +673,7 @@ impl Default for Config { bufferline: BufferLine::default(), indent_guides: IndentGuidesConfig::default(), color_modes: false, + layout_remap: HashMap::new(), } } } diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 30fa72c496532..fefd7982bdc65 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -73,6 +73,24 @@ impl KeyEvent { } } + /// Translates a `KeyCode` using the `f` function. Only + /// `KeyCode::Char(c)` are translated. This method should be used + /// whenever a translation for the characters is desired, e.g. when + /// remapping a keyboard layout from non-Latin to Latin. + pub fn translate(&self, f: F) -> KeyEvent + where + F: Fn(char) -> char, + { + if let KeyCode::Char(c) = self.code { + KeyEvent { + code: KeyCode::Char(f(c)), + modifiers: self.modifiers, + } + } else { + *self + } + } + /// Format the key in such a way that a concatenated sequence /// of keys can be read easily. ///