From 55c3fc9141f3709c2e0ab607e291ba27d9e65fa0 Mon Sep 17 00:00:00 2001 From: JustForFun88 <100504524+JustForFun88@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:42:15 +0500 Subject: [PATCH] Improve keybinding parsing for Unicode support (#14020) # Description This pull request enhances the `add_parsed_keybinding` function to provide greater flexibility in specifying keycodes for keybindings in Nushell. Previously, the function only supported specifying keycodes directly through character notation (e.g., `char_e` for the character `e`). This limited users to a small set of keybindings, especially in scenarios where specific non-English characters were needed. With this new version, users can also specify characters using their Unicode codes, such as `char_u003B` for the semicolon (`;`), providing a more flexible approach to customization, for example like this: ```nushell { name: move_to_line_end_or_take_history_hint modifier: shift keycode: char_u003B # char_; mode: vi_normal event: { until: [ { send: historyhintcomplete } { edit: movetolineend } ] } } ``` # User-Facing Changes Added support for specifying keycodes using Unicode codes, e.g., char_u002C (comma - `,`): ```nushell { name: , # name of the command modifier: none, # key modifier keycode: char_u002C, # Unicode code for the comma (',') mode: vi_normal, # mode in which this binding should work event: { send: # action to be performed } } ``` --- crates/nu-cli/src/reedline_config.rs | 118 +++++++++++++++------------ 1 file changed, 65 insertions(+), 53 deletions(-) diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index 37fc6228b9..48140b4427 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -833,64 +833,76 @@ fn add_parsed_keybinding( } }; - let keycode = match keybinding + let keycode_str = keybinding .keycode .to_expanded_string("", config) - .to_ascii_lowercase() - .as_str() - { - "backspace" => KeyCode::Backspace, - "enter" => KeyCode::Enter, - c if c.starts_with("char_") => { - let mut char_iter = c.chars().skip(5); - let pos1 = char_iter.next(); - let pos2 = char_iter.next(); + .to_ascii_lowercase(); - let char = if let (Some(char), None) = (pos1, pos2) { - char - } else { + let keycode = if let Some(rest) = keycode_str.strip_prefix("char_") { + let error = |exp: &str, value| ShellError::UnsupportedConfigValue { + expected: exp.to_string(), + value, + span: keybinding.keycode.span(), + }; + + let mut char_iter = rest.chars(); + let char = match (char_iter.next(), char_iter.next()) { + (Some(char), None) => char, + (Some('u'), Some(_)) => { + // This will never panic as we know there are at least two symbols + let Ok(code_point) = u32::from_str_radix(&rest[1..], 16) else { + return Err(error("valid hex code in keycode", keycode_str)); + }; + + char::from_u32(code_point).ok_or(error("valid Unicode code point", keycode_str))? + } + _ => { + return Err(error( + "format 'char_' or 'char_u'", + keycode_str, + )) + } + }; + + KeyCode::Char(char) + } else { + match keycode_str.as_str() { + "backspace" => KeyCode::Backspace, + "enter" => KeyCode::Enter, + "space" => KeyCode::Char(' '), + "down" => KeyCode::Down, + "up" => KeyCode::Up, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "tab" => KeyCode::Tab, + "backtab" => KeyCode::BackTab, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + c if c.starts_with('f') => { + let fn_num: u8 = c[1..] + .parse() + .ok() + .filter(|num| matches!(num, 1..=20)) + .ok_or(ShellError::UnsupportedConfigValue { + expected: "(f1|f2|...|f20)".to_string(), + value: format!("unknown function key: {c}"), + span: keybinding.keycode.span(), + })?; + KeyCode::F(fn_num) + } + "null" => KeyCode::Null, + "esc" | "escape" => KeyCode::Esc, + _ => { return Err(ShellError::UnsupportedConfigValue { - expected: "char_".to_string(), - value: c.to_string(), + expected: "crossterm KeyCode".to_string(), + value: keybinding.keycode.to_abbreviated_string(config), span: keybinding.keycode.span(), - }); - }; - - KeyCode::Char(char) - } - "space" => KeyCode::Char(' '), - "down" => KeyCode::Down, - "up" => KeyCode::Up, - "left" => KeyCode::Left, - "right" => KeyCode::Right, - "home" => KeyCode::Home, - "end" => KeyCode::End, - "pageup" => KeyCode::PageUp, - "pagedown" => KeyCode::PageDown, - "tab" => KeyCode::Tab, - "backtab" => KeyCode::BackTab, - "delete" => KeyCode::Delete, - "insert" => KeyCode::Insert, - c if c.starts_with('f') => { - let fn_num: u8 = c[1..] - .parse() - .ok() - .filter(|num| matches!(num, 1..=20)) - .ok_or(ShellError::UnsupportedConfigValue { - expected: "(f1|f2|...|f20)".to_string(), - value: format!("unknown function key: {c}"), - span: keybinding.keycode.span(), - })?; - KeyCode::F(fn_num) - } - "null" => KeyCode::Null, - "esc" | "escape" => KeyCode::Esc, - _ => { - return Err(ShellError::UnsupportedConfigValue { - expected: "crossterm KeyCode".to_string(), - value: keybinding.keycode.to_abbreviated_string(config), - span: keybinding.keycode.span(), - }) + }) + } } }; if let Some(event) = parse_event(&keybinding.event, config)? {