Skip to content

Commit b0e4fc2

Browse files
committed
Fix paste burst newline handling
1 parent b6165ae commit b0e4fc2

File tree

1 file changed

+117
-12
lines changed

1 file changed

+117
-12
lines changed

codex-rs/tui/src/bottom_pane/chat_composer.rs

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -922,11 +922,19 @@ impl ChatComposer {
922922
.next()
923923
.unwrap_or("")
924924
.starts_with('/');
925-
if self.paste_burst.is_active() && !in_slash_context {
925+
if !in_slash_context {
926926
let now = Instant::now();
927927
if self.paste_burst.append_newline_if_active(now) {
928928
return (InputResult::None, true);
929929
}
930+
if self
931+
.paste_burst
932+
.newline_should_insert_instead_of_submit(now)
933+
{
934+
self.textarea.insert_str("\n");
935+
self.paste_burst.extend_window(now);
936+
return (InputResult::None, true);
937+
}
930938
}
931939
// If we have pending placeholder pastes, submit immediately to expand them.
932940
if !self.pending_pastes.is_empty() {
@@ -945,17 +953,6 @@ impl ChatComposer {
945953
return (InputResult::Submitted(text), true);
946954
}
947955

948-
// During a paste-like burst, treat Enter as a newline instead of submit.
949-
let now = Instant::now();
950-
if self
951-
.paste_burst
952-
.newline_should_insert_instead_of_submit(now)
953-
&& !in_slash_context
954-
{
955-
self.textarea.insert_str("\n");
956-
self.paste_burst.extend_window(now);
957-
return (InputResult::None, true);
958-
}
959956
let mut text = self.textarea.text().to_string();
960957
let original_input = text.clone();
961958
self.textarea.set_text("");
@@ -1002,6 +999,7 @@ impl ChatComposer {
1002999
match self.paste_burst.flush_if_due(now) {
10031000
FlushResult::Paste(pasted) => {
10041001
self.handle_paste(pasted);
1002+
self.paste_burst.extend_window(now);
10051003
true
10061004
}
10071005
FlushResult::Typed(ch) => {
@@ -3211,6 +3209,113 @@ mod tests {
32113209
assert!(composer.pending_pastes[0].1.chars().all(|c| c == 'x'));
32123210
}
32133211

3212+
fn test_composer() -> ChatComposer {
3213+
let (tx, _rx) = unbounded_channel::<AppEvent>();
3214+
let sender = AppEventSender::new(tx);
3215+
ChatComposer::new(
3216+
true,
3217+
sender,
3218+
false,
3219+
"Ask Codex to do anything".to_string(),
3220+
false,
3221+
)
3222+
}
3223+
3224+
fn seed_large_paste(composer: &mut ChatComposer) -> String {
3225+
use crossterm::event::KeyCode;
3226+
use crossterm::event::KeyEvent;
3227+
use crossterm::event::KeyModifiers;
3228+
3229+
let count = LARGE_PASTE_CHAR_THRESHOLD + 25;
3230+
for _ in 0..count {
3231+
let _ =
3232+
composer.handle_key_event(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
3233+
}
3234+
3235+
std::thread::sleep(ChatComposer::recommended_paste_flush_delay());
3236+
assert!(
3237+
composer.flush_paste_burst_if_due(),
3238+
"expected buffered paste to flush"
3239+
);
3240+
assert_eq!(1, composer.pending_pastes.len());
3241+
composer.pending_pastes[0].0.clone()
3242+
}
3243+
3244+
#[test]
3245+
fn burst_paste_large_newline_does_not_submit() {
3246+
use crossterm::event::KeyCode;
3247+
use crossterm::event::KeyEvent;
3248+
use crossterm::event::KeyModifiers;
3249+
3250+
let mut composer = test_composer();
3251+
let placeholder = seed_large_paste(&mut composer);
3252+
3253+
let (result, _needs_redraw) =
3254+
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
3255+
3256+
assert_eq!(InputResult::None, result);
3257+
assert_eq!(1, composer.pending_pastes.len());
3258+
let text_after = composer.textarea.text().to_string();
3259+
assert!(
3260+
text_after.contains(&placeholder),
3261+
"placeholder should remain after newline: {text_after:?}"
3262+
);
3263+
assert!(
3264+
text_after.ends_with('\n'),
3265+
"newline from paste should be preserved: {text_after:?}"
3266+
);
3267+
}
3268+
3269+
#[test]
3270+
fn burst_paste_large_shift_enter_does_not_submit() {
3271+
use crossterm::event::KeyCode;
3272+
use crossterm::event::KeyEvent;
3273+
use crossterm::event::KeyModifiers;
3274+
3275+
let mut composer = test_composer();
3276+
let placeholder = seed_large_paste(&mut composer);
3277+
3278+
let (result, _needs_redraw) =
3279+
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::SHIFT));
3280+
3281+
assert_eq!(InputResult::None, result);
3282+
assert_eq!(1, composer.pending_pastes.len());
3283+
let text_after = composer.textarea.text().to_string();
3284+
assert!(
3285+
text_after.contains(&placeholder),
3286+
"placeholder should remain after Shift+Enter: {text_after:?}"
3287+
);
3288+
assert!(
3289+
text_after.ends_with('\n'),
3290+
"Shift+Enter should insert newline: {text_after:?}"
3291+
);
3292+
}
3293+
3294+
#[test]
3295+
fn burst_paste_large_ctrl_enter_does_not_submit() {
3296+
use crossterm::event::KeyCode;
3297+
use crossterm::event::KeyEvent;
3298+
use crossterm::event::KeyModifiers;
3299+
3300+
let mut composer = test_composer();
3301+
let placeholder = seed_large_paste(&mut composer);
3302+
3303+
let (result, _needs_redraw) =
3304+
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL));
3305+
3306+
assert_eq!(InputResult::None, result);
3307+
assert_eq!(1, composer.pending_pastes.len());
3308+
let text_after = composer.textarea.text().to_string();
3309+
assert!(
3310+
text_after.contains(&placeholder),
3311+
"placeholder should remain after Ctrl+Enter: {text_after:?}"
3312+
);
3313+
assert!(
3314+
text_after.ends_with('\n'),
3315+
"Ctrl+Enter should insert newline: {text_after:?}"
3316+
);
3317+
}
3318+
32143319
#[test]
32153320
fn humanlike_typing_1000_chars_appears_live_no_placeholder() {
32163321
let (tx, _rx) = unbounded_channel::<AppEvent>();

0 commit comments

Comments
 (0)