mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## What changed - Add incremental, case-insensitive literal search across transcript details, loading older history as needed. Use `F3` to start Find, or `/` in the transcript pager, and navigate matches in either direction. Closing Find restores the previous presentation and reading position. - Add clickable activity detail controls and `F4` keyboard focus for inspecting individual activity groups in the owned transcript. Preserve expansion state as live activity becomes history. - Expose configurable `global.find_transcript`, `global.focus_activity`, and `pager.find` bindings under `tui.keymap`. Let new defaults yield to existing custom bindings and show available completions for pending chords. ## Testing Add regression coverage for Unicode matching, bounded scanning, paginated and changing history, selection during search, composer input isolation, activity controls, and custom shortcut conflicts. GitOrigin-RevId: 5bcbcdfbca7c5c296c635ec9b079ccd7a908e336
598 lines
20 KiB
Rust
598 lines
20 KiB
Rust
use super::KeyChordMatch;
|
|
use super::KeyChordMatcher;
|
|
use super::KeymapContextSet;
|
|
use super::dispatch_binding;
|
|
use crate::key_hint;
|
|
use crate::key_hint::KeyBindingListExt;
|
|
use crate::keymap::RuntimeKeymap;
|
|
use crate::keymap::bindings::KeymapContext;
|
|
use crate::keymap::bindings::keymap_action_ids;
|
|
use crate::keymap::keymap_action_id;
|
|
use crate::keymap::user_bindings;
|
|
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;
|
|
use pretty_assertions::assert_eq;
|
|
use std::collections::HashSet;
|
|
|
|
fn binding(spec: &str) -> KeybindingsSpec {
|
|
KeybindingsSpec::One(KeybindingSpec(spec.to_string()))
|
|
}
|
|
|
|
fn bindings(specs: &[&str]) -> KeybindingsSpec {
|
|
KeybindingsSpec::Many(
|
|
specs
|
|
.iter()
|
|
.map(|spec| KeybindingSpec((*spec).to_string()))
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_fixed_resume_picker_keys_inside_list_chords() {
|
|
for (chord, reserved_action) in [
|
|
("ctrl-c home", "cancel"),
|
|
("ctrl-t home", "resume_picker.toggle_transcript"),
|
|
("ctrl-e home", "resume_picker.toggle_expansion"),
|
|
("ctrl-o home", "resume_picker.toggle_density"),
|
|
] {
|
|
let mut config = TuiKeymap::default();
|
|
config.list.jump_top = Some(binding(chord));
|
|
|
|
let error = RuntimeKeymap::from_config(&config)
|
|
.expect_err("fixed picker actions must remain available");
|
|
|
|
assert!(error.contains(reserved_action), "{error}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_ctrl_c_inside_approval_chords() {
|
|
let mut config = TuiKeymap::default();
|
|
config.approval.approve = Some(binding("ctrl-x ctrl-c"));
|
|
|
|
let error =
|
|
RuntimeKeymap::from_config(&config).expect_err("Ctrl+C must continue cancelling approvals");
|
|
|
|
assert!(error.contains("cancel"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn resolves_chords_for_actions_in_different_contexts() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(bindings(&["ctrl-t", "ctrl-x ctrl-t"]));
|
|
config.composer.submit = Some(binding("ctrl-x enter"));
|
|
config.list.jump_top = Some(binding("ctrl-x home"));
|
|
|
|
let actual = RuntimeKeymap::from_config(&config).expect("valid chord keymap");
|
|
|
|
assert_eq!(
|
|
user_bindings(&actual.app.open_transcript),
|
|
vec![key_hint::ctrl(KeyCode::Char('t'))]
|
|
);
|
|
for (context, action, expected) in [
|
|
(
|
|
"global",
|
|
"open_transcript",
|
|
vec!["ctrl-t".to_string(), "ctrl-x ctrl-t".to_string()],
|
|
),
|
|
("composer", "submit", vec!["ctrl-x enter".to_string()]),
|
|
("list", "jump_top", vec!["ctrl-x home".to_string()]),
|
|
] {
|
|
let action = keymap_action_id(context, action).expect("known action");
|
|
assert_eq!(
|
|
actual.chords.configured_specs(action),
|
|
Some(expected.as_slice())
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn default_vim_jump_top_uses_contextual_gg_chords() {
|
|
let runtime = RuntimeKeymap::defaults();
|
|
let g = key_event(key_hint::plain(KeyCode::Char('g')));
|
|
|
|
for (context, action, target) in [
|
|
(
|
|
KeymapContext::VimNormal,
|
|
"jump_top",
|
|
runtime.vim_normal.jump_top.as_slice(),
|
|
),
|
|
(
|
|
KeymapContext::VimOperator,
|
|
"motion_jump_top",
|
|
runtime.vim_operator.motion_jump_top.as_slice(),
|
|
),
|
|
] {
|
|
let action_id = keymap_action_id(context.config_name(), action).expect("known Vim action");
|
|
assert!(runtime.chords.bindings.iter().any(|binding| {
|
|
binding.action == action_id
|
|
&& binding.chord.prefix == key_hint::plain(KeyCode::Char('g'))
|
|
&& binding.chord.completion == key_hint::plain(KeyCode::Char('g'))
|
|
}));
|
|
assert!(matches!(
|
|
runtime.primary_hint(context, action),
|
|
Some(crate::key_hint::ShortcutHint::Chord { .. })
|
|
));
|
|
|
|
let mut matcher = KeyChordMatcher::default();
|
|
let contexts = KeymapContextSet::new(context);
|
|
assert!(matches!(
|
|
matcher.advance(g, &runtime.chords, contexts),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
let KeyChordMatch::Completed(event) = matcher.advance(g, &runtime.chords, contexts) else {
|
|
panic!("gg must dispatch the active Vim jump action");
|
|
};
|
|
assert!(target.is_pressed(event));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn default_vim_jump_chords_yield_to_configured_singles_and_chords() {
|
|
let mut config = TuiKeymap::default();
|
|
config.vim_normal.move_line_start = Some(binding("g g"));
|
|
config.vim_operator.motion_line_start = Some(binding("g"));
|
|
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("configured legacy bindings win");
|
|
|
|
assert!(runtime.vim_normal.jump_top.is_empty());
|
|
assert!(runtime.vim_operator.motion_jump_top.is_empty());
|
|
assert!(
|
|
runtime
|
|
.vim_operator
|
|
.motion_line_start
|
|
.is_pressed(key_event(key_hint::plain(KeyCode::Char('g'))))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vim_jump_top_supports_single_and_custom_chord_remaps() {
|
|
let mut config = TuiKeymap::default();
|
|
config.vim_normal.jump_top = Some(binding("home"));
|
|
config.vim_operator.motion_jump_top = Some(binding("ctrl-x g"));
|
|
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("custom Vim jumps are valid");
|
|
|
|
assert!(runtime.vim_normal.jump_top.is_pressed(KeyCode::Home.into()));
|
|
let action = keymap_action_id("vim_operator", "motion_jump_top").expect("known Vim action");
|
|
assert!(runtime.chords.bindings.iter().any(|binding| {
|
|
binding.action == action
|
|
&& binding.chord.prefix == key_hint::ctrl(KeyCode::Char('x'))
|
|
&& binding.chord.completion == key_hint::plain(KeyCode::Char('g'))
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn composer_chord_inherits_global_fallback() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.submit = Some(binding("ctrl-x enter"));
|
|
|
|
let actual = RuntimeKeymap::from_config(&config).expect("valid global fallback chord");
|
|
let action = keymap_action_id("composer", "submit").expect("known action");
|
|
|
|
assert_eq!(
|
|
actual.chords.configured_specs(action),
|
|
Some(["ctrl-x enter".to_string()].as_slice())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn allows_shared_prefixes_with_distinct_completions() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-x ctrl-t"));
|
|
config.global.copy = Some(binding("ctrl-x ctrl-o"));
|
|
|
|
RuntimeKeymap::from_config(&config).expect("prefix trie can branch on the second stroke");
|
|
}
|
|
|
|
#[test]
|
|
fn allows_plain_prefixes_in_vim_and_repeated_chords_in_all_contexts() {
|
|
let mut config = TuiKeymap::default();
|
|
config.vim_normal.move_line_start = Some(binding("g g"));
|
|
config.vim_operator.motion_line_start = Some(binding("g g"));
|
|
config.global.open_transcript = Some(binding("ctrl-t ctrl-t"));
|
|
|
|
RuntimeKeymap::from_config(&config)
|
|
.expect("Vim owns printable prefixes and all contexts support repeated chords");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_plain_printable_prefixes_outside_vim() {
|
|
let mut config = TuiKeymap::default();
|
|
config.composer.submit = Some(binding("x enter"));
|
|
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("typing must not start a chord");
|
|
|
|
assert!(error.contains("outside Vim"), "{error}");
|
|
assert!(error.contains("ordinary text input"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_prefixes_that_shadow_overlapping_single_bindings() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-k ctrl-t"));
|
|
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("ctrl-k already edits text");
|
|
|
|
assert!(error.contains("editor.kill_line_end"), "{error}");
|
|
assert!(error.contains("ctrl-k ctrl-t"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn permits_the_same_chord_in_non_overlapping_contexts() {
|
|
let mut config = TuiKeymap::default();
|
|
config.editor.move_line_start = Some(binding("ctrl-x home"));
|
|
config.list.jump_top = Some(binding("ctrl-x home"));
|
|
|
|
RuntimeKeymap::from_config(&config)
|
|
.expect("editor and list contexts are never active together");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_duplicate_chords_in_overlapping_contexts() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-x ctrl-t"));
|
|
config.composer.submit = Some(binding("ctrl-x ctrl-t"));
|
|
|
|
let error =
|
|
RuntimeKeymap::from_config(&config).expect_err("one active chord must identify one action");
|
|
|
|
assert!(error.contains("global.open_transcript"), "{error}");
|
|
assert!(error.contains("composer.submit"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_duplicate_chat_and_list_chords_used_by_request_user_input() {
|
|
let mut config = TuiKeymap::default();
|
|
config.chat.interrupt_turn = Some(binding("ctrl-x home"));
|
|
config.list.jump_top = Some(binding("ctrl-x home"));
|
|
|
|
let error =
|
|
RuntimeKeymap::from_config(&config).expect_err("request input activates both contexts");
|
|
|
|
assert!(error.contains("chat.interrupt_turn"), "{error}");
|
|
assert!(error.contains("list.jump_top"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn normalizes_ctrl_slash_terminal_aliases() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-7 ctrl-t"));
|
|
|
|
let error = RuntimeKeymap::from_config(&config)
|
|
.expect_err("Ctrl-7 must remain the Ctrl-/ side-conversation alias");
|
|
|
|
assert!(error.contains("global.toggle_side_conversation"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_alias_equivalent_duplicate_chords() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-x ctrl-/"));
|
|
config.global.copy = Some(binding("ctrl-x ctrl-7"));
|
|
config.global.toggle_side_conversation = Some(KeybindingsSpec::Many(Vec::new()));
|
|
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("aliases must identify one action");
|
|
|
|
assert!(error.contains("global.open_transcript"), "{error}");
|
|
assert!(error.contains("global.copy"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_plain_escape_inside_a_chord() {
|
|
let mut config = TuiKeymap::default();
|
|
config.list.cancel = Some(binding("ctrl-x esc"));
|
|
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("Esc cancels pending chords");
|
|
|
|
assert!(error.contains("reserved for cancelling"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_reserved_interrupt_keys_inside_a_main_chord() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-x ctrl-c"));
|
|
|
|
let error = RuntimeKeymap::from_config(&config).expect_err("ctrl-c must remain available");
|
|
|
|
assert!(error.contains("fixed.interrupt_or_quit"), "{error}");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn rejects_unix_suspend_key_in_any_chord_stroke() {
|
|
for chord in ["ctrl-z ctrl-t", "ctrl-x ctrl-z"] {
|
|
let mut config = TuiKeymap::default();
|
|
config.list.jump_top = Some(binding(chord));
|
|
|
|
let error = RuntimeKeymap::from_config(&config)
|
|
.expect_err("the Unix suspend key must not be consumed");
|
|
|
|
assert!(error.contains("suspending"), "{error}");
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn rejects_windows_altgr_prefixes_outside_vim() {
|
|
let mut config = TuiKeymap::default();
|
|
config.composer.submit = Some(binding("ctrl-alt-x enter"));
|
|
|
|
let error =
|
|
RuntimeKeymap::from_config(&config).expect_err("AltGr may be ordinary Windows text input");
|
|
|
|
assert!(error.contains("AltGr"), "{error}");
|
|
}
|
|
|
|
#[test]
|
|
fn dispatch_tokens_are_unique_and_cover_the_action_inventory() {
|
|
let mut tokens = HashSet::new();
|
|
let actions = keymap_action_ids().collect::<Vec<_>>();
|
|
|
|
for action in &actions {
|
|
assert!(
|
|
tokens.insert(dispatch_binding(*action).expect("action inventory must fit token band")),
|
|
"duplicate token for {}",
|
|
action.config_path()
|
|
);
|
|
}
|
|
|
|
assert_eq!(tokens.len(), actions.len());
|
|
}
|
|
|
|
#[test]
|
|
fn physical_dispatch_tokens_do_not_clear_pending_chords() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-x ctrl-t"));
|
|
let keymap = RuntimeKeymap::from_config(&config).expect("valid chord keymap");
|
|
let contexts = KeymapContextSet::new(KeymapContext::Global);
|
|
let mut matcher = KeyChordMatcher::default();
|
|
|
|
assert!(matches!(
|
|
matcher.advance(
|
|
key_event(key_hint::ctrl(KeyCode::Char('x'))),
|
|
&keymap.chords,
|
|
contexts
|
|
),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
|
|
let action = keymap_action_id("global", "open_transcript").expect("known keymap action");
|
|
let token = dispatch_binding(action).expect("internal dispatch token");
|
|
assert_eq!(
|
|
matcher.advance(key_event(token), &keymap.chords, contexts),
|
|
KeyChordMatch::Ignored
|
|
);
|
|
assert!(matcher.is_pending());
|
|
assert!(matches!(
|
|
matcher.advance(
|
|
key_event(key_hint::ctrl(KeyCode::Char('t'))),
|
|
&keymap.chords,
|
|
contexts
|
|
),
|
|
KeyChordMatch::Completed(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn matching_chords_dispatches_the_active_context_action() {
|
|
let mut config = TuiKeymap::default();
|
|
config.global.open_transcript = Some(binding("ctrl-t ctrl-t"));
|
|
config.editor.move_line_start = Some(binding("ctrl-x home"));
|
|
config.list.jump_top = Some(binding("ctrl-x home"));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("non-overlapping chord contexts");
|
|
|
|
for (context, prefix, completion, target, other) in [
|
|
(
|
|
KeymapContext::Global,
|
|
key_event(key_hint::ctrl(KeyCode::Char('t'))),
|
|
key_event(key_hint::ctrl(KeyCode::Char('t'))),
|
|
runtime.app.open_transcript.as_slice(),
|
|
runtime.app.copy.as_slice(),
|
|
),
|
|
(
|
|
KeymapContext::Editor,
|
|
key_event(key_hint::ctrl(KeyCode::Char('x'))),
|
|
KeyEvent::new(KeyCode::Home, crossterm::event::KeyModifiers::NONE),
|
|
runtime.editor.move_line_start.as_slice(),
|
|
runtime.list.jump_top.as_slice(),
|
|
),
|
|
(
|
|
KeymapContext::List,
|
|
key_event(key_hint::ctrl(KeyCode::Char('x'))),
|
|
KeyEvent::new(KeyCode::Home, crossterm::event::KeyModifiers::NONE),
|
|
runtime.list.jump_top.as_slice(),
|
|
runtime.editor.move_line_start.as_slice(),
|
|
),
|
|
] {
|
|
let mut matcher = KeyChordMatcher::default();
|
|
assert!(matches!(
|
|
matcher.advance(prefix, &runtime.chords, KeymapContextSet::new(context)),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
let KeyChordMatch::Completed(dispatch_event) =
|
|
matcher.advance(completion, &runtime.chords, KeymapContextSet::new(context))
|
|
else {
|
|
panic!("second stroke must complete the active context chord");
|
|
};
|
|
assert!(target.is_pressed(dispatch_event));
|
|
assert!(!other.is_pressed(dispatch_event));
|
|
}
|
|
}
|
|
|
|
#[tokio::test(start_paused = true)]
|
|
async fn matcher_cancels_on_escape_context_change_and_timeout() {
|
|
let mut config = TuiKeymap::default();
|
|
config.list.jump_top = Some(binding("ctrl-x ctrl-t"));
|
|
let runtime = RuntimeKeymap::from_config(&config).expect("valid list chord");
|
|
let list = KeymapContextSet::new(KeymapContext::List);
|
|
let pager = KeymapContextSet::new(KeymapContext::Pager);
|
|
let prefix = key_event(key_hint::ctrl(KeyCode::Char('x')));
|
|
|
|
let mut matcher = KeyChordMatcher::default();
|
|
assert!(matches!(
|
|
matcher.advance(prefix, &runtime.chords, list),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
assert_eq!(
|
|
matcher.advance(
|
|
KeyEvent::new(KeyCode::Esc, crossterm::event::KeyModifiers::NONE),
|
|
&runtime.chords,
|
|
list
|
|
),
|
|
KeyChordMatch::Cancelled
|
|
);
|
|
|
|
assert!(matches!(
|
|
matcher.advance(prefix, &runtime.chords, list),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
assert!(matcher.expire(pager));
|
|
matcher.advance(prefix, &runtime.chords, list);
|
|
tokio::time::advance(super::KEY_CHORD_TIMEOUT).await;
|
|
assert_eq!(
|
|
matcher.advance(
|
|
key_event(key_hint::ctrl(KeyCode::Char('t'))),
|
|
&runtime.chords,
|
|
list
|
|
),
|
|
KeyChordMatch::PassThrough,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn context_transition_discards_prefix_before_routing_the_next_stroke() {
|
|
let mut config = TuiKeymap::default();
|
|
config.list.jump_top = Some(binding("ctrl-x home"));
|
|
config.pager.jump_top = Some(binding("ctrl-x home"));
|
|
let runtime =
|
|
RuntimeKeymap::from_config(&config).expect("non-overlapping list and pager chords");
|
|
let mut matcher = KeyChordMatcher::default();
|
|
assert!(matches!(
|
|
matcher.advance(
|
|
key_event(key_hint::ctrl(KeyCode::Char('x'))),
|
|
&runtime.chords,
|
|
KeymapContextSet::new(KeymapContext::List),
|
|
),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
assert_eq!(
|
|
matcher.advance(
|
|
KeyCode::Home.into(),
|
|
&runtime.chords,
|
|
KeymapContextSet::new(KeymapContext::Pager),
|
|
),
|
|
KeyChordMatch::PassThrough
|
|
);
|
|
assert!(!matcher.is_pending());
|
|
}
|
|
|
|
fn key_event(binding: crate::key_hint::KeyBinding) -> KeyEvent {
|
|
let (code, modifiers) = binding.parts();
|
|
KeyEvent::new(code, modifiers)
|
|
}
|
|
|
|
#[test]
|
|
fn activity_context_preserves_list_chords_and_its_remapped_focus_toggle() {
|
|
let config = serde_json::from_value(serde_json::json!({
|
|
"global": {"copy": "ctrl-x c", "focus_activity": "f12 t"},
|
|
"list": {"jump_top": "ctrl-x c"},
|
|
}))
|
|
.unwrap();
|
|
let runtime =
|
|
RuntimeKeymap::from_config(&config).expect("existing disjoint contexts remain valid");
|
|
for (contexts, prefix, completion, target, other) in [
|
|
(
|
|
KeymapContextSet::new(KeymapContext::Global),
|
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
|
|
KeyCode::Char('c'),
|
|
&runtime.app.copy,
|
|
&runtime.list.jump_top,
|
|
),
|
|
(
|
|
KeymapContextSet::activity(),
|
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
|
|
KeyCode::Char('c'),
|
|
&runtime.list.jump_top,
|
|
&runtime.app.copy,
|
|
),
|
|
(
|
|
KeymapContextSet::activity(),
|
|
KeyCode::F(12).into(),
|
|
KeyCode::Char('t'),
|
|
&runtime.app.focus_activity,
|
|
&runtime.list.jump_top,
|
|
),
|
|
] {
|
|
let mut matcher = KeyChordMatcher::default();
|
|
assert!(matches!(
|
|
matcher.advance(prefix, &runtime.chords, contexts),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
let KeyChordMatch::Completed(event) =
|
|
matcher.advance(completion.into(), &runtime.chords, contexts)
|
|
else {
|
|
panic!("active action chord must complete");
|
|
};
|
|
assert!(target.is_pressed(event));
|
|
assert!(!other.is_pressed(event));
|
|
}
|
|
|
|
let mut matcher = KeyChordMatcher::default();
|
|
matcher.advance(
|
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
|
|
&runtime.chords,
|
|
KeymapContextSet::activity(),
|
|
);
|
|
assert_eq!(
|
|
matcher.pending_hint_items(&runtime.chords, /*width*/ 80),
|
|
Some(vec![
|
|
("ctrl+x".to_string(), "then".to_string()),
|
|
("c".to_string(), "jump top".to_string()),
|
|
("esc".to_string(), "cancel".to_string()),
|
|
]),
|
|
);
|
|
matcher.cancel();
|
|
let combined = KeymapContextSet::new(KeymapContext::List).with(KeymapContext::Global);
|
|
assert!(matches!(
|
|
matcher.advance(KeyCode::F(12).into(), &runtime.chords, combined),
|
|
KeyChordMatch::Pending(_)
|
|
));
|
|
assert!(matcher.expire(KeymapContextSet::activity()));
|
|
assert!(!matcher.is_pending());
|
|
}
|
|
|
|
#[test]
|
|
fn pending_hint_skips_whole_oversized_configured_completions() {
|
|
let config = serde_json::from_value(serde_json::json!({"global": {
|
|
"copy": "ctrl-x ctrl-alt-shift-f12", "focus_activity": "ctrl-x alt-shift-f11"
|
|
}}))
|
|
.unwrap();
|
|
let runtime = RuntimeKeymap::from_config(&config).unwrap();
|
|
let mut matcher = KeyChordMatcher::default();
|
|
matcher.advance(
|
|
KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL),
|
|
&runtime.chords,
|
|
KeymapContextSet::new(KeymapContext::Global),
|
|
);
|
|
let mut scenes = Vec::new();
|
|
for width in [80, 32, 12] {
|
|
let hints = matcher.pending_hint_items(&runtime.chords, width).unwrap();
|
|
let line = crate::bottom_pane::footer_hint_items_line(&hints);
|
|
assert!(line.width() <= usize::from(width));
|
|
scenes.push(format!("{width}: {line}"));
|
|
}
|
|
assert_eq!(
|
|
scenes,
|
|
[
|
|
"80: ctrl+x then · shift+⌥+f11 activity · ctrl+shift+⌥+f12 copy · esc cancel",
|
|
"32: shift+⌥+f11 inspect · esc cancel",
|
|
"12: esc cancel",
|
|
]
|
|
);
|
|
}
|