1#[derive(Debug, Clone, Default)]
7pub struct TextEdit {
8 chars: Vec<char>,
9 cursor: usize,
11}
12
13impl TextEdit {
14 pub fn new(text: &str) -> Self {
16 let chars: Vec<char> = text.chars().collect();
17 let cursor = chars.len();
18 Self { chars, cursor }
19 }
20
21 pub fn text(&self) -> String {
22 self.chars.iter().collect()
23 }
24
25 pub fn is_empty(&self) -> bool {
26 self.chars.is_empty()
27 }
28
29 pub fn len(&self) -> usize {
30 self.chars.len()
31 }
32
33 pub fn insert(&mut self, s: &str) {
35 for c in s.chars() {
36 self.chars.insert(self.cursor, c);
37 self.cursor += 1;
38 }
39 }
40
41 pub fn backspace(&mut self) -> bool {
43 if self.cursor == 0 {
44 return false;
45 }
46 self.cursor -= 1;
47 self.chars.remove(self.cursor);
48 true
49 }
50
51 pub fn delete(&mut self) -> bool {
53 if self.cursor >= self.chars.len() {
54 return false;
55 }
56 self.chars.remove(self.cursor);
57 true
58 }
59
60 pub fn left(&mut self) {
61 self.cursor = self.cursor.saturating_sub(1);
62 }
63
64 pub fn right(&mut self) {
65 if self.cursor < self.chars.len() {
66 self.cursor += 1;
67 }
68 }
69
70 pub fn home(&mut self) {
71 self.cursor = 0;
72 }
73
74 pub fn end(&mut self) {
75 self.cursor = self.chars.len();
76 }
77
78 pub fn word_left(&mut self) {
80 while self.cursor > 0 && !is_word(self.chars[self.cursor - 1]) {
81 self.cursor -= 1;
82 }
83 while self.cursor > 0 && is_word(self.chars[self.cursor - 1]) {
84 self.cursor -= 1;
85 }
86 }
87
88 pub fn word_right(&mut self) {
90 let n = self.chars.len();
91 while self.cursor < n && !is_word(self.chars[self.cursor]) {
92 self.cursor += 1;
93 }
94 while self.cursor < n && is_word(self.chars[self.cursor]) {
95 self.cursor += 1;
96 }
97 }
98
99 pub fn delete_word_back(&mut self) -> bool {
102 let end = self.cursor;
103 self.word_left();
104 if self.cursor < end {
105 self.chars.drain(self.cursor..end);
106 true
107 } else {
108 false
109 }
110 }
111
112 pub fn delete_word_forward(&mut self) -> bool {
115 let start = self.cursor;
116 let n = self.chars.len();
117 let mut end = self.cursor;
118 while end < n && !is_word(self.chars[end]) {
119 end += 1;
120 }
121 while end < n && is_word(self.chars[end]) {
122 end += 1;
123 }
124 if end > start {
125 self.chars.drain(start..end);
126 true
127 } else {
128 false
129 }
130 }
131
132 pub fn delete_to_start(&mut self) -> bool {
135 if self.cursor == 0 {
136 return false;
137 }
138 self.chars.drain(0..self.cursor);
139 self.cursor = 0;
140 true
141 }
142
143 pub fn delete_to_end(&mut self) -> bool {
146 if self.cursor >= self.chars.len() {
147 return false;
148 }
149 self.chars.truncate(self.cursor);
150 true
151 }
152
153 pub fn split(&self) -> (String, String) {
155 (
156 self.chars[..self.cursor].iter().collect(),
157 self.chars[self.cursor..].iter().collect(),
158 )
159 }
160
161 pub fn up(&mut self) {
164 self.vmove(-1);
165 }
166
167 pub fn down(&mut self) {
169 self.vmove(1);
170 }
171
172 fn line_col(&self) -> (usize, usize) {
174 let mut line = 0;
175 let mut col = 0;
176 for &c in &self.chars[..self.cursor] {
177 if c == '\n' {
178 line += 1;
179 col = 0;
180 } else {
181 col += 1;
182 }
183 }
184 (line, col)
185 }
186
187 fn line_bounds(&self) -> Vec<(usize, usize)> {
189 let mut out = Vec::new();
190 let mut start = 0;
191 let mut len = 0;
192 for (i, &c) in self.chars.iter().enumerate() {
193 if c == '\n' {
194 out.push((start, len));
195 start = i + 1;
196 len = 0;
197 } else {
198 len += 1;
199 }
200 }
201 out.push((start, len));
202 out
203 }
204
205 fn vmove(&mut self, dir: isize) {
206 let (line, col) = self.line_col();
207 let bounds = self.line_bounds();
208 let target = line as isize + dir;
209 if target < 0 || target as usize >= bounds.len() {
210 return;
211 }
212 let (start, len) = bounds[target as usize];
213 self.cursor = start + col.min(len);
214 }
215}
216
217fn is_word(c: char) -> bool {
219 c.is_alphanumeric() || c == '_'
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn new_places_cursor_at_end() {
228 let e = TextEdit::new("abc");
229 assert_eq!(e.split(), ("abc".into(), "".into()));
230 }
231
232 #[test]
233 fn insert_at_cursor() {
234 let mut e = TextEdit::new("ac");
235 e.left();
236 e.insert("b");
237 assert_eq!(e.text(), "abc");
238 assert_eq!(e.split(), ("ab".into(), "c".into()));
239 }
240
241 #[test]
242 fn backspace_and_delete() {
243 let mut e = TextEdit::new("abc");
244 assert!(e.backspace());
245 assert_eq!(e.text(), "ab");
246 e.home();
247 assert!(e.delete());
248 assert_eq!(e.text(), "b");
249 e.home();
250 assert!(!e.backspace());
251 e.end();
252 assert!(!e.delete());
253 }
254
255 #[test]
256 fn handles_unicode() {
257 let mut e = TextEdit::new("café");
258 assert!(e.backspace());
259 assert_eq!(e.text(), "caf");
260 e.insert("é");
261 assert_eq!(e.text(), "café");
262 }
263
264 #[test]
265 fn vertical_movement_keeps_column() {
266 let mut e = TextEdit::new("hello\nhi");
268 e.up();
270 assert_eq!(e.split().0, "he");
272 e.down();
273 assert_eq!(e.split(), ("hello\nhi".into(), "".into()));
275 }
276
277 #[test]
278 fn vertical_movement_stops_at_edges() {
279 let mut e = TextEdit::new("a\nb");
280 e.home(); e.up(); assert_eq!(e.split().0, "");
283 }
284
285 #[test]
286 fn word_navigation() {
287 let mut e = TextEdit::new("foo bar baz");
288 e.word_left();
289 assert_eq!(e.split(), ("foo bar ".into(), "baz".into()));
290 e.word_left();
291 assert_eq!(e.split(), ("foo ".into(), "bar baz".into()));
292 e.word_right();
293 assert_eq!(e.split(), ("foo bar".into(), " baz".into()));
294 }
295
296 #[test]
297 fn delete_word_back_and_forward() {
298 let mut e = TextEdit::new("foo bar baz");
299 assert!(e.delete_word_back());
300 assert_eq!(e.text(), "foo bar ");
301 e.home();
302 assert!(e.delete_word_forward());
303 assert_eq!(e.text(), " bar ");
304 e.home();
306 assert!(!e.delete_word_back());
307 }
308
309 #[test]
310 fn delete_to_line_edges() {
311 let mut e = TextEdit::new("hello world");
312 e.home();
313 e.right();
314 e.right();
315 assert!(e.delete_to_start());
316 assert_eq!(e.text(), "llo world");
317 assert!(e.delete_to_end());
318 assert_eq!(e.text(), "");
319 assert!(!e.delete_to_end());
320 }
321}