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 self.follow_row(layout.visual_row_of(cursor))
390 }
391
392 pub fn follow_row(&mut self, row: usize) -> bool {
394 if self.height == 0 {
395 return false;
396 }
397 let was = self.top;
398 if row < self.top {
399 self.top = row;
400 } else if row >= self.top + self.height {
401 self.top = row + 1 - self.height;
402 }
403 self.top != was
404 }
405
406 pub fn scroll_by(&mut self, layout: &Layout, delta: isize) -> bool {
409 let was = self.top;
410 self.top = self
411 .top
412 .saturating_add_signed(delta)
413 .min(self.max_top(layout));
414 self.top != was
415 }
416
417 pub fn max_top(&self, layout: &Layout) -> usize {
420 layout.visual_line_count().saturating_sub(self.height)
421 }
422
423 pub fn clamp(&mut self, layout: &Layout) {
426 self.top = self.top.min(self.max_top(layout));
427 }
428}
429
430struct Cluster {
434 chars: usize,
435 bytes: usize,
436 len: usize,
438 breakable: bool,
441}
442
443fn hint_for<'a>(hints: &'a [RowHints<'a>], row: usize) -> RowHints<'a> {
444 hints.get(row).copied().unwrap_or_default()
445}
446
447fn visible(hint: &RowHints<'_>, chars: usize) -> bool {
448 hint.visible.get(chars).copied().unwrap_or(true)
449}
450
451fn wrap_row(
453 text: &Text,
454 row: usize,
455 width: usize,
456 metrics: Metrics,
457 hint: RowHints<'_>,
458 scratch: &mut Vec<Cluster>,
459 out: &mut Vec<VisualLine>,
460) {
461 let width = if hint.inset == 0 {
465 width
466 } else {
467 width.saturating_sub(hint.inset).max(1)
468 };
469
470 let Some(source) = text.line(row) else {
471 return;
472 };
473
474 scratch.clear();
475 let mut chars = 0;
476 for (bytes, cluster) in source.grapheme_indices(true) {
477 let len = cluster.chars().count();
478 scratch.push(Cluster {
479 chars,
480 bytes,
481 len: cluster.len(),
482 breakable: len == 1 && cluster.chars().next().is_some_and(char::is_whitespace),
483 });
484 chars += len;
485 }
486 let total_chars = chars;
487 let total_bytes = source.len();
488
489 if scratch.is_empty() || width == 0 {
490 out.push(VisualLine {
491 logical_row: row,
492 chars: 0..0,
493 bytes: 0..0,
494 first: true,
495 });
496 return;
497 }
498
499 let cell_width = |index: usize, column: usize| -> usize {
500 let cluster = &scratch[index];
501 if visible(&hint, cluster.chars) {
502 let at = cluster.bytes;
503 metrics.width_at(&source[at..at + cluster.len], column)
504 } else {
505 0
506 }
507 };
508 let char_at =
509 |index: usize| -> usize { scratch.get(index).map(|c| c.chars).unwrap_or(total_chars) };
510 let byte_at =
511 |index: usize| -> usize { scratch.get(index).map(|c| c.bytes).unwrap_or(total_bytes) };
512
513 let total = scratch.len();
514 let mut start = 0;
515 let mut first = true;
516
517 while start < total {
518 let fit_end = {
522 let mut column = 0;
523 let mut index = start;
524 while index < total {
525 let cells = cell_width(index, column);
526 if column + cells > width {
527 break;
528 }
529 column += cells;
530 index += 1;
531 }
532 if index == start { start + 1 } else { index }
535 };
536
537 if fit_end >= total {
538 out.push(VisualLine {
539 logical_row: row,
540 chars: char_at(start)..total_chars,
541 bytes: byte_at(start)..total_bytes,
542 first,
543 });
544 break;
545 }
546
547 let (content_end, next_start) = if scratch[fit_end].breakable {
550 (fit_end, fit_end + 1)
551 } else {
552 match scratch[start..fit_end]
553 .iter()
554 .enumerate()
555 .rev()
556 .find(|(_, cluster)| cluster.breakable)
557 {
558 Some((offset, _)) => (start + offset, start + offset + 1),
559 None => (fit_end, fit_end),
560 }
561 };
562
563 out.push(VisualLine {
564 logical_row: row,
565 chars: char_at(start)..char_at(content_end),
566 bytes: byte_at(start)..byte_at(content_end),
567 first,
568 });
569 start = next_start;
570 first = false;
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 fn text(s: &str) -> Text {
579 Text::from(s)
580 }
581
582 fn plain(text: &Text, width: usize) -> Layout {
583 Layout::compute(text, width, Metrics::default(), &[])
584 }
585
586 fn drawn(text: &Text, layout: &Layout) -> Vec<String> {
588 layout
589 .visual_lines()
590 .iter()
591 .map(|line| {
592 let row = text.line(line.logical_row).expect("row exists");
593 row[line.bytes.clone()].to_string()
594 })
595 .collect()
596 }
597
598 fn at(text: &Text, row: usize, col: usize) -> Position {
599 text.position(row, Column::new(col)).expect("addressable")
600 }
601
602 #[test]
605 fn a_row_that_fits_is_one_visual_line() {
606 let t = text("short");
607 assert_eq!(drawn(&t, &plain(&t, 10)), ["short"]);
608 }
609
610 #[test]
611 fn wrapping_prefers_a_space() {
612 let t = text("aaaa bbbb");
613 assert_eq!(drawn(&t, &plain(&t, 6)), ["aaaa", "bbbb"]);
614 }
615
616 #[test]
617 fn a_word_longer_than_the_width_breaks_mid_word() {
618 let t = text("aaaaaaaa");
619 assert_eq!(drawn(&t, &plain(&t, 3)), ["aaa", "aaa", "aa"]);
620 }
621
622 #[test]
623 fn an_empty_row_is_still_a_visual_line() {
624 let t = text("a\n\nb");
625 let layout = plain(&t, 10);
626 assert_eq!(layout.visual_line_count(), 3);
627 assert_eq!(drawn(&t, &layout), ["a", "", "b"]);
628 }
629
630 #[test]
631 fn a_zero_width_pane_still_produces_one_line_per_row() {
632 let t = text("a\nb");
633 let layout = plain(&t, 0);
634 assert_eq!(layout.visual_line_count(), 2);
635 }
636
637 #[test]
638 fn a_cluster_wider_than_the_pane_still_advances() {
639 let t = text("\u{3042}\u{3042}");
641 let layout = plain(&t, 1);
642 assert_eq!(layout.visual_line_count(), 2);
643 }
644
645 #[test]
646 fn a_cluster_is_never_split_across_visual_lines() {
647 let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
651 let t = text(&format!("ab{family}cd"));
652 for width in 1..10 {
653 let layout = plain(&t, width);
654 let row = t.line(0).expect("one row");
655 for line in layout.visual_lines() {
656 assert!(
657 row.is_char_boundary(line.bytes.start) && row.is_char_boundary(line.bytes.end),
658 "width {width}: {:?} splits a character",
659 line.bytes
660 );
661 let intact: Vec<usize> = row.grapheme_indices(true).map(|(at, _)| at).collect();
662 assert!(
663 intact.contains(&line.bytes.start) || line.bytes.start == row.len(),
664 "width {width}: {:?} starts inside a cluster",
665 line.bytes
666 );
667 assert!(
668 intact.contains(&line.bytes.end) || line.bytes.end == row.len(),
669 "width {width}: {:?} ends inside a cluster",
670 line.bytes
671 );
672 }
673 }
674 }
675
676 #[test]
677 fn a_gutter_eats_into_the_width() {
678 let t = text("aaaa bbbb");
679 let hints = [RowHints {
680 visible: &[],
681 inset: 2,
682 }];
683 let layout = Layout::compute(&t, 9, Metrics::default(), &hints);
684 assert_eq!(
685 drawn(&t, &layout),
686 ["aaaa", "bbbb"],
687 "nine cells less a two-cell gutter does not fit nine characters"
688 );
689 assert_eq!(
690 plain(&t, 9).visual_line_count(),
691 1,
692 "and without the gutter it does"
693 );
694 }
695
696 #[test]
697 fn a_gutter_as_wide_as_the_pane_still_makes_progress() {
698 let t = text("aaaa");
699 let hints = [RowHints {
700 visible: &[],
701 inset: 4,
702 }];
703 let layout = Layout::compute(&t, 4, Metrics::default(), &hints);
704 assert_eq!(
705 layout.visual_line_count(),
706 4,
707 "one cell per line, not a loop"
708 );
709 }
710
711 #[test]
712 fn undrawn_clusters_take_no_width() {
713 let t = text("## heading");
716 let visible = vec![
717 false, false, false, true, true, true, true, true, true, true,
718 ];
719 let hints = [RowHints {
720 visible: &visible,
721 inset: 0,
722 }];
723 let layout = Layout::compute(&t, 7, Metrics::default(), &hints);
724 assert_eq!(layout.visual_line_count(), 1);
725 assert_eq!(
726 plain(&t, 7).visual_line_count(),
727 2,
728 "measuring the sigils would wrap it"
729 );
730 }
731
732 #[test]
733 fn a_tab_is_measured_to_its_stop_when_wrapping() {
734 let t = text("\tabcd");
736 assert_eq!(plain(&t, 8).visual_line_count(), 1);
737 assert_eq!(plain(&t, 7).visual_line_count(), 2);
738 }
739
740 #[test]
741 fn a_tab_is_itself_a_place_to_break() {
742 let t = text("ab cd\tef");
745 assert_eq!(drawn(&t, &plain(&t, 5)), ["ab cd", "ef"]);
746 }
747
748 #[test]
749 fn a_tab_measures_from_the_start_of_its_own_visual_line() {
750 let t = text("aaaa bb\tc");
755 assert_eq!(drawn(&t, &plain(&t, 4)), ["aaaa", "bb", "c"]);
756 }
757
758 #[test]
761 fn a_position_knows_which_visual_line_draws_it() {
762 let t = text("aaaa bbbb cccc");
763 let layout = plain(&t, 5);
764 assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "cccc"]);
765 assert_eq!(layout.visual_row_of(at(&t, 0, 0)), 0);
766 assert_eq!(layout.visual_row_of(at(&t, 0, 5)), 1);
767 assert_eq!(layout.visual_row_of(at(&t, 0, 12)), 2);
768 }
769
770 #[test]
771 fn visual_rows_are_found_across_logical_rows() {
772 let t = text("aaaa bbbb\nsecond");
773 let layout = plain(&t, 5);
774 assert_eq!(drawn(&t, &layout), ["aaaa", "bbbb", "secon", "d"]);
775 assert_eq!(layout.visual_row_of(at(&t, 1, 0)), 2);
776 assert_eq!(layout.visual_row_of(at(&t, 1, 5)), 3);
777 }
778
779 #[test]
780 fn a_cell_accounts_for_the_gutter() {
781 let t = text("abc");
782 let hints = [RowHints {
783 visible: &[],
784 inset: 2,
785 }];
786 let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
787 assert_eq!(
788 layout.cell_of(&t, &hints, at(&t, 0, 1)),
789 Cell { row: 0, column: 3 }
790 );
791 }
792
793 #[test]
794 fn a_cell_skips_hidden_clusters() {
795 let t = text("## heading");
796 let visible = vec![false, false, false];
797 let hints = [RowHints {
798 visible: &visible,
799 inset: 0,
800 }];
801 let layout = Layout::compute(&t, 40, Metrics::default(), &hints);
802 assert_eq!(
803 layout.cell_of(&t, &hints, at(&t, 0, 3)),
804 Cell { row: 0, column: 0 },
805 "the first drawn character is in the first cell"
806 );
807 }
808
809 #[test]
810 fn a_cell_counts_a_wide_cluster_as_two() {
811 let t = text("\u{3042}b");
812 let layout = plain(&t, 40);
813 assert_eq!(layout.cell_of(&t, &[], at(&t, 0, 1)).column, 2);
814 }
815
816 #[test]
817 fn a_click_inside_a_wide_cluster_means_that_cluster() {
818 let t = text("\u{3042}b");
819 let layout = plain(&t, 40);
820 for column in [0, 1] {
821 let landed = layout
822 .position_at_cell(&t, &[], Cell { row: 0, column })
823 .expect("inside the line");
824 assert_eq!(landed.column().get(), 0, "column {column}");
825 }
826 let landed = layout
827 .position_at_cell(&t, &[], Cell { row: 0, column: 2 })
828 .expect("inside the line");
829 assert_eq!(landed.column().get(), 1);
830 }
831
832 #[test]
833 fn a_click_past_the_end_of_a_visual_line_lands_at_its_end() {
834 let t = text("aaaa bbbb");
835 let layout = plain(&t, 5);
836 let landed = layout
837 .position_at_cell(&t, &[], Cell { row: 0, column: 99 })
838 .expect("inside the line");
839 assert_eq!(landed.column().get(), 4, "the end of the first visual line");
840 }
841
842 #[test]
843 fn a_click_in_the_gutter_lands_at_the_start_of_the_text() {
844 let t = text("abc");
845 let hints = [RowHints {
846 visible: &[],
847 inset: 3,
848 }];
849 let layout = Layout::compute(&t, 10, Metrics::default(), &hints);
850 let landed = layout
851 .position_at_cell(&t, &hints, Cell { row: 0, column: 1 })
852 .expect("inside the line");
853 assert_eq!(landed.column().get(), 0);
854 }
855
856 #[test]
857 fn a_click_below_the_text_finds_nothing() {
858 let t = text("abc");
859 let layout = plain(&t, 10);
860 assert!(
861 layout
862 .position_at_cell(&t, &[], Cell { row: 9, column: 0 })
863 .is_none()
864 );
865 }
866
867 #[test]
868 fn cells_and_positions_round_trip() {
869 let t = text("aaaa bbbb cccc");
870 let layout = plain(&t, 5);
871 for column in 0..14 {
872 let position = at(&t, 0, column);
873 let cell = layout.cell_of(&t, &[], position);
874 let back = layout
875 .position_at_cell(&t, &[], cell)
876 .expect("its own cell is inside the line");
877 assert_eq!(back, position, "column {column}");
878 }
879 }
880
881 #[test]
884 fn unwrapped_matches_row_count_and_describes_text() {
885 let t = text("short\na longer row that would wrap at a narrow width\nlast");
886 let layout = Layout::unwrapped(&t);
887 assert_eq!(layout.row_count(), t.line_count());
888 assert_eq!(layout.visual_line_count(), t.line_count());
889 assert!(
890 layout.describes(&t),
891 "unwrapped must describe the text it was built from"
892 );
893 assert_eq!(
894 drawn(&t, &layout),
895 [
896 "short",
897 "a longer row that would wrap at a narrow width",
898 "last"
899 ],
900 "one unwrapped visual line per row"
901 );
902 }
903
904 #[test]
905 fn unwrapped_handles_an_empty_text() {
906 let t = text("");
907 let layout = Layout::unwrapped(&t);
908 assert_eq!(layout.row_count(), t.line_count());
909 assert!(layout.describes(&t));
910 }
911
912 #[test]
915 fn relayout_rewraps_only_what_changed() {
916 let mut buffer = crate::ropetext::EditBuffer::new(text("aaaa bbbb\nkeep\ntail"));
917 let mut layout = plain(buffer.text(), 5);
918 assert_eq!(layout.visual_line_count(), 4);
919
920 let end = buffer.text().position(0, Column::new(9)).unwrap();
921 let mut txn = buffer.begin();
922 txn.delete(buffer_span(&txn, 0, 4, 0, 9));
923 let change = txn.commit().expect("changed");
924 let _ = end;
925
926 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
927 assert_eq!(drawn(buffer.text(), &layout), ["aaaa", "keep", "tail"]);
928 assert_eq!(layout.row_count(), buffer.text().line_count());
929 }
930
931 #[test]
932 fn relayout_follows_added_rows() {
933 let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo"));
934 let mut layout = plain(buffer.text(), 10);
935 let at_end = buffer.text().position(0, Column::new(3)).unwrap();
936 let mut txn = buffer.begin();
937 txn.insert(at_end, "\nmiddle");
938 let change = txn.commit().expect("changed");
939
940 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
941 assert_eq!(drawn(buffer.text(), &layout), ["one", "middle", "two"]);
942 assert_eq!(layout.row_count(), 3);
943 }
944
945 #[test]
946 fn relayout_follows_removed_rows() {
947 let mut buffer = crate::ropetext::EditBuffer::new(text("one\ntwo\nthree\nfour"));
948 let mut layout = plain(buffer.text(), 10);
949 let mut txn = buffer.begin();
950 txn.delete(buffer_span(&txn, 0, 3, 2, 5));
951 let change = txn.commit().expect("changed");
952
953 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
954 assert_eq!(drawn(buffer.text(), &layout), ["one", "four"]);
955 assert_eq!(layout.row_count(), 2);
956 }
957
958 #[test]
959 fn relayout_matches_a_full_recompute() {
960 for (initial, row, col, inserted) in [
963 ("aaaa bbbb\nkeep", 0, 4, " cccc"),
964 ("one\ntwo\nthree", 1, 3, "\nsplit"),
965 ("one\ntwo", 0, 0, "prefix "),
966 ("wrapped line that is long\nnext", 0, 8, "\n"),
967 ] {
968 let mut buffer = crate::ropetext::EditBuffer::new(text(initial));
969 let mut layout = plain(buffer.text(), 6);
970 let position = buffer.text().position(row, Column::new(col)).unwrap();
971 let mut txn = buffer.begin();
972 txn.insert(position, inserted);
973 let change = txn.commit().expect("changed");
974
975 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
976 let fresh = plain(buffer.text(), 6);
977 assert_eq!(
978 layout.visual_lines(),
979 fresh.visual_lines(),
980 "relayout disagreed for {initial:?} + {inserted:?}"
981 );
982 }
983 }
984
985 fn buffer_span(
986 txn: &crate::ropetext::Txn<'_>,
987 r1: usize,
988 c1: usize,
989 r2: usize,
990 c2: usize,
991 ) -> crate::ropetext::Span {
992 let text = txn.text();
993 let a = text.position(r1, Column::new(c1)).expect("addressable");
994 let b = text.position(r2, Column::new(c2)).expect("addressable");
995 text.span(a, b).expect("same text")
996 }
997
998 #[test]
1001 fn a_viewport_shows_its_height_of_lines() {
1002 let t = text("a\nb\nc\nd\ne");
1003 let layout = plain(&t, 10);
1004 let view = Viewport::new(3);
1005 assert_eq!(view.rows(&layout), 0..3);
1006 }
1007
1008 #[test]
1009 fn a_viewport_clamps_to_what_there_is() {
1010 let t = text("a\nb");
1011 let layout = plain(&t, 10);
1012 let view = Viewport::new(10);
1013 assert_eq!(view.rows(&layout), 0..2);
1014 }
1015
1016 #[test]
1017 fn following_the_cursor_scrolls_the_least_it_can() {
1018 let t = text("a\nb\nc\nd\ne");
1019 let layout = plain(&t, 10);
1020 let mut view = Viewport::new(3);
1021 assert!(view.follow(&layout, at(&t, 4, 0)));
1022 assert_eq!(view.top(), 2, "just enough to show the last row");
1023 assert!(!view.follow(&layout, at(&t, 3, 0)), "already on screen");
1024 assert!(view.follow(&layout, at(&t, 0, 0)));
1025 assert_eq!(view.top(), 0);
1026 }
1027
1028 #[test]
1029 fn following_the_cursor_counts_visual_lines_not_rows() {
1030 let t = text("aaaa bbbb cccc\nlast");
1031 let layout = plain(&t, 5);
1032 assert_eq!(layout.visual_line_count(), 4);
1033 let mut view = Viewport::new(2);
1034 view.follow(&layout, at(&t, 0, 12));
1035 assert_eq!(view.top(), 1, "the third visual line of the first row");
1036 }
1037
1038 #[test]
1039 fn scrolling_does_not_run_past_the_end() {
1040 let t = text("a\nb\nc");
1041 let layout = plain(&t, 10);
1042 let mut view = Viewport::new(2);
1043 view.scroll_by(&layout, 99);
1044 assert_eq!(view.top(), 1, "the last line sits on the bottom row");
1045 assert!(!view.scroll_by(&layout, 1), "already at the bottom");
1046 view.scroll_by(&layout, -99);
1047 assert_eq!(view.top(), 0);
1048 assert!(!view.scroll_by(&layout, -1), "already at the top");
1049 }
1050
1051 #[test]
1052 fn a_taller_pane_pulls_the_content_back_down() {
1053 let t = text("a\nb\nc\nd\ne");
1054 let layout = plain(&t, 10);
1055 let mut view = Viewport::new(2);
1056 view.scroll_by(&layout, 99);
1057 assert_eq!(view.top(), 3);
1058 view.set_height(4);
1059 view.clamp(&layout);
1060 assert_eq!(view.top(), 1);
1061 }
1062
1063 #[test]
1064 fn a_viewport_of_no_height_follows_nothing() {
1065 let t = text("a\nb");
1066 let layout = plain(&t, 10);
1067 let mut view = Viewport::new(0);
1068 assert!(!view.follow(&layout, at(&t, 1, 0)));
1069 }
1070
1071 mod properties {
1074 use super::*;
1075 use proptest::prelude::*;
1076
1077 proptest! {
1078 #![proptest_config(ProptestConfig::with_cases(200))]
1079
1080 #[test]
1088 fn relayout_agrees_with_a_full_recompute(
1089 initial in "[a-z \n]{0,40}",
1090 inserted in "[a-z \n]{0,8}",
1091 byte in 0usize..48,
1092 width in 1usize..8,
1093 ) {
1094 let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1095 let Some(position) = buffer.text().position_at_byte(byte.min(buffer.text().len_bytes()))
1096 else {
1097 return Ok(());
1098 };
1099 let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1100
1101 let mut txn = buffer.begin();
1102 txn.insert(position, &inserted);
1103 let Some(change) = txn.commit() else {
1104 return Ok(());
1105 };
1106
1107 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1108 let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1109 prop_assert_eq!(
1110 layout.visual_lines(),
1111 fresh.visual_lines(),
1112 "{:?} + {:?} at byte {} width {}", initial, inserted, byte, width
1113 );
1114 prop_assert_eq!(layout.row_count(), fresh.row_count());
1115 }
1116
1117 #[test]
1119 fn relayout_agrees_after_a_deletion(
1120 initial in "[a-z \n]{1,40}",
1121 from in 0usize..48,
1122 len in 0usize..12,
1123 width in 1usize..8,
1124 ) {
1125 let mut buffer = crate::ropetext::EditBuffer::new(Text::from(initial.as_str()));
1126 let end = buffer.text().len_bytes();
1127 let Some(start) = buffer.text().position_at_byte(from.min(end)) else {
1128 return Ok(());
1129 };
1130 let Some(stop) = buffer.text().position_at_byte((from + len).min(end)) else {
1131 return Ok(());
1132 };
1133 let span = buffer.text().span(start, stop).expect("same text");
1134 let mut layout = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1135
1136 let mut txn = buffer.begin();
1137 txn.delete(span);
1138 let Some(change) = txn.commit() else {
1139 return Ok(());
1140 };
1141
1142 layout.relayout_rows(buffer.text(), &[], change.rows(), change.line_delta());
1143 let fresh = Layout::compute(buffer.text(), width, Metrics::default(), &[]);
1144 prop_assert_eq!(
1145 layout.visual_lines(),
1146 fresh.visual_lines(),
1147 "{:?} minus {}..{} at width {}", initial, from, from + len, width
1148 );
1149 }
1150
1151 #[test]
1154 fn visual_lines_tile_their_rows(
1155 initial in ".{0,40}",
1156 width in 1usize..8,
1157 ) {
1158 let t = Text::from(initial.as_str());
1159 let layout = Layout::compute(&t, width, Metrics::default(), &[]);
1160 prop_assert_eq!(layout.row_count(), t.line_count());
1161 let mut seen_rows = 0;
1162 let mut expected_row = 0;
1163 let mut cursor = 0;
1164 for line in layout.visual_lines() {
1165 if line.first {
1166 prop_assert_eq!(line.logical_row, expected_row, "rows must be in order");
1167 expected_row += 1;
1168 seen_rows += 1;
1169 cursor = 0;
1170 }
1171 let row = t.line(line.logical_row).expect("a named row exists");
1172 prop_assert!(line.bytes.end <= row.len(), "slice past the row");
1173 prop_assert!(line.chars.start >= cursor, "a line went backwards");
1174 prop_assert!(row.is_char_boundary(line.bytes.start));
1175 prop_assert!(row.is_char_boundary(line.bytes.end));
1176 let breaks: Vec<usize> = row
1179 .grapheme_indices(true)
1180 .map(|(at, _)| at)
1181 .chain(std::iter::once(row.len()))
1182 .collect();
1183 prop_assert!(breaks.contains(&line.bytes.start), "start splits a cluster");
1184 prop_assert!(breaks.contains(&line.bytes.end), "end splits a cluster");
1185 cursor = line.chars.end;
1186 }
1187 prop_assert_eq!(seen_rows, t.line_count(), "every row is drawn");
1188 }
1189 }
1190 }
1191}