1use ratatui::buffer::Buffer as TermBuffer;
15use ratatui::layout::Rect;
16use ratatui::style::Style;
17use ratatui::widgets::Widget;
18use unicode_width::UnicodeWidthChar;
19
20use crate::wrap::wrap_segments;
21use crate::{Buffer, Selection, Wrap};
22
23pub trait StyleResolver {
27 fn resolve(&self, style_id: u32) -> Style;
28}
29
30impl<F: Fn(u32) -> Style> StyleResolver for F {
32 fn resolve(&self, style_id: u32) -> Style {
33 self(style_id)
34 }
35}
36
37pub struct BufferView<'a, R: StyleResolver> {
41 pub buffer: &'a Buffer,
42 pub selection: Option<Selection>,
43 pub resolver: &'a R,
44 pub cursor_line_bg: Style,
47 pub cursor_column_bg: Style,
50 pub selection_bg: Style,
52 pub cursor_style: Style,
55 pub gutter: Option<Gutter>,
59 pub search_bg: Style,
63 pub signs: &'a [Sign],
68 pub conceals: &'a [Conceal],
72}
73
74#[derive(Debug, Clone, Copy)]
79pub struct Gutter {
80 pub width: u16,
81 pub style: Style,
82}
83
84#[derive(Debug, Clone, Copy)]
89pub struct Sign {
90 pub row: usize,
91 pub ch: char,
92 pub style: Style,
93 pub priority: u8,
94}
95
96#[derive(Debug, Clone)]
101pub struct Conceal {
102 pub row: usize,
103 pub start_byte: usize,
104 pub end_byte: usize,
105 pub replacement: String,
106}
107
108impl<R: StyleResolver> Widget for BufferView<'_, R> {
109 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
110 let viewport = self.buffer.viewport();
111 let cursor = self.buffer.cursor();
112 let lines = self.buffer.lines();
113 let spans = self.buffer.spans();
114 let folds = self.buffer.folds();
115 let top_row = viewport.top_row;
116 let top_col = viewport.top_col;
117
118 let gutter_width = self.gutter.map(|g| g.width).unwrap_or(0);
119 let text_area = Rect {
120 x: area.x.saturating_add(gutter_width),
121 y: area.y,
122 width: area.width.saturating_sub(gutter_width),
123 height: area.height,
124 };
125
126 let total_rows = lines.len();
127 let mut doc_row = top_row;
128 let mut screen_row: u16 = 0;
129 let wrap_mode = viewport.wrap;
130 let seg_width = if viewport.text_width > 0 {
131 viewport.text_width
132 } else {
133 text_area.width
134 };
135 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
141 while doc_row < total_rows && screen_row < area.height {
145 if folds.iter().any(|f| f.hides(doc_row)) {
148 doc_row += 1;
149 continue;
150 }
151 let folded_at_start = folds
152 .iter()
153 .find(|f| f.closed && f.start_row == doc_row)
154 .copied();
155 let line = &lines[doc_row];
156 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
157 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
158 let is_cursor_row = doc_row == cursor.row;
159 if let Some(fold) = folded_at_start {
160 if let Some(gutter) = self.gutter {
161 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
162 self.paint_signs(term_buf, area, screen_row, doc_row);
163 }
164 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
165 search_hit_at_cursor_col.push(false);
166 screen_row += 1;
167 doc_row = fold.end_row + 1;
168 continue;
169 }
170 let search_ranges = self.row_search_ranges(line);
171 let row_has_hit_at_cursor_col = search_ranges
172 .iter()
173 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
174 let row_conceals: Vec<&Conceal> = {
176 let mut v: Vec<&Conceal> =
177 self.conceals.iter().filter(|c| c.row == doc_row).collect();
178 v.sort_by_key(|c| c.start_byte);
179 v
180 };
181 let segments = match wrap_mode {
189 Wrap::None => vec![(top_col, usize::MAX)],
190 _ => wrap_segments(line, seg_width, wrap_mode),
191 };
192 let last_seg_idx = segments.len().saturating_sub(1);
193 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
194 if screen_row >= area.height {
195 break;
196 }
197 if let Some(gutter) = self.gutter {
198 if seg_idx == 0 {
199 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
200 self.paint_signs(term_buf, area, screen_row, doc_row);
201 } else {
202 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
203 }
204 }
205 self.paint_row(
206 term_buf,
207 text_area,
208 screen_row,
209 line,
210 row_spans,
211 sel_range,
212 &search_ranges,
213 is_cursor_row,
214 cursor.col,
215 seg_start,
216 seg_end,
217 seg_idx == last_seg_idx,
218 &row_conceals,
219 );
220 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
221 screen_row += 1;
222 }
223 doc_row += 1;
224 }
225 if matches!(wrap_mode, Wrap::None)
232 && self.cursor_column_bg != Style::default()
233 && cursor.col >= top_col
234 && (cursor.col - top_col) < text_area.width as usize
235 {
236 let x = text_area.x + (cursor.col - top_col) as u16;
237 for sy in 0..screen_row {
238 if search_hit_at_cursor_col
242 .get(sy as usize)
243 .copied()
244 .unwrap_or(false)
245 {
246 continue;
247 }
248 let y = text_area.y + sy;
249 if let Some(cell) = term_buf.cell_mut((x, y)) {
250 cell.set_style(cell.style().patch(self.cursor_column_bg));
251 }
252 }
253 }
254 }
255}
256
257impl<R: StyleResolver> BufferView<'_, R> {
258 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
262 let Some(re) = self.buffer.search_pattern() else {
263 return Vec::new();
264 };
265 re.find_iter(line)
266 .map(|m| {
267 let start = line[..m.start()].chars().count();
268 let end = line[..m.end()].chars().count();
269 (start, end)
270 })
271 .collect()
272 }
273
274 fn paint_fold_marker(
275 &self,
276 term_buf: &mut TermBuffer,
277 area: Rect,
278 screen_row: u16,
279 fold: crate::Fold,
280 first_line: &str,
281 is_cursor_row: bool,
282 ) {
283 let y = area.y + screen_row;
284 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
285 self.cursor_line_bg
286 } else {
287 Style::default()
288 };
289 for x in area.x..(area.x + area.width) {
291 if let Some(cell) = term_buf.cell_mut((x, y)) {
292 cell.set_style(style);
293 }
294 }
295 let prefix = first_line.trim();
299 let count = fold.line_count();
300 let label = if prefix.is_empty() {
301 format!("▸ {count} lines folded")
302 } else {
303 const MAX_PREFIX: usize = 60;
304 let trimmed = if prefix.chars().count() > MAX_PREFIX {
305 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
306 format!("{head}…")
307 } else {
308 prefix.to_string()
309 };
310 format!("▸ {trimmed} ({count} lines)")
311 };
312 let mut x = area.x;
313 let row_end_x = area.x + area.width;
314 for ch in label.chars() {
315 if x >= row_end_x {
316 break;
317 }
318 let width = ch.width().unwrap_or(1) as u16;
319 if x + width > row_end_x {
320 break;
321 }
322 if let Some(cell) = term_buf.cell_mut((x, y)) {
323 cell.set_char(ch);
324 cell.set_style(style);
325 }
326 x = x.saturating_add(width);
327 }
328 }
329
330 fn paint_signs(&self, term_buf: &mut TermBuffer, area: Rect, screen_row: u16, doc_row: usize) {
331 let Some(sign) = self
332 .signs
333 .iter()
334 .filter(|s| s.row == doc_row)
335 .max_by_key(|s| s.priority)
336 else {
337 return;
338 };
339 let y = area.y + screen_row;
340 let x = area.x;
341 if let Some(cell) = term_buf.cell_mut((x, y)) {
342 cell.set_char(sign.ch);
343 cell.set_style(sign.style);
344 }
345 }
346
347 fn paint_blank_gutter(
350 &self,
351 term_buf: &mut TermBuffer,
352 area: Rect,
353 screen_row: u16,
354 gutter: Gutter,
355 ) {
356 let y = area.y + screen_row;
357 for x in area.x..(area.x + gutter.width) {
358 if let Some(cell) = term_buf.cell_mut((x, y)) {
359 cell.set_char(' ');
360 cell.set_style(gutter.style);
361 }
362 }
363 }
364
365 fn paint_gutter(
366 &self,
367 term_buf: &mut TermBuffer,
368 area: Rect,
369 screen_row: u16,
370 doc_row: usize,
371 gutter: Gutter,
372 ) {
373 let y = area.y + screen_row;
374 let number_width = gutter.width.saturating_sub(1) as usize;
376 let label = format!("{:>width$}", doc_row + 1, width = number_width);
377 let mut x = area.x;
378 for ch in label.chars() {
379 if x >= area.x + gutter.width.saturating_sub(1) {
380 break;
381 }
382 if let Some(cell) = term_buf.cell_mut((x, y)) {
383 cell.set_char(ch);
384 cell.set_style(gutter.style);
385 }
386 x = x.saturating_add(1);
387 }
388 let spacer_x = area.x + gutter.width.saturating_sub(1);
391 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
392 cell.set_char(' ');
393 cell.set_style(gutter.style);
394 }
395 }
396
397 #[allow(clippy::too_many_arguments)]
398 fn paint_row(
399 &self,
400 term_buf: &mut TermBuffer,
401 area: Rect,
402 screen_row: u16,
403 line: &str,
404 row_spans: &[crate::Span],
405 sel_range: crate::RowSpan,
406 search_ranges: &[(usize, usize)],
407 is_cursor_row: bool,
408 cursor_col: usize,
409 seg_start: usize,
410 seg_end: usize,
411 is_last_segment: bool,
412 conceals: &[&Conceal],
413 ) {
414 let y = area.y + screen_row;
415 let mut screen_x = area.x;
416 let row_end_x = area.x + area.width;
417
418 if is_cursor_row && self.cursor_line_bg != Style::default() {
422 for x in area.x..row_end_x {
423 if let Some(cell) = term_buf.cell_mut((x, y)) {
424 cell.set_style(self.cursor_line_bg);
425 }
426 }
427 }
428
429 let mut byte_offset: usize = 0;
430 let mut chars_iter = line.chars().enumerate().peekable();
431 while let Some((col_idx, ch)) = chars_iter.next() {
432 let ch_byte_len = ch.len_utf8();
433 if col_idx >= seg_end {
434 break;
435 }
436 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
441 if col_idx >= seg_start {
442 let mut style = if is_cursor_row {
443 self.cursor_line_bg
444 } else {
445 Style::default()
446 };
447 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
448 style = style.patch(span_style);
449 }
450 for rch in conc.replacement.chars() {
451 let rwidth = rch.width().unwrap_or(1) as u16;
452 if screen_x + rwidth > row_end_x {
453 break;
454 }
455 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
456 cell.set_char(rch);
457 cell.set_style(style);
458 }
459 screen_x += rwidth;
460 }
461 }
462 let mut consumed = ch_byte_len;
465 byte_offset += ch_byte_len;
466 while byte_offset < conc.end_byte {
467 let Some((_, next_ch)) = chars_iter.next() else {
468 break;
469 };
470 consumed += next_ch.len_utf8();
471 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
472 }
473 let _ = consumed;
474 continue;
475 }
476 if col_idx < seg_start {
479 byte_offset += ch_byte_len;
480 continue;
481 }
482 let width = ch.width().unwrap_or(1) as u16;
484 if screen_x + width > row_end_x {
485 break;
486 }
487
488 let mut style = if is_cursor_row {
490 self.cursor_line_bg
491 } else {
492 Style::default()
493 };
494 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
495 style = style.patch(span_style);
496 }
497 if let Some((lo, hi)) = sel_range
498 && col_idx >= lo
499 && col_idx <= hi
500 {
501 style = style.patch(self.selection_bg);
502 }
503 if self.search_bg != Style::default()
504 && search_ranges
505 .iter()
506 .any(|&(s, e)| col_idx >= s && col_idx < e)
507 {
508 style = style.patch(self.search_bg);
509 }
510 if is_cursor_row && col_idx == cursor_col {
511 style = style.patch(self.cursor_style);
512 }
513
514 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
515 cell.set_char(ch);
516 cell.set_style(style);
517 }
518 screen_x += width;
519 byte_offset += ch_byte_len;
520 }
521
522 if is_cursor_row
527 && is_last_segment
528 && cursor_col >= line.chars().count()
529 && cursor_col >= seg_start
530 {
531 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
532 if pad_x < row_end_x
533 && let Some(cell) = term_buf.cell_mut((pad_x, y))
534 {
535 cell.set_char(' ');
536 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
537 }
538 }
539 }
540
541 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
544 for span in row_spans {
545 if byte_offset >= span.start_byte && byte_offset < span.end_byte {
546 return Some(self.resolver.resolve(span.style));
547 }
548 }
549 None
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use ratatui::style::{Color, Modifier};
557 use ratatui::widgets::Widget;
558
559 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
560 let area = Rect::new(0, 0, w, h);
561 let mut buf = TermBuffer::empty(area);
562 view.render(area, &mut buf);
563 buf
564 }
565
566 fn no_styles(_id: u32) -> Style {
567 Style::default()
568 }
569
570 #[test]
571 fn renders_plain_chars_into_terminal_buffer() {
572 let mut b = Buffer::from_str("hello\nworld");
573 b.viewport_mut().width = 20;
574 b.viewport_mut().height = 5;
575 let view = BufferView {
576 buffer: &b,
577 selection: None,
578 resolver: &(no_styles as fn(u32) -> Style),
579 cursor_line_bg: Style::default(),
580 cursor_column_bg: Style::default(),
581 selection_bg: Style::default().bg(Color::Blue),
582 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
583 gutter: None,
584 search_bg: Style::default(),
585 signs: &[],
586 conceals: &[],
587 };
588 let term = run_render(view, 20, 5);
589 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
590 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
591 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
592 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
593 }
594
595 #[test]
596 fn cursor_cell_gets_reversed_style() {
597 let mut b = Buffer::from_str("abc");
598 b.viewport_mut().width = 10;
599 b.viewport_mut().height = 1;
600 b.set_cursor(crate::Position::new(0, 1));
601 let view = BufferView {
602 buffer: &b,
603 selection: None,
604 resolver: &(no_styles as fn(u32) -> Style),
605 cursor_line_bg: Style::default(),
606 cursor_column_bg: Style::default(),
607 selection_bg: Style::default().bg(Color::Blue),
608 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
609 gutter: None,
610 search_bg: Style::default(),
611 signs: &[],
612 conceals: &[],
613 };
614 let term = run_render(view, 10, 1);
615 let cursor_cell = term.cell((1, 0)).unwrap();
616 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
617 }
618
619 #[test]
620 fn selection_bg_applies_only_to_selected_cells() {
621 use crate::{Position, Selection};
622 let mut b = Buffer::from_str("abcdef");
623 b.viewport_mut().width = 10;
624 b.viewport_mut().height = 1;
625 let view = BufferView {
626 buffer: &b,
627 selection: Some(Selection::Char {
628 anchor: Position::new(0, 1),
629 head: Position::new(0, 3),
630 }),
631 resolver: &(no_styles as fn(u32) -> Style),
632 cursor_line_bg: Style::default(),
633 cursor_column_bg: Style::default(),
634 selection_bg: Style::default().bg(Color::Blue),
635 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
636 gutter: None,
637 search_bg: Style::default(),
638 signs: &[],
639 conceals: &[],
640 };
641 let term = run_render(view, 10, 1);
642 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
643 for x in 1..=3 {
644 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
645 }
646 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
647 }
648
649 #[test]
650 fn syntax_span_fg_resolves_via_table() {
651 use crate::Span;
652 let mut b = Buffer::from_str("SELECT foo");
653 b.viewport_mut().width = 20;
654 b.viewport_mut().height = 1;
655 b.set_spans_for_test(vec![vec![Span::new(0, 6, 7)]]);
656 let resolver = |id: u32| -> Style {
657 if id == 7 {
658 Style::default().fg(Color::Red)
659 } else {
660 Style::default()
661 }
662 };
663 let view = BufferView {
664 buffer: &b,
665 selection: None,
666 resolver: &resolver,
667 cursor_line_bg: Style::default(),
668 cursor_column_bg: Style::default(),
669 selection_bg: Style::default().bg(Color::Blue),
670 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
671 gutter: None,
672 search_bg: Style::default(),
673 signs: &[],
674 conceals: &[],
675 };
676 let term = run_render(view, 20, 1);
677 for x in 0..6 {
678 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
679 }
680 }
681
682 #[test]
683 fn gutter_renders_right_aligned_line_numbers() {
684 let mut b = Buffer::from_str("a\nb\nc");
685 b.viewport_mut().width = 10;
686 b.viewport_mut().height = 3;
687 let view = BufferView {
688 buffer: &b,
689 selection: None,
690 resolver: &(no_styles as fn(u32) -> Style),
691 cursor_line_bg: Style::default(),
692 cursor_column_bg: Style::default(),
693 selection_bg: Style::default().bg(Color::Blue),
694 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
695 gutter: Some(Gutter {
696 width: 4,
697 style: Style::default().fg(Color::Yellow),
698 }),
699 search_bg: Style::default(),
700 signs: &[],
701 conceals: &[],
702 };
703 let term = run_render(view, 10, 3);
704 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
706 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
707 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
708 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
709 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
711 }
712
713 #[test]
714 fn search_bg_paints_match_cells() {
715 use regex::Regex;
716 let mut b = Buffer::from_str("foo bar foo");
717 b.viewport_mut().width = 20;
718 b.viewport_mut().height = 1;
719 b.set_search_pattern(Some(Regex::new("foo").unwrap()));
720 let view = BufferView {
721 buffer: &b,
722 selection: None,
723 resolver: &(no_styles as fn(u32) -> Style),
724 cursor_line_bg: Style::default(),
725 cursor_column_bg: Style::default(),
726 selection_bg: Style::default().bg(Color::Blue),
727 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
728 gutter: None,
729 search_bg: Style::default().bg(Color::Magenta),
730 signs: &[],
731 conceals: &[],
732 };
733 let term = run_render(view, 20, 1);
734 for x in 0..3 {
735 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
736 }
737 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
739 for x in 8..11 {
740 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
741 }
742 }
743
744 #[test]
745 fn search_bg_survives_cursorcolumn_overlay() {
746 use regex::Regex;
747 let mut b = Buffer::from_str("foo bar foo");
751 b.viewport_mut().width = 20;
752 b.viewport_mut().height = 1;
753 b.set_search_pattern(Some(Regex::new("foo").unwrap()));
754 b.set_cursor(crate::Position::new(0, 1));
756 let view = BufferView {
757 buffer: &b,
758 selection: None,
759 resolver: &(no_styles as fn(u32) -> Style),
760 cursor_line_bg: Style::default(),
761 cursor_column_bg: Style::default().bg(Color::DarkGray),
762 selection_bg: Style::default().bg(Color::Blue),
763 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
764 gutter: None,
765 search_bg: Style::default().bg(Color::Magenta),
766 signs: &[],
767 conceals: &[],
768 };
769 let term = run_render(view, 20, 1);
770 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
772 }
773
774 #[test]
775 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
776 let mut b = Buffer::from_str("a\nb\nc");
777 b.viewport_mut().width = 10;
778 b.viewport_mut().height = 3;
779 let signs = [
780 Sign {
781 row: 0,
782 ch: 'W',
783 style: Style::default().fg(Color::Yellow),
784 priority: 1,
785 },
786 Sign {
787 row: 0,
788 ch: 'E',
789 style: Style::default().fg(Color::Red),
790 priority: 2,
791 },
792 ];
793 let view = BufferView {
794 buffer: &b,
795 selection: None,
796 resolver: &(no_styles as fn(u32) -> Style),
797 cursor_line_bg: Style::default(),
798 cursor_column_bg: Style::default(),
799 selection_bg: Style::default().bg(Color::Blue),
800 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
801 gutter: Some(Gutter {
802 width: 3,
803 style: Style::default().fg(Color::DarkGray),
804 }),
805 search_bg: Style::default(),
806 signs: &signs,
807 conceals: &[],
808 };
809 let term = run_render(view, 10, 3);
810 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
811 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
812 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
814 }
815
816 #[test]
817 fn conceal_replaces_byte_range() {
818 let mut b = Buffer::from_str("see https://example.com end");
819 b.viewport_mut().width = 30;
820 b.viewport_mut().height = 1;
821 let conceals = vec![Conceal {
822 row: 0,
823 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
826 }];
827 let view = BufferView {
828 buffer: &b,
829 selection: None,
830 resolver: &(no_styles as fn(u32) -> Style),
831 cursor_line_bg: Style::default(),
832 cursor_column_bg: Style::default(),
833 selection_bg: Style::default(),
834 cursor_style: Style::default(),
835 gutter: None,
836 search_bg: Style::default(),
837 signs: &[],
838 conceals: &conceals,
839 };
840 let term = run_render(view, 30, 1);
841 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
843 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
844 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
847 }
848
849 #[test]
850 fn closed_fold_collapses_rows_and_paints_marker() {
851 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
852 b.viewport_mut().width = 30;
853 b.viewport_mut().height = 5;
854 b.add_fold(1, 3, true);
856 let view = BufferView {
857 buffer: &b,
858 selection: None,
859 resolver: &(no_styles as fn(u32) -> Style),
860 cursor_line_bg: Style::default(),
861 cursor_column_bg: Style::default(),
862 selection_bg: Style::default().bg(Color::Blue),
863 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
864 gutter: None,
865 search_bg: Style::default(),
866 signs: &[],
867 conceals: &[],
868 };
869 let term = run_render(view, 30, 5);
870 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
872 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
875 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
877 }
878
879 #[test]
880 fn open_fold_renders_normally() {
881 let mut b = Buffer::from_str("a\nb\nc");
882 b.viewport_mut().width = 5;
883 b.viewport_mut().height = 3;
884 b.add_fold(0, 2, false); let view = BufferView {
886 buffer: &b,
887 selection: None,
888 resolver: &(no_styles as fn(u32) -> Style),
889 cursor_line_bg: Style::default(),
890 cursor_column_bg: Style::default(),
891 selection_bg: Style::default().bg(Color::Blue),
892 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
893 gutter: None,
894 search_bg: Style::default(),
895 signs: &[],
896 conceals: &[],
897 };
898 let term = run_render(view, 5, 3);
899 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
900 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
901 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
902 }
903
904 #[test]
905 fn horizontal_scroll_clips_left_chars() {
906 let mut b = Buffer::from_str("abcdefgh");
907 b.viewport_mut().width = 4;
908 b.viewport_mut().height = 1;
909 b.viewport_mut().top_col = 3;
910 let view = BufferView {
911 buffer: &b,
912 selection: None,
913 resolver: &(no_styles as fn(u32) -> Style),
914 cursor_line_bg: Style::default(),
915 cursor_column_bg: Style::default(),
916 selection_bg: Style::default().bg(Color::Blue),
917 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
918 gutter: None,
919 search_bg: Style::default(),
920 signs: &[],
921 conceals: &[],
922 };
923 let term = run_render(view, 4, 1);
924 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
925 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
926 }
927
928 fn make_wrap_view<'a>(
929 b: &'a Buffer,
930 resolver: &'a (impl StyleResolver + 'a),
931 gutter: Option<Gutter>,
932 ) -> BufferView<'a, impl StyleResolver + 'a> {
933 BufferView {
934 buffer: b,
935 selection: None,
936 resolver,
937 cursor_line_bg: Style::default(),
938 cursor_column_bg: Style::default(),
939 selection_bg: Style::default().bg(Color::Blue),
940 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
941 gutter,
942 search_bg: Style::default(),
943 signs: &[],
944 conceals: &[],
945 }
946 }
947
948 #[test]
949 fn wrap_segments_char_breaks_at_width() {
950 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
951 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
952 }
953
954 #[test]
955 fn wrap_segments_word_backs_up_to_whitespace() {
956 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
957 assert_eq!(segs[0], (0, 6));
959 assert_eq!(segs[1], (6, 11));
961 assert_eq!(segs[2], (11, 16));
962 }
963
964 #[test]
965 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
966 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
967 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
969 }
970
971 #[test]
972 fn wrap_char_paints_continuation_rows() {
973 let mut b = Buffer::from_str("abcdefghij");
974 {
975 let v = b.viewport_mut();
976 v.width = 4;
977 v.height = 3;
978 v.wrap = Wrap::Char;
979 v.text_width = 4;
980 }
981 let r = no_styles as fn(u32) -> Style;
982 let view = make_wrap_view(&b, &r, None);
983 let term = run_render(view, 4, 3);
984 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
986 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
987 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
989 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
990 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
992 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
993 }
994
995 #[test]
996 fn wrap_char_gutter_blank_on_continuation() {
997 let mut b = Buffer::from_str("abcdefgh");
998 {
999 let v = b.viewport_mut();
1000 v.width = 6;
1001 v.height = 3;
1002 v.wrap = Wrap::Char;
1003 v.text_width = 3;
1005 }
1006 let r = no_styles as fn(u32) -> Style;
1007 let gutter = Gutter {
1008 width: 3,
1009 style: Style::default().fg(Color::Yellow),
1010 };
1011 let view = make_wrap_view(&b, &r, Some(gutter));
1012 let term = run_render(view, 6, 3);
1013 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1015 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1016 for x in 0..2 {
1018 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1019 }
1020 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1021 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1022 }
1023
1024 #[test]
1025 fn wrap_char_cursor_lands_on_correct_segment() {
1026 let mut b = Buffer::from_str("abcdefghij");
1027 {
1028 let v = b.viewport_mut();
1029 v.width = 4;
1030 v.height = 3;
1031 v.wrap = Wrap::Char;
1032 v.text_width = 4;
1033 }
1034 b.set_cursor(crate::Position::new(0, 6));
1036 let r = no_styles as fn(u32) -> Style;
1037 let mut view = make_wrap_view(&b, &r, None);
1038 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1039 let term = run_render(view, 4, 3);
1040 assert!(
1041 term.cell((2, 1))
1042 .unwrap()
1043 .modifier
1044 .contains(Modifier::REVERSED)
1045 );
1046 }
1047
1048 #[test]
1049 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1050 let mut b = Buffer::from_str("abcdef");
1051 {
1052 let v = b.viewport_mut();
1053 v.width = 4;
1054 v.height = 3;
1055 v.wrap = Wrap::Char;
1056 v.text_width = 4;
1057 }
1058 b.set_cursor(crate::Position::new(0, 6));
1060 let r = no_styles as fn(u32) -> Style;
1061 let mut view = make_wrap_view(&b, &r, None);
1062 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1063 let term = run_render(view, 4, 3);
1064 assert!(
1066 term.cell((2, 1))
1067 .unwrap()
1068 .modifier
1069 .contains(Modifier::REVERSED)
1070 );
1071 }
1072
1073 #[test]
1074 fn wrap_word_breaks_at_whitespace() {
1075 let mut b = Buffer::from_str("alpha beta gamma");
1076 {
1077 let v = b.viewport_mut();
1078 v.width = 8;
1079 v.height = 3;
1080 v.wrap = Wrap::Word;
1081 v.text_width = 8;
1082 }
1083 let r = no_styles as fn(u32) -> Style;
1084 let view = make_wrap_view(&b, &r, None);
1085 let term = run_render(view, 8, 3);
1086 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1088 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1089 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1091 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1092 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1094 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1095 }
1096}