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