mirror of
https://github.com/openai/codex.git
synced 2026-11-07 20:30:33 +00:00
## What changed - Add `tui.keymap.global.select_transcript` with an implicit `leader [` shortcut that follows the configured leader, respects explicit remapping or unbinding, and yields to existing bindings. - Connect transcript overlays to keyboard selection, including Find, mouse selection, and navigation to the beginning of paged history. Give selection commands priority over remapped pager shortcuts. - Close selection overlays on exit or after a confirmed copy-and-follow operation, preserving the pending close decision while clipboard confirmation is asynchronous. - Show selection navigation and copy hints that adapt to the available footer width. ## Testing Add coverage for shortcut dispatch, remapping, unbinding, and conflicts; overlay exit behavior; selection across history paging; mouse and keyboard selection interaction; and cursor and footer rendering snapshots. GitOrigin-RevId: 8a920e5fa2b06ff0544d829ba30f8ecb654db0ee
100 lines
3.2 KiB
Rust
100 lines
3.2 KiB
Rust
use super::*;
|
|
use codex_config::types::KeybindingSpec;
|
|
use codex_config::types::KeybindingsSpec;
|
|
use codex_config::types::TuiKeymap;
|
|
use crossterm::event::KeyCode;
|
|
use crossterm::event::KeyEvent;
|
|
use crossterm::event::KeyModifiers;
|
|
|
|
fn binding(spec: &str) -> KeybindingsSpec {
|
|
KeybindingsSpec::One(KeybindingSpec(spec.to_string()))
|
|
}
|
|
|
|
fn dispatch_select(
|
|
runtime: &RuntimeKeymap,
|
|
prefix: crossterm::event::KeyEvent,
|
|
contexts: KeymapContextSet,
|
|
) -> bool {
|
|
let mut matcher = KeyChordMatcher::default();
|
|
assert!(matches!(
|
|
matcher.advance(prefix, &runtime.chords, contexts),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
let KeyChordMatch::Completed(key) =
|
|
matcher.advance(KeyCode::Char('[').into(), &runtime.chords, contexts)
|
|
else {
|
|
return false;
|
|
};
|
|
runtime.app.select_transcript.is_pressed(key)
|
|
}
|
|
|
|
#[test]
|
|
fn select_transcript_uses_the_shared_leader_in_main_activity_and_overlay_contexts() {
|
|
let runtime = RuntimeKeymap::defaults();
|
|
for contexts in [
|
|
KeymapContextSet::new(KeymapContext::Global),
|
|
KeymapContextSet::activity(),
|
|
KeymapContextSet::transcript(),
|
|
] {
|
|
assert!(dispatch_select(
|
|
&runtime,
|
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
|
|
contexts
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn select_transcript_can_be_remapped_or_unbound() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.leader = Some(binding("f6"));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("remapped leader");
|
|
assert!(dispatch_select(
|
|
&runtime,
|
|
KeyCode::F(6).into(),
|
|
KeymapContextSet::new(KeymapContext::Global)
|
|
));
|
|
|
|
config.global.select_transcript = Some(KeybindingsSpec::Many(Vec::new()));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("unbound selection action");
|
|
assert!(runtime.app.select_transcript.is_empty());
|
|
assert!(
|
|
runtime
|
|
.chords
|
|
.bindings
|
|
.iter()
|
|
.all(|binding| binding.action.action != "select_transcript")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn default_select_suffix_yields_but_explicit_conflicts_fail() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("leader ["));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("existing leader consumer wins");
|
|
assert!(
|
|
runtime
|
|
.chords
|
|
.bindings
|
|
.iter()
|
|
.all(|binding| binding.action.action != "select_transcript")
|
|
);
|
|
|
|
config.global.open_transcript = None;
|
|
config.pager.scroll_up = Some(binding("ctrl-x"));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("leader prefix consumer wins");
|
|
assert!(
|
|
runtime
|
|
.chords
|
|
.bindings
|
|
.iter()
|
|
.all(|binding| binding.action.action != "select_transcript")
|
|
);
|
|
|
|
config.pager.scroll_up = None;
|
|
config.global.open_transcript = Some(binding("leader ["));
|
|
config.global.select_transcript = Some(binding("leader ["));
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("explicit conflict");
|
|
assert!(error.contains("select_transcript") && error.contains("open_transcript"));
|
|
}
|