datui_lib/widgets/textarea/
cursor.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum CursorMove {
12 Forward,
14 Back,
16 Up,
18 Down,
20 Head,
22 End,
24 Top,
26 Bottom,
28 WordForward,
30 WordBack,
32 UpBy(usize),
34 DownBy(usize),
36 Jump(usize, usize),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42enum CharClass {
43 Space,
44 Word,
45 Punctuation,
46}
47
48fn class_of(c: char) -> CharClass {
49 if c.is_whitespace() {
50 CharClass::Space
51 } else if c.is_alphanumeric() || c == '_' {
52 CharClass::Word
53 } else {
54 CharClass::Punctuation
55 }
56}
57
58pub(super) fn next_word_start(chars: &[char], col: usize) -> usize {
61 let len = chars.len();
62 let mut i = col.min(len);
63 if i == len {
64 return len;
65 }
66 let start_class = class_of(chars[i]);
68 if start_class != CharClass::Space {
69 while i < len && class_of(chars[i]) == start_class {
70 i += 1;
71 }
72 }
73 while i < len && class_of(chars[i]) == CharClass::Space {
74 i += 1;
75 }
76 i
77}
78
79pub(super) fn prev_word_start(chars: &[char], col: usize) -> usize {
81 let mut i = col.min(chars.len());
82 while i > 0 && class_of(chars[i - 1]) == CharClass::Space {
83 i -= 1;
84 }
85 if i == 0 {
86 return 0;
87 }
88 let class = class_of(chars[i - 1]);
89 while i > 0 && class_of(chars[i - 1]) == class {
90 i -= 1;
91 }
92 i
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 fn chars(s: &str) -> Vec<char> {
100 s.chars().collect()
101 }
102
103 #[test]
104 fn next_word_start_skips_run_then_whitespace() {
105 let c = chars("hello world");
106 assert_eq!(next_word_start(&c, 0), 6);
107 assert_eq!(next_word_start(&c, 3), 6);
108 assert_eq!(next_word_start(&c, 6), 11);
109 assert_eq!(next_word_start(&c, 11), 11);
110 }
111
112 #[test]
113 fn next_word_start_treats_punctuation_as_its_own_run() {
114 let c = chars("a == b");
115 assert_eq!(next_word_start(&c, 0), 2);
116 assert_eq!(next_word_start(&c, 2), 5);
117 }
118
119 #[test]
120 fn prev_word_start_walks_back_over_whitespace() {
121 let c = chars("hello world");
122 assert_eq!(prev_word_start(&c, 11), 6);
123 assert_eq!(prev_word_start(&c, 6), 0);
124 assert_eq!(prev_word_start(&c, 0), 0);
125 assert_eq!(prev_word_start(&c, 8), 6);
126 }
127
128 #[test]
129 fn word_moves_handle_empty_lines() {
130 let c: Vec<char> = Vec::new();
131 assert_eq!(next_word_start(&c, 0), 0);
132 assert_eq!(prev_word_start(&c, 0), 0);
133 }
134
135 #[test]
136 fn word_moves_are_unicode_aware() {
137 let c = chars("héllo wörld");
138 assert_eq!(next_word_start(&c, 0), 6);
139 assert_eq!(prev_word_start(&c, 11), 6);
140 }
141}