@@ -964,27 +964,64 @@ mod tests {
964964
965965 // Click on row 1 ("second word") way past the visible end.
966966 assert ! ( composer. set_cursor_from_wrapped_position( 4 , 80 , 1 , 60 ) ) ;
967- assert_eq ! (
968- composer. cursor( ) ,
969- "first line\n second word" . chars( ) . count( )
970- ) ;
967+ assert_eq ! ( composer. cursor( ) , "first line\n second word" . chars( ) . count( ) ) ;
971968
972969 // Wrapped logical line: clicking past the end of visual row 0
973- // must land at the end of that visual row, NOT at the end of the
974- // whole logical line (which would be on a later visual row).
970+ // must land in a position that *renders* on visual row 0, not on
971+ // the next wrapped row. Position == row_end is the start of the
972+ // next wrapped row visually (no character separates wrapped rows),
973+ // so we clamp one position earlier on non-last visual rows so the
974+ // cursor stays on the clicked row.
975975 let mut composer = Composer :: default ( ) ;
976976 // width=8, prompt "> " takes 2 cols => content/wrap width = 6.
977977 // "abcdefghij" wraps into "abcdef" and "ghij".
978978 composer. set_input ( "abcdefghij" . to_string ( ) ) ;
979979 // Click on row 0 at the rightmost in-rect column (col 7, past 'f').
980980 assert ! ( composer. set_cursor_from_wrapped_position( 4 , 8 , 0 , 7 ) ) ;
981- assert_eq ! ( composer. cursor( ) , "abcdef" . chars( ) . count( ) ) ;
981+ // cursor=5 (between 'e' and 'f') renders at (row=0, col=5) — last
982+ // cell of visual row 0. cursor=6 would render at (row=1, col=0)
983+ // because in soft-wrapped text, position == wrap_width has no
984+ // visual home on row 0.
985+ assert_eq ! ( composer. cursor( ) , 5 ) ;
986+ let ( vrow, _vcol) = composer. cursor_visual_row_col_wrapped ( 6 ) ;
987+ assert_eq ! ( vrow, 0 , "cursor must render on the clicked visual row" ) ;
982988 // Click on row 1 past the end of "ghij" still pins to end of row 1
983- // (which is also the end of the logical line here).
989+ // (which is the LAST visual row of the logical line, so clamping
990+ // to line_len is fine — there's no next wrapped row to spill into).
984991 assert ! ( composer. set_cursor_from_wrapped_position( 4 , 8 , 1 , 7 ) ) ;
985992 assert_eq ! ( composer. cursor( ) , "abcdefghij" . chars( ) . count( ) ) ;
986993 }
987994
995+ #[ test]
996+ fn click_past_end_of_plain_line_in_multiline_composer_stays_on_clicked_row ( ) {
997+ // Magnus's repro: composer has multi-line text via Shift+Enter
998+ // (so logical lines separated by \n, each non-wrapped). Clicking
999+ // far past the visible end of a line must place the cursor at the
1000+ // end of that line AND render it on the clicked row, not on the
1001+ // next logical line.
1002+ let mut composer = Composer :: default ( ) ;
1003+ composer. set_input ( "first line\n second line\n third line" . to_string ( ) ) ;
1004+
1005+ // Click row 0 at col 100 (way past "first line"'s 10 chars).
1006+ assert ! ( composer. set_cursor_from_wrapped_position( 4 , 80 , 0 , 100 ) ) ;
1007+ assert_eq ! (
1008+ composer. cursor( ) ,
1009+ "first line" . chars( ) . count( ) ,
1010+ "cursor byte position should land at end of first line"
1011+ ) ;
1012+ let ( vrow, _) = composer. cursor_visual_row_col_wrapped ( 78 ) ;
1013+ assert_eq ! ( vrow, 0 , "cursor must render on row 0, not the next line" ) ;
1014+
1015+ // Click row 1 at col 100.
1016+ assert ! ( composer. set_cursor_from_wrapped_position( 4 , 80 , 1 , 100 ) ) ;
1017+ assert_eq ! (
1018+ composer. cursor( ) ,
1019+ "first line\n second line" . chars( ) . count( ) ,
1020+ ) ;
1021+ let ( vrow, _) = composer. cursor_visual_row_col_wrapped ( 78 ) ;
1022+ assert_eq ! ( vrow, 1 , "cursor must render on row 1, not the next line" ) ;
1023+ }
1024+
9881025 #[ test]
9891026 fn ctrl_w_keeps_whitespace_delimited_shell_behavior ( ) {
9901027 let mut composer = Composer :: default ( ) ;
0 commit comments