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, Span, Viewport, 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> {
52 pub buffer: &'a Buffer,
53 pub viewport: &'a Viewport,
57 pub selection: Option<Selection>,
58 pub resolver: &'a R,
59 pub cursor_line_bg: Style,
62 pub cursor_column_bg: Style,
65 pub selection_bg: Style,
67 pub cursor_style: Style,
70 pub gutter: Option<Gutter>,
74 pub search_bg: Style,
77 pub signs: &'a [Sign],
82 pub conceals: &'a [Conceal],
86 pub spans: &'a [Vec<Span>],
95 pub search_pattern: Option<&'a regex::Regex>,
103}
104
105#[derive(Debug, Clone, Copy)]
110pub struct Gutter {
111 pub width: u16,
112 pub style: Style,
113}
114
115#[derive(Debug, Clone, Copy)]
120pub struct Sign {
121 pub row: usize,
122 pub ch: char,
123 pub style: Style,
124 pub priority: u8,
125}
126
127#[derive(Debug, Clone)]
132pub struct Conceal {
133 pub row: usize,
134 pub start_byte: usize,
135 pub end_byte: usize,
136 pub replacement: String,
137}
138
139impl<R: StyleResolver> Widget for BufferView<'_, R> {
140 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
141 let viewport = *self.viewport;
142 let cursor = self.buffer.cursor();
143 let lines = self.buffer.lines();
144 let spans = self.spans;
145 let folds = self.buffer.folds();
146 let top_row = viewport.top_row;
147 let top_col = viewport.top_col;
148
149 let gutter_width = self.gutter.map(|g| g.width).unwrap_or(0);
150 let text_area = Rect {
151 x: area.x.saturating_add(gutter_width),
152 y: area.y,
153 width: area.width.saturating_sub(gutter_width),
154 height: area.height,
155 };
156
157 let total_rows = lines.len();
158 let mut doc_row = top_row;
159 let mut screen_row: u16 = 0;
160 let wrap_mode = viewport.wrap;
161 let seg_width = if viewport.text_width > 0 {
162 viewport.text_width
163 } else {
164 text_area.width
165 };
166 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
172 while doc_row < total_rows && screen_row < area.height {
176 if folds.iter().any(|f| f.hides(doc_row)) {
179 doc_row += 1;
180 continue;
181 }
182 let folded_at_start = folds
183 .iter()
184 .find(|f| f.closed && f.start_row == doc_row)
185 .copied();
186 let line = &lines[doc_row];
187 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
188 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
189 let is_cursor_row = doc_row == cursor.row;
190 if let Some(fold) = folded_at_start {
191 if let Some(gutter) = self.gutter {
192 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
193 self.paint_signs(term_buf, area, screen_row, doc_row);
194 }
195 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
196 search_hit_at_cursor_col.push(false);
197 screen_row += 1;
198 doc_row = fold.end_row + 1;
199 continue;
200 }
201 let search_ranges = self.row_search_ranges(line);
202 let row_has_hit_at_cursor_col = search_ranges
203 .iter()
204 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
205 let row_conceals: Vec<&Conceal> = {
207 let mut v: Vec<&Conceal> =
208 self.conceals.iter().filter(|c| c.row == doc_row).collect();
209 v.sort_by_key(|c| c.start_byte);
210 v
211 };
212 let segments = match wrap_mode {
220 Wrap::None => vec![(top_col, usize::MAX)],
221 _ => wrap_segments(line, seg_width, wrap_mode),
222 };
223 let last_seg_idx = segments.len().saturating_sub(1);
224 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
225 if screen_row >= area.height {
226 break;
227 }
228 if let Some(gutter) = self.gutter {
229 if seg_idx == 0 {
230 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
231 self.paint_signs(term_buf, area, screen_row, doc_row);
232 } else {
233 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
234 }
235 }
236 self.paint_row(
237 term_buf,
238 text_area,
239 screen_row,
240 line,
241 row_spans,
242 sel_range,
243 &search_ranges,
244 is_cursor_row,
245 cursor.col,
246 seg_start,
247 seg_end,
248 seg_idx == last_seg_idx,
249 &row_conceals,
250 );
251 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
252 screen_row += 1;
253 }
254 doc_row += 1;
255 }
256 if matches!(wrap_mode, Wrap::None)
263 && self.cursor_column_bg != Style::default()
264 && cursor.col >= top_col
265 && (cursor.col - top_col) < text_area.width as usize
266 {
267 let x = text_area.x + (cursor.col - top_col) as u16;
268 for sy in 0..screen_row {
269 if search_hit_at_cursor_col
273 .get(sy as usize)
274 .copied()
275 .unwrap_or(false)
276 {
277 continue;
278 }
279 let y = text_area.y + sy;
280 if let Some(cell) = term_buf.cell_mut((x, y)) {
281 cell.set_style(cell.style().patch(self.cursor_column_bg));
282 }
283 }
284 }
285 }
286}
287
288impl<R: StyleResolver> BufferView<'_, R> {
289 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
293 let Some(re) = self.search_pattern else {
294 return Vec::new();
295 };
296 re.find_iter(line)
297 .map(|m| {
298 let start = line[..m.start()].chars().count();
299 let end = line[..m.end()].chars().count();
300 (start, end)
301 })
302 .collect()
303 }
304
305 fn paint_fold_marker(
306 &self,
307 term_buf: &mut TermBuffer,
308 area: Rect,
309 screen_row: u16,
310 fold: crate::Fold,
311 first_line: &str,
312 is_cursor_row: bool,
313 ) {
314 let y = area.y + screen_row;
315 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
316 self.cursor_line_bg
317 } else {
318 Style::default()
319 };
320 for x in area.x..(area.x + area.width) {
322 if let Some(cell) = term_buf.cell_mut((x, y)) {
323 cell.set_style(style);
324 }
325 }
326 let prefix = first_line.trim();
330 let count = fold.line_count();
331 let label = if prefix.is_empty() {
332 format!("▸ {count} lines folded")
333 } else {
334 const MAX_PREFIX: usize = 60;
335 let trimmed = if prefix.chars().count() > MAX_PREFIX {
336 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
337 format!("{head}…")
338 } else {
339 prefix.to_string()
340 };
341 format!("▸ {trimmed} ({count} lines)")
342 };
343 let mut x = area.x;
344 let row_end_x = area.x + area.width;
345 for ch in label.chars() {
346 if x >= row_end_x {
347 break;
348 }
349 let width = ch.width().unwrap_or(1) as u16;
350 if x + width > row_end_x {
351 break;
352 }
353 if let Some(cell) = term_buf.cell_mut((x, y)) {
354 cell.set_char(ch);
355 cell.set_style(style);
356 }
357 x = x.saturating_add(width);
358 }
359 }
360
361 fn paint_signs(&self, term_buf: &mut TermBuffer, area: Rect, screen_row: u16, doc_row: usize) {
362 let Some(sign) = self
363 .signs
364 .iter()
365 .filter(|s| s.row == doc_row)
366 .max_by_key(|s| s.priority)
367 else {
368 return;
369 };
370 let y = area.y + screen_row;
371 let x = area.x;
372 if let Some(cell) = term_buf.cell_mut((x, y)) {
373 cell.set_char(sign.ch);
374 cell.set_style(sign.style);
375 }
376 }
377
378 fn paint_blank_gutter(
381 &self,
382 term_buf: &mut TermBuffer,
383 area: Rect,
384 screen_row: u16,
385 gutter: Gutter,
386 ) {
387 let y = area.y + screen_row;
388 for x in area.x..(area.x + gutter.width) {
389 if let Some(cell) = term_buf.cell_mut((x, y)) {
390 cell.set_char(' ');
391 cell.set_style(gutter.style);
392 }
393 }
394 }
395
396 fn paint_gutter(
397 &self,
398 term_buf: &mut TermBuffer,
399 area: Rect,
400 screen_row: u16,
401 doc_row: usize,
402 gutter: Gutter,
403 ) {
404 let y = area.y + screen_row;
405 let number_width = gutter.width.saturating_sub(1) as usize;
407 let label = format!("{:>width$}", doc_row + 1, width = number_width);
408 let mut x = area.x;
409 for ch in label.chars() {
410 if x >= area.x + gutter.width.saturating_sub(1) {
411 break;
412 }
413 if let Some(cell) = term_buf.cell_mut((x, y)) {
414 cell.set_char(ch);
415 cell.set_style(gutter.style);
416 }
417 x = x.saturating_add(1);
418 }
419 let spacer_x = area.x + gutter.width.saturating_sub(1);
422 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
423 cell.set_char(' ');
424 cell.set_style(gutter.style);
425 }
426 }
427
428 #[allow(clippy::too_many_arguments)]
429 fn paint_row(
430 &self,
431 term_buf: &mut TermBuffer,
432 area: Rect,
433 screen_row: u16,
434 line: &str,
435 row_spans: &[crate::Span],
436 sel_range: crate::RowSpan,
437 search_ranges: &[(usize, usize)],
438 is_cursor_row: bool,
439 cursor_col: usize,
440 seg_start: usize,
441 seg_end: usize,
442 is_last_segment: bool,
443 conceals: &[&Conceal],
444 ) {
445 let y = area.y + screen_row;
446 let mut screen_x = area.x;
447 let row_end_x = area.x + area.width;
448
449 if is_cursor_row && self.cursor_line_bg != Style::default() {
453 for x in area.x..row_end_x {
454 if let Some(cell) = term_buf.cell_mut((x, y)) {
455 cell.set_style(self.cursor_line_bg);
456 }
457 }
458 }
459
460 let mut byte_offset: usize = 0;
461 let mut chars_iter = line.chars().enumerate().peekable();
462 while let Some((col_idx, ch)) = chars_iter.next() {
463 let ch_byte_len = ch.len_utf8();
464 if col_idx >= seg_end {
465 break;
466 }
467 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
472 if col_idx >= seg_start {
473 let mut style = if is_cursor_row {
474 self.cursor_line_bg
475 } else {
476 Style::default()
477 };
478 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
479 style = style.patch(span_style);
480 }
481 for rch in conc.replacement.chars() {
482 let rwidth = rch.width().unwrap_or(1) as u16;
483 if screen_x + rwidth > row_end_x {
484 break;
485 }
486 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
487 cell.set_char(rch);
488 cell.set_style(style);
489 }
490 screen_x += rwidth;
491 }
492 }
493 let mut consumed = ch_byte_len;
496 byte_offset += ch_byte_len;
497 while byte_offset < conc.end_byte {
498 let Some((_, next_ch)) = chars_iter.next() else {
499 break;
500 };
501 consumed += next_ch.len_utf8();
502 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
503 }
504 let _ = consumed;
505 continue;
506 }
507 if col_idx < seg_start {
510 byte_offset += ch_byte_len;
511 continue;
512 }
513 let width = ch.width().unwrap_or(1) as u16;
515 if screen_x + width > row_end_x {
516 break;
517 }
518
519 let mut style = if is_cursor_row {
521 self.cursor_line_bg
522 } else {
523 Style::default()
524 };
525 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
526 style = style.patch(span_style);
527 }
528 if let Some((lo, hi)) = sel_range
529 && col_idx >= lo
530 && col_idx <= hi
531 {
532 style = style.patch(self.selection_bg);
533 }
534 if self.search_bg != Style::default()
535 && search_ranges
536 .iter()
537 .any(|&(s, e)| col_idx >= s && col_idx < e)
538 {
539 style = style.patch(self.search_bg);
540 }
541 if is_cursor_row && col_idx == cursor_col {
542 style = style.patch(self.cursor_style);
543 }
544
545 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
546 cell.set_char(ch);
547 cell.set_style(style);
548 }
549 screen_x += width;
550 byte_offset += ch_byte_len;
551 }
552
553 if is_cursor_row
558 && is_last_segment
559 && cursor_col >= line.chars().count()
560 && cursor_col >= seg_start
561 {
562 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
563 if pad_x < row_end_x
564 && let Some(cell) = term_buf.cell_mut((pad_x, y))
565 {
566 cell.set_char(' ');
567 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
568 }
569 }
570 }
571
572 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
575 for span in row_spans {
576 if byte_offset >= span.start_byte && byte_offset < span.end_byte {
577 return Some(self.resolver.resolve(span.style));
578 }
579 }
580 None
581 }
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587 use ratatui::style::{Color, Modifier};
588 use ratatui::widgets::Widget;
589
590 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
591 let area = Rect::new(0, 0, w, h);
592 let mut buf = TermBuffer::empty(area);
593 view.render(area, &mut buf);
594 buf
595 }
596
597 fn no_styles(_id: u32) -> Style {
598 Style::default()
599 }
600
601 fn vp(width: u16, height: u16) -> Viewport {
603 Viewport {
604 top_row: 0,
605 top_col: 0,
606 width,
607 height,
608 wrap: Wrap::None,
609 text_width: width,
610 }
611 }
612
613 #[test]
614 fn renders_plain_chars_into_terminal_buffer() {
615 let b = Buffer::from_str("hello\nworld");
616 let v = vp(20, 5);
617 let view = BufferView {
618 buffer: &b,
619 viewport: &v,
620 selection: None,
621 resolver: &(no_styles as fn(u32) -> Style),
622 cursor_line_bg: Style::default(),
623 cursor_column_bg: Style::default(),
624 selection_bg: Style::default().bg(Color::Blue),
625 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
626 gutter: None,
627 search_bg: Style::default(),
628 signs: &[],
629 conceals: &[],
630 spans: &[],
631 search_pattern: None,
632 };
633 let term = run_render(view, 20, 5);
634 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
635 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
636 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
637 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
638 }
639
640 #[test]
641 fn cursor_cell_gets_reversed_style() {
642 let mut b = Buffer::from_str("abc");
643 let v = vp(10, 1);
644 b.set_cursor(crate::Position::new(0, 1));
645 let view = BufferView {
646 buffer: &b,
647 viewport: &v,
648 selection: None,
649 resolver: &(no_styles as fn(u32) -> Style),
650 cursor_line_bg: Style::default(),
651 cursor_column_bg: Style::default(),
652 selection_bg: Style::default().bg(Color::Blue),
653 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
654 gutter: None,
655 search_bg: Style::default(),
656 signs: &[],
657 conceals: &[],
658 spans: &[],
659 search_pattern: None,
660 };
661 let term = run_render(view, 10, 1);
662 let cursor_cell = term.cell((1, 0)).unwrap();
663 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
664 }
665
666 #[test]
667 fn selection_bg_applies_only_to_selected_cells() {
668 use crate::{Position, Selection};
669 let b = Buffer::from_str("abcdef");
670 let v = vp(10, 1);
671 let view = BufferView {
672 buffer: &b,
673 viewport: &v,
674 selection: Some(Selection::Char {
675 anchor: Position::new(0, 1),
676 head: Position::new(0, 3),
677 }),
678 resolver: &(no_styles as fn(u32) -> Style),
679 cursor_line_bg: Style::default(),
680 cursor_column_bg: Style::default(),
681 selection_bg: Style::default().bg(Color::Blue),
682 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
683 gutter: None,
684 search_bg: Style::default(),
685 signs: &[],
686 conceals: &[],
687 spans: &[],
688 search_pattern: None,
689 };
690 let term = run_render(view, 10, 1);
691 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
692 for x in 1..=3 {
693 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
694 }
695 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
696 }
697
698 #[test]
699 fn syntax_span_fg_resolves_via_table() {
700 use crate::Span;
701 let b = Buffer::from_str("SELECT foo");
702 let v = vp(20, 1);
703 let spans = vec![vec![Span::new(0, 6, 7)]];
704 let resolver = |id: u32| -> Style {
705 if id == 7 {
706 Style::default().fg(Color::Red)
707 } else {
708 Style::default()
709 }
710 };
711 let view = BufferView {
712 buffer: &b,
713 viewport: &v,
714 selection: None,
715 resolver: &resolver,
716 cursor_line_bg: Style::default(),
717 cursor_column_bg: Style::default(),
718 selection_bg: Style::default().bg(Color::Blue),
719 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
720 gutter: None,
721 search_bg: Style::default(),
722 signs: &[],
723 conceals: &[],
724 spans: &spans,
725 search_pattern: None,
726 };
727 let term = run_render(view, 20, 1);
728 for x in 0..6 {
729 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
730 }
731 }
732
733 #[test]
734 fn gutter_renders_right_aligned_line_numbers() {
735 let b = Buffer::from_str("a\nb\nc");
736 let v = vp(10, 3);
737 let view = BufferView {
738 buffer: &b,
739 viewport: &v,
740 selection: None,
741 resolver: &(no_styles as fn(u32) -> Style),
742 cursor_line_bg: Style::default(),
743 cursor_column_bg: Style::default(),
744 selection_bg: Style::default().bg(Color::Blue),
745 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
746 gutter: Some(Gutter {
747 width: 4,
748 style: Style::default().fg(Color::Yellow),
749 }),
750 search_bg: Style::default(),
751 signs: &[],
752 conceals: &[],
753 spans: &[],
754 search_pattern: None,
755 };
756 let term = run_render(view, 10, 3);
757 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
759 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
760 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
761 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
762 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
764 }
765
766 #[test]
767 fn search_bg_paints_match_cells() {
768 use regex::Regex;
769 let b = Buffer::from_str("foo bar foo");
770 let v = vp(20, 1);
771 let pat = Regex::new("foo").unwrap();
772 let view = BufferView {
773 buffer: &b,
774 viewport: &v,
775 selection: None,
776 resolver: &(no_styles as fn(u32) -> Style),
777 cursor_line_bg: Style::default(),
778 cursor_column_bg: Style::default(),
779 selection_bg: Style::default().bg(Color::Blue),
780 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
781 gutter: None,
782 search_bg: Style::default().bg(Color::Magenta),
783 signs: &[],
784 conceals: &[],
785 spans: &[],
786 search_pattern: Some(&pat),
787 };
788 let term = run_render(view, 20, 1);
789 for x in 0..3 {
790 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
791 }
792 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
794 for x in 8..11 {
795 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
796 }
797 }
798
799 #[test]
800 fn search_bg_survives_cursorcolumn_overlay() {
801 use regex::Regex;
802 let mut b = Buffer::from_str("foo bar foo");
806 let v = vp(20, 1);
807 let pat = Regex::new("foo").unwrap();
808 b.set_cursor(crate::Position::new(0, 1));
810 let view = BufferView {
811 buffer: &b,
812 viewport: &v,
813 selection: None,
814 resolver: &(no_styles as fn(u32) -> Style),
815 cursor_line_bg: Style::default(),
816 cursor_column_bg: Style::default().bg(Color::DarkGray),
817 selection_bg: Style::default().bg(Color::Blue),
818 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
819 gutter: None,
820 search_bg: Style::default().bg(Color::Magenta),
821 signs: &[],
822 conceals: &[],
823 spans: &[],
824 search_pattern: Some(&pat),
825 };
826 let term = run_render(view, 20, 1);
827 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
829 }
830
831 #[test]
832 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
833 let b = Buffer::from_str("a\nb\nc");
834 let v = vp(10, 3);
835 let signs = [
836 Sign {
837 row: 0,
838 ch: 'W',
839 style: Style::default().fg(Color::Yellow),
840 priority: 1,
841 },
842 Sign {
843 row: 0,
844 ch: 'E',
845 style: Style::default().fg(Color::Red),
846 priority: 2,
847 },
848 ];
849 let view = BufferView {
850 buffer: &b,
851 viewport: &v,
852 selection: None,
853 resolver: &(no_styles as fn(u32) -> Style),
854 cursor_line_bg: Style::default(),
855 cursor_column_bg: Style::default(),
856 selection_bg: Style::default().bg(Color::Blue),
857 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
858 gutter: Some(Gutter {
859 width: 3,
860 style: Style::default().fg(Color::DarkGray),
861 }),
862 search_bg: Style::default(),
863 signs: &signs,
864 conceals: &[],
865 spans: &[],
866 search_pattern: None,
867 };
868 let term = run_render(view, 10, 3);
869 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
870 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
871 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
873 }
874
875 #[test]
876 fn conceal_replaces_byte_range() {
877 let b = Buffer::from_str("see https://example.com end");
878 let v = vp(30, 1);
879 let conceals = vec![Conceal {
880 row: 0,
881 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
884 }];
885 let view = BufferView {
886 buffer: &b,
887 viewport: &v,
888 selection: None,
889 resolver: &(no_styles as fn(u32) -> Style),
890 cursor_line_bg: Style::default(),
891 cursor_column_bg: Style::default(),
892 selection_bg: Style::default(),
893 cursor_style: Style::default(),
894 gutter: None,
895 search_bg: Style::default(),
896 signs: &[],
897 conceals: &conceals,
898 spans: &[],
899 search_pattern: None,
900 };
901 let term = run_render(view, 30, 1);
902 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
904 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
905 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
908 }
909
910 #[test]
911 fn closed_fold_collapses_rows_and_paints_marker() {
912 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
913 let v = vp(30, 5);
914 b.add_fold(1, 3, true);
916 let view = BufferView {
917 buffer: &b,
918 viewport: &v,
919 selection: None,
920 resolver: &(no_styles as fn(u32) -> Style),
921 cursor_line_bg: Style::default(),
922 cursor_column_bg: Style::default(),
923 selection_bg: Style::default().bg(Color::Blue),
924 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
925 gutter: None,
926 search_bg: Style::default(),
927 signs: &[],
928 conceals: &[],
929 spans: &[],
930 search_pattern: None,
931 };
932 let term = run_render(view, 30, 5);
933 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
935 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
938 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
940 }
941
942 #[test]
943 fn open_fold_renders_normally() {
944 let mut b = Buffer::from_str("a\nb\nc");
945 let v = vp(5, 3);
946 b.add_fold(0, 2, false); let view = BufferView {
948 buffer: &b,
949 viewport: &v,
950 selection: None,
951 resolver: &(no_styles as fn(u32) -> Style),
952 cursor_line_bg: Style::default(),
953 cursor_column_bg: Style::default(),
954 selection_bg: Style::default().bg(Color::Blue),
955 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
956 gutter: None,
957 search_bg: Style::default(),
958 signs: &[],
959 conceals: &[],
960 spans: &[],
961 search_pattern: None,
962 };
963 let term = run_render(view, 5, 3);
964 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
965 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
966 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
967 }
968
969 #[test]
970 fn horizontal_scroll_clips_left_chars() {
971 let b = Buffer::from_str("abcdefgh");
972 let mut v = vp(4, 1);
973 v.top_col = 3;
974 let view = BufferView {
975 buffer: &b,
976 viewport: &v,
977 selection: None,
978 resolver: &(no_styles as fn(u32) -> Style),
979 cursor_line_bg: Style::default(),
980 cursor_column_bg: Style::default(),
981 selection_bg: Style::default().bg(Color::Blue),
982 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
983 gutter: None,
984 search_bg: Style::default(),
985 signs: &[],
986 conceals: &[],
987 spans: &[],
988 search_pattern: None,
989 };
990 let term = run_render(view, 4, 1);
991 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
992 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
993 }
994
995 fn make_wrap_view<'a>(
996 b: &'a Buffer,
997 viewport: &'a Viewport,
998 resolver: &'a (impl StyleResolver + 'a),
999 gutter: Option<Gutter>,
1000 ) -> BufferView<'a, impl StyleResolver + 'a> {
1001 BufferView {
1002 buffer: b,
1003 viewport,
1004 selection: None,
1005 resolver,
1006 cursor_line_bg: Style::default(),
1007 cursor_column_bg: Style::default(),
1008 selection_bg: Style::default().bg(Color::Blue),
1009 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1010 gutter,
1011 search_bg: Style::default(),
1012 signs: &[],
1013 conceals: &[],
1014 spans: &[],
1015 search_pattern: None,
1016 }
1017 }
1018
1019 #[test]
1020 fn wrap_segments_char_breaks_at_width() {
1021 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1022 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1023 }
1024
1025 #[test]
1026 fn wrap_segments_word_backs_up_to_whitespace() {
1027 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1028 assert_eq!(segs[0], (0, 6));
1030 assert_eq!(segs[1], (6, 11));
1032 assert_eq!(segs[2], (11, 16));
1033 }
1034
1035 #[test]
1036 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1037 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1038 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1040 }
1041
1042 #[test]
1043 fn wrap_char_paints_continuation_rows() {
1044 let b = Buffer::from_str("abcdefghij");
1045 let v = Viewport {
1046 top_row: 0,
1047 top_col: 0,
1048 width: 4,
1049 height: 3,
1050 wrap: Wrap::Char,
1051 text_width: 4,
1052 };
1053 let r = no_styles as fn(u32) -> Style;
1054 let view = make_wrap_view(&b, &v, &r, None);
1055 let term = run_render(view, 4, 3);
1056 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1058 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1059 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1061 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1062 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1064 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1065 }
1066
1067 #[test]
1068 fn wrap_char_gutter_blank_on_continuation() {
1069 let b = Buffer::from_str("abcdefgh");
1070 let v = Viewport {
1071 top_row: 0,
1072 top_col: 0,
1073 width: 6,
1074 height: 3,
1075 wrap: Wrap::Char,
1076 text_width: 3,
1078 };
1079 let r = no_styles as fn(u32) -> Style;
1080 let gutter = Gutter {
1081 width: 3,
1082 style: Style::default().fg(Color::Yellow),
1083 };
1084 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1085 let term = run_render(view, 6, 3);
1086 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1088 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1089 for x in 0..2 {
1091 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1092 }
1093 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1094 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1095 }
1096
1097 #[test]
1098 fn wrap_char_cursor_lands_on_correct_segment() {
1099 let mut b = Buffer::from_str("abcdefghij");
1100 let v = Viewport {
1101 top_row: 0,
1102 top_col: 0,
1103 width: 4,
1104 height: 3,
1105 wrap: Wrap::Char,
1106 text_width: 4,
1107 };
1108 b.set_cursor(crate::Position::new(0, 6));
1110 let r = no_styles as fn(u32) -> Style;
1111 let mut view = make_wrap_view(&b, &v, &r, None);
1112 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1113 let term = run_render(view, 4, 3);
1114 assert!(
1115 term.cell((2, 1))
1116 .unwrap()
1117 .modifier
1118 .contains(Modifier::REVERSED)
1119 );
1120 }
1121
1122 #[test]
1123 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1124 let mut b = Buffer::from_str("abcdef");
1125 let v = Viewport {
1126 top_row: 0,
1127 top_col: 0,
1128 width: 4,
1129 height: 3,
1130 wrap: Wrap::Char,
1131 text_width: 4,
1132 };
1133 b.set_cursor(crate::Position::new(0, 6));
1135 let r = no_styles as fn(u32) -> Style;
1136 let mut view = make_wrap_view(&b, &v, &r, None);
1137 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1138 let term = run_render(view, 4, 3);
1139 assert!(
1141 term.cell((2, 1))
1142 .unwrap()
1143 .modifier
1144 .contains(Modifier::REVERSED)
1145 );
1146 }
1147
1148 #[test]
1149 fn wrap_word_breaks_at_whitespace() {
1150 let b = Buffer::from_str("alpha beta gamma");
1151 let v = Viewport {
1152 top_row: 0,
1153 top_col: 0,
1154 width: 8,
1155 height: 3,
1156 wrap: Wrap::Word,
1157 text_width: 8,
1158 };
1159 let r = no_styles as fn(u32) -> Style;
1160 let view = make_wrap_view(&b, &v, &r, None);
1161 let term = run_render(view, 8, 3);
1162 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1164 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1165 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1167 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1168 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1170 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1171 }
1172
1173 fn view_with<'a>(
1179 b: &'a Buffer,
1180 viewport: &'a Viewport,
1181 resolver: &'a (impl StyleResolver + 'a),
1182 spans: &'a [Vec<Span>],
1183 search_pattern: Option<&'a regex::Regex>,
1184 ) -> BufferView<'a, impl StyleResolver + 'a> {
1185 BufferView {
1186 buffer: b,
1187 viewport,
1188 selection: None,
1189 resolver,
1190 cursor_line_bg: Style::default(),
1191 cursor_column_bg: Style::default(),
1192 selection_bg: Style::default().bg(Color::Blue),
1193 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1194 gutter: None,
1195 search_bg: Style::default().bg(Color::Magenta),
1196 signs: &[],
1197 conceals: &[],
1198 spans,
1199 search_pattern,
1200 }
1201 }
1202
1203 #[test]
1204 fn empty_spans_param_renders_default_style() {
1205 let b = Buffer::from_str("hello");
1206 let v = vp(10, 1);
1207 let r = no_styles as fn(u32) -> Style;
1208 let view = view_with(&b, &v, &r, &[], None);
1209 let term = run_render(view, 10, 1);
1210 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1211 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
1212 }
1213
1214 #[test]
1215 fn spans_param_paints_styled_byte_range() {
1216 let b = Buffer::from_str("abcdef");
1217 let v = vp(10, 1);
1218 let resolver = |id: u32| -> Style {
1219 if id == 3 {
1220 Style::default().fg(Color::Green)
1221 } else {
1222 Style::default()
1223 }
1224 };
1225 let spans = vec![vec![Span::new(0, 3, 3)]];
1226 let view = view_with(&b, &v, &resolver, &spans, None);
1227 let term = run_render(view, 10, 1);
1228 for x in 0..3 {
1229 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
1230 }
1231 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
1232 }
1233
1234 #[test]
1235 fn spans_param_handles_per_row_overlay() {
1236 let b = Buffer::from_str("abc\ndef");
1237 let v = vp(10, 2);
1238 let resolver = |id: u32| -> Style {
1239 if id == 1 {
1240 Style::default().fg(Color::Red)
1241 } else {
1242 Style::default().fg(Color::Green)
1243 }
1244 };
1245 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
1246 let view = view_with(&b, &v, &resolver, &spans, None);
1247 let term = run_render(view, 10, 2);
1248 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1249 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
1250 }
1251
1252 #[test]
1253 fn spans_param_rows_beyond_get_no_styling() {
1254 let b = Buffer::from_str("abc\ndef\nghi");
1255 let v = vp(10, 3);
1256 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1257 let spans = vec![vec![Span::new(0, 3, 0)]];
1259 let view = view_with(&b, &v, &resolver, &spans, None);
1260 let term = run_render(view, 10, 3);
1261 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1262 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
1263 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
1264 }
1265
1266 #[test]
1267 fn search_pattern_none_disables_hlsearch() {
1268 let b = Buffer::from_str("foo bar foo");
1269 let v = vp(20, 1);
1270 let r = no_styles as fn(u32) -> Style;
1271 let view = view_with(&b, &v, &r, &[], None);
1273 let term = run_render(view, 20, 1);
1274 for x in 0..11 {
1275 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1276 }
1277 }
1278
1279 #[test]
1280 fn search_pattern_regex_paints_match_bg() {
1281 use regex::Regex;
1282 let b = Buffer::from_str("xyz foo xyz");
1283 let v = vp(20, 1);
1284 let r = no_styles as fn(u32) -> Style;
1285 let pat = Regex::new("foo").unwrap();
1286 let view = view_with(&b, &v, &r, &[], Some(&pat));
1287 let term = run_render(view, 20, 1);
1288 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1290 for x in 4..7 {
1291 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1292 }
1293 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
1294 }
1295
1296 #[test]
1297 fn search_pattern_unicode_columns_are_charwise() {
1298 use regex::Regex;
1299 let b = Buffer::from_str("tablé foo");
1301 let v = vp(20, 1);
1302 let r = no_styles as fn(u32) -> Style;
1303 let pat = Regex::new("foo").unwrap();
1304 let view = view_with(&b, &v, &r, &[], Some(&pat));
1305 let term = run_render(view, 20, 1);
1306 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
1308 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
1309 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
1310 }
1311
1312 #[test]
1313 fn spans_param_clamps_short_row_overlay() {
1314 let b = Buffer::from_str("abc");
1316 let v = vp(10, 1);
1317 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1318 let spans = vec![vec![Span::new(0, 100, 0)]];
1319 let view = view_with(&b, &v, &resolver, &spans, None);
1320 let term = run_render(view, 10, 1);
1321 for x in 0..3 {
1322 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1323 }
1324 }
1325
1326 #[test]
1327 fn spans_and_search_pattern_compose() {
1328 use regex::Regex;
1330 let b = Buffer::from_str("foo");
1331 let v = vp(10, 1);
1332 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
1333 let spans = vec![vec![Span::new(0, 3, 0)]];
1334 let pat = Regex::new("foo").unwrap();
1335 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
1336 let term = run_render(view, 10, 1);
1337 let cell = term.cell((1, 0)).unwrap();
1338 assert_eq!(cell.fg, Color::Green);
1339 assert_eq!(cell.bg, Color::Magenta);
1340 }
1341}