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