mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
fix(tui): route empty vim insert escape to composer
Let Vim Insert handle plain `Esc` before the chat edit-previous and backtrack shortcut layers so an empty composer can still return to Normal mode. Keep existing empty-composer backtrack behavior after Vim has reached Normal mode, and cover both composer-local and app-level routing.
This commit is contained in:
@@ -153,7 +153,7 @@ impl App {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.keymap.chat.edit_previous_message.is_pressed(key_event) {
|
||||
if self.should_route_edit_previous_message(key_event) {
|
||||
// Esc primes/advances backtracking only in normal (not working) mode
|
||||
// with the composer focused and empty. In any other state, forward
|
||||
// Esc so the active UI (e.g. status indicator, modals, popups)
|
||||
@@ -226,4 +226,9 @@ impl App {
|
||||
pub(super) fn refresh_status_line(&mut self) {
|
||||
self.chat_widget.refresh_status_line();
|
||||
}
|
||||
|
||||
pub(super) fn should_route_edit_previous_message(&self, key_event: KeyEvent) -> bool {
|
||||
self.keymap.chat.edit_previous_message.is_pressed(key_event)
|
||||
&& !self.chat_widget.should_handle_vim_insert_escape(key_event)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4824,6 +4824,29 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft prompt");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn edit_previous_shortcut_does_not_steal_empty_vim_insert_escape() {
|
||||
let mut app = make_test_app().await;
|
||||
let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
|
||||
|
||||
assert!(app.chat_widget.composer_is_empty());
|
||||
assert!(app.should_route_edit_previous_message(esc));
|
||||
|
||||
app.chat_widget.toggle_vim_mode_and_notify();
|
||||
assert!(app.should_route_edit_previous_message(esc));
|
||||
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
assert!(app.chat_widget.should_handle_vim_insert_escape(esc));
|
||||
assert!(!app.should_route_edit_previous_message(esc));
|
||||
|
||||
app.chat_widget.handle_key_event(esc);
|
||||
|
||||
assert!(!app.backtrack.primed);
|
||||
assert!(!app.chat_widget.should_handle_vim_insert_escape(esc));
|
||||
assert!(app.should_route_edit_previous_message(esc));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_skips_when_no_usage_or_resume_hint() {
|
||||
assert!(
|
||||
|
||||
@@ -970,6 +970,10 @@ impl ChatComposer {
|
||||
self.textarea.is_vim_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn should_handle_vim_insert_escape(&self, key_event: KeyEvent) -> bool {
|
||||
self.textarea.should_handle_vim_insert_escape(key_event)
|
||||
}
|
||||
|
||||
fn vim_mode_indicator_span(&self) -> Option<Span<'static>> {
|
||||
self.textarea.vim_mode_label().map(|label| match label {
|
||||
"Normal" => "Vim: Normal".magenta(),
|
||||
@@ -2922,6 +2926,9 @@ impl ChatComposer {
|
||||
if self.handle_shortcut_overlay_key(&key_event) {
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
if self.should_handle_vim_insert_escape(key_event) {
|
||||
return self.handle_input_basic(key_event);
|
||||
}
|
||||
if key_event.code == KeyCode::Esc {
|
||||
if self.is_empty() {
|
||||
let next_mode = esc_hint_mode(self.footer_mode, self.is_task_running);
|
||||
@@ -5040,6 +5047,44 @@ mod tests {
|
||||
assert!(!composer.esc_backtrack_hint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_vim_insert_escape_enters_normal_without_esc_hint() {
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyModifiers;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ true,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
composer.set_vim_enabled(true);
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE));
|
||||
|
||||
assert!(composer.is_empty());
|
||||
assert_eq!(
|
||||
composer.vim_mode_indicator_span(),
|
||||
Some("Vim: Insert".green())
|
||||
);
|
||||
|
||||
let (result, needs_redraw) =
|
||||
composer.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
|
||||
assert!(matches!(result, InputResult::None));
|
||||
assert!(needs_redraw);
|
||||
assert!(composer.is_empty());
|
||||
assert_eq!(
|
||||
composer.vim_mode_indicator_span(),
|
||||
Some("Vim: Normal".magenta())
|
||||
);
|
||||
assert_eq!(composer.footer_mode, FooterMode::ComposerEmpty);
|
||||
assert!(!composer.esc_backtrack_hint);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_footer_mode_tracks_empty_state_after_quit_hint_expires() {
|
||||
use crossterm::event::KeyCode;
|
||||
|
||||
@@ -963,6 +963,10 @@ impl BottomPane {
|
||||
self.composer.is_vim_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn composer_should_handle_vim_insert_escape(&self, key_event: KeyEvent) -> bool {
|
||||
self.composer.should_handle_vim_insert_escape(key_event)
|
||||
}
|
||||
|
||||
pub(crate) fn is_task_running(&self) -> bool {
|
||||
self.is_task_running
|
||||
}
|
||||
|
||||
@@ -241,6 +241,14 @@ impl TextArea {
|
||||
self.vim_enabled && self.vim_mode == VimMode::Insert
|
||||
}
|
||||
|
||||
pub(crate) fn should_handle_vim_insert_escape(&self, event: KeyEvent) -> bool {
|
||||
self.vim_enabled
|
||||
&& self.vim_mode == VimMode::Insert
|
||||
&& event.code == KeyCode::Esc
|
||||
&& event.modifiers == KeyModifiers::NONE
|
||||
&& matches!(event.kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
}
|
||||
|
||||
pub(crate) fn vim_mode_label(&self) -> Option<&'static str> {
|
||||
if !self.vim_enabled {
|
||||
return None;
|
||||
|
||||
@@ -11342,6 +11342,11 @@ impl ChatWidget {
|
||||
self.bottom_pane.is_normal_backtrack_mode()
|
||||
}
|
||||
|
||||
pub(crate) fn should_handle_vim_insert_escape(&self, key_event: KeyEvent) -> bool {
|
||||
self.bottom_pane
|
||||
.composer_should_handle_vim_insert_escape(key_event)
|
||||
}
|
||||
|
||||
pub(crate) fn insert_str(&mut self, text: &str) {
|
||||
self.bottom_pane.insert_str(text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user