1use crate::ropetext::layout::{Cell, Layout, RowHints};
23use crate::ropetext::position::{Column, Position};
24use crate::ropetext::text::Text;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Class {
29 Blank,
31 Word,
33 Punctuation,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Words {
45 Small,
47 Big,
49}
50
51impl Words {
52 pub fn class_of(self, c: char) -> Class {
58 self.classify(c)
59 }
60
61 fn classify(self, c: char) -> Class {
62 if c.is_whitespace() {
63 Class::Blank
64 } else if self == Words::Big || c.is_alphanumeric() || c == '_' {
65 Class::Word
66 } else {
67 Class::Punctuation
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum Goal {
81 Column(Column),
83 RowEnd,
85}
86
87pub fn right(text: &Text, from: Position) -> Position {
91 let end = row_end(text, from);
92 if from.byte() >= end.byte() {
93 return from;
94 }
95 position(text, text.next_cluster_byte(from.byte()))
96}
97
98pub fn left(text: &Text, from: Position) -> Position {
100 let start = row_start(text, from);
101 if from.byte() <= start.byte() {
102 return from;
103 }
104 position(text, text.prev_cluster_byte(from.byte()))
105}
106
107pub fn next_cluster(text: &Text, from: Position) -> Position {
109 position(text, text.next_cluster_byte(from.byte()))
110}
111
112pub fn prev_cluster(text: &Text, from: Position) -> Position {
114 position(text, text.prev_cluster_byte(from.byte()))
115}
116
117pub fn row_start(text: &Text, from: Position) -> Position {
119 text.position(from.row(), Column::ZERO)
120 .unwrap_or_else(|| text.start())
121}
122
123pub fn row_end(text: &Text, from: Position) -> Position {
125 let len = text.line_len_chars(from.row()).unwrap_or(0);
126 text.position(from.row(), Column::new(len)).unwrap_or(from)
127}
128
129pub fn first_non_blank(text: &Text, from: Position) -> Position {
131 let Some(line) = text.line(from.row()) else {
132 return from;
133 };
134 let blanks = line.chars().take_while(|c| c.is_whitespace()).count();
135 text.position(from.row(), Column::new(blanks))
136 .unwrap_or_else(|| row_end(text, from))
137}
138
139pub fn last_non_blank(text: &Text, from: Position) -> Position {
141 let Some(line) = text.line(from.row()) else {
142 return from;
143 };
144 let trimmed = line.trim_end();
145 let chars = trimmed.chars().count();
146 text.position(from.row(), Column::new(chars))
147 .unwrap_or_else(|| row_end(text, from))
148}
149
150pub fn text_start(text: &Text) -> Position {
154 text.start()
155}
156
157pub fn text_end(text: &Text) -> Position {
159 text.end()
160}
161
162pub fn goto_row(text: &Text, row: usize) -> Position {
164 let row = row.min(text.line_count().saturating_sub(1));
165 let at = text
166 .position(row, Column::ZERO)
167 .unwrap_or_else(|| text.start());
168 first_non_blank(text, at)
169}
170
171pub fn vertical(text: &Text, from: Position, rows: isize, goal: Goal) -> Position {
176 let last = text.line_count().saturating_sub(1);
177 let row = if rows >= 0 {
178 from.row().saturating_add(rows.unsigned_abs()).min(last)
179 } else {
180 from.row().saturating_sub(rows.unsigned_abs())
181 };
182 let len = text.line_len_chars(row).unwrap_or(0);
183 let column = match goal {
184 Goal::RowEnd => len,
185 Goal::Column(column) => column.get().min(len),
186 };
187 let mut column = column;
191 loop {
192 if let Some(at) = text.position(row, Column::new(column)) {
193 return at;
194 }
195 if column == 0 {
196 return text.position(row, Column::ZERO).unwrap_or(from);
197 }
198 column -= 1;
199 }
200}
201
202pub fn paragraph_forward(text: &Text, from: Position) -> Position {
207 let last = text.line_count().saturating_sub(1);
208 let mut row = from.row() + 1;
209 while row <= last {
210 if is_blank_row(text, row) {
211 return text
212 .position(row, Column::ZERO)
213 .unwrap_or_else(|| text.end());
214 }
215 row += 1;
216 }
217 text.end()
218}
219
220pub fn paragraph_back(text: &Text, from: Position) -> Position {
222 let mut row = from.row();
223 while row > 0 {
224 row -= 1;
225 if is_blank_row(text, row) {
226 return text
227 .position(row, Column::ZERO)
228 .unwrap_or_else(|| text.start());
229 }
230 }
231 text.start()
232}
233
234pub fn word_start_forward(text: &Text, from: Position, words: Words) -> Position {
241 let len = text.len_bytes();
242 let mut at = from.byte();
243 if at >= len {
244 return text.end();
245 }
246 let start_class = class_at(text, at, words);
247 let start_row = from.row();
248
249 if start_class != Class::Blank {
251 while at < len && class_at(text, at, words) == start_class {
252 at = text.next_cluster_byte(at);
253 }
254 }
255 while at < len && class_at(text, at, words) == Class::Blank {
257 if is_empty_row_at(text, at) && text.row_of_byte(at) != start_row {
260 return position(text, at);
261 }
262 at = text.next_cluster_byte(at);
263 }
264 position(text, at)
265}
266
267pub fn word_start_back(text: &Text, from: Position, words: Words) -> Position {
269 let mut at = from.byte();
270 if at == 0 {
271 return text.start();
272 }
273 at = text.prev_cluster_byte(at);
275 while at > 0 && class_at(text, at, words) == Class::Blank {
276 if is_empty_row_at(text, at) {
277 return position(text, at);
278 }
279 at = text.prev_cluster_byte(at);
280 }
281 let run = class_at(text, at, words);
282 if run == Class::Blank {
283 return position(text, at);
284 }
285 while at > 0 {
287 let previous = text.prev_cluster_byte(at);
288 if class_at(text, previous, words) != run {
289 break;
290 }
291 at = previous;
292 }
293 position(text, at)
294}
295
296pub fn word_end_forward(text: &Text, from: Position, words: Words) -> Option<Position> {
301 let len = text.len_bytes();
302 let mut at = text.next_cluster_byte(from.byte());
303 while at < len && class_at(text, at, words) == Class::Blank {
304 at = text.next_cluster_byte(at);
305 }
306 if at >= len {
307 return None;
308 }
309 let run = class_at(text, at, words);
310 while at < len && class_at(text, at, words) == run {
311 at = text.next_cluster_byte(at);
312 }
313 Some(position(text, at))
314}
315
316pub fn word_end_at_or_after(text: &Text, from: Position, words: Words) -> Option<Position> {
326 let len = text.len_bytes();
327 let mut at = from.byte();
328 while at < len && class_at(text, at, words) == Class::Blank {
329 at = text.next_cluster_byte(at);
330 }
331 if at >= len {
332 return None;
333 }
334 let run = class_at(text, at, words);
335 while at < len && class_at(text, at, words) == run {
336 at = text.next_cluster_byte(at);
337 }
338 Some(position(text, at))
339}
340
341pub fn word_end_back(text: &Text, from: Position, words: Words) -> Option<Position> {
345 let mut at = text.prev_cluster_byte(from.byte());
349 while at > 0 {
350 if is_word_end(text, at, words) {
351 return Some(position(text, at));
352 }
353 at = text.prev_cluster_byte(at);
354 }
355 None
356}
357
358fn is_word_end(text: &Text, byte: usize, words: Words) -> bool {
361 let behind = class_at(text, text.prev_cluster_byte(byte), words);
362 behind != Class::Blank && class_at(text, byte, words) != behind
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum VisualGoal {
370 Cell(usize),
373 LineEnd,
375}
376
377pub fn visual_vertical(
385 text: &Text,
386 layout: &Layout,
387 hints: &[RowHints<'_>],
388 from: Position,
389 lines: isize,
390 goal: VisualGoal,
391) -> Position {
392 let last = layout.visual_line_count().saturating_sub(1);
393 let current = layout.visual_row_of(from);
394 let row = if lines >= 0 {
395 current.saturating_add(lines.unsigned_abs()).min(last)
396 } else {
397 current.saturating_sub(lines.unsigned_abs())
398 };
399 let column = match goal {
400 VisualGoal::Cell(column) => column,
401 VisualGoal::LineEnd => usize::MAX,
402 };
403 layout
404 .position_at_cell(text, hints, Cell { row, column })
405 .unwrap_or(from)
406}
407
408pub fn visual_line_start(
413 text: &Text,
414 layout: &Layout,
415 hints: &[RowHints<'_>],
416 from: Position,
417) -> Position {
418 let row = layout.visual_row_of(from);
419 layout
420 .position_at_cell(text, hints, Cell { row, column: 0 })
421 .unwrap_or(from)
422}
423
424pub fn visual_line_end(
426 text: &Text,
427 layout: &Layout,
428 hints: &[RowHints<'_>],
429 from: Position,
430) -> Position {
431 let row = layout.visual_row_of(from);
432 layout
433 .position_at_cell(
434 text,
435 hints,
436 Cell {
437 row,
438 column: usize::MAX,
439 },
440 )
441 .unwrap_or(from)
442}
443
444pub fn find_char_forward(
452 text: &Text,
453 from: Position,
454 needle: char,
455 till: bool,
456) -> Option<Position> {
457 let line = text.line(from.row())?;
458 let row_start = text.row_start_byte(from.row());
459 let from_offset = from.byte().saturating_sub(row_start);
460 let mut found = None;
461 for (offset, cluster) in indices(&line) {
462 if offset > from_offset && cluster.starts_with(needle) {
463 found = Some(offset);
464 break;
465 }
466 }
467 let offset = found?;
468 let byte = row_start + offset;
469 Some(position(
470 text,
471 if till {
472 text.prev_cluster_byte(byte)
473 } else {
474 byte
475 },
476 ))
477}
478
479pub fn find_char_back(text: &Text, from: Position, needle: char, till: bool) -> Option<Position> {
483 let line = text.line(from.row())?;
484 let row_start = text.row_start_byte(from.row());
485 let from_offset = from.byte().saturating_sub(row_start);
486 let mut found = None;
487 for (offset, cluster) in indices(&line) {
488 if offset < from_offset && cluster.starts_with(needle) {
489 found = Some(offset);
490 }
491 }
492 let offset = found?;
493 let byte = row_start + offset;
494 Some(position(
495 text,
496 if till {
497 text.next_cluster_byte(byte)
498 } else {
499 byte
500 },
501 ))
502}
503
504pub fn matching_bracket(text: &Text, from: Position) -> Option<Position> {
512 const PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
513
514 let len = text.len_bytes();
515 let row_end = row_end(text, from).byte();
516 let mut at = from.byte();
517 let (open, close, forward) = loop {
518 if at >= row_end {
519 return None;
520 }
521 let c = text.scalar_at(at)?;
522 if let Some(&(open, close)) = PAIRS.iter().find(|(open, _)| *open == c) {
523 break (open, close, true);
524 }
525 if let Some(&(open, close)) = PAIRS.iter().find(|(_, close)| *close == c) {
526 break (open, close, false);
527 }
528 at = text.next_cluster_byte(at);
529 };
530
531 let mut depth = 0i32;
532 loop {
533 let c = text.scalar_at(at)?;
534 if c == open {
535 depth += if forward { 1 } else { -1 };
536 } else if c == close {
537 depth += if forward { -1 } else { 1 };
538 }
539 if depth == 0 {
540 return Some(position(text, at));
541 }
542 if forward {
543 at = text.next_cluster_byte(at);
544 if at >= len {
545 return None;
546 }
547 } else {
548 if at == 0 {
549 return None;
550 }
551 at = text.prev_cluster_byte(at);
552 }
553 }
554}
555
556fn position(text: &Text, byte: usize) -> Position {
559 text.position_at_derived_byte(byte)
560}
561
562fn class_at(text: &Text, byte: usize, words: Words) -> Class {
563 text.scalar_at(byte)
564 .map(|c| words.classify(c))
565 .unwrap_or(Class::Blank)
566}
567
568fn is_blank_row(text: &Text, row: usize) -> bool {
569 text.line(row)
570 .map(|line| line.trim().is_empty())
571 .unwrap_or(true)
572}
573
574fn is_empty_row_at(text: &Text, byte: usize) -> bool {
580 let row = text.row_of_byte(byte);
581 text.line_len_chars(row) == Some(0)
582}
583
584fn indices(line: &str) -> impl Iterator<Item = (usize, &str)> {
585 use unicode_segmentation::UnicodeSegmentation;
586 line.grapheme_indices(true)
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592
593 const COMBINING: &str = "e\u{301}x";
595
596 fn text(s: &str) -> Text {
597 Text::from(s)
598 }
599
600 fn at(text: &Text, row: usize, col: usize) -> Position {
601 text.position(row, Column::new(col))
602 .expect("addressable in the test fixture")
603 }
604
605 fn rc(position: Position) -> (usize, usize) {
607 (position.row(), position.column().get())
608 }
609
610 #[test]
613 fn stepping_within_a_row_stops_at_its_ends() {
614 let t = text("ab\ncd");
615 assert_eq!(rc(right(&t, at(&t, 0, 2))), (0, 2), "row end holds");
616 assert_eq!(rc(left(&t, at(&t, 1, 0))), (1, 0), "row start holds");
617 assert_eq!(rc(right(&t, at(&t, 0, 1))), (0, 2));
618 assert_eq!(rc(left(&t, at(&t, 0, 1))), (0, 0));
619 }
620
621 #[test]
622 fn stepping_across_rows_crosses_the_break() {
623 let t = text("ab\ncd");
624 assert_eq!(rc(next_cluster(&t, at(&t, 0, 2))), (1, 0));
625 assert_eq!(rc(prev_cluster(&t, at(&t, 1, 0))), (0, 2));
626 }
627
628 #[test]
629 fn stepping_at_the_ends_of_the_text_holds() {
630 let t = text("ab");
631 assert_eq!(rc(next_cluster(&t, at(&t, 0, 2))), (0, 2));
632 assert_eq!(rc(prev_cluster(&t, at(&t, 0, 0))), (0, 0));
633 }
634
635 #[test]
636 fn one_step_crosses_a_whole_cluster() {
637 let t = text(COMBINING);
640 let stepped = right(&t, at(&t, 0, 0));
641 assert_eq!(stepped.column().get(), 2);
642 assert_eq!(rc(left(&t, stepped)), (0, 0));
643 }
644
645 #[test]
648 fn row_bounds_ignore_the_line_break() {
649 let t = text("one\ntwo");
650 assert_eq!(rc(row_start(&t, at(&t, 0, 2))), (0, 0));
651 assert_eq!(rc(row_end(&t, at(&t, 0, 1))), (0, 3));
652 }
653
654 #[test]
655 fn non_blank_bounds_skip_the_padding() {
656 let t = text(" padded ");
657 assert_eq!(rc(first_non_blank(&t, at(&t, 0, 0))), (0, 2));
658 assert_eq!(
659 rc(last_non_blank(&t, at(&t, 0, 0))),
660 (0, 8),
661 "just past the last non-blank, since a position sits between clusters"
662 );
663 }
664
665 #[test]
666 fn an_all_blank_row_has_no_non_blank_to_find() {
667 let t = text(" ");
668 assert_eq!(rc(first_non_blank(&t, at(&t, 0, 0))), (0, 4));
669 assert_eq!(rc(last_non_blank(&t, at(&t, 0, 2))), (0, 0));
670 }
671
672 #[test]
675 fn a_short_row_does_not_eat_the_goal_column() {
676 let t = text("long enough\nab\nlong enough");
677 let start = at(&t, 0, 9);
678 let goal = Goal::Column(start.column());
679 let middle = vertical(&t, start, 1, goal);
680 assert_eq!(rc(middle), (1, 2), "clamped to the short row");
681 let bottom = vertical(&t, middle, 1, goal);
682 assert_eq!(
683 rc(bottom),
684 (2, 9),
685 "and back out to the column the caller still wants"
686 );
687 }
688
689 #[test]
690 fn aiming_at_the_row_end_follows_the_row() {
691 let t = text("long enough\nab");
692 let landed = vertical(&t, at(&t, 0, 11), 1, Goal::RowEnd);
693 assert_eq!(rc(landed), (1, 2));
694 }
695
696 #[test]
697 fn vertical_saturates_at_the_first_and_last_row() {
698 let t = text("a\nb\nc");
699 let goal = Goal::Column(Column::ZERO);
700 assert_eq!(rc(vertical(&t, at(&t, 0, 0), -3, goal)), (0, 0));
701 assert_eq!(rc(vertical(&t, at(&t, 0, 0), 99, goal)), (2, 0));
702 }
703
704 #[test]
705 fn vertical_will_not_land_inside_a_cluster() {
706 let t = text(&format!("abc\n{COMBINING}"));
708 let landed = vertical(&t, at(&t, 0, 1), 1, Goal::Column(Column::new(1)));
709 assert_eq!(rc(landed), (1, 0));
710 }
711
712 #[test]
715 fn paragraph_motions_stop_on_blank_rows() {
716 let t = text("one\ntwo\n\nthree\n\nfour");
717 assert_eq!(rc(paragraph_forward(&t, at(&t, 0, 0))), (2, 0));
718 assert_eq!(rc(paragraph_forward(&t, at(&t, 3, 0))), (4, 0));
719 assert_eq!(rc(paragraph_back(&t, at(&t, 5, 0))), (4, 0));
720 assert_eq!(rc(paragraph_back(&t, at(&t, 3, 0))), (2, 0));
721 }
722
723 #[test]
724 fn paragraph_motions_saturate_at_the_ends() {
725 let t = text("one\ntwo");
726 assert_eq!(paragraph_forward(&t, at(&t, 0, 0)).byte(), t.len_bytes());
727 assert_eq!(rc(paragraph_back(&t, at(&t, 1, 0))), (0, 0));
728 }
729
730 #[test]
731 fn a_row_of_only_blanks_counts_as_a_paragraph_break() {
732 let t = text("one\n \ntwo");
733 assert_eq!(rc(paragraph_forward(&t, at(&t, 0, 0))), (1, 0));
734 }
735
736 #[test]
739 fn a_small_word_ends_where_the_character_class_changes() {
740 let t = text("foo.bar");
741 assert_eq!(
742 rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
743 (0, 3),
744 "the punctuation run is its own word"
745 );
746 }
747
748 #[test]
749 fn a_big_word_is_any_run_of_non_blanks() {
750 let t = text("foo.bar baz");
751 assert_eq!(
752 rc(word_start_forward(&t, at(&t, 0, 0), Words::Big)),
753 (0, 8),
754 "punctuation does not divide a WORD"
755 );
756 }
757
758 #[test]
759 fn a_word_motion_crosses_rows() {
760 let t = text("foo\nbar");
761 assert_eq!(
762 rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
763 (1, 0)
764 );
765 assert_eq!(rc(word_start_back(&t, at(&t, 1, 0), Words::Small)), (0, 0));
766 }
767
768 #[test]
769 fn an_empty_row_is_a_word_of_its_own() {
770 let t = text("a\n\nb");
773 assert_eq!(
774 rc(word_start_forward(&t, at(&t, 0, 0), Words::Small)),
775 (1, 0)
776 );
777 assert_eq!(rc(word_start_back(&t, at(&t, 2, 0), Words::Small)), (1, 0));
778 }
779
780 #[test]
781 fn a_forward_word_motion_saturates_at_the_end_of_the_text() {
782 let t = text("one two");
783 let last = word_start_forward(&t, at(&t, 0, 4), Words::Small);
784 assert_eq!(last.byte(), t.len_bytes());
785 }
786
787 #[test]
788 fn a_backward_word_motion_finds_the_front_of_the_run_it_is_in() {
789 let t = text("one two three");
790 assert_eq!(rc(word_start_back(&t, at(&t, 0, 6), Words::Small)), (0, 4));
791 assert_eq!(rc(word_start_back(&t, at(&t, 0, 4), Words::Small)), (0, 0));
792 assert_eq!(rc(word_start_back(&t, at(&t, 0, 0), Words::Small)), (0, 0));
793 }
794
795 #[test]
796 fn a_word_end_is_just_past_the_last_cluster_of_the_word() {
797 let t = text("one two");
798 assert_eq!(
799 rc(word_end_forward(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
800 (0, 3)
801 );
802 assert_eq!(
803 rc(word_end_forward(&t, at(&t, 0, 3), Words::Small).expect("a word ahead")),
804 (0, 7)
805 );
806 }
807
808 #[test]
809 fn a_word_end_with_no_word_ahead_finds_nothing() {
810 let t = text("one ");
813 assert!(word_end_forward(&t, at(&t, 0, 3), Words::Small).is_none());
814 assert!(word_end_forward(&t, at(&t, 0, 6), Words::Small).is_none());
815 }
816
817 #[test]
818 fn a_word_end_at_or_after_accepts_the_word_the_cursor_is_in() {
819 let t = text("a bb");
820 assert_eq!(
822 rc(word_end_forward(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
823 (0, 4)
824 );
825 assert_eq!(
827 rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word here")),
828 (0, 1)
829 );
830 }
831
832 #[test]
833 fn a_word_end_at_or_after_skips_leading_blanks() {
834 let t = text(" ab");
835 assert_eq!(
836 rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
837 (0, 4)
838 );
839 }
840
841 #[test]
842 fn a_word_end_at_or_after_crosses_rows_to_find_one() {
843 let t = text("\nab");
844 assert_eq!(
845 rc(word_end_at_or_after(&t, at(&t, 0, 0), Words::Small).expect("a word ahead")),
846 (1, 2)
847 );
848 }
849
850 #[test]
851 fn a_word_end_at_or_after_finds_nothing_in_blanks() {
852 let t = text("a ");
853 assert!(word_end_at_or_after(&t, at(&t, 0, 2), Words::Small).is_none());
854 }
855
856 #[test]
857 fn a_backward_word_end_finds_the_previous_word() {
858 let t = text("one two");
859 assert_eq!(
860 rc(word_end_back(&t, at(&t, 0, 5), Words::Small).expect("a word behind")),
861 (0, 3)
862 );
863 assert!(word_end_back(&t, at(&t, 0, 0), Words::Small).is_none());
864 }
865
866 #[test]
867 fn word_classes_are_the_ones_the_motions_use() {
868 assert_eq!(Words::Small.class_of('a'), Class::Word);
869 assert_eq!(Words::Small.class_of('_'), Class::Word);
870 assert_eq!(Words::Small.class_of('.'), Class::Punctuation);
871 assert_eq!(Words::Big.class_of('.'), Class::Word);
872 assert_eq!(Words::Small.class_of(' '), Class::Blank);
873 assert_eq!(Words::Big.class_of('\n'), Class::Blank);
874 }
875
876 #[test]
879 fn finding_a_character_lands_on_it_or_before_it() {
880 let t = text("hello");
881 assert_eq!(
882 rc(find_char_forward(&t, at(&t, 0, 0), 'l', false).expect("found")),
883 (0, 2)
884 );
885 assert_eq!(
886 rc(find_char_forward(&t, at(&t, 0, 0), 'l', true).expect("found")),
887 (0, 1),
888 "till stops one short"
889 );
890 }
891
892 #[test]
893 fn finding_backwards_takes_the_nearest_one_behind() {
894 let t = text("hello");
895 assert_eq!(
896 rc(find_char_back(&t, at(&t, 0, 4), 'l', false).expect("found")),
897 (0, 3)
898 );
899 }
900
901 #[test]
902 fn finding_a_character_never_leaves_the_row() {
903 let t = text("abc\nxbz");
904 assert!(find_char_forward(&t, at(&t, 0, 0), 'x', false).is_none());
905 assert!(find_char_back(&t, at(&t, 1, 2), 'a', false).is_none());
906 }
907
908 #[test]
909 fn finding_a_character_that_is_not_there_finds_nothing() {
910 let t = text("hello");
911 assert!(find_char_forward(&t, at(&t, 0, 0), 'q', false).is_none());
912 assert!(
913 find_char_forward(&t, at(&t, 0, 4), 'h', false).is_none(),
914 "the search starts past the cursor"
915 );
916 }
917
918 #[test]
921 fn a_bracket_matches_its_partner() {
922 let t = text("(a[b]c)");
923 assert_eq!(
924 rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
925 (0, 6),
926 "the inner pair of a different kind is not the partner"
927 );
928 }
929
930 #[test]
931 fn nesting_is_counted() {
932 let t = text("((x))");
933 assert_eq!(
934 rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
935 (0, 4)
936 );
937 assert_eq!(
938 rc(matching_bracket(&t, at(&t, 0, 1)).expect("balanced")),
939 (0, 3)
940 );
941 }
942
943 #[test]
944 fn a_closing_bracket_matches_backwards() {
945 let t = text("((x))");
946 assert_eq!(
947 rc(matching_bracket(&t, at(&t, 0, 4)).expect("balanced")),
948 (0, 0)
949 );
950 }
951
952 #[test]
953 fn the_search_starts_at_the_first_bracket_on_the_row() {
954 let t = text("if cond {\n}");
956 assert_eq!(
957 rc(matching_bracket(&t, at(&t, 0, 0)).expect("balanced")),
958 (1, 0)
959 );
960 }
961
962 #[test]
963 fn a_partner_may_be_on_another_row() {
964 let t = text("fn a(\n b,\n) {}");
965 assert_eq!(
966 rc(matching_bracket(&t, at(&t, 0, 4)).expect("balanced")),
967 (2, 0)
968 );
969 }
970
971 #[test]
972 fn an_unbalanced_bracket_matches_nothing() {
973 let opening = text("(a");
974 assert!(matching_bracket(&opening, opening.start()).is_none());
975 let closing = text("a)");
976 assert!(matching_bracket(&closing, closing.start()).is_none());
977 }
978
979 #[test]
980 fn a_row_without_a_bracket_matches_nothing() {
981 let t = text("plain text\n()");
982 assert!(
983 matching_bracket(&t, at(&t, 0, 0)).is_none(),
984 "the bracket on the next row is not this row's"
985 );
986 }
987
988 #[test]
989 fn a_word_motion_leaves_an_empty_row_it_starts_on() {
990 let t = text("a\n\nb");
993 assert_eq!(
994 rc(word_start_forward(&t, at(&t, 1, 0), Words::Small)),
995 (2, 0)
996 );
997 }
998
999 #[test]
1000 fn a_backward_word_end_crosses_a_punctuation_run() {
1001 let t = text("foo.bar");
1002 assert_eq!(
1003 rc(word_end_back(&t, at(&t, 0, 5), Words::Small).expect("a run behind")),
1004 (0, 4),
1005 "the punctuation run's end, not the word before it"
1006 );
1007 assert!(
1008 word_end_back(&t, at(&t, 0, 5), Words::Big).is_none(),
1009 "to a WORD, foo.bar is one run, so no word ends behind the cursor"
1010 );
1011 }
1012
1013 mod visual {
1016 use super::*;
1017 use crate::ropetext::layout::{Layout, RowHints, Viewport};
1018 use crate::ropetext::width::Metrics;
1019
1020 fn layout(text: &Text, width: usize) -> Layout {
1021 Layout::compute(text, width, Metrics::default(), &[])
1022 }
1023
1024 #[test]
1025 fn down_moves_one_drawn_line_not_one_row() {
1026 let t = text("aaaa bbbb cccc\nnext");
1030 let l = layout(&t, 5);
1031 let start = at(&t, 0, 0);
1032 let one = visual_vertical(&t, &l, &[], start, 1, VisualGoal::Cell(0));
1033 assert_eq!(rc(one), (0, 5), "the second drawn line of the same row");
1034 let two = visual_vertical(&t, &l, &[], one, 1, VisualGoal::Cell(0));
1035 assert_eq!(rc(two), (0, 10));
1036 let three = visual_vertical(&t, &l, &[], two, 1, VisualGoal::Cell(0));
1037 assert_eq!(rc(three), (1, 0), "and only now the next row");
1038 }
1039
1040 #[test]
1041 fn up_and_down_are_symmetric() {
1042 let t = text("aaaa bbbb cccc");
1043 let l = layout(&t, 5);
1044 let middle = at(&t, 0, 5);
1045 assert_eq!(
1046 rc(visual_vertical(
1047 &t,
1048 &l,
1049 &[],
1050 middle,
1051 -1,
1052 VisualGoal::Cell(0)
1053 )),
1054 (0, 0)
1055 );
1056 assert_eq!(
1057 rc(visual_vertical(&t, &l, &[], middle, 1, VisualGoal::Cell(0))),
1058 (0, 10)
1059 );
1060 }
1061
1062 #[test]
1063 fn the_goal_cell_is_kept_across_a_short_drawn_line() {
1064 let t = text("aaaaa\nb\nccccc");
1065 let l = layout(&t, 10);
1066 let start = at(&t, 0, 4);
1067 let goal = VisualGoal::Cell(4);
1068 let middle = visual_vertical(&t, &l, &[], start, 1, goal);
1069 assert_eq!(rc(middle), (1, 1), "clamped to the short row");
1070 let bottom = visual_vertical(&t, &l, &[], middle, 1, goal);
1071 assert_eq!(rc(bottom), (2, 4), "and back out to the cell still wanted");
1072 }
1073
1074 #[test]
1075 fn aiming_at_the_line_end_follows_the_wrap() {
1076 let t = text("aaaa bbbb\nxy");
1077 let l = layout(&t, 5);
1078 let landed = visual_vertical(&t, &l, &[], at(&t, 0, 0), 1, VisualGoal::LineEnd);
1079 assert_eq!(rc(landed), (0, 9), "the end of the second drawn line");
1080 }
1081
1082 #[test]
1083 fn vertical_movement_saturates_at_the_first_and_last_drawn_line() {
1084 let t = text("aaaa bbbb");
1085 let l = layout(&t, 5);
1086 let goal = VisualGoal::Cell(0);
1087 assert_eq!(
1088 rc(visual_vertical(&t, &l, &[], at(&t, 0, 0), -9, goal)),
1089 (0, 0)
1090 );
1091 assert_eq!(
1092 rc(visual_vertical(&t, &l, &[], at(&t, 0, 0), 9, goal)),
1093 (0, 5)
1094 );
1095 }
1096
1097 #[test]
1098 fn line_bounds_are_the_drawn_line_not_the_row() {
1099 let t = text("aaaa bbbb cccc");
1100 let l = layout(&t, 5);
1101 let inside = at(&t, 0, 7);
1102 assert_eq!(rc(visual_line_start(&t, &l, &[], inside)), (0, 5));
1103 assert_eq!(rc(visual_line_end(&t, &l, &[], inside)), (0, 9));
1104 assert_eq!(rc(row_start(&t, inside)), (0, 0));
1107 assert_eq!(rc(row_end(&t, inside)), (0, 14));
1108 }
1109
1110 #[test]
1111 fn a_gutter_shifts_the_cells_a_visual_motion_aims_at() {
1112 let t = text("abcd\nefgh");
1113 let hints = [
1114 RowHints {
1115 visible: &[],
1116 inset: 2,
1117 },
1118 RowHints {
1119 visible: &[],
1120 inset: 2,
1121 },
1122 ];
1123 let l = Layout::compute(&t, 10, Metrics::default(), &hints);
1124 let landed = visual_vertical(&t, &l, &hints, at(&t, 0, 0), 1, VisualGoal::Cell(3));
1125 assert_eq!(
1126 rc(landed),
1127 (1, 1),
1128 "cell 3 is the second character past a two-cell gutter"
1129 );
1130 }
1131
1132 #[test]
1133 fn a_viewport_can_follow_a_visual_motion() {
1134 let t = text("aaaa bbbb cccc dddd");
1135 let l = layout(&t, 5);
1136 let mut view = Viewport::new(2);
1137 let mut position = at(&t, 0, 0);
1138 for _ in 0..3 {
1139 position = visual_vertical(&t, &l, &[], position, 1, VisualGoal::Cell(0));
1140 view.follow(&l, position);
1141 }
1142 assert_eq!(view.top(), 2, "scrolled by drawn lines, not by rows");
1143 }
1144 }
1145
1146 mod properties {
1149 use super::*;
1150 use proptest::prelude::*;
1151
1152 fn addressable(t: &Text) -> Vec<Position> {
1153 (0..=t.len_bytes())
1154 .filter_map(|byte| t.position_at_byte(byte))
1155 .collect()
1156 }
1157
1158 fn all(t: &Text, from: Position) -> Vec<Position> {
1160 let mut landed = vec![
1161 right(t, from),
1162 left(t, from),
1163 next_cluster(t, from),
1164 prev_cluster(t, from),
1165 row_start(t, from),
1166 row_end(t, from),
1167 first_non_blank(t, from),
1168 last_non_blank(t, from),
1169 paragraph_forward(t, from),
1170 paragraph_back(t, from),
1171 goto_row(t, from.row()),
1172 vertical(t, from, 1, Goal::Column(from.column())),
1173 vertical(t, from, -1, Goal::RowEnd),
1174 ];
1175 for words in [Words::Small, Words::Big] {
1176 landed.push(word_start_forward(t, from, words));
1177 landed.push(word_start_back(t, from, words));
1178 landed.extend(word_end_forward(t, from, words));
1179 landed.extend(word_end_at_or_after(t, from, words));
1180 landed.extend(word_end_back(t, from, words));
1181 }
1182 landed.extend(matching_bracket(t, from));
1183 landed.extend(find_char_forward(t, from, 'a', false));
1184 landed.extend(find_char_forward(t, from, 'a', true));
1185 landed.extend(find_char_back(t, from, 'a', false));
1186 landed.extend(find_char_back(t, from, 'a', true));
1187 landed
1188 }
1189
1190 proptest! {
1191 #![proptest_config(ProptestConfig::with_cases(64))]
1195
1196 #[test]
1200 fn every_motion_lands_somewhere_addressable(s in ".{0,40}") {
1201 let t = Text::from(s.as_str());
1202 for from in addressable(&t) {
1203 for landed in all(&t, from) {
1204 prop_assert!(
1205 t.position_at_byte(landed.byte()).is_some(),
1206 "byte {} of {:?} is not addressable", landed.byte(), s
1207 );
1208 prop_assert!(!t.is_stale(landed));
1209 prop_assert_eq!(
1210 t.position_at_byte(landed.byte()),
1211 Some(landed),
1212 "row and column disagree with the byte"
1213 );
1214 }
1215 }
1216 }
1217
1218 #[test]
1222 fn motions_keep_their_direction(s in ".{0,40}") {
1223 let t = Text::from(s.as_str());
1224 for from in addressable(&t) {
1225 for words in [Words::Small, Words::Big] {
1226 prop_assert!(word_start_forward(&t, from, words).byte() >= from.byte());
1227 prop_assert!(word_start_back(&t, from, words).byte() <= from.byte());
1228 if let Some(end) = word_end_forward(&t, from, words) {
1229 prop_assert!(end.byte() > from.byte());
1230 }
1231 if let Some(end) = word_end_at_or_after(&t, from, words) {
1232 prop_assert!(end.byte() > from.byte());
1233 }
1234 if let Some(end) = word_end_back(&t, from, words) {
1235 prop_assert!(end.byte() < from.byte());
1236 }
1237 }
1238 prop_assert!(right(&t, from).byte() >= from.byte());
1239 prop_assert!(left(&t, from).byte() <= from.byte());
1240 prop_assert!(next_cluster(&t, from).byte() >= from.byte());
1241 prop_assert!(prev_cluster(&t, from).byte() <= from.byte());
1242 prop_assert!(paragraph_forward(&t, from).byte() >= from.byte());
1243 prop_assert!(paragraph_back(&t, from).byte() <= from.byte());
1244 }
1245 }
1246
1247 #[test]
1251 fn walking_by_words_terminates(s in ".{0,60}") {
1252 let t = Text::from(s.as_str());
1253 for words in [Words::Small, Words::Big] {
1254 let mut at = t.start();
1255 let mut steps = 0;
1256 loop {
1257 let next = word_start_forward(&t, at, words);
1258 if next.byte() == at.byte() {
1259 break;
1260 }
1261 at = next;
1262 steps += 1;
1263 prop_assert!(steps <= t.len_bytes() + 2, "no progress in {:?}", s);
1264 }
1265 prop_assert_eq!(at.byte(), t.len_bytes(), "stopped short in {:?}", s);
1266 }
1267 }
1268 }
1269 }
1270}