Share editor keymaps across TUI composer components (#38837)

## What changed

- Store the editor portion of `RuntimeKeymap` in an `Arc` so the chat composer
  and its embedded textarea use the same keymap snapshot.
- Initialize the textarea from the composer's default runtime keymap and keep
  custom binding updates shared between both components.
- Use copy-on-write when tests and binding resolution need to mutate editor
  bindings.

## Testing

- Verify that applying custom editor bindings shares the same keymap allocation
  with the composer and textarea.

GitOrigin-RevId: 7f3b0afd6cc784e23deee8819e5c2c37a3709c68
This commit is contained in:
Benjamin Carlsson authored and copyberry committed 2026-08-16 05:59:34 +00:00
1 parent 73abda8bfe
commit 9bfaf7a076
5 files changed
+36 -19

No files matched your search

@@ -322,6 +322,7 @@ use std::collections::HashSet;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::ops::Range; use std::ops::Range;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
@@ -515,7 +516,7 @@ pub(crate) struct ChatComposer {
toggle_shortcuts_keys: Vec<KeyBinding>, toggle_shortcuts_keys: Vec<KeyBinding>,
history_search_previous_keys: Vec<KeyBinding>, history_search_previous_keys: Vec<KeyBinding>,
history_search_next_keys: Vec<KeyBinding>, history_search_next_keys: Vec<KeyBinding>,
editor_keymap: EditorKeymap, editor_keymap: Arc<EditorKeymap>,
vim_normal_keymap: VimNormalKeymap, vim_normal_keymap: VimNormalKeymap,
} }
@@ -707,6 +708,7 @@ impl ChatComposer {
editor_keymap: default_editor_keymap, editor_keymap: default_editor_keymap,
vim_normal_keymap: default_vim_normal_keymap, vim_normal_keymap: default_vim_normal_keymap,
}; };
this.draft.textarea.set_keymap_bindings(&default_keymap);
// Apply configuration via the setter to keep side-effects centralized. // Apply configuration via the setter to keep side-effects centralized.
this.set_disable_paste_burst(disable_paste_burst); this.set_disable_paste_burst(disable_paste_burst);
this this
@@ -11222,8 +11224,10 @@ mod tests {
assert!(matches!(result, InputResult::Submitted { .. })); assert!(matches!(result, InputResult::Submitted { .. }));
let mut keymap = RuntimeKeymap::defaults(); let mut keymap = RuntimeKeymap::defaults();
keymap.editor.move_up = vec![key_hint::plain(KeyCode::F(2))]; Arc::make_mut(&mut keymap.editor).move_up = vec![key_hint::plain(KeyCode::F(2))];
composer.set_keymap_bindings(&keymap); composer.set_keymap_bindings(&keymap);
assert!(Arc::ptr_eq(&composer.editor_keymap, &keymap.editor));
assert_eq!(Arc::strong_count(&keymap.editor), 3);
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
assert!(composer.draft.textarea.is_empty()); assert!(composer.draft.textarea.is_empty());
+8 -7
View File
@@ -39,6 +39,7 @@ use std::borrow::Cow;
use std::cell::Ref; use std::cell::Ref;
use std::cell::RefCell; use std::cell::RefCell;
use std::ops::Range; use std::ops::Range;
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
mod vim; mod vim;
@@ -127,7 +128,7 @@ pub(crate) struct TextArea {
vim_enabled: bool, vim_enabled: bool,
vim_mode: VimMode, vim_mode: VimMode,
vim_pending: VimPending, vim_pending: VimPending,
editor_keymap: EditorKeymap, editor_keymap: Arc<EditorKeymap>,
vim_normal_keymap: VimNormalKeymap, vim_normal_keymap: VimNormalKeymap,
vim_operator_keymap: VimOperatorKeymap, vim_operator_keymap: VimOperatorKeymap,
vim_text_object_keymap: VimTextObjectKeymap, vim_text_object_keymap: VimTextObjectKeymap,
@@ -182,7 +183,7 @@ impl TextArea {
/// the kill buffer, so callers can safely apply a live config update while /// the kill buffer, so callers can safely apply a live config update while
/// preserving the current draft exactly as typed. /// preserving the current draft exactly as typed.
pub fn set_keymap_bindings(&mut self, keymap: &RuntimeKeymap) { pub fn set_keymap_bindings(&mut self, keymap: &RuntimeKeymap) {
self.editor_keymap = keymap.editor.clone(); self.editor_keymap = Arc::clone(&keymap.editor);
self.vim_normal_keymap = keymap.vim_normal.clone(); self.vim_normal_keymap = keymap.vim_normal.clone();
self.vim_operator_keymap = keymap.vim_operator.clone(); self.vim_operator_keymap = keymap.vim_operator.clone();
self.vim_text_object_keymap = keymap.vim_text_object.clone(); self.vim_text_object_keymap = keymap.vim_text_object.clone();
@@ -2971,8 +2972,8 @@ mod tests {
let mut t = ta_with("abc\ndef\nghi"); let mut t = ta_with("abc\ndef\nghi");
t.set_cursor(/*pos*/ 5); t.set_cursor(/*pos*/ 5);
let mut keymap = RuntimeKeymap::defaults().editor; let mut keymap = RuntimeKeymap::defaults().editor;
keymap.kill_line_start.clear(); Arc::make_mut(&mut keymap).kill_line_start.clear();
keymap.kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))]; Arc::make_mut(&mut keymap).kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))];
t.input_with_keymap( t.input_with_keymap(
KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
@@ -3222,7 +3223,7 @@ mod tests {
let mut t = ta_with("a\nb"); let mut t = ta_with("a\nb");
t.set_cursor(/*pos*/ 2); t.set_cursor(/*pos*/ 2);
let mut keymap = RuntimeKeymap::defaults().editor; let mut keymap = RuntimeKeymap::defaults().editor;
keymap.move_up.clear(); Arc::make_mut(&mut keymap).move_up.clear();
t.input_with_keymap( t.input_with_keymap(
KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE), KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE),
@@ -3237,8 +3238,8 @@ mod tests {
let mut t = ta_with("a\nb"); let mut t = ta_with("a\nb");
t.set_cursor(/*pos*/ 0); t.set_cursor(/*pos*/ 0);
let mut keymap = RuntimeKeymap::defaults().editor; let mut keymap = RuntimeKeymap::defaults().editor;
keymap.move_up.clear(); Arc::make_mut(&mut keymap).move_up.clear();
keymap.move_down = vec![crate::key_hint::ctrl(KeyCode::Char('p'))]; Arc::make_mut(&mut keymap).move_down = vec![crate::key_hint::ctrl(KeyCode::Char('p'))];
t.input_with_keymap( t.input_with_keymap(
KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE), KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE),
+5 -5
View File
@@ -63,7 +63,7 @@ pub(crate) struct RuntimeKeymap {
pub(crate) chords: Arc<RuntimeChordKeymap>, pub(crate) chords: Arc<RuntimeChordKeymap>,
pub(crate) chat: ChatKeymap, pub(crate) chat: ChatKeymap,
pub(crate) composer: ComposerKeymap, pub(crate) composer: ComposerKeymap,
pub(crate) editor: EditorKeymap, pub(crate) editor: Arc<EditorKeymap>,
pub(crate) vim_normal: VimNormalKeymap, pub(crate) vim_normal: VimNormalKeymap,
pub(crate) vim_operator: VimOperatorKeymap, pub(crate) vim_operator: VimOperatorKeymap,
pub(crate) vim_text_object: VimTextObjectKeymap, pub(crate) vim_text_object: VimTextObjectKeymap,
@@ -625,7 +625,7 @@ impl RuntimeKeymap {
history_search_next: resolve_local!(keymap, defaults, composer, history_search_next), history_search_next: resolve_local!(keymap, defaults, composer, history_search_next),
}; };
let editor = EditorKeymap { let editor = Arc::new(EditorKeymap {
insert_newline: resolve_local!(keymap, defaults, editor, insert_newline), insert_newline: resolve_local!(keymap, defaults, editor, insert_newline),
move_left: resolve_local!(keymap, defaults, editor, move_left), move_left: resolve_local!(keymap, defaults, editor, move_left),
move_right: resolve_local!(keymap, defaults, editor, move_right), move_right: resolve_local!(keymap, defaults, editor, move_right),
@@ -643,7 +643,7 @@ impl RuntimeKeymap {
kill_whole_line: resolve_local!(keymap, defaults, editor, kill_whole_line), kill_whole_line: resolve_local!(keymap, defaults, editor, kill_whole_line),
kill_line_end: resolve_local!(keymap, defaults, editor, kill_line_end), kill_line_end: resolve_local!(keymap, defaults, editor, kill_line_end),
yank: resolve_local!(keymap, defaults, editor, yank), yank: resolve_local!(keymap, defaults, editor, yank),
}; });
let mut vim_normal = VimNormalKeymap { let mut vim_normal = VimNormalKeymap {
enter_insert: resolve_local!(keymap, defaults, vim_normal, enter_insert), enter_insert: resolve_local!(keymap, defaults, vim_normal, enter_insert),
@@ -1127,7 +1127,7 @@ impl RuntimeKeymap {
history_search_previous: default_bindings![ctrl(KeyCode::Char('r'))], history_search_previous: default_bindings![ctrl(KeyCode::Char('r'))],
history_search_next: default_bindings![ctrl(KeyCode::Char('s'))], history_search_next: default_bindings![ctrl(KeyCode::Char('s'))],
}, },
editor: EditorKeymap { editor: Arc::new(EditorKeymap {
insert_newline: default_bindings![ insert_newline: default_bindings![
ctrl(KeyCode::Char('j')), ctrl(KeyCode::Char('j')),
ctrl(KeyCode::Char('m')), ctrl(KeyCode::Char('m')),
@@ -1187,7 +1187,7 @@ impl RuntimeKeymap {
kill_whole_line: default_bindings![], kill_whole_line: default_bindings![],
kill_line_end: default_bindings![ctrl(KeyCode::Char('k'))], kill_line_end: default_bindings![ctrl(KeyCode::Char('k'))],
yank: default_bindings![ctrl(KeyCode::Char('y'))], yank: default_bindings![ctrl(KeyCode::Char('y'))],
}, }),
vim_normal: VimNormalKeymap { vim_normal: VimNormalKeymap {
enter_insert: default_bindings![plain(KeyCode::Char('i')), plain(KeyCode::Insert)], enter_insert: default_bindings![plain(KeyCode::Char('i')), plain(KeyCode::Insert)],
append_after_cursor: default_bindings![plain(KeyCode::Char('a'))], append_after_cursor: default_bindings![plain(KeyCode::Char('a'))],
+11 -1
View File
@@ -8,6 +8,7 @@ use super::RuntimeKeymap;
use crate::key_hint::KeyBinding; use crate::key_hint::KeyBinding;
use codex_config::types::KeybindingsSpec; use codex_config::types::KeybindingsSpec;
use codex_config::types::TuiKeymap; use codex_config::types::TuiKeymap;
use std::sync::Arc;
/// Config context in which a keymap action is active. /// Config context in which a keymap action is active.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -94,6 +95,15 @@ pub(super) struct RuntimeActionBinding<'a> {
pub(super) bindings: &'a [KeyBinding], pub(super) bindings: &'a [KeyBinding],
} }
macro_rules! runtime_group_mut {
($runtime_keymap:expr, editor) => {
Arc::make_mut(&mut $runtime_keymap.editor)
};
($runtime_keymap:expr, $group:ident) => {
&mut $runtime_keymap.$group
};
}
macro_rules! define_runtime_action_bindings { macro_rules! define_runtime_action_bindings {
($($context:literal => $context_id:ident, $group:ident, $config_group:ident [$($action:ident),+ $(,)?]),+ $(,)?) => { ($($context:literal => $context_id:ident, $group:ident, $config_group:ident [$($action:ident),+ $(,)?]),+ $(,)?) => {
/// Resolve a config context/action pair to its runtime identity. /// Resolve a config context/action pair to its runtime identity.
@@ -176,7 +186,7 @@ macro_rules! define_runtime_action_bindings {
$( $(
$( $(
($context, stringify!($action)) => { ($context, stringify!($action)) => {
runtime_keymap.$group.$action.push(binding); runtime_group_mut!(runtime_keymap, $group).$action.push(binding);
true true
} }
)+ )+
+6 -4
View File
@@ -5,6 +5,7 @@ use crossterm::event::KeyModifiers;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::Rect; use ratatui::layout::Rect;
use std::sync::Arc;
use tokio::sync::mpsc::unbounded_channel; use tokio::sync::mpsc::unbounded_channel;
use super::StartupDraftInitialScreen; use super::StartupDraftInitialScreen;
@@ -483,10 +484,11 @@ fn startup_draft_allows_local_editor_shortcuts_without_startup_actions() {
assert_eq!(pump.bottom_pane.composer_text(), "first "); assert_eq!(pump.bottom_pane.composer_text(), "first ");
let mut keymap = crate::keymap::RuntimeKeymap::defaults(); let mut keymap = crate::keymap::RuntimeKeymap::defaults();
keymap.editor.move_line_start = vec![crate::key_hint::ctrl(KeyCode::Char('z'))]; let editor = Arc::make_mut(&mut keymap.editor);
keymap.editor.move_line_end = vec![crate::key_hint::ctrl(KeyCode::Char('v'))]; editor.move_line_start = vec![crate::key_hint::ctrl(KeyCode::Char('z'))];
keymap.editor.move_left = vec![crate::key_hint::ctrl(KeyCode::Char('s'))]; editor.move_line_end = vec![crate::key_hint::ctrl(KeyCode::Char('v'))];
keymap.editor.insert_newline = vec![crate::key_hint::plain(KeyCode::Enter)]; editor.move_left = vec![crate::key_hint::ctrl(KeyCode::Char('s'))];
editor.insert_newline = vec![crate::key_hint::plain(KeyCode::Enter)];
keymap.composer.submit = vec![crate::key_hint::ctrl(KeyCode::Char('s'))]; keymap.composer.submit = vec![crate::key_hint::ctrl(KeyCode::Char('s'))];
pump.bottom_pane.set_keymap_bindings(&keymap); pump.bottom_pane.set_keymap_bindings(&keymap);
handle_startup_draft_key( handle_startup_draft_key(