@@ -130,6 +130,75 @@ impl TextInputComponent {
130
130
Some ( index)
131
131
}
132
132
133
+ /// Helper for `next/previous_word_position`.
134
+ fn at_alphanumeric ( & self , i : usize ) -> bool {
135
+ self . msg [ i..]
136
+ . chars ( )
137
+ . next ( )
138
+ . map_or ( false , char:: is_alphanumeric)
139
+ }
140
+
141
+ /// Get the position of the first character of the next word, or, if there
142
+ /// isn't a next word, the `msg.len()`.
143
+ /// Returns None when the cursor is already at `msg.len()`.
144
+ ///
145
+ /// A Word is continuous sequence of alphanumeric characters.
146
+ fn next_word_position ( & self ) -> Option < usize > {
147
+ if self . cursor_position >= self . msg . len ( ) {
148
+ return None ;
149
+ }
150
+
151
+ let mut was_in_word =
152
+ self . at_alphanumeric ( self . cursor_position ) ;
153
+
154
+ let mut index = self . cursor_position . saturating_add ( 1 ) ;
155
+ while index < self . msg . len ( ) {
156
+ if !self . msg . is_char_boundary ( index) {
157
+ index += 1 ;
158
+ continue ;
159
+ }
160
+
161
+ let is_in_word = self . at_alphanumeric ( index) ;
162
+ if !was_in_word && is_in_word {
163
+ break ;
164
+ }
165
+ was_in_word = is_in_word;
166
+ index += 1 ;
167
+ }
168
+ Some ( index)
169
+ }
170
+
171
+ /// Get the position of the first character of the previous word, or, if there
172
+ /// isn't a previous word, returns `0`.
173
+ /// Returns None when the cursor is already at `0`.
174
+ ///
175
+ /// A Word is continuous sequence of alphanumeric characters.
176
+ fn previous_word_position ( & self ) -> Option < usize > {
177
+ if self . cursor_position == 0 {
178
+ return None ;
179
+ }
180
+
181
+ let mut was_in_word = false ;
182
+
183
+ let mut last_pos = self . cursor_position ;
184
+ let mut index = self . cursor_position ;
185
+ while index > 0 {
186
+ index -= 1 ;
187
+ if !self . msg . is_char_boundary ( index) {
188
+ continue ;
189
+ }
190
+
191
+ let is_in_word = self . at_alphanumeric ( index) ;
192
+ if was_in_word && !is_in_word {
193
+ return Some ( last_pos) ;
194
+ }
195
+
196
+ last_pos = index;
197
+ was_in_word = is_in_word;
198
+ }
199
+ Some ( 0 )
200
+ }
201
+
133
202
fn backspace ( & mut self ) {
134
203
if self . cursor_position > 0 {
135
204
self . decr_cursor ( ) ;
@@ -366,6 +435,43 @@ impl Component for TextInputComponent {
366
435
self . incr_cursor ( ) ;
367
436
return Ok ( EventState :: Consumed ) ;
368
437
}
438
+ KeyCode :: Delete if is_ctrl => {
439
+ if let Some ( pos) = self . next_word_position ( ) {
440
+ self . msg . replace_range (
441
+ self . cursor_position ..pos,
442
+ "" ,
443
+ ) ;
444
+ }
445
+ return Ok ( EventState :: Consumed ) ;
446
+ }
447
+ KeyCode :: Backspace | KeyCode :: Char ( 'w' )
448
+ if is_ctrl =>
449
+ {
450
+ if let Some ( pos) =
451
+ self . previous_word_position ( )
452
+ {
453
+ self . msg . replace_range (
454
+ pos..self . cursor_position ,
455
+ "" ,
456
+ ) ;
457
+ self . cursor_position = pos;
458
+ }
459
+ return Ok ( EventState :: Consumed ) ;
460
+ }
461
+ KeyCode :: Left if is_ctrl => {
462
+ if let Some ( pos) =
463
+ self . previous_word_position ( )
464
+ {
465
+ self . cursor_position = pos;
466
+ }
467
+ return Ok ( EventState :: Consumed ) ;
468
+ }
469
+ KeyCode :: Right if is_ctrl => {
470
+ if let Some ( pos) = self . next_word_position ( ) {
471
+ self . cursor_position = pos;
472
+ }
473
+ return Ok ( EventState :: Consumed ) ;
474
+ }
369
475
KeyCode :: Delete => {
370
476
if self . cursor_position < self . msg . len ( ) {
371
477
self . msg . remove ( self . cursor_position ) ;
@@ -558,6 +664,96 @@ mod tests {
558
664
assert_eq ! ( get_text( & txt. lines[ 1 ] . 0 [ 0 ] ) , Some ( "b" ) ) ;
559
665
}
560
666
667
+ #[ test]
668
+ fn test_next_word_position ( ) {
669
+ let mut comp = TextInputComponent :: new (
670
+ SharedTheme :: default ( ) ,
671
+ SharedKeyConfig :: default ( ) ,
672
+ "" ,
673
+ "" ,
674
+ false ,
675
+ ) ;
676
+
677
+ comp. set_text ( String :: from ( "aa b;c" ) ) ;
678
+ // from word start
679
+ comp. cursor_position = 0 ;
680
+ assert_eq ! ( comp. next_word_position( ) , Some ( 3 ) ) ;
681
+ // from inside start
682
+ comp. cursor_position = 4 ;
683
+ assert_eq ! ( comp. next_word_position( ) , Some ( 5 ) ) ;
684
+ // to string end
685
+ comp. cursor_position = 5 ;
686
+ assert_eq ! ( comp. next_word_position( ) , Some ( 6 ) ) ;
687
+ // from string end
688
+ comp. cursor_position = 6 ;
689
+ assert_eq ! ( comp. next_word_position( ) , None ) ;
690
+ }
691
+
692
+ #[ test]
693
+ fn test_previous_word_position ( ) {
694
+ let mut comp = TextInputComponent :: new (
695
+ SharedTheme :: default ( ) ,
696
+ SharedKeyConfig :: default ( ) ,
697
+ "" ,
698
+ "" ,
699
+ false ,
700
+ ) ;
701
+
702
+ comp. set_text ( String :: from ( " a bb;c" ) ) ;
703
+ // from string end
704
+ comp. cursor_position = 7 ;
705
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 6 ) ) ;
706
+ // from inside word
707
+ comp. cursor_position = 4 ;
708
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 3 ) ) ;
709
+ // from word start
710
+ comp. cursor_position = 3 ;
711
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 1 ) ) ;
712
+ // to string start
713
+ comp. cursor_position = 1 ;
714
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 0 ) ) ;
715
+ // from string start
716
+ comp. cursor_position = 0 ;
717
+ assert_eq ! ( comp. previous_word_position( ) , None ) ;
718
+ }
719
+
720
+ #[ test]
721
+ fn test_next_word_multibyte ( ) {
722
+ let mut comp = TextInputComponent :: new (
723
+ SharedTheme :: default ( ) ,
724
+ SharedKeyConfig :: default ( ) ,
725
+ "" ,
726
+ "" ,
727
+ false ,
728
+ ) ;
729
+
730
+ // "01245 89A EFG"
731
+ let text = dbg ! ( "a à \u{2764} ab\u{1F92F} a" ) ;
732
+
733
+ comp. set_text ( String :: from ( text) ) ;
734
+
735
+ comp. cursor_position = 0 ;
736
+ assert_eq ! ( comp. next_word_position( ) , Some ( 2 ) ) ;
737
+ comp. cursor_position = 2 ;
738
+ assert_eq ! ( comp. next_word_position( ) , Some ( 8 ) ) ;
739
+ comp. cursor_position = 8 ;
740
+ assert_eq ! ( comp. next_word_position( ) , Some ( 15 ) ) ;
741
+ comp. cursor_position = 15 ;
742
+ assert_eq ! ( comp. next_word_position( ) , Some ( 16 ) ) ;
743
+ comp. cursor_position = 16 ;
744
+ assert_eq ! ( comp. next_word_position( ) , None ) ;
745
+
746
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 15 ) ) ;
747
+ comp. cursor_position = 15 ;
748
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 8 ) ) ;
749
+ comp. cursor_position = 8 ;
750
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 2 ) ) ;
751
+ comp. cursor_position = 2 ;
752
+ assert_eq ! ( comp. previous_word_position( ) , Some ( 0 ) ) ;
753
+ comp. cursor_position = 0 ;
754
+ assert_eq ! ( comp. previous_word_position( ) , None ) ;
755
+ }
756
+
561
757
fn get_text < ' a > ( t : & ' a Span ) -> Option < & ' a str > {
562
758
Some ( & t. content )
563
759
}
0 commit comments