1const DEFAULT_CAPACITY: usize = 1024;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Key {
15 Char(char),
17 Backspace,
19 MoveLeft,
22 MoveRight,
25 WordLeft,
29 WordRight,
32 LineStart,
36 LineEnd,
38 Reset,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct WordAtCaret {
51 pub word: String,
54 pub trailing: String,
57 pub chars_before_caret: usize,
60 pub chars_after_caret: usize,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Sentence {
71 pub sentence: String,
73 pub buffer_byte_start: usize,
75 pub buffer_byte_end: usize,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SentenceAtCaret {
85 pub sentence: String,
88 pub trailing: String,
91 pub chars_before_caret: usize,
93 pub chars_after_caret: usize,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct NearbyWord {
106 pub word: String,
108 pub byte_start: usize,
110 pub byte_end: usize,
112 pub caret_offset_chars: i32,
118}
119
120#[derive(Debug)]
128pub struct Buffer {
129 text: String,
130 caret: usize,
133 capacity: usize,
134}
135
136impl Default for Buffer {
137 fn default() -> Self {
138 Self::with_capacity(DEFAULT_CAPACITY)
139 }
140}
141
142impl Buffer {
143 pub fn with_capacity(capacity: usize) -> Self {
145 Self {
146 text: String::new(),
147 caret: 0,
148 capacity: capacity.max(1),
149 }
150 }
151
152 pub fn push(&mut self, key: Key) {
154 match key {
155 Key::Char(c) => {
156 self.text.insert(self.caret, c);
157 self.caret += c.len_utf8();
158 self.trim_to_capacity();
159 }
160 Key::Backspace => {
161 if self.caret == 0 {
162 return;
163 }
164 let prev = prev_char_boundary(&self.text, self.caret);
165 self.text.drain(prev..self.caret);
166 self.caret = prev;
167 }
168 Key::MoveLeft => {
169 if self.caret == 0 {
170 return;
171 }
172 self.caret = prev_char_boundary(&self.text, self.caret);
173 }
174 Key::MoveRight => {
175 if self.caret >= self.text.len() {
176 return;
177 }
178 self.caret = next_char_boundary(&self.text, self.caret);
179 }
180 Key::WordLeft => {
181 self.caret = prev_word_boundary(&self.text, self.caret);
182 }
183 Key::WordRight => {
184 self.caret = next_word_boundary(&self.text, self.caret);
185 }
186 Key::LineStart => {
187 self.caret = 0;
188 }
189 Key::LineEnd => {
190 self.caret = self.text.len();
191 }
192 Key::Reset => {
193 self.text.clear();
194 self.caret = 0;
195 }
196 }
197 }
198
199 pub fn clear(&mut self) {
201 self.text.clear();
202 self.caret = 0;
203 }
204
205 pub fn is_empty(&self) -> bool {
207 self.text.is_empty()
208 }
209
210 pub fn text(&self) -> &str {
212 &self.text
213 }
214
215 pub fn text_before_caret(&self) -> &str {
218 &self.text[..self.caret]
219 }
220
221 pub fn sentence_at_caret(&self) -> Option<SentenceAtCaret> {
233 let caret = self.caret;
234 let s = self.sentence_containing(caret)?;
235 let caret_in_range = caret.clamp(s.buffer_byte_start, s.buffer_byte_end);
236 let chars_before = self.text[s.buffer_byte_start..caret_in_range]
237 .chars()
238 .count();
239 let chars_after = self.text[caret_in_range..s.buffer_byte_end].chars().count();
240 let trailing = if caret > s.buffer_byte_end {
244 self.text[s.buffer_byte_end..caret].to_string()
245 } else {
246 String::new()
247 };
248 Some(SentenceAtCaret {
249 sentence: s.sentence,
250 trailing,
251 chars_before_caret: chars_before,
252 chars_after_caret: chars_after,
253 })
254 }
255
256 pub fn sentence_containing(&self, byte_offset: usize) -> Option<Sentence> {
265 let text = &self.text;
266 if text.is_empty() {
267 return None;
268 }
269 let mut ranges: Vec<(usize, usize)> = Vec::new();
273 let mut start = 0;
274 for (i, c) in text.char_indices() {
275 if matches!(c, '.' | '!' | '?') {
276 ranges.push((start, i + c.len_utf8()));
277 start = i + c.len_utf8();
278 }
279 }
280 if start < text.len() {
281 ranges.push((start, text.len()));
282 }
283 if ranges.is_empty() {
284 return None;
285 }
286 let mut idx = ranges
292 .iter()
293 .rposition(|&(s, e)| s <= byte_offset && byte_offset <= e)?;
294 if text[ranges[idx].0..ranges[idx].1].trim().is_empty() && idx > 0 {
295 idx -= 1;
296 }
297 let (range_start, range_end) = ranges[idx];
298 let raw = &text[range_start..range_end];
299 let leading_ws = raw.len() - raw.trim_start().len();
300 let sentence_start = range_start + leading_ws;
301 let sentence_end = range_start + raw.trim_end().len();
302 if sentence_start >= sentence_end {
303 return None;
304 }
305 Some(Sentence {
306 sentence: text[sentence_start..sentence_end].to_string(),
307 buffer_byte_start: sentence_start,
308 buffer_byte_end: sentence_end,
309 })
310 }
311
312 pub fn word_at_caret(&self) -> Option<WordAtCaret> {
322 let caret = self.caret;
323 let text = &self.text;
324
325 let prev_is_word = text[..caret].chars().next_back().is_some_and(is_word_char);
326 if prev_is_word {
327 let right_span: usize = text[caret..]
329 .chars()
330 .take_while(|&c| is_word_char(c))
331 .map(char::len_utf8)
332 .sum();
333 let left_span: usize = text[..caret]
334 .chars()
335 .rev()
336 .take_while(|&c| is_word_char(c))
337 .map(char::len_utf8)
338 .sum();
339 let word_start = caret - left_span;
340 let word_end = caret + right_span;
341 if word_start == word_end {
342 return None;
343 }
344 return Some(WordAtCaret {
345 word: text[word_start..word_end].to_string(),
346 trailing: String::new(),
347 chars_before_caret: text[word_start..caret].chars().count(),
348 chars_after_caret: text[caret..word_end].chars().count(),
349 });
350 }
351 let before = &text[..caret];
357 let trimmed_right = before.trim_end_matches(|c: char| !is_word_char(c));
358 if trimmed_right.is_empty() {
359 return None;
360 }
361 let word_chars: usize = trimmed_right
362 .chars()
363 .rev()
364 .take_while(|&c| is_word_char(c))
365 .map(char::len_utf8)
366 .sum();
367 if word_chars == 0 {
368 return None;
369 }
370 let word_end = trimmed_right.len();
371 let word_start = word_end - word_chars;
372 Some(WordAtCaret {
373 word: text[word_start..word_end].to_string(),
374 trailing: text[word_end..caret].to_string(),
375 chars_before_caret: text[word_start..word_end].chars().count(),
376 chars_after_caret: 0,
377 })
378 }
379
380 pub fn apply_around_caret(&mut self, backspaces: usize, deletes: usize, insert: &str) {
386 for _ in 0..backspaces {
387 if self.caret == 0 {
388 break;
389 }
390 let prev = prev_char_boundary(&self.text, self.caret);
391 self.text.drain(prev..self.caret);
392 self.caret = prev;
393 }
394 for _ in 0..deletes {
395 if self.caret >= self.text.len() {
396 break;
397 }
398 let next = next_char_boundary(&self.text, self.caret);
399 self.text.drain(self.caret..next);
400 }
401 self.text.insert_str(self.caret, insert);
402 self.caret += insert.len();
403 self.trim_to_capacity();
404 }
405
406 pub fn apply(&mut self, backspaces: usize, insert: &str) {
410 self.apply_around_caret(backspaces, 0, insert);
411 }
412
413 pub fn caret(&self) -> usize {
417 self.caret
418 }
419
420 pub fn apply_at_word(&mut self, byte_start: usize, byte_end: usize, insert: &str) {
427 debug_assert!(byte_start <= byte_end && byte_end <= self.text.len());
428 self.text.replace_range(byte_start..byte_end, insert);
429 self.caret = byte_start + insert.len();
430 self.trim_to_capacity();
431 }
432
433 pub fn words_near_caret(&self) -> Vec<NearbyWord> {
441 let text = &self.text;
442 let caret = self.caret;
443 let mut out: Vec<NearbyWord> = Vec::new();
444 let mut current_start: Option<usize> = None;
445 for (i, c) in text.char_indices() {
446 if is_word_char(c) {
447 if current_start.is_none() {
448 current_start = Some(i);
449 }
450 } else if let Some(start) = current_start.take() {
451 out.push(make_nearby_word(text, caret, start, i));
452 }
453 }
454 if let Some(start) = current_start {
455 out.push(make_nearby_word(text, caret, start, text.len()));
456 }
457 out.sort_by_key(|nw| nw.caret_offset_chars.abs());
458 out
459 }
460
461 fn trim_to_capacity(&mut self) {
465 while self.text.chars().count() > self.capacity {
466 let first = self.text.chars().next().map_or(0, char::len_utf8);
467 self.text.drain(..first);
468 self.caret = self.caret.saturating_sub(first);
469 }
470 }
471}
472
473fn prev_char_boundary(s: &str, pos: usize) -> usize {
476 s[..pos].char_indices().next_back().map_or(0, |(i, _)| i)
477}
478
479fn next_char_boundary(s: &str, pos: usize) -> usize {
482 s[pos..].chars().next().map_or(pos, |c| pos + c.len_utf8())
483}
484
485fn make_nearby_word(text: &str, caret: usize, start: usize, end: usize) -> NearbyWord {
488 let caret_offset_chars = if end >= caret {
489 text[caret..end].chars().count() as i32
490 } else {
491 -(text[end..caret].chars().count() as i32)
492 };
493 NearbyWord {
494 word: text[start..end].to_string(),
495 byte_start: start,
496 byte_end: end,
497 caret_offset_chars,
498 }
499}
500
501fn is_word_char(c: char) -> bool {
509 c.is_alphanumeric() || c == '\''
510}
511
512fn is_nav_word_char(c: char) -> bool {
523 is_word_char(c) || matches!(c, '.' | '-' | '_')
524}
525
526fn prev_word_boundary(s: &str, from: usize) -> usize {
530 let left = &s[..from];
531 let trim: usize = left
532 .chars()
533 .rev()
534 .take_while(|&c| !is_nav_word_char(c))
535 .map(char::len_utf8)
536 .sum();
537 let trimmed_end = left.len() - trim;
538 let word_chars: usize = left[..trimmed_end]
539 .chars()
540 .rev()
541 .take_while(|&c| is_nav_word_char(c))
542 .map(char::len_utf8)
543 .sum();
544 trimmed_end - word_chars
545}
546
547fn next_word_boundary(s: &str, from: usize) -> usize {
550 let right = &s[from..];
551 let skip: usize = right
552 .chars()
553 .take_while(|&c| !is_nav_word_char(c))
554 .map(char::len_utf8)
555 .sum();
556 let word_chars: usize = right[skip..]
557 .chars()
558 .take_while(|&c| is_nav_word_char(c))
559 .map(char::len_utf8)
560 .sum();
561 from + skip + word_chars
562}
563
564#[cfg(test)]
565mod tests {
566 use super::*;
567
568 fn type_str(buf: &mut Buffer, s: &str) {
570 for c in s.chars() {
571 buf.push(Key::Char(c));
572 }
573 }
574
575 #[test]
576 fn empty_buffer_has_no_word() {
577 let buf = Buffer::default();
578 assert!(buf.is_empty());
579 assert_eq!(buf.word_at_caret(), None);
580 }
581
582 #[test]
583 fn word_at_caret_at_end_of_word() {
584 let mut buf = Buffer::default();
585 type_str(&mut buf, "vernuer");
586 let at = buf.word_at_caret().unwrap();
587 assert_eq!(at.word, "vernuer");
588 assert_eq!(at.trailing, "");
589 assert_eq!(at.chars_before_caret, 7);
590 assert_eq!(at.chars_after_caret, 0);
591 }
592
593 #[test]
594 fn word_at_caret_in_trailing_whitespace_picks_the_left_word() {
595 let mut buf = Buffer::default();
596 type_str(&mut buf, "vernuer ");
597 let at = buf.word_at_caret().unwrap();
598 assert_eq!(at.word, "vernuer");
599 assert_eq!(at.trailing, " ");
600 assert_eq!(at.chars_before_caret, 7);
601 assert_eq!(at.chars_after_caret, 0);
602 }
603
604 #[test]
605 fn word_at_caret_inside_word_expands_both_directions() {
606 let mut buf = Buffer::default();
607 type_str(&mut buf, "vernuer");
608 for _ in 0..4 {
610 buf.push(Key::MoveLeft);
611 }
612 let at = buf.word_at_caret().unwrap();
613 assert_eq!(at.word, "vernuer");
614 assert_eq!(at.chars_before_caret, 3);
615 assert_eq!(at.chars_after_caret, 4);
616 }
617
618 #[test]
619 fn word_at_caret_picks_the_final_word() {
620 let mut buf = Buffer::default();
621 type_str(&mut buf, "the quick vernuer ");
622 let at = buf.word_at_caret().unwrap();
623 assert_eq!(at.word, "vernuer");
624 }
625
626 #[test]
627 fn all_whitespace_has_no_word_at_caret() {
628 let mut buf = Buffer::default();
629 type_str(&mut buf, " ");
630 assert_eq!(buf.word_at_caret(), None);
631 }
632
633 #[test]
634 fn word_at_caret_handles_multibyte_chars() {
635 let mut buf = Buffer::default();
636 type_str(&mut buf, "café ");
637 let at = buf.word_at_caret().unwrap();
638 assert_eq!(at.word, "café");
639 assert_eq!(at.chars_before_caret, 4);
640 }
641
642 #[test]
643 fn backspace_removes_the_last_character() {
644 let mut buf = Buffer::default();
645 type_str(&mut buf, "vernuer");
646 buf.push(Key::Backspace);
647 assert_eq!(buf.text(), "vernue");
648 }
649
650 #[test]
651 fn backspace_on_empty_buffer_is_a_no_op() {
652 let mut buf = Buffer::default();
653 buf.push(Key::Backspace);
654 assert!(buf.is_empty());
655 }
656
657 #[test]
658 fn reset_clears_the_buffer() {
659 let mut buf = Buffer::default();
660 type_str(&mut buf, "vernuer ");
661 buf.push(Key::Reset);
662 assert!(buf.is_empty());
663 assert_eq!(buf.word_at_caret(), None);
664 }
665
666 #[test]
667 fn buffer_is_bounded_by_capacity() {
668 let mut buf = Buffer::with_capacity(5);
669 type_str(&mut buf, "abcdefgh");
670 assert_eq!(buf.text(), "defgh");
671 }
672
673 #[test]
674 fn sentence_at_caret_after_an_ender() {
675 let mut buf = Buffer::default();
676 type_str(&mut buf, "Hello there. how are you ");
677 let at = buf.sentence_at_caret().unwrap();
678 assert_eq!(at.sentence, "how are you");
679 assert_eq!(at.trailing, " ");
680 assert_eq!(at.chars_after_caret, 0);
681 }
682
683 #[test]
684 fn sentence_at_caret_when_no_ender_yet() {
685 let mut buf = Buffer::default();
686 type_str(&mut buf, "the quick brown fox");
687 let at = buf.sentence_at_caret().unwrap();
688 assert_eq!(at.sentence, "the quick brown fox");
689 assert_eq!(at.trailing, "");
690 }
691
692 #[test]
693 fn sentence_at_caret_with_multiple_enders_picks_current() {
694 let mut buf = Buffer::default();
695 type_str(&mut buf, "Hi! Hello there. How are yu");
696 let at = buf.sentence_at_caret().unwrap();
697 assert_eq!(at.sentence, "How are yu");
698 }
699
700 #[test]
701 fn sentence_at_caret_includes_the_trailing_ender() {
702 let mut buf = Buffer::default();
703 type_str(&mut buf, "Hello there.");
704 let at = buf.sentence_at_caret().unwrap();
705 assert_eq!(at.sentence, "Hello there.");
706 }
707
708 #[test]
709 fn sentence_at_caret_picks_the_final_of_multiple_complete_sentences() {
710 let mut buf = Buffer::default();
711 type_str(&mut buf, "First sentence. Second sentence!");
712 let at = buf.sentence_at_caret().unwrap();
713 assert_eq!(at.sentence, "Second sentence!");
714 }
715
716 #[test]
717 fn sentence_at_caret_after_complete_one_then_trailing_ws() {
718 let mut buf = Buffer::default();
719 type_str(&mut buf, "Hello there. ");
720 let at = buf.sentence_at_caret().unwrap();
721 assert_eq!(at.sentence, "Hello there.");
722 assert_eq!(at.trailing, " ");
723 }
724
725 #[test]
726 fn sentence_at_caret_returns_none_for_whitespace_only() {
727 let mut buf = Buffer::default();
728 type_str(&mut buf, " ");
729 assert!(buf.sentence_at_caret().is_none());
730 }
731
732 #[test]
733 fn sentence_at_caret_in_middle_spans_both_sides() {
734 let mut buf = Buffer::default();
735 type_str(&mut buf, "the quick brown fox jumps");
736 for _ in 0..10 {
738 buf.push(Key::MoveLeft);
739 }
740 let at = buf.sentence_at_caret().unwrap();
741 assert_eq!(at.sentence, "the quick brown fox jumps");
742 assert_eq!(at.chars_before_caret, 15);
743 assert_eq!(at.chars_after_caret, 10);
744 }
745
746 #[test]
747 fn sentence_containing_returns_the_active_sentence_with_buffer_offsets() {
748 let mut buf = Buffer::default();
749 type_str(&mut buf, "Hello world. The quick brown fox.");
750 let fox_buf_start = buf.text().find("fox").unwrap();
755 let fox_buf_end = fox_buf_start + "fox".len();
756 let s = buf.sentence_containing(fox_buf_start).unwrap();
757 assert_eq!(s.sentence, "The quick brown fox.");
758 assert_eq!(s.buffer_byte_start, 13);
759 assert_eq!(s.buffer_byte_end, 33);
760 let start_in_sentence = fox_buf_start - s.buffer_byte_start;
761 let end_in_sentence = fox_buf_end - s.buffer_byte_start;
762 assert_eq!(&s.sentence[start_in_sentence..end_in_sentence], "fox");
763 }
764
765 #[test]
766 fn sentence_containing_picks_first_sentence_for_offset_in_it() {
767 let mut buf = Buffer::default();
768 type_str(&mut buf, "Hello world. The quick brown fox.");
769 let s = buf.sentence_containing(3).unwrap();
770 assert_eq!(s.sentence, "Hello world.");
771 assert_eq!(s.buffer_byte_start, 0);
772 assert_eq!(s.buffer_byte_end, 12);
773 }
774
775 #[test]
776 fn sentence_containing_returns_none_for_whitespace_only_buffer() {
777 let mut buf = Buffer::default();
778 type_str(&mut buf, " ");
779 assert!(buf.sentence_containing(1).is_none());
780 }
781
782 #[test]
783 fn sentence_containing_steps_back_from_inter_sentence_whitespace() {
784 let mut buf = Buffer::default();
785 type_str(&mut buf, "Hello world. ");
786 let s = buf.sentence_containing(13).unwrap();
790 assert_eq!(s.sentence, "Hello world.");
791 }
792
793 #[test]
794 fn apply_mirrors_a_correction_at_end() {
795 let mut buf = Buffer::default();
796 type_str(&mut buf, "vernuer ");
797 buf.apply(8, "veneer ");
798 assert_eq!(buf.text(), "veneer ");
799 assert_eq!(buf.word_at_caret().unwrap().word, "veneer");
800 }
801
802 #[test]
803 fn apply_around_caret_mirrors_a_mid_word_fix() {
804 let mut buf = Buffer::default();
805 type_str(&mut buf, "vernuer trailing");
806 for _ in 0..9 {
808 buf.push(Key::MoveLeft);
809 }
810 for _ in 0..4 {
814 buf.push(Key::MoveLeft);
815 }
816 buf.apply_around_caret(3, 4, "veneer");
817 assert_eq!(buf.text(), "veneer trailing");
818 }
819
820 #[test]
821 fn move_left_walks_caret_back_without_clearing_text() {
822 let mut buf = Buffer::default();
823 type_str(&mut buf, "hello world");
824 for _ in 0..6 {
825 buf.push(Key::MoveLeft);
826 }
827 assert_eq!(buf.text(), "hello world");
828 assert_eq!(buf.text_before_caret(), "hello");
829 }
830
831 #[test]
832 fn typing_after_move_left_inserts_at_caret() {
833 let mut buf = Buffer::default();
834 type_str(&mut buf, "helloworld");
835 for _ in 0..5 {
836 buf.push(Key::MoveLeft);
837 }
838 type_str(&mut buf, " ");
839 assert_eq!(buf.text(), "hello world");
840 }
841
842 #[test]
843 fn backspace_after_move_left_removes_the_char_before_caret() {
844 let mut buf = Buffer::default();
845 type_str(&mut buf, "hello world");
846 for _ in 0..6 {
847 buf.push(Key::MoveLeft);
848 }
849 buf.push(Key::Backspace);
850 assert_eq!(buf.text(), "hell world");
851 }
852
853 #[test]
854 fn move_right_at_end_is_a_no_op() {
855 let mut buf = Buffer::default();
856 type_str(&mut buf, "abc");
857 buf.push(Key::MoveRight);
858 assert_eq!(buf.text_before_caret(), "abc");
859 }
860
861 #[test]
862 fn move_left_at_start_is_a_no_op() {
863 let mut buf = Buffer::default();
864 type_str(&mut buf, "abc");
865 for _ in 0..10 {
866 buf.push(Key::MoveLeft);
867 }
868 assert_eq!(buf.text_before_caret(), "");
869 assert_eq!(buf.text(), "abc");
870 }
871
872 #[test]
873 fn line_start_and_line_end_jump_to_the_edges() {
874 let mut buf = Buffer::default();
875 type_str(&mut buf, "hello world");
876 buf.push(Key::LineStart);
877 assert_eq!(buf.text_before_caret(), "");
878 buf.push(Key::LineEnd);
879 assert_eq!(buf.text_before_caret(), "hello world");
880 }
881
882 #[test]
883 fn word_left_jumps_to_previous_word_start() {
884 let mut buf = Buffer::default();
885 type_str(&mut buf, "the quick brown fox");
886 buf.push(Key::WordLeft);
887 assert_eq!(buf.text_before_caret(), "the quick brown ");
888 buf.push(Key::WordLeft);
889 assert_eq!(buf.text_before_caret(), "the quick ");
890 buf.push(Key::WordLeft);
891 assert_eq!(buf.text_before_caret(), "the ");
892 buf.push(Key::WordLeft);
893 assert_eq!(buf.text_before_caret(), "");
894 }
895
896 #[test]
897 fn word_right_jumps_to_next_word_end() {
898 let mut buf = Buffer::default();
899 type_str(&mut buf, "the quick brown fox");
900 buf.push(Key::LineStart);
901 buf.push(Key::WordRight);
902 assert_eq!(buf.text_before_caret(), "the");
903 buf.push(Key::WordRight);
904 assert_eq!(buf.text_before_caret(), "the quick");
905 }
906
907 #[test]
908 fn word_left_from_mid_word_lands_at_word_start() {
909 let mut buf = Buffer::default();
910 type_str(&mut buf, "hello world");
911 for _ in 0..3 {
912 buf.push(Key::MoveLeft);
913 }
914 assert_eq!(buf.text_before_caret(), "hello wo");
916 buf.push(Key::WordLeft);
917 assert_eq!(buf.text_before_caret(), "hello ");
918 }
919
920 #[test]
921 fn ctrl_left_skips_commas_like_a_typical_editor() {
922 let mut buf = Buffer::default();
923 type_str(&mut buf, "hello, world");
924 buf.push(Key::WordLeft);
925 assert_eq!(buf.text_before_caret(), "hello, ");
926 buf.push(Key::WordLeft);
927 assert_eq!(buf.text_before_caret(), "");
930 }
931
932 #[test]
933 fn word_at_caret_excludes_trailing_punctuation() {
934 let mut buf = Buffer::default();
935 type_str(&mut buf, "recieve,");
936 let at = buf.word_at_caret().expect("word at caret");
937 assert_eq!(at.word, "recieve");
938 assert_eq!(at.trailing, ",");
939 }
940
941 #[test]
942 fn word_at_caret_keeps_apostrophes_for_contractions() {
943 let mut buf = Buffer::default();
944 type_str(&mut buf, "don't");
945 let at = buf.word_at_caret().expect("word at caret");
946 assert_eq!(at.word, "don't");
947 }
948
949 #[test]
950 fn ctrl_right_skips_dot_runs_as_one_nav_word() {
951 let mut buf = Buffer::default();
956 type_str(&mut buf, "deal...do you");
957 buf.push(Key::LineStart);
958 buf.push(Key::WordRight);
959 assert_eq!(buf.text_before_caret(), "deal...do");
960 buf.push(Key::WordRight);
961 assert_eq!(buf.text_before_caret(), "deal...do you");
962 }
963
964 #[test]
965 fn word_at_caret_does_not_pull_dot_neighbors_in() {
966 let mut buf = Buffer::default();
970 type_str(&mut buf, "deal...do");
971 let at = buf.word_at_caret().expect("word at caret");
972 assert_eq!(at.word, "do");
973 }
974
975 #[test]
976 fn words_near_caret_orders_by_char_distance() {
977 let mut buf = Buffer::default();
978 type_str(&mut buf, "the quick brown fox");
979 let nearby = buf.words_near_caret();
981 let just_words: Vec<&str> = nearby.iter().map(|nw| nw.word.as_str()).collect();
982 assert_eq!(just_words, vec!["fox", "brown", "quick", "the"]);
983 assert_eq!(nearby[0].caret_offset_chars, 0);
984 assert_eq!(nearby[1].caret_offset_chars, -4);
986 }
987
988 #[test]
989 fn words_near_caret_walks_outward_from_mid_buffer_caret() {
990 let mut buf = Buffer::default();
991 type_str(&mut buf, "the quick brown fox");
992 for _ in 0..("brown fox".len()) {
994 buf.push(Key::MoveLeft);
995 }
996 assert_eq!(buf.text_before_caret(), "the quick ");
997 let nearby = buf.words_near_caret();
998 assert_eq!(nearby[0].word, "quick");
1000 assert_eq!(nearby[0].caret_offset_chars, -1);
1001 assert_eq!(nearby[1].word, "brown");
1002 assert_eq!(nearby[1].caret_offset_chars, 5);
1003 }
1004
1005 #[test]
1006 fn apply_at_word_replaces_and_sets_caret() {
1007 let mut buf = Buffer::default();
1008 type_str(&mut buf, "the quick brown fox");
1009 buf.apply_at_word(10, 15, "red");
1012 assert_eq!(buf.text(), "the quick red fox");
1013 assert_eq!(buf.caret(), 13);
1014 }
1015}