mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Open slash commands from empty drafts in Vim Normal mode (#50788)
## Why Pressing `/` in Vim Normal mode starts a composer search, even when the draft is empty. Allow this key to open slash commands directly on an empty draft. ## What changed When slash commands are enabled, a standalone `/` on an empty draft switches to Insert mode, inserts `/`, and opens command completion. Preserve Vim search behavior for nonempty drafts, `?`, and pending operators. ## Testing Add a snapshot test for the Insert mode transition and command picker. Exercise Vim slash-command entry in the transcript overlay test across terminal sizes, and extend command sparkle coverage to Vim mode. GitOrigin-RevId: 806c2f6b841190ccdc1a258f88dfa590773ba33a
This commit is contained in:
committed by
copyberry
parent
acf9818fae
commit
b8dceb0d4f
@@ -1185,10 +1185,30 @@ async fn slash_picker_overlays_history_without_moving_the_transcript_or_composer
|
||||
))];
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
tui.set_owned_screen(/*owned*/ true)?;
|
||||
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?;
|
||||
for (width, height) in [(80, 14), (32, 14), (80, 7), (80, 5)] {
|
||||
let size = Size::new(width, height);
|
||||
tui.terminal.resize(size)?;
|
||||
app.chat_widget.apply_external_edit("/m".to_string());
|
||||
app.chat_widget.apply_external_edit(String::new());
|
||||
app.chat_widget.toggle_vim_mode_and_notify();
|
||||
app.chat_widget.handle_key_event(KeyCode::Esc.into());
|
||||
let slash = KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE);
|
||||
assert!(!app.handle_owned_transcript_event(
|
||||
&mut tui,
|
||||
&mut app_server,
|
||||
&TuiEvent::Key(slash),
|
||||
)?);
|
||||
tokio::time::pause();
|
||||
for key in [slash, KeyCode::Char('m').into()] {
|
||||
app.chat_widget.handle_key_event(key);
|
||||
tokio::time::advance(crate::bottom_pane::ChatComposer::recommended_paste_flush_delay())
|
||||
.await;
|
||||
app.chat_widget
|
||||
.handle_paste_burst_tick(tui.frame_requester());
|
||||
}
|
||||
tokio::time::resume();
|
||||
assert_eq!(app.chat_widget.composer_text_with_pending(), "/m");
|
||||
app.chat_widget.toggle_vim_mode_and_notify();
|
||||
app.chat_widget
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
|
||||
app.render_owned_transcript(&mut tui, size)?;
|
||||
@@ -1260,6 +1280,7 @@ async fn slash_picker_overlays_history_without_moving_the_transcript_or_composer
|
||||
);
|
||||
}
|
||||
tui.set_owned_screen(/*owned*/ false)?;
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
//! [`ChatComposer::handle_key_event_without_popup`]. After every handled key, we call
|
||||
//! [`ChatComposer::sync_popups`] so UI state follows the latest buffer/cursor.
|
||||
//! Fresh Vim drafts start in Insert; Normal `/` and `?` search the composer.
|
||||
//! On an empty draft, a standalone Normal `/` enters Insert and opens command completion.
|
||||
//! Backspace on an empty Vim search query cancels search and any pending operator.
|
||||
//!
|
||||
//! # Completion and Popup Dismissal
|
||||
@@ -1915,6 +1916,18 @@ impl ChatComposer {
|
||||
return (InputResult::None, false);
|
||||
}
|
||||
|
||||
if self.history_search.is_none()
|
||||
&& !self.popups.active()
|
||||
&& self.config.slash_commands_enabled
|
||||
&& self.is_empty()
|
||||
&& self.draft.textarea.wants_vim_search_key(key_event)
|
||||
&& !self.draft.textarea.is_vim_operator_pending()
|
||||
&& key_event.code == KeyCode::Char('/')
|
||||
&& key_event.modifiers.is_empty()
|
||||
{
|
||||
self.draft.textarea.enter_vim_insert_mode();
|
||||
}
|
||||
|
||||
self.draft.textarea_state.get_mut().follow_cursor();
|
||||
let before = self.before_sparkle_key(key_event);
|
||||
let result = self.handle_key_event_inner(key_event);
|
||||
|
||||
@@ -806,8 +806,9 @@ fn fresh_draft_replacement_ends_search_and_dismisses_sparkles() {
|
||||
fn arbitrary_commands_preserve_a_never_used_fresh_opportunity() {
|
||||
palette(|| {
|
||||
let now = Instant::now();
|
||||
for completion in [false, true] {
|
||||
for (completion, vim) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut pane = pane();
|
||||
pane.set_vim_enabled(vim);
|
||||
pane.mark_fresh_task_for_sparkle("gpt-5.5", &enabled());
|
||||
assert!(dots(&draw(&pane.composer, /*width*/ 80, now).0).is_empty());
|
||||
for command in ["/status", "/diff", "/pwd"] {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::super::InputResult;
|
||||
use super::super::tests::snapshot_composer_state_with_width;
|
||||
use super::super::tests::type_chars_humanlike;
|
||||
use crate::keymap::KeyChordMatch;
|
||||
use crate::keymap::KeyChordMatcher;
|
||||
use crate::keymap::RuntimeKeymap;
|
||||
@@ -48,6 +49,9 @@ fn backspace_cancels_empty_vim_search() {
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
let cursor = composer.cursor();
|
||||
for prefix in ["/", "?", "d/", "d?", "c/", "c?", "y/", "y?"] {
|
||||
if text.is_empty() && prefix == "/" {
|
||||
continue;
|
||||
}
|
||||
for ch in prefix.chars() {
|
||||
composer
|
||||
.handle_key_event(KeyEvent::new(KeyCode::Char(ch), KeyModifiers::NONE));
|
||||
@@ -107,3 +111,18 @@ fn vim_search_query_edits_and_paste_preserve_the_draft() {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_vim_normal_slash_opens_commands() {
|
||||
snapshot_composer_state_with_width(
|
||||
"vim_empty_normal_slash",
|
||||
/*width*/ 60,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
|composer| {
|
||||
composer.set_vim_enabled(/*enabled*/ true);
|
||||
type_chars_humanlike(composer, &['/']);
|
||||
assert_eq!(composer.current_text(), "/");
|
||||
assert_eq!(composer.draft.textarea.vim_mode_label(), Some("Insert"));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 5217
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /model choose what model and reasoning effort to "
|
||||
" use "
|
||||
" /ide include current selection, open files, and"
|
||||
" other context from your IDE "
|
||||
" /permissions choose what Codex is allowed to do "
|
||||
" "
|
||||
"› / "
|
||||
" "
|
||||
" 100% context left | Vim: Insert "
|
||||
Reference in New Issue
Block a user