Skip to main content

nu_protocol/config/
reedline.rs

1use std::collections::BTreeSet;
2
3use super::{config_update_string_enum, prelude::*};
4use crate as nu_protocol;
5use crate::{FromValue, engine::Closure};
6
7/// Definition of a parsed keybinding from the config object
8#[derive(Clone, Debug, FromValue, IntoValue, Serialize, Deserialize)]
9pub struct ParsedKeybinding {
10    pub name: Option<Value>,
11    pub modifier: Value,
12    pub keycode: Value,
13    pub event: Value,
14    pub mode: Value,
15}
16
17pub(crate) fn name_of(kb: &ParsedKeybinding) -> Option<String> {
18    kb.name
19        .as_ref()
20        .and_then(|v| v.coerce_str().ok())
21        .map(|s| s.to_string())
22}
23
24#[derive(Debug, PartialEq, Eq)]
25pub(super) struct KeyIdentity {
26    modifier: BTreeSet<String>,
27    keycode: String,
28    modes: BTreeSet<String>,
29}
30
31impl KeyIdentity {
32    pub(crate) fn of(kb: &ParsedKeybinding) -> Self {
33        let lower = |v: &Value| {
34            v.coerce_str()
35                .map(|s| s.to_ascii_lowercase())
36                .unwrap_or_default()
37        };
38        let modes = match &kb.mode {
39            Value::List { vals, .. } => vals.iter().map(lower).collect(),
40            v => BTreeSet::from([lower(v)]),
41        };
42
43        Self {
44            // Best-effort mirror of `add_parsed_keybinding`'s reading: modifiers
45            // are an unordered `_`-joined set and `esc`/`escape` are aliases. The
46            // exotic overlaps (`space` vs `char_ `, `char_u<hex>` vs `char_<c>`)
47            // are deliberately not canonicalized; a mismatch only costs an
48            // append plus a warning, never a lost binding.
49            modifier: lower(&kb.modifier).split('_').map(|s| s.into()).collect(),
50            keycode: match lower(&kb.keycode).as_str() {
51                "esc" => "escape".into(),
52                other => other.into(),
53            },
54            modes,
55        }
56    }
57}
58
59/// Definition of a parsed menu from the config object
60#[derive(Clone, Debug, FromValue, IntoValue, Serialize, Deserialize)]
61pub struct ParsedMenu {
62    pub name: Value,
63    pub marker: Value,
64    /// Legacy two-state input behavior. Required unless `input_mode` is set,
65    /// which supersedes it.
66    pub only_buffer_difference: Option<Value>,
67    /// Optional reedline `InputMode` ("diff" / "cursor_prefix" / "full_buffer").
68    /// Supersedes `only_buffer_difference` when set; absent keeps current behavior.
69    pub input_mode: Option<Value>,
70    /// Optional reedline `OutputMode` ("suggested_span" / "full_buffer" / "extend_to_end").
71    pub output_mode: Option<Value>,
72    pub style: Value,
73    pub r#type: Value,
74    pub source: Option<Closure>,
75}
76
77/// Definition of a Nushell CursorShape (to be mapped to crossterm::cursor::CursorShape)
78#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
79pub enum NuCursorShape {
80    Underscore,
81    Line,
82    Block,
83    BlinkUnderscore,
84    BlinkLine,
85    BlinkBlock,
86    #[default]
87    Inherit,
88}
89
90impl FromStr for NuCursorShape {
91    type Err = &'static str;
92
93    fn from_str(s: &str) -> Result<NuCursorShape, &'static str> {
94        match s.to_ascii_lowercase().as_str() {
95            "line" => Ok(NuCursorShape::Line),
96            "block" => Ok(NuCursorShape::Block),
97            "underscore" => Ok(NuCursorShape::Underscore),
98            "blink_line" => Ok(NuCursorShape::BlinkLine),
99            "blink_block" => Ok(NuCursorShape::BlinkBlock),
100            "blink_underscore" => Ok(NuCursorShape::BlinkUnderscore),
101            "inherit" => Ok(NuCursorShape::Inherit),
102            _ => Err(
103                "'line', 'block', 'underscore', 'blink_line', 'blink_block', 'blink_underscore' or 'inherit'",
104            ),
105        }
106    }
107}
108
109impl UpdateFromValue for NuCursorShape {
110    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
111        config_update_string_enum(self, value, path, errors)
112    }
113}
114
115#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
116pub struct CursorShapeConfig {
117    pub emacs: NuCursorShape,
118    pub vi_insert: NuCursorShape,
119    pub vi_normal: NuCursorShape,
120    /// Only takes effect in builds with the `helix` feature.
121    pub helix_normal: NuCursorShape,
122    pub helix_select: NuCursorShape,
123    pub helix_insert: NuCursorShape,
124}
125
126impl UpdateFromValue for CursorShapeConfig {
127    fn update<'a>(
128        &mut self,
129        value: &'a Value,
130        path: &mut ConfigPath<'a>,
131        errors: &mut ConfigErrors,
132    ) {
133        let Value::Record { val: record, .. } = value else {
134            errors.type_mismatch(path, Type::record(), value);
135            return;
136        };
137
138        for (col, val) in record.iter() {
139            let path = &mut path.push(col);
140            match col.as_str() {
141                "vi_insert" => self.vi_insert.update(val, path, errors),
142                "vi_normal" => self.vi_normal.update(val, path, errors),
143                "emacs" => self.emacs.update(val, path, errors),
144                "helix_normal" => self.helix_normal.update(val, path, errors),
145                "helix_select" => self.helix_select.update(val, path, errors),
146                "helix_insert" => self.helix_insert.update(val, path, errors),
147                _ => errors.unknown_option(path, val),
148            }
149        }
150    }
151}
152
153#[derive(Clone, Copy, Debug, Default, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
154pub enum EditBindings {
155    Vi,
156    #[default]
157    Emacs,
158    /// Only usable in builds with the `helix` feature; selecting it elsewhere
159    /// reports an error when the keybindings are constructed.
160    Helix,
161}
162
163impl FromStr for EditBindings {
164    type Err = &'static str;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        match s.to_ascii_lowercase().as_str() {
168            "vi" => Ok(Self::Vi),
169            "emacs" => Ok(Self::Emacs),
170            "helix" => Ok(Self::Helix),
171            _ => Err("'emacs', 'vi' or 'helix'"),
172        }
173    }
174}
175
176impl UpdateFromValue for EditBindings {
177    fn update(&mut self, value: &Value, path: &mut ConfigPath, errors: &mut ConfigErrors) {
178        config_update_string_enum(self, value, path, errors)
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    fn kb(modifier: &str, keycode: &str, mode: Value) -> ParsedKeybinding {
187        ParsedKeybinding {
188            name: None,
189            modifier: Value::test_string(modifier),
190            keycode: Value::test_string(keycode),
191            event: Value::test_nothing(),
192            mode,
193        }
194    }
195
196    #[test]
197    fn a_bare_mode_and_its_singleton_list_are_the_same_key() {
198        let bare = kb("control", "char_r", Value::test_string("emacs"));
199        let listed = kb(
200            "control",
201            "char_r",
202            Value::test_list(vec![Value::test_string("emacs")]),
203        );
204        assert_eq!(KeyIdentity::of(&bare), KeyIdentity::of(&listed));
205    }
206
207    #[test]
208    fn mode_list_order_does_not_matter() {
209        let forward = kb(
210            "control",
211            "char_r",
212            Value::test_list(vec![
213                Value::test_string("emacs"),
214                Value::test_string("vi_insert"),
215            ]),
216        );
217        let reversed = kb(
218            "control",
219            "char_r",
220            Value::test_list(vec![
221                Value::test_string("vi_insert"),
222                Value::test_string("emacs"),
223            ]),
224        );
225        assert_eq!(KeyIdentity::of(&forward), KeyIdentity::of(&reversed));
226    }
227
228    #[test]
229    fn spelling_case_does_not_matter() {
230        let lower = kb("control", "char_r", Value::test_string("emacs"));
231        let upper = kb("Control", "Char_R", Value::test_string("Emacs"));
232        assert_eq!(KeyIdentity::of(&lower), KeyIdentity::of(&upper));
233    }
234
235    // Canonicalization to match `add_parsed_keybinding`'s reading of the fields:
236    // modifiers are an unordered `_`-joined set, `esc`/`escape` are aliases,
237    // and mode names keep their underscores.
238
239    #[test]
240    fn modifier_component_order_does_not_matter() {
241        let cs = kb("control_shift", "char_r", Value::test_string("emacs"));
242        let sc = kb("shift_control", "char_r", Value::test_string("emacs"));
243        assert_eq!(KeyIdentity::of(&cs), KeyIdentity::of(&sc));
244    }
245
246    #[test]
247    fn esc_and_escape_are_the_same_key() {
248        let esc = kb("none", "esc", Value::test_string("emacs"));
249        let escape = kb("none", "escape", Value::test_string("emacs"));
250        assert_eq!(KeyIdentity::of(&esc), KeyIdentity::of(&escape));
251    }
252
253    #[test]
254    fn mode_names_are_not_split_on_underscores() {
255        // Guards the tokenizer split: `vi_normal` is one mode, not `vi` + `normal`.
256        let whole = kb("none", "char_r", Value::test_string("vi_normal"));
257        let parts = kb(
258            "none",
259            "char_r",
260            Value::test_list(vec![Value::test_string("vi"), Value::test_string("normal")]),
261        );
262        assert_ne!(KeyIdentity::of(&whole), KeyIdentity::of(&parts));
263    }
264
265    #[test]
266    fn a_different_key_is_a_different_identity() {
267        let ctrl_r = kb("control", "char_r", Value::test_string("emacs"));
268        let up = kb("none", "up", Value::test_string("emacs"));
269        assert_ne!(KeyIdentity::of(&ctrl_r), KeyIdentity::of(&up));
270    }
271}