@@ -427,6 +427,7 @@ impl TextInput {
427427 fn go ( & mut self , motion : Motion , extend : bool , cx : & mut Context < Self > ) {
428428 let rows = self . rows ( ) ;
429429 self . editor . go ( motion, extend, rows. as_ref ( ) ) ;
430+ if extend { self . publish_primary ( cx) ; }
430431 cx. notify ( ) ;
431432 }
432433
@@ -585,6 +586,7 @@ impl TextInput {
585586
586587 fn select_all ( & mut self , _: & SelectAll , _: & mut Window , cx : & mut Context < Self > ) {
587588 self . editor . select_all ( ) ;
589+ self . publish_primary ( cx) ;
588590 cx. notify ( ) ;
589591 }
590592
@@ -694,22 +696,38 @@ impl TextInput {
694696 }
695697 }
696698
697- fn on_mouse_down ( & mut self , event : & MouseDownEvent , _: & mut Window , cx : & mut Context < Self > ) {
699+ /// Linux PRIMARY is selected text, separate from the Copy/Paste clipboard.
700+ fn publish_primary ( & self , _cx : & App ) {
701+ #[ cfg( any( target_os = "linux" , target_os = "freebsd" ) ) ]
702+ if !self . editor . selection ( ) . is_empty ( ) {
703+ _cx. write_to_primary ( ClipboardItem :: new_string ( self . editor . selected_text ( ) . to_owned ( ) ) ) ;
704+ }
705+ }
706+
707+ #[ cfg( any( target_os = "linux" , target_os = "freebsd" ) ) ]
708+ fn on_middle_down ( & mut self , event : & MouseDownEvent , window : & mut Window , cx : & mut Context < Self > ) {
709+ // Read before moving the caret: the selected text may belong to this field.
710+ let text = cx. read_from_primary ( ) . and_then ( |item| item. text ( ) ) ;
698711 let offset = self . index_for_mouse_position ( event. position ) ;
699- match ( event. modifiers . shift , event. click_count ) {
700- ( true , _) => self . editor . select_to ( offset) ,
701- ( false , 2 ) => self . editor . select_word_at ( offset) ,
702- ( false , 3 ..) => self . editor . select_line_at ( offset) ,
703- ( false , _) => {
704- self . is_selecting = true ;
705- self . editor . place ( offset) ;
706- }
712+ window. focus ( & self . focus_handle ( cx) , cx) ;
713+ self . is_selecting = false ;
714+ if paste_primary_text ( & mut self . editor , text. as_deref ( ) , offset) {
715+ self . edited ( cx) ;
707716 }
717+ cx. stop_propagation ( ) ;
718+ }
719+
720+ fn on_mouse_down ( & mut self , event : & MouseDownEvent , _: & mut Window , cx : & mut Context < Self > ) {
721+ let offset = self . index_for_mouse_position ( event. position ) ;
722+ self . is_selecting = select_on_press ( & mut self . editor , offset, event. modifiers . shift , event. click_count ) ;
723+ if !self . is_selecting { self . publish_primary ( cx) ; }
708724 cx. notify ( ) ;
709725 }
710726
711- fn on_mouse_up ( & mut self , _: & MouseUpEvent , _: & mut Window , _: & mut Context < Self > ) {
712- self . is_selecting = false ;
727+ fn on_mouse_up ( & mut self , _: & MouseUpEvent , _: & mut Window , cx : & mut Context < Self > ) {
728+ if std:: mem:: replace ( & mut self . is_selecting , false ) {
729+ self . publish_primary ( cx) ;
730+ }
713731 }
714732
715733 fn on_mouse_move ( & mut self , event : & MouseMoveEvent , _: & mut Window , cx : & mut Context < Self > ) {
@@ -771,6 +789,26 @@ enum Pasted {
771789 Nothing ,
772790}
773791
792+ /// Only a plain click starts a drag. Word, line and shift selections are complete
793+ /// on press and must survive a move event at the same pointer position.
794+ fn select_on_press ( editor : & mut Editor , offset : usize , shift : bool , clicks : usize ) -> bool {
795+ match ( shift, clicks) {
796+ ( true , _) => editor. select_to ( offset) ,
797+ ( false , 2 ) => editor. select_word_at ( offset) ,
798+ ( false , 3 ..) => editor. select_line_at ( offset) ,
799+ ( false , _) => { editor. place ( offset) ; return true ; }
800+ }
801+ false
802+ }
803+
804+ #[ cfg( any( target_os = "linux" , target_os = "freebsd" , test) ) ]
805+ fn paste_primary_text ( editor : & mut Editor , text : Option < & str > , offset : usize ) -> bool {
806+ let Some ( text) = text. filter ( |text| !text. is_empty ( ) ) else { return false ; } ;
807+ editor. place ( offset) ;
808+ editor. insert ( text) ;
809+ true
810+ }
811+
774812fn pasted ( entries : & [ ClipboardEntry ] ) -> Pasted {
775813 let paths: Vec < std:: path:: PathBuf > = entries
776814 . iter ( )
@@ -1237,6 +1275,11 @@ impl Render for TextInput {
12371275 . on_action ( cx. listener ( Self :: escape) )
12381276 . on_action ( cx. listener ( Self :: tab) )
12391277 . on_mouse_down ( MouseButton :: Left , cx. listener ( Self :: on_mouse_down) )
1278+ . map ( |input| {
1279+ #[ cfg( any( target_os = "linux" , target_os = "freebsd" ) ) ]
1280+ let input = input. on_mouse_down ( MouseButton :: Middle , cx. listener ( Self :: on_middle_down) ) ;
1281+ input
1282+ } )
12401283 . on_mouse_up ( MouseButton :: Left , cx. listener ( Self :: on_mouse_up) )
12411284 . on_mouse_up_out ( MouseButton :: Left , cx. listener ( Self :: on_mouse_up) )
12421285 . on_mouse_move ( cx. listener ( Self :: on_mouse_move) )
@@ -1257,6 +1300,51 @@ mod tests {
12571300 use gpui:: { ClipboardEntry , ClipboardString , ExternalPaths , Image , ImageFormat } ;
12581301 use std:: path:: PathBuf ;
12591302
1303+ #[ test]
1304+ fn word_line_and_shift_selections_survive_a_stationary_move ( ) {
1305+ for ( clicks, shift, expected) in [ ( 2 , false , "second" ) , ( 3 , false , "first second third" ) , ( 1 , true , "second thi" ) ] {
1306+ let mut editor = super :: Editor :: new ( true ) ;
1307+ editor. set_text ( "first second third" ) ;
1308+ editor. place ( 16 ) ;
1309+ let dragging = super :: select_on_press ( & mut editor, 6 , shift, clicks) ;
1310+ if dragging { editor. select_to ( 6 ) ; }
1311+ assert ! ( !dragging) ;
1312+ assert_eq ! ( editor. selected_text( ) , expected) ;
1313+ }
1314+ let mut editor = super :: Editor :: new ( true ) ;
1315+ editor. set_text ( "first second" ) ;
1316+ assert ! ( super :: select_on_press( & mut editor, 6 , false , 1 ) ) ;
1317+ editor. select_to ( 12 ) ;
1318+ assert_eq ! ( editor. selected_text( ) , "second" ) ;
1319+ }
1320+
1321+ #[ test]
1322+ fn middle_paste_inserts_at_the_click_without_replacing_selected_text ( ) {
1323+ let mut editor = super :: Editor :: new ( true ) ;
1324+ editor. set_text ( "hello 世界" ) ;
1325+ editor. select_all ( ) ;
1326+ assert ! ( super :: paste_primary_text( & mut editor, Some ( "https://example.invalid " ) , 6 ) ) ;
1327+ assert_eq ! ( editor. text( ) , "hello https://example.invalid 世界" ) ;
1328+ assert ! ( editor. selection( ) . is_empty( ) ) ;
1329+ editor. undo ( ) ;
1330+ assert_eq ! ( editor. text( ) , "hello 世界" ) ;
1331+ }
1332+
1333+ #[ test]
1334+ fn an_empty_primary_selection_does_not_move_or_replace_the_draft ( ) {
1335+ let mut editor = super :: Editor :: new ( true ) ;
1336+ editor. set_text ( "draft" ) ;
1337+ editor. select_all ( ) ;
1338+ for text in [ None , Some ( "" ) ] {
1339+ assert ! ( !super :: paste_primary_text( & mut editor, text, 2 ) ) ;
1340+ assert_eq ! ( editor. text( ) , "draft" ) ;
1341+ assert_eq ! ( editor. selected_text( ) , "draft" ) ;
1342+ }
1343+ let mut search = super :: Editor :: new ( false ) ;
1344+ assert ! ( super :: paste_primary_text( & mut search, Some ( "one\r \n two" ) , 0 ) ) ;
1345+ assert_eq ! ( search. text( ) , "one two" ) ;
1346+ }
1347+
12601348 #[ test]
12611349 fn finder_files_come_first_then_a_picture_without_words_else_words ( ) {
12621350 let words = ClipboardEntry :: String ( ClipboardString :: new ( "trail.pdf" . into ( ) ) ) ;
0 commit comments