From 4494bfe3686479277c16f29e77bef3fc35da6089 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Mon, 25 May 2026 18:42:20 -0300 Subject: [PATCH] feat(tui): add vim named registers --- codex-rs/config/src/tui_keymap.rs | 2 + codex-rs/core/config.schema.json | 10 ++ codex-rs/tui/src/bottom_pane/textarea.rs | 134 ++++++++++++++++-- codex-rs/tui/src/bottom_pane/textarea/vim.rs | 8 ++ .../tui/src/bottom_pane/textarea/vim/count.rs | 7 +- .../tui/src/bottom_pane/textarea/vim/find.rs | 1 + .../src/bottom_pane/textarea/vim/register.rs | 73 ++++++++++ codex-rs/tui/src/keymap.rs | 22 +++ codex-rs/tui/src/keymap_setup/actions.rs | 3 + ...ap_setup__tests__keymap_picker_custom.snap | 2 +- ...ests__keymap_picker_fast_mode_enabled.snap | 2 +- ...p__tests__keymap_picker_first_actions.snap | 4 +- ...ap_setup__tests__keymap_picker_narrow.snap | 2 +- ...ymap_setup__tests__keymap_picker_wide.snap | 2 +- 14 files changed, 248 insertions(+), 24 deletions(-) create mode 100644 codex-rs/tui/src/bottom_pane/textarea/vim/register.rs diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 7d11247c73..2b338a5aa7 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -245,6 +245,8 @@ pub struct TuiVimNormalKeymap { pub yank_line: Option, /// Paste after cursor (`p`). pub paste_after: Option, + /// Select a named register for the next Vim command (`"{register}`). + pub select_register: Option, /// Begin delete operator; next key selects motion (`d`). pub start_delete_operator: Option, /// Begin yank operator; next key selects motion (`y`). diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 7170dc0850..a3513da31f 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2845,6 +2845,7 @@ "paste_after": null, "repeat_find": null, "repeat_find_reverse": null, + "select_register": null, "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, @@ -3544,6 +3545,7 @@ "paste_after": null, "repeat_find": null, "repeat_find_reverse": null, + "select_register": null, "start_change_operator": null, "start_delete_operator": null, "start_yank_operator": null, @@ -3996,6 +3998,14 @@ ], "description": "Repeat the most recent find/till motion in the opposite direction (`,`)." }, + "select_register": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Select a named register for the next Vim command (`\"{register}`)." + }, "start_change_operator": { "allOf": [ { diff --git a/codex-rs/tui/src/bottom_pane/textarea.rs b/codex-rs/tui/src/bottom_pane/textarea.rs index 66f8e3f583..54b76cc9e3 100644 --- a/codex-rs/tui/src/bottom_pane/textarea.rs +++ b/codex-rs/tui/src/bottom_pane/textarea.rs @@ -31,6 +31,7 @@ use ratatui::widgets::StatefulWidgetRef; use ratatui::widgets::WidgetRef; use std::cell::Ref; use std::cell::RefCell; +use std::collections::BTreeMap; use std::ops::Range; use textwrap::Options; use unicode_segmentation::UnicodeSegmentation; @@ -42,6 +43,7 @@ use self::vim::VimMode; use self::vim::VimMotion; use self::vim::VimOperator; use self::vim::VimPending; +use self::vim::VimRegisterSelection; use self::vim::VimTextObjectScope; const WORD_SEPARATORS: &str = "`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?"; @@ -108,6 +110,8 @@ pub(crate) struct TextArea { next_element_id: u64, kill_buffer: String, kill_buffer_kind: KillBufferKind, + vim_named_registers: BTreeMap, + vim_selected_register: Option, vim_enabled: bool, vim_mode: VimMode, vim_pending: VimPending, @@ -152,6 +156,8 @@ impl TextArea { next_element_id: 1, kill_buffer: String::new(), kill_buffer_kind: KillBufferKind::Characterwise, + vim_named_registers: BTreeMap::new(), + vim_selected_register: None, vim_enabled: false, vim_mode: VimMode::Insert, vim_pending: VimPending::None, @@ -239,6 +245,7 @@ impl TextArea { self.vim_enabled = enabled; self.vim_pending = VimPending::None; self.clear_vim_counts(); + self.clear_vim_register_selection(); self.vim_mode = if enabled { VimMode::Normal } else { @@ -287,6 +294,7 @@ impl TextArea { self.vim_mode = VimMode::Insert; self.vim_pending = VimPending::None; self.clear_vim_counts(); + self.clear_vim_register_selection(); } } @@ -301,6 +309,7 @@ impl TextArea { self.vim_mode = VimMode::Normal; self.vim_pending = VimPending::None; self.clear_vim_counts(); + self.clear_vim_register_selection(); self.preferred_col = None; } } @@ -660,6 +669,10 @@ impl TextArea { let pending = std::mem::replace(&mut self.vim_pending, VimPending::None); match pending { VimPending::None => {} + VimPending::Register => { + self.handle_vim_register_name(event); + return; + } VimPending::Operator(op) => { self.handle_vim_operator(op, event); return; @@ -674,13 +687,19 @@ impl TextArea { } } + if self.vim_normal_keymap.select_register.is_pressed(event) { + self.vim_pending = VimPending::Register; + return; + } if self.vim_normal_keymap.enter_insert.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); self.vim_mode = VimMode::Insert; return; } if self.vim_normal_keymap.append_after_cursor.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); let next = self.next_atomic_boundary(self.cursor_pos); self.set_cursor(next); self.vim_mode = VimMode::Insert; @@ -688,18 +707,21 @@ impl TextArea { } if self.vim_normal_keymap.append_line_end.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); self.set_cursor(self.end_of_current_line()); self.vim_mode = VimMode::Insert; return; } if self.vim_normal_keymap.insert_line_start.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); self.set_cursor(self.first_non_blank_of_current_line()); self.vim_mode = VimMode::Insert; return; } if self.vim_normal_keymap.open_line_below.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); let eol = self.end_of_current_line(); let insert_at = if eol < self.text.len() { eol + 1 } else { eol }; self.insert_str_at(insert_at, "\n"); @@ -714,6 +736,7 @@ impl TextArea { } if self.vim_normal_keymap.open_line_above.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); let bol = self.beginning_of_current_line(); self.insert_str_at(bol, "\n"); self.set_cursor(bol); @@ -785,6 +808,7 @@ impl TextArea { if self.vim_normal_keymap.delete_char.is_pressed(event) { let count = self.take_vim_count(); self.delete_forward_kill(count); + self.clear_vim_register_selection(); return; } if self.vim_normal_keymap.delete_to_line_end.is_pressed(event) { @@ -800,22 +824,25 @@ impl TextArea { if self.vim_normal_keymap.substitute_char.is_pressed(event) { let count = self.take_vim_count(); self.delete_forward_kill(count); + self.clear_vim_register_selection(); self.vim_mode = VimMode::Insert; return; } if self.vim_normal_keymap.substitute_line.is_pressed(event) { let count = self.take_vim_count(); self.vim_change_current_lines(count); + self.clear_vim_register_selection(); return; } if self.vim_normal_keymap.yank_line.is_pressed(event) { let count = self.take_vim_count(); self.vim_yank_current_lines(count); + self.clear_vim_register_selection(); return; } if self.vim_normal_keymap.paste_after.is_pressed(event) { let count = self.take_vim_count(); - self.paste_after_cursor_counted(count); + self.vim_paste_after_cursor_counted(count); return; } if self @@ -841,29 +868,35 @@ impl TextArea { if self.vim_normal_keymap.cancel_operator.is_pressed(event) { self.vim_pending = VimPending::None; self.clear_vim_counts(); + self.clear_vim_register_selection(); return; } self.clear_vim_counts(); + self.clear_vim_register_selection(); } fn handle_vim_operator(&mut self, op: VimOperator, event: KeyEvent) -> bool { if op == VimOperator::Delete && self.vim_operator_keymap.delete_line.is_pressed(event) { let count = self.take_vim_operator_count(); self.vim_kill_current_lines(count); + self.clear_vim_register_selection(); return true; } if op == VimOperator::Yank && self.vim_operator_keymap.yank_line.is_pressed(event) { let count = self.take_vim_operator_count(); self.vim_yank_current_lines(count); + self.clear_vim_register_selection(); return true; } if op == VimOperator::Change && self.vim_operator_keymap.change_line.is_pressed(event) { let count = self.take_vim_operator_count(); self.vim_change_current_lines(count); + self.clear_vim_register_selection(); return true; } if self.vim_operator_keymap.cancel.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); return true; } if let Some(scope) = self.vim_text_object_scope_for_event(event) { @@ -901,6 +934,7 @@ impl TextArea { return true; } self.clear_vim_counts(); + self.clear_vim_register_selection(); false } @@ -912,16 +946,19 @@ impl TextArea { ) -> bool { if self.vim_text_object_keymap.cancel.is_pressed(event) { self.clear_vim_counts(); + self.clear_vim_register_selection(); return true; } let Some(object) = self.vim_text_object_for_event(event) else { self.clear_vim_counts(); + self.clear_vim_register_selection(); return false; }; let count = self.take_vim_operator_count(); if let Some(range) = self.counted_text_object_range(object, scope, count) { self.apply_vim_operator_to_range(op, range); } + self.clear_vim_register_selection(); true } @@ -985,6 +1022,7 @@ impl TextArea { } } } + self.clear_vim_register_selection(); } fn range_for_change_motion(&mut self, motion: VimMotion, count: usize) -> Option> { @@ -1010,6 +1048,7 @@ impl TextArea { self.vim_mode = VimMode::Insert; } } + self.clear_vim_register_selection(); } fn range_for_motion(&mut self, motion: VimMotion, count: usize) -> Option> { @@ -1254,25 +1293,25 @@ impl TextArea { } fn store_kill_buffer(&mut self, text: String, kind: KillBufferKind) { + self.write_selected_vim_register(&text, kind); self.kill_buffer = text; self.kill_buffer_kind = kind; } - fn paste_after_cursor(&mut self) { - if self.kill_buffer.is_empty() { + fn paste_text_after_cursor(&mut self, text: &str, kind: KillBufferKind) { + if text.is_empty() { return; } - if self.kill_buffer_kind == KillBufferKind::Linewise { - self.paste_line_after_current_line(); + if kind == KillBufferKind::Linewise { + self.paste_line_after_current_line(text); return; } let insert_at = self.next_atomic_boundary(self.cursor_pos); self.set_cursor(insert_at); - let text = self.kill_buffer.clone(); - self.insert_str(&text); + self.insert_str(text); } - fn paste_line_after_current_line(&mut self) { + fn paste_line_after_current_line(&mut self, buffer: &str) { let eol = self.end_of_current_line(); let insert_at = if eol < self.text.len() { eol + 1 } else { eol }; let cursor = if eol < self.text.len() { @@ -1281,13 +1320,13 @@ impl TextArea { insert_at + 1 }; let text = if eol < self.text.len() { - if self.kill_buffer.ends_with('\n') { - self.kill_buffer.clone() + if buffer.ends_with('\n') { + buffer.to_string() } else { - format!("{}\n", self.kill_buffer) + format!("{buffer}\n") } } else { - format!("\n{}", self.kill_buffer.trim_end_matches('\n')) + format!("\n{}", buffer.trim_end_matches('\n')) }; self.insert_str_at(insert_at, &text); self.set_cursor(cursor.min(self.text.len())); @@ -2748,6 +2787,77 @@ mod tests { assert_eq!(t.vim_mode_label(), Some("Insert")); } + #[test] + fn vim_named_registers_write_and_paste_independently_of_unnamed() { + let mut t = ta_with("one two three"); + t.set_cursor(/*pos*/ 0); + t.set_vim_enabled(/*enabled*/ true); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + assert_eq!( + t.vim_named_registers.get(&'a'), + Some(&("one ".to_string(), KillBufferKind::Characterwise)) + ); + + t.set_cursor(/*pos*/ "one ".len()); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + assert_eq!(t.text(), "one three"); + assert_eq!(t.kill_buffer, "two "); + + t.set_cursor(t.text().len()); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)); + assert_eq!(t.text(), "one threeone "); + assert_eq!(t.kill_buffer, "two "); + + let before_empty_paste = t.text().to_string(); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::NONE)); + assert_eq!(t.text(), before_empty_paste); + } + + #[test] + fn vim_uppercase_register_writes_append_and_change_captures_named_text() { + let mut t = ta_with("one two three"); + t.set_cursor(/*pos*/ 0); + t.set_vim_enabled(/*enabled*/ true); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + t.set_cursor(/*pos*/ "one ".len()); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('A'), KeyModifiers::SHIFT)); + t.input(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + assert_eq!( + t.vim_named_registers.get(&'a'), + Some(&("one two ".to_string(), KillBufferKind::Characterwise)) + ); + + t.set_text_clearing_elements("value"); + t.set_cursor(/*pos*/ 0); + t.set_vim_enabled(/*enabled*/ true); + t.input(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + t.input(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE)); + assert_eq!(t.text(), ""); + assert_eq!(t.kill_buffer, "value"); + assert_eq!( + t.vim_named_registers.get(&'c'), + Some(&("value".to_string(), KillBufferKind::Characterwise)) + ); + } + #[test] fn vim_find_till_motions_repeat_and_stay_on_current_line() { let mut t = ta_with("ab,cd,ef\naz"); diff --git a/codex-rs/tui/src/bottom_pane/textarea/vim.rs b/codex-rs/tui/src/bottom_pane/textarea/vim.rs index adbcc4e4fe..90d30a0c1f 100644 --- a/codex-rs/tui/src/bottom_pane/textarea/vim.rs +++ b/codex-rs/tui/src/bottom_pane/textarea/vim.rs @@ -7,6 +7,7 @@ use std::ops::Range; mod count; mod find; mod prose; +mod register; mod tag; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -27,6 +28,7 @@ pub(super) enum VimOperator { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum VimPending { None, + Register, Operator(VimOperator), TextObject { operator: VimOperator, @@ -38,6 +40,12 @@ pub(super) enum VimPending { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct VimRegisterSelection { + pub(super) name: char, + pub(super) append: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum VimMotion { Left, diff --git a/codex-rs/tui/src/bottom_pane/textarea/vim/count.rs b/codex-rs/tui/src/bottom_pane/textarea/vim/count.rs index 93f11e8f87..ffdbab5fa1 100644 --- a/codex-rs/tui/src/bottom_pane/textarea/vim/count.rs +++ b/codex-rs/tui/src/bottom_pane/textarea/vim/count.rs @@ -55,6 +55,7 @@ impl TextArea { motion: VimMotion, count: usize, ) { + self.clear_vim_register_selection(); if motion == VimMotion::LineEnd { for _ in 1..count { self.move_cursor_down(); @@ -132,12 +133,6 @@ impl TextArea { self.vim_mode = super::VimMode::Insert; } - pub(in super::super) fn paste_after_cursor_counted(&mut self, count: usize) { - for _ in 0..count { - self.paste_after_cursor(); - } - } - fn current_line_range_with_count(&self, count: usize) -> Range { let start = self.beginning_of_current_line(); let mut end = self.current_line_range_with_newline().end; diff --git a/codex-rs/tui/src/bottom_pane/textarea/vim/find.rs b/codex-rs/tui/src/bottom_pane/textarea/vim/find.rs index 3d33075a05..606aff9216 100644 --- a/codex-rs/tui/src/bottom_pane/textarea/vim/find.rs +++ b/codex-rs/tui/src/bottom_pane/textarea/vim/find.rs @@ -107,6 +107,7 @@ impl TextArea { } else if let Some(target) = self.target_for_find(find, count) { self.set_cursor(target); } + self.clear_vim_register_selection(); } pub(in super::super) fn target_for_find(&self, find: VimFind, count: usize) -> Option { diff --git a/codex-rs/tui/src/bottom_pane/textarea/vim/register.rs b/codex-rs/tui/src/bottom_pane/textarea/vim/register.rs new file mode 100644 index 0000000000..67404f9e8f --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/textarea/vim/register.rs @@ -0,0 +1,73 @@ +use super::super::KillBufferKind; +use super::super::TextArea; +use super::VimRegisterSelection; +use crate::key_hint::KeyBindingListExt; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; + +impl TextArea { + pub(in super::super) fn handle_vim_register_name(&mut self, event: KeyEvent) -> bool { + if self.vim_normal_keymap.cancel_operator.is_pressed(event) { + self.clear_vim_register_selection(); + self.clear_vim_counts(); + return true; + } + let KeyCode::Char(name) = event.code else { + self.clear_vim_register_selection(); + self.clear_vim_counts(); + return true; + }; + if !name.is_ascii_alphabetic() { + self.clear_vim_register_selection(); + self.clear_vim_counts(); + return true; + } + self.vim_selected_register = Some(VimRegisterSelection { + name: name.to_ascii_lowercase(), + append: name.is_ascii_uppercase(), + }); + true + } + + pub(in super::super) fn clear_vim_register_selection(&mut self) { + self.vim_selected_register = None; + } + + pub(in super::super) fn write_selected_vim_register( + &mut self, + text: &str, + kind: KillBufferKind, + ) { + let Some(selection) = self.vim_selected_register.take() else { + return; + }; + if selection.append { + let entry = self + .vim_named_registers + .entry(selection.name) + .or_insert_with(|| (String::new(), kind)); + entry.0.push_str(text); + entry.1 = kind; + } else { + self.vim_named_registers + .insert(selection.name, (text.to_string(), kind)); + } + } + + pub(in super::super) fn vim_paste_after_cursor_counted(&mut self, count: usize) { + let source = if let Some(selection) = self.vim_selected_register.take() { + self.vim_named_registers + .get(&selection.name) + .cloned() + .unwrap_or_else(|| (String::new(), KillBufferKind::Characterwise)) + } else { + (self.kill_buffer.clone(), self.kill_buffer_kind) + }; + if source.0.is_empty() { + return; + } + for _ in 0..count { + self.paste_text_after_cursor(&source.0, source.1); + } + } +} diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index a8b071511a..1e12e861b8 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -168,6 +168,7 @@ pub(crate) struct VimNormalKeymap { pub(crate) substitute_line: Vec, pub(crate) yank_line: Vec, pub(crate) paste_after: Vec, + pub(crate) select_register: Vec, pub(crate) start_delete_operator: Vec, pub(crate) start_yank_operator: Vec, pub(crate) start_change_operator: Vec, @@ -517,6 +518,7 @@ impl RuntimeKeymap { substitute_line: resolve_local!(keymap, defaults, vim_normal, substitute_line), yank_line: resolve_local!(keymap, defaults, vim_normal, yank_line), paste_after: resolve_local!(keymap, defaults, vim_normal, paste_after), + select_register: resolve_local!(keymap, defaults, vim_normal, select_register), start_delete_operator: resolve_local!( keymap, defaults, @@ -705,6 +707,11 @@ impl RuntimeKeymap { .repeat_find_reverse .retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding)); } + if keymap.vim_normal.select_register.is_none() { + vim_normal + .select_register + .retain(|binding| !configured_vim_normal_bindings_to_preserve.contains(binding)); + } let mut vim_operator = VimOperatorKeymap { delete_line: resolve_local!(keymap, defaults, vim_operator, delete_line), @@ -1252,6 +1259,7 @@ impl RuntimeKeymap { ], yank_line: default_bindings![shift(KeyCode::Char('y')), plain(KeyCode::Char('Y'))], paste_after: default_bindings![plain(KeyCode::Char('p'))], + select_register: default_bindings![plain(KeyCode::Char('"'))], start_delete_operator: default_bindings![plain(KeyCode::Char('d'))], start_yank_operator: default_bindings![plain(KeyCode::Char('y'))], start_change_operator: default_bindings![plain(KeyCode::Char('c'))], @@ -1709,6 +1717,10 @@ impl RuntimeKeymap { ), ("yank_line", self.vim_normal.yank_line.as_slice()), ("paste_after", self.vim_normal.paste_after.as_slice()), + ( + "select_register", + self.vim_normal.select_register.as_slice(), + ), ( "start_delete_operator", self.vim_normal.start_delete_operator.as_slice(), @@ -2603,6 +2615,16 @@ mod tests { assert_eq!(runtime.vim_operator.till_forward, Vec::new()); } + #[test] + fn configured_legacy_vim_bindings_prune_new_register_prefix_default() { + let mut keymap = TuiKeymap::default(); + keymap.vim_normal.move_left = Some(one("\"")); + + let runtime = RuntimeKeymap::from_config(&keymap).expect("config should parse"); + + assert_eq!(runtime.vim_normal.select_register, Vec::new()); + } + #[test] fn configured_legacy_vim_operator_bindings_prune_new_text_object_defaults() { let mut keymap = TuiKeymap::default(); diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index d1014241af..430835f4fa 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -146,6 +146,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("vim_normal", "Vim normal", "substitute_line", "Substitute the current line and enter insert mode."), action("vim_normal", "Vim normal", "yank_line", "Yank the entire line."), action("vim_normal", "Vim normal", "paste_after", "Paste after the cursor."), + action("vim_normal", "Vim normal", "select_register", "Select a named register for the next command."), action("vim_normal", "Vim normal", "start_delete_operator", "Begin a delete operator and wait for a motion."), action("vim_normal", "Vim normal", "start_yank_operator", "Begin a yank operator and wait for a motion."), action("vim_normal", "Vim normal", "start_change_operator", "Begin a change operator and wait for a motion or text object."), @@ -305,6 +306,7 @@ pub(super) fn binding_slot<'a>( ("vim_normal", "substitute_line") => Some(&mut keymap.vim_normal.substitute_line), ("vim_normal", "yank_line") => Some(&mut keymap.vim_normal.yank_line), ("vim_normal", "paste_after") => Some(&mut keymap.vim_normal.paste_after), + ("vim_normal", "select_register") => Some(&mut keymap.vim_normal.select_register), ("vim_normal", "start_delete_operator") => Some(&mut keymap.vim_normal.start_delete_operator), ("vim_normal", "start_yank_operator") => Some(&mut keymap.vim_normal.start_yank_operator), ("vim_normal", "start_change_operator") => Some(&mut keymap.vim_normal.start_change_operator), @@ -446,6 +448,7 @@ pub(super) fn bindings_for_action<'a>( ("vim_normal", "substitute_line") => Some(runtime_keymap.vim_normal.substitute_line.as_slice()), ("vim_normal", "yank_line") => Some(runtime_keymap.vim_normal.yank_line.as_slice()), ("vim_normal", "paste_after") => Some(runtime_keymap.vim_normal.paste_after.as_slice()), + ("vim_normal", "select_register") => Some(runtime_keymap.vim_normal.select_register.as_slice()), ("vim_normal", "start_delete_operator") => Some(runtime_keymap.vim_normal.start_delete_operator.as_slice()), ("vim_normal", "start_yank_operator") => Some(runtime_keymap.vim_normal.start_yank_operator.as_slice()), ("vim_normal", "start_change_operator") => Some(runtime_keymap.vim_normal.start_change_operator.as_slice()), diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index a6b4e26cd0..c28d189747 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 124 actions, 1 customized, 2 unbound. + 125 actions, 1 customized, 2 unbound. [All] Common Customized (1) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index 9b659c749f..05bb5ad19e 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 125 actions, 0 customized, 3 unbound. + 126 actions, 0 customized, 3 unbound. [All] Common Customized (0) Unbound (3) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index c383460ceb..f103916de6 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,14 +2,14 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (124 selectable) +tab: All (125 selectable) tab: Common (19 selectable) tab: Customized (0) (0 selectable) tab: Unbound (2) (2 selectable) tab: App (9 selectable) tab: Composer (5 selectable) tab: Editor (17 selectable) -tab: Vim (65 selectable) +tab: Vim (66 selectable) tab: Navigation (20 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index b1371537b7..5963f99bca 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 124 actions, 0 customized, 2 unbound. + 125 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index b581150192..0850c189db 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 124 actions, 0 customized, 2 unbound. + 125 actions, 0 customized, 2 unbound. [All] Common Customized (0) Unbound (2) App Composer Editor Vim Navigation Approval Debug