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