Skip to content

Commit 106118f

Browse files
committed
add word motions to text input
1 parent edf798b commit 106118f

File tree

1 file changed

+158
-0
lines changed

1 file changed

+158
-0
lines changed

src/components/textinput.rs

+158
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,74 @@ impl TextInputComponent {
129129
Some(index)
130130
}
131131

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+
132200
fn backspace(&mut self) {
133201
if self.cursor_position > 0 {
134202
self.decr_cursor();
@@ -365,6 +433,43 @@ impl Component for TextInputComponent {
365433
self.incr_cursor();
366434
return Ok(EventState::Consumed);
367435
}
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+
}
368473
KeyCode::Delete => {
369474
if self.cursor_position < self.msg.len() {
370475
self.msg.remove(self.cursor_position);
@@ -557,6 +662,59 @@ mod tests {
557662
assert_eq!(get_text(&txt.lines[1].0[0]), Some("b"));
558663
}
559664

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+
560718
fn get_text<'a>(t: &'a Span) -> Option<&'a str> {
561719
Some(&t.content)
562720
}

0 commit comments

Comments
 (0)