1use std::ops::Range;
21
22use unicode_segmentation::UnicodeSegmentation;
23
24use crate::ropetext::position::{Column, Position, Revision};
25use crate::ropetext::text::Text;
26use crate::ropetext::width::Metrics;
27
28#[derive(Debug, Clone, Copy, Default)]
30pub struct RowHints<'a> {
31 pub visible: &'a [bool],
39 pub inset: usize,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct VisualLine {
47 pub logical_row: usize,
48 pub chars: Range<usize>,
50 pub bytes: Range<usize>,
52 pub first: bool,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct Cell {
59 pub row: usize,
61 pub column: usize,
63}
64
65#[derive(Debug, Clone)]
67pub struct Layout {
68 lines: Vec<VisualLine>,
69 row_starts: Vec<usize>,
72 width: usize,
73 metrics: Metrics,
74 revision: Revision,
83}
84
85impl Layout {
86 pub fn describes(&self, text: &Text) -> bool {
93 self.revision == text.revision()
94 }
95
96 pub fn unwrapped(text: &Text) -> Self {
107 let mut lines = Vec::with_capacity(text.line_count());
108 let mut row_starts = Vec::with_capacity(text.line_count());
109 for row in 0..text.line_count() {
110 row_starts.push(lines.len());
111 let Some(source) = text.line(row) else {
112 continue;
113 };
114 lines.push(VisualLine {
115 logical_row: row,
116 chars: 0..source.chars().count(),
117 bytes: 0..source.len(),
118 first: true,
119 });
120 }
121 Self {
122 lines,
123 row_starts,
124 width: 0,
125 metrics: Metrics::default(),
126 revision: text.revision(),
127 }
128 }
129
130 pub fn compute(text: &Text, width: usize, metrics: Metrics, hints: &[RowHints<'_>]) -> Self {
132 let mut layout = Self {
133 lines: Vec::new(),
134 row_starts: Vec::with_capacity(text.line_count()),
135 width,
136 metrics,
137 revision: text.revision(),
138 };
139 let mut scratch = Vec::new();
140 for row in 0..text.line_count() {
141 layout.row_starts.push(layout.lines.len());
142 wrap_row(
143 text,
144 row,
145 width,
146 metrics,
147 hint_for(hints, row),
148 &mut scratch,
149 &mut layout.lines,
150 );
151 }
152 layout
153 }
154
155 pub fn relayout_rows(
161 &mut self,
162 text: &Text,
163 hints: &[RowHints<'_>],
164 rows: Range<usize>,
165 line_delta: isize,
166 ) {
167 self.revision = text.revision();
169 let rows = rows.start.min(text.line_count())..rows.end.min(text.line_count());
174 let old_rows = {
175 let end = (rows.end as isize - line_delta).max(rows.start as isize) as usize;
176 rows.start.min(self.row_starts.len())..end.min(self.row_starts.len())
177 };
178 if rows.is_empty() && old_rows.is_empty() {
179 return;
180 }
181
182 let old_start = self
183 .row_starts
184 .get(old_rows.start)
185 .copied()
186 .unwrap_or(self.lines.len());
187 let old_end = self
188 .row_starts
189 .get(old_rows.end)
190 .copied()
191 .unwrap_or(self.lines.len());
192
193 let mut replacement = Vec::new();
194 let mut starts = Vec::with_capacity(rows.len());
195 let mut scratch = Vec::new();
196 for row in rows.clone() {
197 starts.push(old_start + replacement.len());
198 wrap_row(
199 text,
200 row,
201 self.width,
202 self.metrics,
203 hint_for(hints, row),
204 &mut scratch,
205 &mut replacement,
206 );
207 }
208
209 let added = replacement.len();
210 self.lines.splice(old_start..old_end, replacement);
211
212 if line_delta != 0 {
218 for line in &mut self.lines[old_start + added..] {
219 line.logical_row = (line.logical_row as isize + line_delta) as usize;
220 }
221 }
222
223 self.row_starts.splice(old_rows, starts);
224 let shift = added as isize - (old_end - old_start) as isize;
225 if shift != 0 {
226 let tail = rows.end.min(self.row_starts.len());
227 for start in &mut self.row_starts[tail..] {
228 *start = (*start as isize + shift) as usize;
229 }
230 }
231 debug_assert_eq!(
232 self.row_starts.len(),
233 text.line_count(),
234 "relayout left the layout describing a different number of rows"
235 );
236 }
237
238 pub fn visual_lines(&self) -> &[VisualLine] {
239 &self.lines
240 }
241
242 pub fn visual_line_count(&self) -> usize {
244 self.lines.len()
245 }
246
247 pub fn row_count(&self) -> usize {
250 self.row_starts.len()
251 }
252
253 pub fn width(&self) -> usize {
254 self.width
255 }
256
257 pub fn visual_row_of(&self, position: Position) -> usize {
259 let row = position.row().min(self.row_starts.len().saturating_sub(1));
260 let first = self.row_starts.get(row).copied().unwrap_or(0);
261 let column = position.column().get();
262 self.lines[first..]
263 .iter()
264 .take_while(|line| line.logical_row == row)
265 .enumerate()
266 .filter(|(_, line)| line.chars.start <= column)
267 .map(|(offset, _)| first + offset)
268 .last()
269 .unwrap_or(first)
270 }
271
272 pub fn cell_of(&self, text: &Text, hints: &[RowHints<'_>], position: Position) -> Cell {
277 debug_assert!(
281 self.describes(text),
282 "cell_of read against a text this layout does not describe"
283 );
284 let row = self.visual_row_of(position);
285 let line = &self.lines[row];
286 let hint = hint_for(hints, line.logical_row);
287 let Some(source) = text.line(line.logical_row) else {
288 return Cell {
289 row,
290 column: hint.inset,
291 };
292 };
293 let mut column = hint.inset;
294 let mut chars = line.chars.start;
295 for cluster in source[line.bytes.clone()].graphemes(true) {
296 if chars >= position.column().get() {
297 break;
298 }
299 if visible(&hint, chars) {
300 column += self.metrics.width_at(cluster, column - hint.inset);
301 }
302 chars += cluster.chars().count();
303 }
304 Cell { row, column }
305 }
306
307 pub fn position_at_cell(
313 &self,
314 text: &Text,
315 hints: &[RowHints<'_>],
316 cell: Cell,
317 ) -> Option<Position> {
318 if !self.describes(text) {
322 return None;
323 }
324 let line = self.lines.get(cell.row)?;
325 let hint = hint_for(hints, line.logical_row);
326 let source = text.line(line.logical_row)?;
327 let mut column = hint.inset;
328 let mut chars = line.chars.start;
329 for cluster in source[line.bytes.clone()].graphemes(true) {
338 let width = if visible(&hint, chars) {
339 self.metrics.width_at(cluster, column - hint.inset)
340 } else {
341 0
342 };
343 if width > 0 && cell.column < column + width {
344 return text.position(line.logical_row, Column::new(chars));
345 }
346 column += width;
347 chars += cluster.chars().count();
348 }
349 text.position(line.logical_row, Column::new(line.chars.end))
350 }
351}
352
353#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
358pub struct Viewport {
359 top: usize,
360 height: usize,
361}
362
363impl Viewport {
364 pub fn new(height: usize) -> Self {
365 Self { top: 0, height }
366 }
367
368 pub fn top(&self) -> usize {
369 self.top
370 }
371
372 pub fn height(&self) -> usize {
373 self.height
374 }
375
376 pub fn set_height(&mut self, height: usize) {
377 self.height = height;
378 }
379
380 pub fn rows(&self, layout: &Layout) -> Range<usize> {
382 let top = self.top.min(layout.visual_line_count().saturating_sub(1));
383 top..(top + self.height).min(layout.visual_line_count())
384 }
385
386 pub fn follow(&mut self, layout: &Layout, cursor: Position) -> bool {
389 if self.height == 0 {
390 return false;
391 }
392 let row = layout.visual_row_of(cursor);
393 let was = self.top;
394 if row < self.top {
395 self.top = row;
396 } else if row >= self.top + self.height {
397 self.top = row + 1 - self.height;
398 }
399 self.top != was
400 }
401
402 pub fn scroll_by(&mut self, layout: &Layout, delta: isize) -> bool {
405 let was = self.top;
406 let last = layout.visual_line_count().saturating_sub(1);
407 self.top = if delta >= 0 {
408 (self.top + delta.unsigned_abs()).min(last)
409 } else {
410 self.top.saturating_sub(delta.unsigned_abs())
411 };
412 self.top != was
413 }
414}
415
416struct Cluster {
420 chars: usize,
421 bytes: usize,
422 len: usize,
424 breakable: bool,
427}
428
429fn hint_for<'a>(hints: &'a [RowHints<'a>], row: usize) -> RowHints<'a> {
430 hints.get(row).copied().unwrap_or_default()
431}
432
433fn visible(hint: &RowHints<'_>, chars: usize) -> bool {
434 hint.visible.get(chars).copied().unwrap_or(true)
435}
436
437fn wrap_row(
439 text: &Text,
440 row: usize,
441 width: usize,
442 metrics: Metrics,
443 hint: RowHints<'_>,
444 scratch: &mut Vec<Cluster>,
445 out: &mut Vec<VisualLine>,
446) {
447 let width = if hint.inset == 0 {
451 width
452 } else {
453 width.saturating_sub(hint.inset).max(1)
454 };
455
456 let Some(source) = text.line(row) else {
457 return;
458 };
459
460 scratch.clear();
461 let mut chars = 0;
462 for (bytes, cluster) in source.grapheme_indices(true) {
463 let len = cluster.chars().count();
464 scratch.push(Cluster {
465 chars,
466 bytes,
467 len: cluster.len(),
468 breakable: len == 1 && cluster.chars().next().is_some_and(char::is_whitespace),
469 });
470 chars += len;
471 }
472 let total_chars = chars;
473 let total_bytes = source.len();
474
475 if scratch.is_empty() || width == 0 {
476 out.push(VisualLine {
477 logical_row: row,
478 chars: 0..0,
479 bytes: 0..0,
480 first: true,
481 });
482 return;
483 }
484
485 let cell_width = |index: usize, column: usize| -> usize {
486 let cluster = &scratch[index];
487 if visible(&hint, cluster.chars) {
488 let at = cluster.bytes;
489 metrics.width_at(&source[at..at + cluster.len], column)
490 } else {
491 0
492 }
493 };
494 let char_at =
495 |index: usize| -> usize { scratch.get(index).map(|c| c.chars).unwrap_or(total_chars) };
496 let byte_at =
497 |index: usize| -> usize { scratch.get(index).map(|c| c.bytes).unwrap_or(total_bytes) };
498
499 let total = scratch.len();
500 let mut start = 0;
501 let mut first = true;
502
503 while start < total {
504 let fit_end = {
508 let mut column = 0;
509 let mut index = start;
510 while index < total {
511 let cells = cell_width(index, column);
512 if column + cells > width {
513 break;
514 }
515 column += cells;
516 index += 1;
517 }
518 if index == start { start + 1 } else { index }
521 };
522
523 if fit_end >= total {
524 out.push(VisualLine {
525 logical_row: row,
526 chars: char_at(start)..total_chars,
527 bytes: byte_at(start)..total_bytes,
528 first,
529 });
530 break;
531 }
532
533 let (content_end, next_start) = if scratch[fit_end].breakable {
536 (fit_end, fit_end + 1)
537 } else {
538 match scratch[start..fit_end]
539 .iter()
540 .enumerate()
541 .rev()
542 .find(|(_, cluster)| cluster.breakable)
543 {
544 Some((offset, _)) => (start + offset, start + offset + 1),
545 None => (fit_end, fit_end),
546 }
547 };
548
549 out.push(VisualLine {
550 logical_row: row,
551 chars: char_at(start)..char_at(content_end),
552 bytes: byte_at(start)..byte_at(content_end),
553 first,
554 });
555 start = next_start;
556 first = false;
557 }
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 fn text(s: &str) -> Text {
565 Text::from(s)
566 }
567
568 fn plain(text: &Text, width: usize) -> Layout {
569 Layout::compute(text, width, Metrics::default(), &[])
570 }
571
572 fn drawn(text: &Text, layout: &Layout) -> Vec<String> {
574 layout
575 .visual_lines()
576 .iter()
577 .map(|line| {
578 let row = text.line(line.logical_row).expect("row exists");
579 row[line.bytes.clone()].to_string()
580 })
581 .collect()
582 }
583
584 fn at(text: &Text, row: usize, col: usize) -> Position {
585 text.position(row, Column::new(col)).expect("addressable")
586 }
587
588 #[test]
591 fn a_row_that_fits_is_one_visual_line() {
592 let t = text("short");
593 assert_eq!(drawn(&t, &plain(&t, 10)), ["short"]);
594 }
595
596 #[test]
597 fn wrapping_prefers_a_space() {
598 let t = text("aaaa bbbb");
599 assert_eq!(drawn(&t, &plain(&t, 6)), ["aaaa", "bbbb"]);
600 }
601
602 #[test]
603 fn a_word_longer_than_the_width_breaks_mid_word() {
604 let t = text("aaaaaaaa");
605 assert_eq!(drawn(&t, &plain(&t, 3)), ["aaa", "aaa", "aa"]);
606 }
607
608 #[test]
609 fn an_empty_row_is_still_a_visual_line() {
610 let t = text("a\n\nb");
611 let layout = plain(&t, 10);
612 assert_eq!(layout.visual_line_count(), 3);
613 assert_eq!(drawn(&t, &layout), ["a", "", "b"]);
614 }
615
616 #[test]
617 fn a_zero_width_pane_still_produces_one_line_per_row() {
618 let t = text("a\nb");
619 let layout = plain(&t, 0);
620 assert_eq!(layout.visual_line_count(), 2);
621 }
622
623 #[test]
624 fn a_cluster_wider_than_the_pane_still_advances() {
625 let t = text("\u{3042}\u{3042}");
627 let layout = plain(&t, 1);
628 assert_eq!(layout.visual_line_count(), 2);
629 }
630
631 #[test]
632 fn a_cluster_is_never_split_across_visual_lines() {
633 let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
637 let t = text(&format!("ab{family}cd"));
638 for width in 1..10 {
639 let layout = plain(&t, width);
640 let row = t.line(0).expect("one row");
641 for line in layout.visual_lines() {
642 assert!(
643 row.is_char_boundary(line.bytes.start) && row.is_char_boundary(line.bytes.end),
644 "width {width}: {:?} splits a character",
645 line.bytes
646 );
647 let intact: Vec<usize> = row.grapheme_indices(true).map(|(at, _)| at).collect();
648 assert!(
649 intact.contains(&line.bytes.start) || line.bytes.start == row.len(),
650 "width {width}: {:?} starts inside a cluster",
651 line.bytes
652 );
653 assert!(
654 intact.contains(&line.bytes.end) || line.bytes.end == row.len(),
655 "width {width}: {:?} ends inside a cluster",
656 line.bytes
657 );
658 }
659 }
660 }
661
662 #[test]
663 fn a_gutter_eats_into_the_width() {
664 let t = text("aaaa bbbb");
665 let hints = [RowHints {
666 visible: &[],
667 inset: 2,
668 }];
669 let layout = Layout::compute(&t, 9, Metrics::default(), &hints);
670 assert_eq!(
671 drawn(&t, &layout),
672 ["aaaa", "bbbb"],
673 "nine cells less a two-cell gutter does not fit nine characters"
674 );
675 assert_eq!(
676 plain(&t, 9).visual_line_count(),
677 1,
678 "and without the gutter it does"
679 );
680 }
681
682 #[test]
683 fn a_gutter_as_wide_as_the_pane_still_makes_progress() {
684 let t = text("aaaa");
685 let hints = [RowHints {
686 visible: &[],
687 inset: 4,
688 }];
689 let layout = Layout::compute(&t, 4, Metrics::default(), &hints);
690 assert_eq!(
691 layout.visual_line_count(),
692 4,
693 "one cell per line, not a loop"
694 );
695 }
696
697 #[test]
698 fn undrawn_clusters_take_no_width() {
699 let t = text("## heading");
702 let visible = vec![
703 false, false, false, true, true, true, true, true, true, true,
704 ];
705 let hints = [RowHints {
706 visible: &visible,
707 inset: 0,
708 }];
709 let layout = Layout::compute(&t, 7, Metrics::default(), &hints);
710 assert_eq!(layout.visual_line_count(), 1);
711 assert_eq!(
712 plain(&t, 7).visual_line_count(),
713 2,
714 "measuring the sigils would wrap it"
715 );
716 }
717
718 #[test]
719 fn a_tab_is_measured_to_its_stop_when_wrapping() {
720 let t = text("\tabcd");
722 assert_eq!(plain(&t, 8).visual_line_count(), 1);
723 assert_eq!(plain(&t, 7).visual_line_count(), 2);
724 }
725
726 #[test]
727 fn a_tab_is_itself_a_place_to_break() {
728 let t = text("ab cd\tef");
731 assert_eq!(drawn(&t, &plain(&t, 5)), ["ab cd", "ef"]);
732 }
733
734 #[test]
735 fn a_tab_measures_from_the_start_of_its_own_visual_line() {
736 let t = text("aaaa bb\tc");
741 assert_eq!(drawn(&t, &plain(&t, 4)), ["aaaa", "bb", "c"]);
742 }
743
744 #[test]
747 fn a_position_knows_which_visual_line_draws_it() {
748 let t = text("aaaa bbbb cccc");
749 let layout = plain(&t, 5);
750 assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "cccc"]);
751 assert_eq!(layout.visual_row_of(at(&t, 0, 0)), 0);
752 assert_eq!(layout.visual_row_of(at(&t, 0, 5)), 1);
753 assert_eq!(layout.visual_row_of(at(&t, 0, 12)), 2);
754 }
755
756 #[test]
757 fn visual_rows_are_found_across_logical_rows() {
758 let t = text("aaaa bbbb\nsecond");
759 let layout = plain(&t, 5);
760 assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "secon", "d"]);
761 assert_eq!(layout.visual_row_of(at(&t, 1, 0)), 2);
762 assert_eq!(layout.visual_row_of(at(&t, 1, 5)), 3);
763 }
764
765 #[test]
766 fn a_cell_accounts_for_the_gutter() {
767 let t = text("abc");
768 let hints = [RowHints {
769 visible: &[],
770 inset: 2,
771 }];
772 let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
773 assert_eq!(
774 layout.cell_of(&t, &hints, at(&t, 0, 1)),
775 Cell { row: 0, column: 3 }
776 );
777 }
778
779 #[test]
780 fn a_cell_skips_hidden_clusters() {
781 let t = text("## heading");
782 let visible = vec![false, false, false];
783 let hints = [RowHints {
784 visible: &visible,
785 inset: 0,
786 }];
787 let layout = Layout::compute(&t, 40, Metrics::default(), &hints);
788 assert_eq!(
789 layout.cell_of(&t, &hints, at(&t, 0, 3)),
790 Cell { row: 0, column: 0 },
791 "the first drawn character is in the first cell"
792 );
793 }
794
795 #[test]
796 fn a_cell_counts_a_wide_cluster_as_two() {
797 let t = text("\u{3042}b");
798 let layout = plain(&t, 40);
799 assert_eq!(layout.cell_of(&t, &[], at(&t, 0, 1)).column, 2);
800 }
801
802 #[test]
803 fn a_click_inside_a_wide_cluster_means_that_cluster() {
804 let t = text("\u{3042}b");
805 let layout = plain(&t, 40);
806 for column in [0, 1] {
807 let landed = layout
808 .position_at_cell(&t, &[], Cell { row: 0, column })
809 .expect("inside the line");
810 assert_eq!(landed.column().get(), 0, "column {column}");
811 }
812 let landed = layout
813 .position_at_cell(&t, &[], Cell { row: 0, column: 2 })
814 .expect("inside the line");
815 assert_eq!(landed.column().get(), 1);
816 }
817
818 #[test]
819 fn a_click_past_the_end_of_a_visual_line_lands_at_its_end() {
820 let t = text("aaaa bbbb");
821 let layout = plain(&t, 5);
822 let landed = layout
823 .position_at_cell(&t, &[], Cell { row: 0, column: 99 })
824 .expect("inside the line");
825 assert_eq!(landed.column().get(), 4, "the end of the first visual line");
826 }
827
828 #[test]
829 fn a_click_in_the_gutter_lands_at_the_start_of_the_text() {
830 let t = text("abc");
831 let hints = [RowHints {
832 visible: &[],
833 inset: 3,
834 }];
835 let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
836 let landed = layout
837 .position_at_cell(&t, &hints, Cell { row: 0, column: 1 })
838 .expect("inside the line");
839 assert_eq!(landed.column().get(), 0);
840 }
841
842 #[test]
843 fn a_click_below_the_text_finds_nothing() {
844 let t = text("abc");
845 let layout = plain(&t, 10);
846 assert!(
847 layout
848 .position_at_cell(&t, &[], Cell { row: 9, column: 0 })
849 .is_none()
850 );
851 }
852
853 #[test]
854 fn cells_and_positions_round_trip() {
855 let t = text("aaaa bbbb cccc");
856 let layout = plain(&t, 5);
857 for column in 0..14 {
858 let position = at(&t, 0, column);
859 let cell = layout.cell_of(&t, &[], position);
860 let back = layout
861 .position_at_cell(&t, &[], cell)
862 .expect("its own cell is inside the line");
863 assert_eq!(back, position, "column {column}");
864 }
865 }
866
867 #[test]
870 fn unwrapped_matches_row_count_and_describes_text() {
871 let t = text("short\na longer row that would wrap at a narrow width\nlast");
872 let layout = Layout::unwrapped(&t);
873 assert_eq!(layout.row_count(), t.line_count());
874 assert_eq!(layout.visual_line_count(), t.line_count());
875 assert!(
876 layout.describes(&t),
877 "unwrapped must describe the text it was built from"
878 );
879 assert_eq!(
880 drawn(&t, &layout),
881 [
882 "short",
883 "a longer row that would wrap at a narrow width",
884 "last"
885 ],
886 "one unwrapped visual line per row"
887 );
888 }
889
890 #[test]
891 fn unwrapped_handles_an_empty_text() {
892 let t = text("");
893 let layout = Layout::unwrapped(&t);
894 assert_eq!(layout.row_count(), t.line_count());
895 assert!(layout.describes(&t));
896 }
897
898 #[test]
901 fn relayout_rewraps_only_what_changed() {
902 let mut buffer = crate::ropetext::EditBuffer::new(text("aaaa bbbb\nkeep\ntail"));
903 let mut layout = plain(buffer.text(), 5);
904 assert_eq!(layout.visual_line_count(), 4);
905
906 let end = buffer.text().position(0, Column::new(9)).unwrap();
907 let mut txn = buffer.begin();
908 txn.delete(buffer_span(&txn, 0, 4, 0, 9));
909 let change = txn.commit().expect("changed");
910 let _ = end;
911
912 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
913 assert_eq!(drawn(buffer.text(), &layout), ["aaaa", "keep", "tail"]);
914 assert_eq!(layout.row_count(), buffer.text().line_count());
915 }
916
917 #[test]
918 fn relayout_follows_added_rows() {
919 let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo"));
920 let mut layout = plain(buffer.text(), 10);
921 let at_end = buffer.text().position(0, Column::new(3)).unwrap();
922 let mut txn = buffer.begin();
923 txn.insert(at_end, "\nmiddle");
924 let change = txn.commit().expect("changed");
925
926 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
927 assert_eq!(drawn(buffer.text(), &layout), ["one", "middle", "two"]);
928 assert_eq!(layout.row_count(), 3);
929 }
930
931 #[test]
932 fn relayout_follows_removed_rows() {
933 let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo\nthree\nfour"));
934 let mut layout = plain(buffer.text(), 10);
935 let mut txn = buffer.begin();
936 txn.delete(buffer_span(&txn, 0, 3, 2, 5));
937 let change = txn.commit().expect("changed");
938
939 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
940 assert_eq!(drawn(buffer.text(), &layout), ["one", "four"]);
941 assert_eq!(layout.row_count(), 2);
942 }
943
944 #[test]
945 fn relayout_matches_a_full_recompute() {
946 for (initial, row, col, inserted) in [
949 ("aaaa bbbb\nkeep", 0, 4, " cccc"),
950 ("one\ntwo\nthree", 1, 3, "\nsplit"),
951 ("one\ntwo", 0, 0, "prefix "),
952 ("wrapped line that is long\nnext", 0, 8, "\n"),
953 ] {
954 let mut buffer = crate::ropetext::EditBuffer::new(text(initial));
955 let mut layout = plain(buffer.text(), 6);
956 let position = buffer.text().position(row, Column::new(col)).unwrap();
957 let mut txn = buffer.begin();
958 txn.insert(position, inserted);
959 let change = txn.commit().expect("changed");
960
961 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
962 let fresh = plain(buffer.text(), 6);
963 assert_eq!(
964 layout.visual_lines(),
965 fresh.visual_lines(),
966 "relayout disagreed for {initial:?} + {inserted:?}"
967 );
968 }
969 }
970
971 fn buffer_span(
972 txn: &crate::ropetext::Txn<'_>,
973 r1: usize,
974 c1: usize,
975 r2: usize,
976 c2: usize,
977 ) -> crate::ropetext::Span {
978 let text = txn.text();
979 let a = text.position(r1, Column::new(c1)).expect("addressable");
980 let b = text.position(r2, Column::new(c2)).expect("addressable");
981 text.span(a, b).expect("same text")
982 }
983
984 #[test]
987 fn a_viewport_shows_its_height_of_lines() {
988 let t = text("a\nb\nc\nd\ne");
989 let layout = plain(&t, 10);
990 let view = Viewport::new(3);
991 assert_eq!(view.rows(&layout), 0..3);
992 }
993
994 #[test]
995 fn a_viewport_clamps_to_what_there_is() {
996 let t = text("a\nb");
997 let layout = plain(&t, 10);
998 let view = Viewport::new(10);
999 assert_eq!(view.rows(&layout), 0..2);
1000 }
1001
1002 #[test]
1003 fn following_the_cursor_scrolls_the_least_it_can() {
1004 let t = text("a\nb\nc\nd\ne");
1005 let layout = plain(&t, 10);
1006 let mut view = Viewport::new(3);
1007 assert!(view.follow(&layout, at(&t, 4, 0)));
1008 assert_eq!(view.top(), 2, "just enough to show the last row");
1009 assert!(!view.follow(&layout, at(&t, 3, 0)), "already on screen");
1010 assert!(view.follow(&layout, at(&t, 0, 0)));
1011 assert_eq!(view.top(), 0);
1012 }
1013
1014 #[test]
1015 fn following_the_cursor_counts_visual_lines_not_rows() {
1016 let t = text("aaaa bbbb cccc\nlast");
1017 let layout = plain(&t, 5);
1018 assert_eq!(layout.visual_line_count(), 4);
1019 let mut view = Viewport::new(2);
1020 view.follow(&layout, at(&t, 0, 12));
1021 assert_eq!(view.top(), 1, "the third visual line of the first row");
1022 }
1023
1024 #[test]
1025 fn scrolling_does_not_run_past_the_end() {
1026 let t = text("a\nb\nc");
1027 let layout = plain(&t, 10);
1028 let mut view = Viewport::new(2);
1029 view.scroll_by(&layout, 99);
1030 assert_eq!(view.top(), 2);
1031 view.scroll_by(&layout, -99);
1032 assert_eq!(view.top(), 0);
1033 assert!(!view.scroll_by(&layout, -1), "already at the top");
1034 }
1035
1036 #[test]
1037 fn a_viewport_of_no_height_follows_nothing() {
1038 let t = text("a\nb");
1039 let layout = plain(&t, 10);
1040 let mut view = Viewport::new(0);
1041 assert!(!view.follow(&layout, at(&t, 1, 0)));
1042 }
1043
1044 mod properties {
1047 use super::*;
1048 use proptest::prelude::*;
1049
1050 proptest! {
1051 #![proptest_config(ProptestConfig::with_cases(200))]
1052
1053 #[test]
1061 fn relayout_agrees_with_a_full_recompute(
1062 initial in "[a-z \n]{0,40}",
1063 inserted in "[a-z \n]{0,8}",
1064 byte in 0usize..48,
1065 width in 1usize..8,
1066 ) {
1067 let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1068 let Some(position) = buffer.text().position_at_byte(byte.min(buffer.text().len_bytes()))
1069 else {
1070 return Ok(());
1071 };
1072 let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1073
1074 let mut txn = buffer.begin();
1075 txn.insert(position, &inserted);
1076 let Some(change) = txn.commit() else {
1077 return Ok(());
1078 };
1079
1080 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1081 let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1082 prop_assert_eq!(
1083 layout.visual_lines(),
1084 fresh.visual_lines(),
1085 "{:?} + {:?} at byte {} width {}", initial, inserted, byte, width
1086 );
1087 prop_assert_eq!(layout.row_count(), fresh.row_count());
1088 }
1089
1090 #[test]
1092 fn relayout_agrees_after_a_deletion(
1093 initial in "[a-z \n]{1,40}",
1094 from in 0usize..48,
1095 len in 0usize..12,
1096 width in 1usize..8,
1097 ) {
1098 let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1099 let end = buffer.text().len_bytes();
1100 let Some(start) = buffer.text().position_at_byte(from.min(end)) else {
1101 return Ok(());
1102 };
1103 let Some(stop) = buffer.text().position_at_byte((from + len).min(end)) else {
1104 return Ok(());
1105 };
1106 let span = buffer.text().span(start, stop).expect("same text");
1107 let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1108
1109 let mut txn = buffer.begin();
1110 txn.delete(span);
1111 let Some(change) = txn.commit() else {
1112 return Ok(());
1113 };
1114
1115 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1116 let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1117 prop_assert_eq!(
1118 layout.visual_lines(),
1119 fresh.visual_lines(),
1120 "{:?} minus {}..{} at width {}", initial, from, from + len, width
1121 );
1122 }
1123
1124 #[test]
1127 fn visual_lines_tile_their_rows(
1128 initial in ".{0,40}",
1129 width in 1usize..8,
1130 ) {
1131 let t = Text::from(initial.as_str());
1132 let layout = Layout::compute(&t, width, Metrics::default(), &[]);
1133 prop_assert_eq!(layout.row_count(), t.line_count());
1134 let mut seen_rows = 0;
1135 let mut expected_row = 0;
1136 let mut cursor = 0;
1137 for line in layout.visual_lines() {
1138 if line.first {
1139 prop_assert_eq!(line.logical_row, expected_row, "rows must be in order");
1140 expected_row += 1;
1141 seen_rows += 1;
1142 cursor = 0;
1143 }
1144 let row = t.line(line.logical_row).expect("a named row exists");
1145 prop_assert!(line.bytes.end <= row.len(), "slice past the row");
1146 prop_assert!(line.chars.start >= cursor, "a line went backwards");
1147 prop_assert!(row.is_char_boundary(line.bytes.start));
1148 prop_assert!(row.is_char_boundary(line.bytes.end));
1149 let breaks: Vec<usize> = row
1152 .grapheme_indices(true)
1153 .map(|(at, _)| at)
1154 .chain(std::iter::once(row.len()))
1155 .collect();
1156 prop_assert!(breaks.contains(&line.bytes.start), "start splits a cluster");
1157 prop_assert!(breaks.contains(&line.bytes.end), "end splits a cluster");
1158 cursor = line.chars.end;
1159 }
1160 prop_assert_eq!(seen_rows, t.line_count(), "every row is drawn");
1161 }
1162 }
1163 }
1164}