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
2026-08-16 05:59:34 +00:00
committed by copyberry
parent 73abda8bfe
commit 9bfaf7a076
5 changed files with 36 additions and 19 deletions
@@ -322,6 +322,7 @@ use std::collections::HashSet;
use std::collections::VecDeque;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use std::time::Instant;
@@ -515,7 +516,7 @@ pub(crate) struct ChatComposer {
toggle_shortcuts_keys: Vec<KeyBinding>,
history_search_previous_keys: Vec<KeyBinding>,
history_search_next_keys: Vec<KeyBinding>,
editor_keymap: EditorKeymap,
editor_keymap: Arc<EditorKeymap>,
vim_normal_keymap: VimNormalKeymap,
}
@@ -707,6 +708,7 @@ impl ChatComposer {
editor_keymap: default_editor_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.
this.set_disable_paste_burst(disable_paste_burst);
this
@@ -11222,8 +11224,10 @@ mod tests {
assert!(matches!(result, InputResult::Submitted { .. }));
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);
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));
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::RefCell;
use std::ops::Range;
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;
mod vim;
@@ -127,7 +128,7 @@ pub(crate) struct TextArea {
vim_enabled: bool,
vim_mode: VimMode,
vim_pending: VimPending,
editor_keymap: EditorKeymap,
editor_keymap: Arc<EditorKeymap>,
vim_normal_keymap: VimNormalKeymap,
vim_operator_keymap: VimOperatorKeymap,
vim_text_object_keymap: VimTextObjectKeymap,
@@ -182,7 +183,7 @@ impl TextArea {
/// the kill buffer, so callers can safely apply a live config update while
/// preserving the current draft exactly as typed.
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_operator_keymap = keymap.vim_operator.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");
t.set_cursor(/*pos*/ 5);
let mut keymap = RuntimeKeymap::defaults().editor;
keymap.kill_line_start.clear();
keymap.kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))];
Arc::make_mut(&mut keymap).kill_line_start.clear();
Arc::make_mut(&mut keymap).kill_whole_line = vec![key_hint::ctrl(KeyCode::Char('u'))];
t.input_with_keymap(
KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL),
@@ -3222,7 +3223,7 @@ mod tests {
let mut t = ta_with("a\nb");
t.set_cursor(/*pos*/ 2);
let mut keymap = RuntimeKeymap::defaults().editor;
keymap.move_up.clear();
Arc::make_mut(&mut keymap).move_up.clear();
t.input_with_keymap(
KeyEvent::new(KeyCode::Char('\u{0010}'), KeyModifiers::NONE),
@@ -3237,8 +3238,8 @@ mod tests {
let mut t = ta_with("a\nb");
t.set_cursor(/*pos*/ 0);
let mut keymap = RuntimeKeymap::defaults().editor;
keymap.move_up.clear();
keymap.move_down = vec![crate::key_hint::ctrl(KeyCode::Char('p'))];
Arc::make_mut(&mut keymap).move_up.clear();
Arc::make_mut(&mut keymap).move_down = vec![crate::key_hint::ctrl(KeyCode::Char('p'))];
t.input_with_keymap(
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) chat: ChatKeymap,
pub(crate) composer: ComposerKeymap,
pub(crate) editor: EditorKeymap,
pub(crate) editor: Arc<EditorKeymap>,
pub(crate) vim_normal: VimNormalKeymap,
pub(crate) vim_operator: VimOperatorKeymap,
pub(crate) vim_text_object: VimTextObjectKeymap,
@@ -625,7 +625,7 @@ impl RuntimeKeymap {
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),
move_left: resolve_local!(keymap, defaults, editor, move_left),
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_line_end: resolve_local!(keymap, defaults, editor, kill_line_end),
yank: resolve_local!(keymap, defaults, editor, yank),
};
});
let mut vim_normal = VimNormalKeymap {
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_next: default_bindings![ctrl(KeyCode::Char('s'))],
},
editor: EditorKeymap {
editor: Arc::new(EditorKeymap {
insert_newline: default_bindings![
ctrl(KeyCode::Char('j')),
ctrl(KeyCode::Char('m')),
@@ -1187,7 +1187,7 @@ impl RuntimeKeymap {
kill_whole_line: default_bindings![],
kill_line_end: default_bindings![ctrl(KeyCode::Char('k'))],
yank: default_bindings![ctrl(KeyCode::Char('y'))],
},
}),
vim_normal: VimNormalKeymap {
enter_insert: default_bindings![plain(KeyCode::Char('i')), plain(KeyCode::Insert)],
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 codex_config::types::KeybindingsSpec;
use codex_config::types::TuiKeymap;
use std::sync::Arc;
/// Config context in which a keymap action is active.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
@@ -94,6 +95,15 @@ pub(super) struct RuntimeActionBinding<'a> {
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 {
($($context:literal => $context_id:ident, $group:ident, $config_group:ident [$($action:ident),+ $(,)?]),+ $(,)?) => {
/// Resolve a config context/action pair to its runtime identity.
@@ -176,7 +186,7 @@ macro_rules! define_runtime_action_bindings {
$(
$(
($context, stringify!($action)) => {
runtime_keymap.$group.$action.push(binding);
runtime_group_mut!(runtime_keymap, $group).$action.push(binding);
true
}
)+
+6 -4
View File
@@ -5,6 +5,7 @@ use crossterm::event::KeyModifiers;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use std::sync::Arc;
use tokio::sync::mpsc::unbounded_channel;
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 ");
let mut keymap = crate::keymap::RuntimeKeymap::defaults();
keymap.editor.move_line_start = vec![crate::key_hint::ctrl(KeyCode::Char('z'))];
keymap.editor.move_line_end = vec![crate::key_hint::ctrl(KeyCode::Char('v'))];
keymap.editor.move_left = vec![crate::key_hint::ctrl(KeyCode::Char('s'))];
keymap.editor.insert_newline = vec![crate::key_hint::plain(KeyCode::Enter)];
let editor = Arc::make_mut(&mut keymap.editor);
editor.move_line_start = vec![crate::key_hint::ctrl(KeyCode::Char('z'))];
editor.move_line_end = vec![crate::key_hint::ctrl(KeyCode::Char('v'))];
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'))];
pump.bottom_pane.set_keymap_bindings(&keymap);
handle_startup_draft_key(