1#[derive(Debug, Clone)]
2pub struct InputBuffer {
3 text: String,
4 cursor: usize,
5 selection_anchor: Option<usize>,
6}
7
8pub fn new() -> InputBuffer {
9 InputBuffer {
10 text: String::new(),
11 cursor: 0,
12 selection_anchor: None,
13 }
14}
15
16pub fn text(buf: &InputBuffer) -> &str {
17 &buf.text
18}
19
20pub fn cursor(buf: &InputBuffer) -> usize {
21 buf.cursor
22}
23
24pub fn selection_range(buf: &InputBuffer) -> Option<(usize, usize)> {
25 buf.selection_anchor.map(|anchor| {
26 let start = anchor.min(buf.cursor);
27 let end = anchor.max(buf.cursor);
28 (start, end)
29 })
30}
31
32pub fn has_selection(buf: &InputBuffer) -> bool {
33 buf.selection_anchor.is_some()
34}
35
36pub fn insert_char(buf: &mut InputBuffer, c: char) {
37 delete_selection_if_active(buf);
38 buf.text.insert(byte_offset(&buf.text, buf.cursor), c);
39 buf.cursor += 1;
40}
41
42pub fn backspace(buf: &mut InputBuffer) {
43 if has_selection(buf) {
44 delete_selection_if_active(buf);
45 return;
46 }
47 if buf.cursor == 0 {
48 return;
49 }
50 let offset = byte_offset(&buf.text, buf.cursor - 1);
51 buf.text.remove(offset);
52 buf.cursor -= 1;
53}
54
55pub fn delete(buf: &mut InputBuffer) {
56 if has_selection(buf) {
57 delete_selection_if_active(buf);
58 return;
59 }
60 let len = char_count(&buf.text);
61 if buf.cursor >= len {
62 return;
63 }
64 let offset = byte_offset(&buf.text, buf.cursor);
65 buf.text.remove(offset);
66}
67
68pub fn move_left(buf: &mut InputBuffer) {
69 buf.selection_anchor = None;
70 if buf.cursor > 0 {
71 buf.cursor -= 1;
72 }
73}
74
75pub fn move_right(buf: &mut InputBuffer) {
76 buf.selection_anchor = None;
77 let len = char_count(&buf.text);
78 if buf.cursor < len {
79 buf.cursor += 1;
80 }
81}
82
83pub fn move_home(buf: &mut InputBuffer) {
84 buf.selection_anchor = None;
85 buf.cursor = 0;
86}
87
88pub fn move_end(buf: &mut InputBuffer) {
89 buf.selection_anchor = None;
90 buf.cursor = char_count(&buf.text);
91}
92
93pub fn select_left(buf: &mut InputBuffer) {
94 if buf.cursor == 0 {
95 return;
96 }
97 if buf.selection_anchor.is_none() {
98 buf.selection_anchor = Some(buf.cursor);
99 }
100 buf.cursor -= 1;
101 collapse_if_empty(buf);
102}
103
104pub fn select_right(buf: &mut InputBuffer) {
105 let len = char_count(&buf.text);
106 if buf.cursor >= len {
107 return;
108 }
109 if buf.selection_anchor.is_none() {
110 buf.selection_anchor = Some(buf.cursor);
111 }
112 buf.cursor += 1;
113 collapse_if_empty(buf);
114}
115
116pub fn select_home(buf: &mut InputBuffer) {
117 if buf.cursor == 0 {
118 return;
119 }
120 if buf.selection_anchor.is_none() {
121 buf.selection_anchor = Some(buf.cursor);
122 }
123 buf.cursor = 0;
124 collapse_if_empty(buf);
125}
126
127pub fn select_end(buf: &mut InputBuffer) {
128 let len = char_count(&buf.text);
129 if buf.cursor >= len {
130 return;
131 }
132 if buf.selection_anchor.is_none() {
133 buf.selection_anchor = Some(buf.cursor);
134 }
135 buf.cursor = len;
136 collapse_if_empty(buf);
137}
138
139pub fn select_all(buf: &mut InputBuffer) {
140 let len = char_count(&buf.text);
141 if len == 0 {
142 return;
143 }
144 buf.selection_anchor = Some(0);
145 buf.cursor = len;
146}
147
148pub fn move_word_left(buf: &mut InputBuffer) {
149 buf.selection_anchor = None;
150 buf.cursor = prev_word_boundary(&buf.text, buf.cursor);
151}
152
153pub fn move_word_right(buf: &mut InputBuffer) {
154 buf.selection_anchor = None;
155 buf.cursor = next_word_boundary(&buf.text, buf.cursor);
156}
157
158pub fn select_word_left(buf: &mut InputBuffer) {
159 let target = prev_word_boundary(&buf.text, buf.cursor);
160 if target == buf.cursor {
161 return;
162 }
163 if buf.selection_anchor.is_none() {
164 buf.selection_anchor = Some(buf.cursor);
165 }
166 buf.cursor = target;
167 collapse_if_empty(buf);
168}
169
170pub fn select_word_right(buf: &mut InputBuffer) {
171 let target = next_word_boundary(&buf.text, buf.cursor);
172 if target == buf.cursor {
173 return;
174 }
175 if buf.selection_anchor.is_none() {
176 buf.selection_anchor = Some(buf.cursor);
177 }
178 buf.cursor = target;
179 collapse_if_empty(buf);
180}
181
182fn delete_selection_if_active(buf: &mut InputBuffer) {
183 let Some(anchor) = buf.selection_anchor.take() else {
184 return;
185 };
186 let start = anchor.min(buf.cursor);
187 let end = anchor.max(buf.cursor);
188
189 let start_byte = byte_offset(&buf.text, start);
190 let end_byte = byte_offset(&buf.text, end);
191 buf.text.replace_range(start_byte..end_byte, "");
192 buf.cursor = start;
193}
194
195fn collapse_if_empty(buf: &mut InputBuffer) {
196 if buf.selection_anchor == Some(buf.cursor) {
197 buf.selection_anchor = None;
198 }
199}
200
201fn byte_offset(text: &str, char_idx: usize) -> usize {
202 text
203 .char_indices()
204 .nth(char_idx)
205 .map(|(i, _)| i)
206 .unwrap_or(text.len())
207}
208
209fn char_count(text: &str) -> usize {
210 text.chars().count()
211}
212
213fn prev_word_boundary(text: &str, cursor: usize) -> usize {
214 if cursor == 0 {
215 return 0;
216 }
217 let chars: Vec<char> = text.chars().collect();
218 let mut pos = cursor - 1;
219
220 while pos > 0 && !chars[pos].is_alphanumeric() {
221 pos -= 1;
222 }
223 while pos > 0 && chars[pos - 1].is_alphanumeric() {
224 pos -= 1;
225 }
226 pos
227}
228
229fn next_word_boundary(text: &str, cursor: usize) -> usize {
230 let chars: Vec<char> = text.chars().collect();
231 let len = chars.len();
232 if cursor >= len {
233 return len;
234 }
235 let mut pos = cursor;
236
237 while pos < len && !chars[pos].is_alphanumeric() {
238 pos += 1;
239 }
240 while pos < len && chars[pos].is_alphanumeric() {
241 pos += 1;
242 }
243 pos
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 #[test]
251 fn insert_appends_at_end() {
252 let mut buf = new();
253 insert_char(&mut buf, 'h');
254 insert_char(&mut buf, 'i');
255
256 assert_eq!(text(&buf), "hi");
257 assert_eq!(cursor(&buf), 2);
258 }
259
260 #[test]
261 fn insert_at_cursor_position() {
262 let mut buf = new();
263 insert_char(&mut buf, 'a');
264 insert_char(&mut buf, 'c');
265 move_left(&mut buf);
266 insert_char(&mut buf, 'b');
267
268 assert_eq!(text(&buf), "abc");
269 assert_eq!(cursor(&buf), 2);
270 }
271
272 #[test]
273 fn backspace_removes_before_cursor() {
274 let mut buf = new();
275 insert_char(&mut buf, 'a');
276 insert_char(&mut buf, 'b');
277 insert_char(&mut buf, 'c');
278 move_left(&mut buf);
279 backspace(&mut buf);
280
281 assert_eq!(text(&buf), "ac");
282 assert_eq!(cursor(&buf), 1);
283 }
284
285 #[test]
286 fn backspace_at_start_does_nothing() {
287 let mut buf = new();
288 insert_char(&mut buf, 'a');
289 move_home(&mut buf);
290 backspace(&mut buf);
291
292 assert_eq!(text(&buf), "a");
293 assert_eq!(cursor(&buf), 0);
294 }
295
296 #[test]
297 fn delete_removes_at_cursor() {
298 let mut buf = new();
299 insert_char(&mut buf, 'a');
300 insert_char(&mut buf, 'b');
301 insert_char(&mut buf, 'c');
302 move_home(&mut buf);
303 delete(&mut buf);
304
305 assert_eq!(text(&buf), "bc");
306 assert_eq!(cursor(&buf), 0);
307 }
308
309 #[test]
310 fn delete_at_end_does_nothing() {
311 let mut buf = new();
312 insert_char(&mut buf, 'a');
313 delete(&mut buf);
314
315 assert_eq!(text(&buf), "a");
316 }
317
318 #[test]
319 fn move_left_right_navigates() {
320 let mut buf = new();
321 insert_char(&mut buf, 'a');
322 insert_char(&mut buf, 'b');
323 insert_char(&mut buf, 'c');
324
325 move_left(&mut buf);
326 assert_eq!(cursor(&buf), 2);
327 move_left(&mut buf);
328 assert_eq!(cursor(&buf), 1);
329 move_right(&mut buf);
330 assert_eq!(cursor(&buf), 2);
331 }
332
333 #[test]
334 fn move_left_clamps_at_zero() {
335 let mut buf = new();
336 move_left(&mut buf);
337 assert_eq!(cursor(&buf), 0);
338 }
339
340 #[test]
341 fn move_right_clamps_at_end() {
342 let mut buf = new();
343 insert_char(&mut buf, 'a');
344 move_right(&mut buf);
345 assert_eq!(cursor(&buf), 1);
346 }
347
348 #[test]
349 fn home_and_end() {
350 let mut buf = new();
351 insert_char(&mut buf, 'a');
352 insert_char(&mut buf, 'b');
353 insert_char(&mut buf, 'c');
354 move_home(&mut buf);
355 assert_eq!(cursor(&buf), 0);
356 move_end(&mut buf);
357 assert_eq!(cursor(&buf), 3);
358 }
359
360 #[test]
361 fn select_left_creates_selection() {
362 let mut buf = new();
363 insert_char(&mut buf, 'a');
364 insert_char(&mut buf, 'b');
365 insert_char(&mut buf, 'c');
366 select_left(&mut buf);
367 select_left(&mut buf);
368
369 assert_eq!(selection_range(&buf), Some((1, 3)));
370 assert_eq!(cursor(&buf), 1);
371 }
372
373 #[test]
374 fn select_right_creates_selection() {
375 let mut buf = new();
376 insert_char(&mut buf, 'a');
377 insert_char(&mut buf, 'b');
378 insert_char(&mut buf, 'c');
379 move_home(&mut buf);
380 select_right(&mut buf);
381
382 assert_eq!(selection_range(&buf), Some((0, 1)));
383 assert_eq!(cursor(&buf), 1);
384 }
385
386 #[test]
387 fn select_all_selects_entire_text() {
388 let mut buf = new();
389 insert_char(&mut buf, 'h');
390 insert_char(&mut buf, 'i');
391 select_all(&mut buf);
392
393 assert_eq!(selection_range(&buf), Some((0, 2)));
394 }
395
396 #[test]
397 fn typing_replaces_selection() {
398 let mut buf = new();
399 insert_char(&mut buf, 'h');
400 insert_char(&mut buf, 'e');
401 insert_char(&mut buf, 'l');
402 insert_char(&mut buf, 'l');
403 insert_char(&mut buf, 'o');
404 select_all(&mut buf);
405 insert_char(&mut buf, 'x');
406
407 assert_eq!(text(&buf), "x");
408 assert_eq!(cursor(&buf), 1);
409 assert!(!has_selection(&buf));
410 }
411
412 #[test]
413 fn backspace_deletes_selection() {
414 let mut buf = new();
415 insert_char(&mut buf, 'a');
416 insert_char(&mut buf, 'b');
417 insert_char(&mut buf, 'c');
418 select_left(&mut buf);
419 select_left(&mut buf);
420 backspace(&mut buf);
421
422 assert_eq!(text(&buf), "a");
423 assert_eq!(cursor(&buf), 1);
424 }
425
426 #[test]
427 fn move_clears_selection() {
428 let mut buf = new();
429 insert_char(&mut buf, 'a');
430 insert_char(&mut buf, 'b');
431 select_left(&mut buf);
432 move_right(&mut buf);
433
434 assert!(!has_selection(&buf));
435 }
436
437 #[test]
438 fn select_home_from_end() {
439 let mut buf = new();
440 insert_char(&mut buf, 'a');
441 insert_char(&mut buf, 'b');
442 insert_char(&mut buf, 'c');
443 select_home(&mut buf);
444
445 assert_eq!(selection_range(&buf), Some((0, 3)));
446 assert_eq!(cursor(&buf), 0);
447 }
448
449 #[test]
450 fn select_end_from_start() {
451 let mut buf = new();
452 insert_char(&mut buf, 'a');
453 insert_char(&mut buf, 'b');
454 move_home(&mut buf);
455 select_end(&mut buf);
456
457 assert_eq!(selection_range(&buf), Some((0, 2)));
458 assert_eq!(cursor(&buf), 2);
459 }
460
461 #[test]
462 fn word_boundaries_navigate_words() {
463 let mut buf = new();
464 for c in "hello world foo".chars() {
465 insert_char(&mut buf, c);
466 }
467 move_home(&mut buf);
468
469 move_word_right(&mut buf);
470 assert_eq!(cursor(&buf), 5);
471 move_word_right(&mut buf);
472 assert_eq!(cursor(&buf), 11);
473 move_word_left(&mut buf);
474 assert_eq!(cursor(&buf), 6);
475 move_word_left(&mut buf);
476 assert_eq!(cursor(&buf), 0);
477 }
478
479 #[test]
480 fn select_word_left_selects_word() {
481 let mut buf = new();
482 for c in "hello world".chars() {
483 insert_char(&mut buf, c);
484 }
485 select_word_left(&mut buf);
486
487 assert_eq!(selection_range(&buf), Some((6, 11)));
488 }
489
490 #[test]
491 fn select_word_right_selects_word() {
492 let mut buf = new();
493 for c in "hello world".chars() {
494 insert_char(&mut buf, c);
495 }
496 move_home(&mut buf);
497 select_word_right(&mut buf);
498
499 assert_eq!(selection_range(&buf), Some((0, 5)));
500 }
501
502 #[test]
503 fn multibyte_chars_handled_correctly() {
504 let mut buf = new();
505 for c in "café".chars() {
506 insert_char(&mut buf, c);
507 }
508 assert_eq!(cursor(&buf), 4);
509 move_left(&mut buf);
510 insert_char(&mut buf, 'x');
511
512 assert_eq!(text(&buf), "cafxé");
513 assert_eq!(cursor(&buf), 4);
514 }
515
516 #[test]
517 fn selection_collapes_when_cursor_meets_anchor() {
518 let mut buf = new();
519 insert_char(&mut buf, 'a');
520 insert_char(&mut buf, 'b');
521 select_left(&mut buf);
522 select_right(&mut buf);
523
524 assert!(!has_selection(&buf));
525 }
526
527 #[test]
530 fn delete_with_selection_removes_selected() {
531 let mut buf = new();
532 for c in "abc".chars() {
533 insert_char(&mut buf, c);
534 }
535 move_home(&mut buf);
536 select_right(&mut buf);
537 select_right(&mut buf);
538 delete(&mut buf);
539 assert_eq!(text(&buf), "c");
540 }
541
542 #[test]
543 fn select_left_at_zero_is_noop() {
544 let mut buf = new();
545 insert_char(&mut buf, 'a');
546 move_home(&mut buf);
547 select_left(&mut buf);
548 assert_eq!(cursor(&buf), 0);
549 assert!(!has_selection(&buf));
550 }
551
552 #[test]
553 fn select_right_at_end_is_noop() {
554 let mut buf = new();
555 insert_char(&mut buf, 'a');
556 select_right(&mut buf);
557 assert_eq!(cursor(&buf), 1);
558 assert!(!has_selection(&buf));
559 }
560
561 #[test]
562 fn select_home_at_zero_is_noop() {
563 let mut buf = new();
564 select_home(&mut buf);
565 assert_eq!(cursor(&buf), 0);
566 assert!(!has_selection(&buf));
567 }
568
569 #[test]
570 fn select_end_at_end_is_noop() {
571 let mut buf = new();
572 insert_char(&mut buf, 'a');
573 select_end(&mut buf);
574 assert_eq!(cursor(&buf), 1);
575 assert!(!has_selection(&buf));
576 }
577
578 #[test]
579 fn select_all_on_empty_is_noop() {
580 let mut buf = new();
581 select_all(&mut buf);
582 assert!(!has_selection(&buf));
583 }
584
585 #[test]
586 fn select_word_left_at_boundary_is_noop() {
587 let mut buf = new();
588 insert_char(&mut buf, 'a');
589 move_home(&mut buf);
590 select_word_left(&mut buf);
591 assert_eq!(cursor(&buf), 0);
592 assert!(!has_selection(&buf));
593 }
594
595 #[test]
596 fn select_word_right_at_boundary_is_noop() {
597 let mut buf = new();
598 insert_char(&mut buf, 'a');
599 select_word_right(&mut buf);
600 assert_eq!(cursor(&buf), 1);
601 assert!(!has_selection(&buf));
602 }
603
604 #[test]
605 fn prev_word_boundary_at_zero_returns_zero() {
606 assert_eq!(prev_word_boundary("hello", 0), 0);
607 }
608
609 #[test]
610 fn next_word_boundary_at_end_returns_len() {
611 assert_eq!(next_word_boundary("hello", 5), 5);
612 }
613}