@@ -2437,6 +2437,8 @@ mod tests {
24372437 ) ;
24382438 }
24392439
2440+ /// Behavior: `?` toggles the shortcut overlay only when the composer is otherwise empty. After
2441+ /// any typing has occurred, `?` should be inserted as a literal character.
24402442 #[ test]
24412443 fn question_mark_only_toggles_on_first_char ( ) {
24422444 use crossterm:: event:: KeyCode ;
@@ -2484,6 +2486,8 @@ mod tests {
24842486 assert_eq ! ( composer. footer_mode( ) , FooterMode :: ContextOnly ) ;
24852487 }
24862488
2489+ /// Behavior: while a paste-like burst is being captured, `?` must not toggle the shortcut
2490+ /// overlay; it should be treated as part of the pasted content.
24872491 #[ test]
24882492 fn question_mark_does_not_toggle_during_paste_burst ( ) {
24892493 use crossterm:: event:: KeyCode ;
@@ -2699,6 +2703,9 @@ mod tests {
26992703 }
27002704 }
27012705
2706+ /// Behavior: if the ASCII path has a pending first char (flicker suppression) and a non-ASCII
2707+ /// char arrives next, the pending ASCII char should still be preserved and the overall input
2708+ /// should submit normally (i.e. we should not misclassify this as a paste burst).
27022709 #[ test]
27032710 fn ascii_prefix_survives_non_ascii_followup ( ) {
27042711 use crossterm:: event:: KeyCode ;
@@ -2732,6 +2739,8 @@ mod tests {
27322739 }
27332740 }
27342741
2742+ /// Behavior: a single non-ASCII char should be inserted immediately (IME-friendly) and should
2743+ /// not create any paste-burst state.
27352744 #[ test]
27362745 fn non_ascii_char_inserts_immediately_without_burst_state ( ) {
27372746 use crossterm:: event:: KeyCode ;
@@ -2758,55 +2767,126 @@ mod tests {
27582767 assert ! ( !composer. is_in_paste_burst( ) ) ;
27592768 }
27602769
2761- // test a variety of non-ascii char sequences to ensure we are handling them correctly
2770+ /// Behavior: while we're capturing a paste-like burst, Enter should be treated as a newline
2771+ /// within the burst (not as "submit"), and the whole payload should flush as one paste.
27622772 #[ test]
2763- fn non_ascii_burst_handles_newline ( ) {
2764- let test_cases = [
2765- // triggers on windows
2766- "天地玄黄 宇宙洪荒
2767- 日月盈昃 辰宿列张
2768- 寒来暑往 秋收冬藏
2769-
2770- 你好世界 编码测试
2771- 汉字处理 UTF-8
2772- 终端显示 正确无误
2773-
2774- 风吹竹林 月照大江
2775- 白云千载 青山依旧
2776- 程序员 与 Unicode 同行" ,
2777- // Simulate pasting "你 好\nhi" with an ideographic space to trigger pastey heuristics.
2778- "你 好\n hi" ,
2779- ] ;
2773+ fn non_ascii_burst_buffers_enter_and_flushes_multiline ( ) {
2774+ use crossterm:: event:: KeyCode ;
2775+ use crossterm:: event:: KeyEvent ;
2776+ use crossterm:: event:: KeyModifiers ;
27802777
2781- for test_case in test_cases {
2782- use crossterm:: event:: KeyCode ;
2783- use crossterm:: event:: KeyEvent ;
2784- use crossterm:: event:: KeyModifiers ;
2778+ let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
2779+ let sender = AppEventSender :: new ( tx) ;
2780+ let mut composer = ChatComposer :: new (
2781+ true ,
2782+ sender,
2783+ false ,
2784+ "Ask Codex to do anything" . to_string ( ) ,
2785+ false ,
2786+ ) ;
27852787
2786- let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
2787- let sender = AppEventSender :: new ( tx) ;
2788- let mut composer = ChatComposer :: new (
2789- true ,
2790- sender,
2791- false ,
2792- "Ask Codex to do anything" . to_string ( ) ,
2793- false ,
2794- ) ;
2788+ composer
2789+ . paste_burst
2790+ . begin_with_retro_grabbed ( String :: new ( ) , Instant :: now ( ) ) ;
27952791
2796- for c in test_case. chars ( ) {
2797- let _ =
2798- composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( c) , KeyModifiers :: NONE ) ) ;
2799- }
2792+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( '你' ) , KeyModifiers :: NONE ) ) ;
2793+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( '好' ) , KeyModifiers :: NONE ) ) ;
2794+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: NONE ) ) ;
2795+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( 'h' ) , KeyModifiers :: NONE ) ) ;
2796+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( 'i' ) , KeyModifiers :: NONE ) ) ;
28002797
2801- assert ! (
2802- composer. textarea. text( ) . is_empty( ) ,
2803- "non-empty textarea before flush: {test_case}" ,
2804- ) ;
2805- let _ = flush_after_paste_burst ( & mut composer) ;
2806- assert_eq ! ( composer. textarea. text( ) , test_case) ;
2798+ assert ! ( composer. textarea. text( ) . is_empty( ) ) ;
2799+ let _ = flush_after_paste_burst ( & mut composer) ;
2800+ assert_eq ! ( composer. textarea. text( ) , "你好\n hi" ) ;
2801+ }
2802+
2803+ /// Behavior: a paste-like burst may include a full-width/ideographic space (U+3000). It should
2804+ /// still be captured as a single paste payload and preserve the exact Unicode content.
2805+ #[ test]
2806+ fn non_ascii_burst_preserves_ideographic_space_and_ascii ( ) {
2807+ use crossterm:: event:: KeyCode ;
2808+ use crossterm:: event:: KeyEvent ;
2809+ use crossterm:: event:: KeyModifiers ;
2810+
2811+ let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
2812+ let sender = AppEventSender :: new ( tx) ;
2813+ let mut composer = ChatComposer :: new (
2814+ true ,
2815+ sender,
2816+ false ,
2817+ "Ask Codex to do anything" . to_string ( ) ,
2818+ false ,
2819+ ) ;
2820+
2821+ composer
2822+ . paste_burst
2823+ . begin_with_retro_grabbed ( String :: new ( ) , Instant :: now ( ) ) ;
2824+
2825+ for ch in [ '你' , ' ' , '好' ] {
2826+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( ch) , KeyModifiers :: NONE ) ) ;
2827+ }
2828+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: NONE ) ) ;
2829+ for ch in [ 'h' , 'i' ] {
2830+ let _ = composer. handle_key_event ( KeyEvent :: new ( KeyCode :: Char ( ch) , KeyModifiers :: NONE ) ) ;
28072831 }
2832+
2833+ assert ! ( composer. textarea. text( ) . is_empty( ) ) ;
2834+ let _ = flush_after_paste_burst ( & mut composer) ;
2835+ assert_eq ! ( composer. textarea. text( ) , "你 好\n hi" ) ;
2836+ }
2837+
2838+ /// Behavior: a large multi-line payload containing both non-ASCII and ASCII (e.g. "UTF-8",
2839+ /// "Unicode") should be captured as a single paste-like burst, and Enter key events should
2840+ /// become `\n` within the buffered content.
2841+ #[ test]
2842+ fn non_ascii_burst_buffers_large_multiline_mixed_ascii_and_unicode ( ) {
2843+ use crossterm:: event:: KeyCode ;
2844+ use crossterm:: event:: KeyEvent ;
2845+ use crossterm:: event:: KeyModifiers ;
2846+
2847+ const LARGE_MIXED_PAYLOAD : & str = "天地玄黄 宇宙洪荒\n \
2848+ 日月盈昃 辰宿列张\n \
2849+ 寒来暑往 秋收冬藏\n \
2850+ \n \
2851+ 你好世界 编码测试\n \
2852+ 汉字处理 UTF-8\n \
2853+ 终端显示 正确无误\n \
2854+ \n \
2855+ 风吹竹林 月照大江\n \
2856+ 白云千载 青山依旧\n \
2857+ 程序员 与 Unicode 同行";
2858+
2859+ let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
2860+ let sender = AppEventSender :: new ( tx) ;
2861+ let mut composer = ChatComposer :: new (
2862+ true ,
2863+ sender,
2864+ false ,
2865+ "Ask Codex to do anything" . to_string ( ) ,
2866+ false ,
2867+ ) ;
2868+
2869+ // Force an active burst so the test doesn't depend on timing heuristics.
2870+ composer
2871+ . paste_burst
2872+ . begin_with_retro_grabbed ( String :: new ( ) , Instant :: now ( ) ) ;
2873+
2874+ for ch in LARGE_MIXED_PAYLOAD . chars ( ) {
2875+ let code = if ch == '\n' {
2876+ KeyCode :: Enter
2877+ } else {
2878+ KeyCode :: Char ( ch)
2879+ } ;
2880+ let _ = composer. handle_key_event ( KeyEvent :: new ( code, KeyModifiers :: NONE ) ) ;
2881+ }
2882+
2883+ assert ! ( composer. textarea. text( ) . is_empty( ) ) ;
2884+ let _ = flush_after_paste_burst ( & mut composer) ;
2885+ assert_eq ! ( composer. textarea. text( ) , LARGE_MIXED_PAYLOAD ) ;
28082886 }
28092887
2888+ /// Behavior: while a paste-like burst is active, Enter should not submit; it should insert a
2889+ /// newline into the buffered payload and flush as a single paste later.
28102890 #[ test]
28112891 fn ascii_burst_treats_enter_as_newline ( ) {
28122892 use crossterm:: event:: KeyCode ;
@@ -2849,6 +2929,8 @@ mod tests {
28492929 assert_eq ! ( composer. textarea. text( ) , "hi\n there" ) ;
28502930 }
28512931
2932+ /// Behavior: a small explicit paste inserts text directly (no placeholder), and the submitted
2933+ /// text matches what is visible in the textarea.
28522934 #[ test]
28532935 fn handle_paste_small_inserts_text ( ) {
28542936 use crossterm:: event:: KeyCode ;
@@ -2911,6 +2993,8 @@ mod tests {
29112993 }
29122994 }
29132995
2996+ /// Behavior: a large explicit paste inserts a placeholder into the textarea, stores the full
2997+ /// content in `pending_pastes`, and expands the placeholder to the full content on submit.
29142998 #[ test]
29152999 fn handle_paste_large_uses_placeholder_and_replaces_on_submit ( ) {
29163000 use crossterm:: event:: KeyCode ;
@@ -2946,6 +3030,8 @@ mod tests {
29463030 assert ! ( composer. pending_pastes. is_empty( ) ) ;
29473031 }
29483032
3033+ /// Behavior: editing that removes a paste placeholder should also clear the associated
3034+ /// `pending_pastes` entry so it cannot be submitted accidentally.
29493035 #[ test]
29503036 fn edit_clears_pending_paste ( ) {
29513037 use crossterm:: event:: KeyCode ;
@@ -3342,6 +3428,8 @@ mod tests {
33423428 assert_eq ! ( composer. textarea. text( ) , "@" ) ;
33433429 }
33443430
3431+ /// Behavior: multiple paste operations can coexist; placeholders should be expanded to their
3432+ /// original content on submission.
33453433 #[ test]
33463434 fn test_multiple_pastes_submission ( ) {
33473435 use crossterm:: event:: KeyCode ;
@@ -3494,6 +3582,8 @@ mod tests {
34943582 ) ;
34953583 }
34963584
3585+ /// Behavior: if multiple large pastes share the same placeholder label (same char count),
3586+ /// deleting one placeholder removes only its corresponding `pending_pastes` entry.
34973587 #[ test]
34983588 fn deleting_duplicate_length_pastes_removes_only_target ( ) {
34993589 use crossterm:: event:: KeyCode ;
@@ -3531,6 +3621,8 @@ mod tests {
35313621 assert_eq ! ( composer. pending_pastes[ 0 ] . 1 , paste) ;
35323622 }
35333623
3624+ /// Behavior: large-paste placeholder numbering does not get reused after deletion, so a new
3625+ /// paste of the same length gets a new unique placeholder label.
35343626 #[ test]
35353627 fn large_paste_numbering_does_not_reuse_after_deletion ( ) {
35363628 use crossterm:: event:: KeyCode ;
@@ -3991,6 +4083,8 @@ mod tests {
39914083 assert ! ( composer. textarea. is_empty( ) ) ;
39924084 }
39934085
4086+ /// Behavior: selecting a custom prompt that includes a large paste placeholder should expand
4087+ /// to the full pasted content before submission.
39944088 #[ test]
39954089 fn custom_prompt_with_large_paste_expands_correctly ( ) {
39964090 use crossterm:: event:: KeyCode ;
@@ -4410,6 +4504,8 @@ mod tests {
44104504 assert_eq ! ( InputResult :: Submitted ( expected) , result) ;
44114505 }
44124506
4507+ /// Behavior: the first fast ASCII character is held briefly to avoid flicker; if no burst
4508+ /// follows, it should eventually flush as normal typed input (not as a paste).
44134509 #[ test]
44144510 fn pending_first_ascii_char_flushes_as_typed ( ) {
44154511 use crossterm:: event:: KeyCode ;
@@ -4437,6 +4533,8 @@ mod tests {
44374533 assert ! ( !composer. is_in_paste_burst( ) ) ;
44384534 }
44394535
4536+ /// Behavior: fast "paste-like" ASCII input should buffer and then flush as a single paste. If
4537+ /// the payload is small, it should insert directly (no placeholder).
44404538 #[ test]
44414539 fn burst_paste_fast_small_buffers_and_flushes_on_stop ( ) {
44424540 use crossterm:: event:: KeyCode ;
@@ -4480,6 +4578,8 @@ mod tests {
44804578 ) ;
44814579 }
44824580
4581+ /// Behavior: fast "paste-like" ASCII input should buffer and then flush as a single paste. If
4582+ /// the payload is large, it should insert a placeholder and defer the full text until submit.
44834583 #[ test]
44844584 fn burst_paste_fast_large_inserts_placeholder_on_flush ( ) {
44854585 use crossterm:: event:: KeyCode ;
@@ -4515,6 +4615,8 @@ mod tests {
45154615 assert ! ( composer. pending_pastes[ 0 ] . 1 . chars( ) . all( |c| c == 'x' ) ) ;
45164616 }
45174617
4618+ /// Behavior: human-like typing (with delays between chars) should not be classified as a paste
4619+ /// burst. Characters should appear immediately and should not trigger a paste placeholder.
45184620 #[ test]
45194621 fn humanlike_typing_1000_chars_appears_live_no_placeholder ( ) {
45204622 let ( tx, _rx) = unbounded_channel :: < AppEvent > ( ) ;
0 commit comments