1use ratatui::buffer::Buffer as TermBuffer;
27use ratatui::layout::Rect;
28use ratatui::style::Style;
29use ratatui::widgets::Widget;
30use unicode_width::UnicodeWidthChar;
31
32use crate::wrap::wrap_segments;
33use crate::{Buffer, Selection, Span, Viewport, Wrap};
34
35pub trait StyleResolver {
39 fn resolve(&self, style_id: u32) -> Style;
40}
41
42impl<F: Fn(u32) -> Style> StyleResolver for F {
44 fn resolve(&self, style_id: u32) -> Style {
45 self(style_id)
46 }
47}
48
49pub struct BufferView<'a, R: StyleResolver> {
64 pub buffer: &'a Buffer,
65 pub viewport: &'a Viewport,
69 pub selection: Option<Selection>,
70 pub resolver: &'a R,
71 pub cursor_line_bg: Style,
74 pub cursor_column_bg: Style,
77 pub selection_bg: Style,
79 pub cursor_style: Style,
82 pub gutter: Option<Gutter>,
86 pub search_bg: Style,
89 pub signs: &'a [Sign],
94 pub conceals: &'a [Conceal],
98 pub spans: &'a [Vec<Span>],
107 pub search_pattern: Option<&'a regex::Regex>,
115 pub non_text_style: Style,
122 pub diag_overlays: &'a [DiagOverlay],
127}
128
129#[derive(Debug, Clone, Copy, Default)]
133pub enum GutterNumbers {
134 None,
136 #[default]
138 Absolute,
139 Relative { cursor_row: usize },
141 Hybrid { cursor_row: usize },
144}
145
146#[derive(Debug, Clone, Copy, Default)]
156pub struct Gutter {
157 pub width: u16,
158 pub style: Style,
159 pub line_offset: usize,
160 pub numbers: GutterNumbers,
162}
163
164#[derive(Debug, Clone, Copy)]
169pub struct Sign {
170 pub row: usize,
171 pub ch: char,
172 pub style: Style,
173 pub priority: u8,
174}
175
176#[derive(Debug, Clone)]
181pub struct Conceal {
182 pub row: usize,
183 pub start_byte: usize,
184 pub end_byte: usize,
185 pub replacement: String,
186}
187
188#[derive(Debug, Clone, Copy)]
194pub struct DiagOverlay {
195 pub row: usize,
197 pub col_start: usize,
199 pub col_end: usize,
201 pub style: Style,
203}
204
205impl<R: StyleResolver> Widget for BufferView<'_, R> {
206 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
207 let viewport = *self.viewport;
208 let cursor = self.buffer.cursor();
209 let lines = self.buffer.lines();
210 let spans = self.spans;
211 let folds = self.buffer.folds();
212 let top_row = viewport.top_row;
213 let top_col = viewport.top_col;
214
215 let gutter_width = self.gutter.map(|g| g.width).unwrap_or(0);
216 let text_area = Rect {
217 x: area.x.saturating_add(gutter_width),
218 y: area.y,
219 width: area.width.saturating_sub(gutter_width),
220 height: area.height,
221 };
222
223 let total_rows = lines.len();
224 let mut doc_row = top_row;
225 let mut screen_row: u16 = 0;
226 let wrap_mode = viewport.wrap;
227 let seg_width = if viewport.text_width > 0 {
228 viewport.text_width
229 } else {
230 text_area.width
231 };
232 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
238 while doc_row < total_rows && screen_row < area.height {
242 if folds.iter().any(|f| f.hides(doc_row)) {
245 doc_row += 1;
246 continue;
247 }
248 let folded_at_start = folds
249 .iter()
250 .find(|f| f.closed && f.start_row == doc_row)
251 .copied();
252 let line = &lines[doc_row];
253 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
254 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
255 let is_cursor_row = doc_row == cursor.row;
256 if let Some(fold) = folded_at_start {
257 if let Some(gutter) = self.gutter {
258 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
259 self.paint_signs(term_buf, area, screen_row, doc_row);
260 }
261 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
262 search_hit_at_cursor_col.push(false);
263 screen_row += 1;
264 doc_row = fold.end_row + 1;
265 continue;
266 }
267 let search_ranges = self.row_search_ranges(line);
268 let row_has_hit_at_cursor_col = search_ranges
269 .iter()
270 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
271 let row_conceals: Vec<&Conceal> = {
273 let mut v: Vec<&Conceal> =
274 self.conceals.iter().filter(|c| c.row == doc_row).collect();
275 v.sort_by_key(|c| c.start_byte);
276 v
277 };
278 let segments = match wrap_mode {
286 Wrap::None => vec![(top_col, usize::MAX)],
287 _ => wrap_segments(line, seg_width, wrap_mode),
288 };
289 let last_seg_idx = segments.len().saturating_sub(1);
290 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
291 if screen_row >= area.height {
292 break;
293 }
294 if let Some(gutter) = self.gutter {
295 if seg_idx == 0 {
296 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
297 self.paint_signs(term_buf, area, screen_row, doc_row);
298 } else {
299 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
300 }
301 }
302 self.paint_row(
303 term_buf,
304 text_area,
305 screen_row,
306 line,
307 row_spans,
308 sel_range,
309 &search_ranges,
310 is_cursor_row,
311 cursor.col,
312 seg_start,
313 seg_end,
314 seg_idx == last_seg_idx,
315 &row_conceals,
316 );
317 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
318 screen_row += 1;
319 }
320 doc_row += 1;
321 }
322 while screen_row < area.height {
325 if let Some(gutter) = self.gutter {
327 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
328 }
329 let y = text_area.y + screen_row;
331 if let Some(cell) = term_buf.cell_mut((text_area.x, y)) {
332 cell.set_char('~');
333 cell.set_style(self.non_text_style);
334 }
335 screen_row += 1;
336 }
337 if matches!(wrap_mode, Wrap::None)
344 && self.cursor_column_bg != Style::default()
345 && cursor.col >= top_col
346 && (cursor.col - top_col) < text_area.width as usize
347 {
348 let x = text_area.x + (cursor.col - top_col) as u16;
349 for sy in 0..screen_row {
350 if search_hit_at_cursor_col
354 .get(sy as usize)
355 .copied()
356 .unwrap_or(false)
357 {
358 continue;
359 }
360 let y = text_area.y + sy;
361 if let Some(cell) = term_buf.cell_mut((x, y)) {
362 cell.set_style(cell.style().patch(self.cursor_column_bg));
363 }
364 }
365 }
366
367 if matches!(wrap_mode, Wrap::None) && !self.diag_overlays.is_empty() {
372 let vp_top = top_row;
376 let vp_bot = vp_top + area.height as usize;
377 for overlay in self.diag_overlays {
378 if overlay.row < vp_top || overlay.row >= vp_bot {
379 continue;
380 }
381 let mut sr: u16 = 0;
384 let mut dr = vp_top;
385 while dr < overlay.row && sr < area.height {
386 if !folds.iter().any(|f| f.hides(dr)) {
387 sr += 1;
388 }
389 dr += 1;
390 }
391 if sr >= area.height {
392 continue;
393 }
394 let y = text_area.y + sr;
395 let col_start = overlay.col_start;
398 let col_end = overlay.col_end.max(col_start + 1);
399 for col in col_start..col_end {
400 if col < top_col {
401 continue;
402 }
403 let screen_col = col - top_col;
404 if screen_col >= text_area.width as usize {
405 break;
406 }
407 let x = text_area.x + screen_col as u16;
408 if let Some(cell) = term_buf.cell_mut((x, y)) {
409 cell.set_style(cell.style().patch(overlay.style));
410 }
411 }
412 }
413 }
414 }
415}
416
417impl<R: StyleResolver> BufferView<'_, R> {
418 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
422 let Some(re) = self.search_pattern else {
423 return Vec::new();
424 };
425 re.find_iter(line)
426 .map(|m| {
427 let start = line[..m.start()].chars().count();
428 let end = line[..m.end()].chars().count();
429 (start, end)
430 })
431 .collect()
432 }
433
434 fn paint_fold_marker(
435 &self,
436 term_buf: &mut TermBuffer,
437 area: Rect,
438 screen_row: u16,
439 fold: crate::Fold,
440 first_line: &str,
441 is_cursor_row: bool,
442 ) {
443 let y = area.y + screen_row;
444 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
445 self.cursor_line_bg
446 } else {
447 Style::default()
448 };
449 for x in area.x..(area.x + area.width) {
451 if let Some(cell) = term_buf.cell_mut((x, y)) {
452 cell.set_style(style);
453 }
454 }
455 let prefix = first_line.trim();
459 let count = fold.line_count();
460 let label = if prefix.is_empty() {
461 format!("▸ {count} lines folded")
462 } else {
463 const MAX_PREFIX: usize = 60;
464 let trimmed = if prefix.chars().count() > MAX_PREFIX {
465 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
466 format!("{head}…")
467 } else {
468 prefix.to_string()
469 };
470 format!("▸ {trimmed} ({count} lines)")
471 };
472 let mut x = area.x;
473 let row_end_x = area.x + area.width;
474 for ch in label.chars() {
475 if x >= row_end_x {
476 break;
477 }
478 let width = ch.width().unwrap_or(1) as u16;
479 if x + width > row_end_x {
480 break;
481 }
482 if let Some(cell) = term_buf.cell_mut((x, y)) {
483 cell.set_char(ch);
484 cell.set_style(style);
485 }
486 x = x.saturating_add(width);
487 }
488 }
489
490 fn paint_signs(&self, term_buf: &mut TermBuffer, area: Rect, screen_row: u16, doc_row: usize) {
491 let Some(sign) = self
492 .signs
493 .iter()
494 .filter(|s| s.row == doc_row)
495 .max_by_key(|s| s.priority)
496 else {
497 return;
498 };
499 let y = area.y + screen_row;
500 let x = area.x;
501 if let Some(cell) = term_buf.cell_mut((x, y)) {
502 cell.set_char(sign.ch);
503 cell.set_style(sign.style);
504 }
505 }
506
507 fn paint_blank_gutter(
510 &self,
511 term_buf: &mut TermBuffer,
512 area: Rect,
513 screen_row: u16,
514 gutter: Gutter,
515 ) {
516 let y = area.y + screen_row;
517 for x in area.x..(area.x + gutter.width) {
518 if let Some(cell) = term_buf.cell_mut((x, y)) {
519 cell.set_char(' ');
520 cell.set_style(gutter.style);
521 }
522 }
523 }
524
525 fn paint_gutter(
526 &self,
527 term_buf: &mut TermBuffer,
528 area: Rect,
529 screen_row: u16,
530 doc_row: usize,
531 gutter: Gutter,
532 ) {
533 let y = area.y + screen_row;
534 let number_width = gutter.width.saturating_sub(1) as usize;
536
537 let label = match gutter.numbers {
539 GutterNumbers::None => {
540 for x in area.x..(area.x + gutter.width) {
542 if let Some(cell) = term_buf.cell_mut((x, y)) {
543 cell.set_char(' ');
544 cell.set_style(gutter.style);
545 }
546 }
547 return;
548 }
549 GutterNumbers::Absolute => {
550 format!(
551 "{:>width$}",
552 doc_row + 1 + gutter.line_offset,
553 width = number_width
554 )
555 }
556 GutterNumbers::Relative { cursor_row } => {
557 let n = if doc_row == cursor_row {
558 0
559 } else {
560 doc_row.abs_diff(cursor_row)
561 };
562 format!("{:>width$}", n, width = number_width)
563 }
564 GutterNumbers::Hybrid { cursor_row } => {
565 let n = if doc_row == cursor_row {
566 doc_row + 1 + gutter.line_offset
567 } else {
568 doc_row.abs_diff(cursor_row)
569 };
570 format!("{:>width$}", n, width = number_width)
571 }
572 };
573
574 let mut x = area.x;
575 for ch in label.chars() {
576 if x >= area.x + gutter.width.saturating_sub(1) {
577 break;
578 }
579 if let Some(cell) = term_buf.cell_mut((x, y)) {
580 cell.set_char(ch);
581 cell.set_style(gutter.style);
582 }
583 x = x.saturating_add(1);
584 }
585 let spacer_x = area.x + gutter.width.saturating_sub(1);
588 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
589 cell.set_char(' ');
590 cell.set_style(gutter.style);
591 }
592 }
593
594 #[allow(clippy::too_many_arguments)]
595 fn paint_row(
596 &self,
597 term_buf: &mut TermBuffer,
598 area: Rect,
599 screen_row: u16,
600 line: &str,
601 row_spans: &[crate::Span],
602 sel_range: crate::RowSpan,
603 search_ranges: &[(usize, usize)],
604 is_cursor_row: bool,
605 cursor_col: usize,
606 seg_start: usize,
607 seg_end: usize,
608 is_last_segment: bool,
609 conceals: &[&Conceal],
610 ) {
611 let y = area.y + screen_row;
612 let mut screen_x = area.x;
613 let row_end_x = area.x + area.width;
614
615 if is_cursor_row && self.cursor_line_bg != Style::default() {
619 for x in area.x..row_end_x {
620 if let Some(cell) = term_buf.cell_mut((x, y)) {
621 cell.set_style(self.cursor_line_bg);
622 }
623 }
624 }
625
626 let tab_width = self.viewport.effective_tab_width();
630 let mut byte_offset: usize = 0;
631 let mut line_col: usize = 0;
632 let mut chars_iter = line.chars().enumerate().peekable();
633 while let Some((col_idx, ch)) = chars_iter.next() {
634 let ch_byte_len = ch.len_utf8();
635 if col_idx >= seg_end {
636 break;
637 }
638 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
643 if col_idx >= seg_start {
644 let mut style = if is_cursor_row {
645 self.cursor_line_bg
646 } else {
647 Style::default()
648 };
649 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
650 style = style.patch(span_style);
651 }
652 for rch in conc.replacement.chars() {
653 let rwidth = rch.width().unwrap_or(1) as u16;
654 if screen_x + rwidth > row_end_x {
655 break;
656 }
657 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
658 cell.set_char(rch);
659 cell.set_style(style);
660 }
661 screen_x += rwidth;
662 }
663 }
664 let mut consumed = ch_byte_len;
667 byte_offset += ch_byte_len;
668 while byte_offset < conc.end_byte {
669 let Some((_, next_ch)) = chars_iter.next() else {
670 break;
671 };
672 consumed += next_ch.len_utf8();
673 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
674 }
675 let _ = consumed;
676 continue;
677 }
678 let visible_width = if ch == '\t' {
683 tab_width - (line_col % tab_width)
684 } else {
685 ch.width().unwrap_or(1)
686 };
687 if col_idx < seg_start {
690 line_col += visible_width;
691 byte_offset += ch_byte_len;
692 continue;
693 }
694 let width = visible_width as u16;
696 if screen_x + width > row_end_x {
697 break;
698 }
699
700 let mut style = if is_cursor_row {
702 self.cursor_line_bg
703 } else {
704 Style::default()
705 };
706 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
707 style = style.patch(span_style);
708 }
709 if self.search_bg != Style::default()
713 && search_ranges
714 .iter()
715 .any(|&(s, e)| col_idx >= s && col_idx < e)
716 {
717 style = style.patch(self.search_bg);
718 }
719 if let Some((lo, hi)) = sel_range
720 && col_idx >= lo
721 && col_idx <= hi
722 {
723 style = style.patch(self.selection_bg);
724 }
725 if is_cursor_row && col_idx == cursor_col {
726 style = style.patch(self.cursor_style);
727 }
728
729 if ch == '\t' {
730 for k in 0..width {
734 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
735 cell.set_char(' ');
736 cell.set_style(style);
737 }
738 }
739 } else if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
740 cell.set_char(ch);
741 cell.set_style(style);
742 }
743 screen_x += width;
744 line_col += visible_width;
745 byte_offset += ch_byte_len;
746 }
747
748 if is_cursor_row
753 && is_last_segment
754 && cursor_col >= line.chars().count()
755 && cursor_col >= seg_start
756 {
757 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
758 if pad_x < row_end_x
759 && let Some(cell) = term_buf.cell_mut((pad_x, y))
760 {
761 cell.set_char(' ');
762 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
763 }
764 }
765 }
766
767 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
770 let mut best: Option<&crate::Span> = None;
775 for span in row_spans {
776 if byte_offset >= span.start_byte && byte_offset < span.end_byte {
777 let len = span.end_byte - span.start_byte;
778 match best {
779 Some(b) if (b.end_byte - b.start_byte) <= len => {}
780 _ => best = Some(span),
781 }
782 }
783 }
784 best.map(|s| self.resolver.resolve(s.style))
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791 use ratatui::style::{Color, Modifier};
792 use ratatui::widgets::Widget;
793
794 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
795 let area = Rect::new(0, 0, w, h);
796 let mut buf = TermBuffer::empty(area);
797 view.render(area, &mut buf);
798 buf
799 }
800
801 fn no_styles(_id: u32) -> Style {
802 Style::default()
803 }
804
805 fn vp(width: u16, height: u16) -> Viewport {
807 Viewport {
808 top_row: 0,
809 top_col: 0,
810 width,
811 height,
812 wrap: Wrap::None,
813 text_width: width,
814 tab_width: 0,
815 }
816 }
817
818 #[test]
819 fn renders_plain_chars_into_terminal_buffer() {
820 let b = Buffer::from_str("hello\nworld");
821 let v = vp(20, 5);
822 let view = BufferView {
823 buffer: &b,
824 viewport: &v,
825 selection: None,
826 resolver: &(no_styles as fn(u32) -> Style),
827 cursor_line_bg: Style::default(),
828 cursor_column_bg: Style::default(),
829 selection_bg: Style::default().bg(Color::Blue),
830 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
831 gutter: None,
832 search_bg: Style::default(),
833 signs: &[],
834 conceals: &[],
835 spans: &[],
836 search_pattern: None,
837 non_text_style: Style::default(),
838 diag_overlays: &[],
839 };
840 let term = run_render(view, 20, 5);
841 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
842 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
843 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
844 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
845 }
846
847 #[test]
848 fn cursor_cell_gets_reversed_style() {
849 let mut b = Buffer::from_str("abc");
850 let v = vp(10, 1);
851 b.set_cursor(crate::Position::new(0, 1));
852 let view = BufferView {
853 buffer: &b,
854 viewport: &v,
855 selection: None,
856 resolver: &(no_styles as fn(u32) -> Style),
857 cursor_line_bg: Style::default(),
858 cursor_column_bg: Style::default(),
859 selection_bg: Style::default().bg(Color::Blue),
860 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
861 gutter: None,
862 search_bg: Style::default(),
863 signs: &[],
864 conceals: &[],
865 spans: &[],
866 search_pattern: None,
867 non_text_style: Style::default(),
868 diag_overlays: &[],
869 };
870 let term = run_render(view, 10, 1);
871 let cursor_cell = term.cell((1, 0)).unwrap();
872 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
873 }
874
875 #[test]
876 fn selection_bg_applies_only_to_selected_cells() {
877 use crate::{Position, Selection};
878 let b = Buffer::from_str("abcdef");
879 let v = vp(10, 1);
880 let view = BufferView {
881 buffer: &b,
882 viewport: &v,
883 selection: Some(Selection::Char {
884 anchor: Position::new(0, 1),
885 head: Position::new(0, 3),
886 }),
887 resolver: &(no_styles as fn(u32) -> Style),
888 cursor_line_bg: Style::default(),
889 cursor_column_bg: Style::default(),
890 selection_bg: Style::default().bg(Color::Blue),
891 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
892 gutter: None,
893 search_bg: Style::default(),
894 signs: &[],
895 conceals: &[],
896 spans: &[],
897 search_pattern: None,
898 non_text_style: Style::default(),
899 diag_overlays: &[],
900 };
901 let term = run_render(view, 10, 1);
902 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
903 for x in 1..=3 {
904 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
905 }
906 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
907 }
908
909 #[test]
910 fn syntax_span_fg_resolves_via_table() {
911 use crate::Span;
912 let b = Buffer::from_str("SELECT foo");
913 let v = vp(20, 1);
914 let spans = vec![vec![Span::new(0, 6, 7)]];
915 let resolver = |id: u32| -> Style {
916 if id == 7 {
917 Style::default().fg(Color::Red)
918 } else {
919 Style::default()
920 }
921 };
922 let view = BufferView {
923 buffer: &b,
924 viewport: &v,
925 selection: None,
926 resolver: &resolver,
927 cursor_line_bg: Style::default(),
928 cursor_column_bg: Style::default(),
929 selection_bg: Style::default().bg(Color::Blue),
930 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
931 gutter: None,
932 search_bg: Style::default(),
933 signs: &[],
934 conceals: &[],
935 spans: &spans,
936 search_pattern: None,
937 non_text_style: Style::default(),
938 diag_overlays: &[],
939 };
940 let term = run_render(view, 20, 1);
941 for x in 0..6 {
942 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
943 }
944 }
945
946 #[test]
947 fn gutter_renders_right_aligned_line_numbers() {
948 let b = Buffer::from_str("a\nb\nc");
949 let v = vp(10, 3);
950 let view = BufferView {
951 buffer: &b,
952 viewport: &v,
953 selection: None,
954 resolver: &(no_styles as fn(u32) -> Style),
955 cursor_line_bg: Style::default(),
956 cursor_column_bg: Style::default(),
957 selection_bg: Style::default().bg(Color::Blue),
958 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
959 gutter: Some(Gutter {
960 width: 4,
961 style: Style::default().fg(Color::Yellow),
962 line_offset: 0,
963 ..Default::default()
964 }),
965 search_bg: Style::default(),
966 signs: &[],
967 conceals: &[],
968 spans: &[],
969 search_pattern: None,
970 non_text_style: Style::default(),
971 diag_overlays: &[],
972 };
973 let term = run_render(view, 10, 3);
974 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
976 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
977 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
978 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
979 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
981 }
982
983 #[test]
984 fn gutter_renders_relative_with_cursor_at_zero() {
985 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
987 b.set_cursor(crate::Position::new(2, 0));
988 let v = vp(10, 5);
989 let view = BufferView {
990 buffer: &b,
991 viewport: &v,
992 selection: None,
993 resolver: &(no_styles as fn(u32) -> Style),
994 cursor_line_bg: Style::default(),
995 cursor_column_bg: Style::default(),
996 selection_bg: Style::default().bg(Color::Blue),
997 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
998 gutter: Some(Gutter {
999 width: 4,
1000 style: Style::default().fg(Color::Yellow),
1001 line_offset: 0,
1002 numbers: GutterNumbers::Relative { cursor_row: 2 },
1003 }),
1004 search_bg: Style::default(),
1005 signs: &[],
1006 conceals: &[],
1007 spans: &[],
1008 search_pattern: None,
1009 non_text_style: Style::default(),
1010 diag_overlays: &[],
1011 };
1012 let term = run_render(view, 10, 5);
1013 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "2");
1016 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "1");
1018 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "0");
1020 assert_eq!(term.cell((2, 3)).unwrap().symbol(), "1");
1022 assert_eq!(term.cell((2, 4)).unwrap().symbol(), "2");
1024 }
1025
1026 #[test]
1027 fn gutter_renders_hybrid_cursor_row_absolute() {
1028 let mut b = Buffer::from_str("a\nb\nc");
1031 b.set_cursor(crate::Position::new(1, 0));
1032 let v = vp(10, 3);
1033 let view = BufferView {
1034 buffer: &b,
1035 viewport: &v,
1036 selection: None,
1037 resolver: &(no_styles as fn(u32) -> Style),
1038 cursor_line_bg: Style::default(),
1039 cursor_column_bg: Style::default(),
1040 selection_bg: Style::default().bg(Color::Blue),
1041 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1042 gutter: Some(Gutter {
1043 width: 4,
1044 style: Style::default().fg(Color::Yellow),
1045 line_offset: 0,
1046 numbers: GutterNumbers::Hybrid { cursor_row: 1 },
1047 }),
1048 search_bg: Style::default(),
1049 signs: &[],
1050 conceals: &[],
1051 spans: &[],
1052 search_pattern: None,
1053 non_text_style: Style::default(),
1054 diag_overlays: &[],
1055 };
1056 let term = run_render(view, 10, 3);
1057 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1059 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1061 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "1");
1063 }
1064
1065 #[test]
1066 fn gutter_none_paints_blank_cells() {
1067 let b = Buffer::from_str("a\nb\nc");
1068 let v = vp(10, 3);
1069 let view = BufferView {
1070 buffer: &b,
1071 viewport: &v,
1072 selection: None,
1073 resolver: &(no_styles as fn(u32) -> Style),
1074 cursor_line_bg: Style::default(),
1075 cursor_column_bg: Style::default(),
1076 selection_bg: Style::default().bg(Color::Blue),
1077 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1078 gutter: Some(Gutter {
1079 width: 4,
1080 style: Style::default().fg(Color::Yellow),
1081 line_offset: 0,
1082 numbers: GutterNumbers::None,
1083 }),
1084 search_bg: Style::default(),
1085 signs: &[],
1086 conceals: &[],
1087 spans: &[],
1088 search_pattern: None,
1089 non_text_style: Style::default(),
1090 diag_overlays: &[],
1091 };
1092 let term = run_render(view, 10, 3);
1093 for row in 0..3u16 {
1095 for x in 0..4u16 {
1096 assert_eq!(
1097 term.cell((x, row)).unwrap().symbol(),
1098 " ",
1099 "expected blank at ({x}, {row})"
1100 );
1101 }
1102 }
1103 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1105 }
1106
1107 #[test]
1108 fn search_bg_paints_match_cells() {
1109 use regex::Regex;
1110 let b = Buffer::from_str("foo bar foo");
1111 let v = vp(20, 1);
1112 let pat = Regex::new("foo").unwrap();
1113 let view = BufferView {
1114 buffer: &b,
1115 viewport: &v,
1116 selection: None,
1117 resolver: &(no_styles as fn(u32) -> Style),
1118 cursor_line_bg: Style::default(),
1119 cursor_column_bg: Style::default(),
1120 selection_bg: Style::default().bg(Color::Blue),
1121 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1122 gutter: None,
1123 search_bg: Style::default().bg(Color::Magenta),
1124 signs: &[],
1125 conceals: &[],
1126 spans: &[],
1127 search_pattern: Some(&pat),
1128 non_text_style: Style::default(),
1129 diag_overlays: &[],
1130 };
1131 let term = run_render(view, 20, 1);
1132 for x in 0..3 {
1133 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1134 }
1135 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1137 for x in 8..11 {
1138 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1139 }
1140 }
1141
1142 #[test]
1143 fn search_bg_survives_cursorcolumn_overlay() {
1144 use regex::Regex;
1145 let mut b = Buffer::from_str("foo bar foo");
1149 let v = vp(20, 1);
1150 let pat = Regex::new("foo").unwrap();
1151 b.set_cursor(crate::Position::new(0, 1));
1153 let view = BufferView {
1154 buffer: &b,
1155 viewport: &v,
1156 selection: None,
1157 resolver: &(no_styles as fn(u32) -> Style),
1158 cursor_line_bg: Style::default(),
1159 cursor_column_bg: Style::default().bg(Color::DarkGray),
1160 selection_bg: Style::default().bg(Color::Blue),
1161 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1162 gutter: None,
1163 search_bg: Style::default().bg(Color::Magenta),
1164 signs: &[],
1165 conceals: &[],
1166 spans: &[],
1167 search_pattern: Some(&pat),
1168 non_text_style: Style::default(),
1169 diag_overlays: &[],
1170 };
1171 let term = run_render(view, 20, 1);
1172 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
1174 }
1175
1176 #[test]
1177 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
1178 let b = Buffer::from_str("a\nb\nc");
1179 let v = vp(10, 3);
1180 let signs = [
1181 Sign {
1182 row: 0,
1183 ch: 'W',
1184 style: Style::default().fg(Color::Yellow),
1185 priority: 1,
1186 },
1187 Sign {
1188 row: 0,
1189 ch: 'E',
1190 style: Style::default().fg(Color::Red),
1191 priority: 2,
1192 },
1193 ];
1194 let view = BufferView {
1195 buffer: &b,
1196 viewport: &v,
1197 selection: None,
1198 resolver: &(no_styles as fn(u32) -> Style),
1199 cursor_line_bg: Style::default(),
1200 cursor_column_bg: Style::default(),
1201 selection_bg: Style::default().bg(Color::Blue),
1202 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1203 gutter: Some(Gutter {
1204 width: 3,
1205 style: Style::default().fg(Color::DarkGray),
1206 line_offset: 0,
1207 ..Default::default()
1208 }),
1209 search_bg: Style::default(),
1210 signs: &signs,
1211 conceals: &[],
1212 spans: &[],
1213 search_pattern: None,
1214 non_text_style: Style::default(),
1215 diag_overlays: &[],
1216 };
1217 let term = run_render(view, 10, 3);
1218 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
1219 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1220 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
1222 }
1223
1224 #[test]
1225 fn conceal_replaces_byte_range() {
1226 let b = Buffer::from_str("see https://example.com end");
1227 let v = vp(30, 1);
1228 let conceals = vec![Conceal {
1229 row: 0,
1230 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
1233 }];
1234 let view = BufferView {
1235 buffer: &b,
1236 viewport: &v,
1237 selection: None,
1238 resolver: &(no_styles as fn(u32) -> Style),
1239 cursor_line_bg: Style::default(),
1240 cursor_column_bg: Style::default(),
1241 selection_bg: Style::default(),
1242 cursor_style: Style::default(),
1243 gutter: None,
1244 search_bg: Style::default(),
1245 signs: &[],
1246 conceals: &conceals,
1247 spans: &[],
1248 search_pattern: None,
1249 non_text_style: Style::default(),
1250 diag_overlays: &[],
1251 };
1252 let term = run_render(view, 30, 1);
1253 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
1255 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
1256 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
1259 }
1260
1261 #[test]
1262 fn closed_fold_collapses_rows_and_paints_marker() {
1263 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1264 let v = vp(30, 5);
1265 b.add_fold(1, 3, true);
1267 let view = BufferView {
1268 buffer: &b,
1269 viewport: &v,
1270 selection: None,
1271 resolver: &(no_styles as fn(u32) -> Style),
1272 cursor_line_bg: Style::default(),
1273 cursor_column_bg: Style::default(),
1274 selection_bg: Style::default().bg(Color::Blue),
1275 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1276 gutter: None,
1277 search_bg: Style::default(),
1278 signs: &[],
1279 conceals: &[],
1280 spans: &[],
1281 search_pattern: None,
1282 non_text_style: Style::default(),
1283 diag_overlays: &[],
1284 };
1285 let term = run_render(view, 30, 5);
1286 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1288 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
1291 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
1293 }
1294
1295 #[test]
1296 fn open_fold_renders_normally() {
1297 let mut b = Buffer::from_str("a\nb\nc");
1298 let v = vp(5, 3);
1299 b.add_fold(0, 2, false); let view = BufferView {
1301 buffer: &b,
1302 viewport: &v,
1303 selection: None,
1304 resolver: &(no_styles as fn(u32) -> Style),
1305 cursor_line_bg: Style::default(),
1306 cursor_column_bg: Style::default(),
1307 selection_bg: Style::default().bg(Color::Blue),
1308 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1309 gutter: None,
1310 search_bg: Style::default(),
1311 signs: &[],
1312 conceals: &[],
1313 spans: &[],
1314 search_pattern: None,
1315 non_text_style: Style::default(),
1316 diag_overlays: &[],
1317 };
1318 let term = run_render(view, 5, 3);
1319 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1320 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1321 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
1322 }
1323
1324 #[test]
1325 fn horizontal_scroll_clips_left_chars() {
1326 let b = Buffer::from_str("abcdefgh");
1327 let mut v = vp(4, 1);
1328 v.top_col = 3;
1329 let view = BufferView {
1330 buffer: &b,
1331 viewport: &v,
1332 selection: None,
1333 resolver: &(no_styles as fn(u32) -> Style),
1334 cursor_line_bg: Style::default(),
1335 cursor_column_bg: Style::default(),
1336 selection_bg: Style::default().bg(Color::Blue),
1337 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1338 gutter: None,
1339 search_bg: Style::default(),
1340 signs: &[],
1341 conceals: &[],
1342 spans: &[],
1343 search_pattern: None,
1344 non_text_style: Style::default(),
1345 diag_overlays: &[],
1346 };
1347 let term = run_render(view, 4, 1);
1348 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
1349 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
1350 }
1351
1352 fn make_wrap_view<'a>(
1353 b: &'a Buffer,
1354 viewport: &'a Viewport,
1355 resolver: &'a (impl StyleResolver + 'a),
1356 gutter: Option<Gutter>,
1357 ) -> BufferView<'a, impl StyleResolver + 'a> {
1358 BufferView {
1359 buffer: b,
1360 viewport,
1361 selection: None,
1362 resolver,
1363 cursor_line_bg: Style::default(),
1364 cursor_column_bg: Style::default(),
1365 selection_bg: Style::default().bg(Color::Blue),
1366 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1367 gutter,
1368 search_bg: Style::default(),
1369 signs: &[],
1370 conceals: &[],
1371 spans: &[],
1372 search_pattern: None,
1373 non_text_style: Style::default(),
1374 diag_overlays: &[],
1375 }
1376 }
1377
1378 #[test]
1379 fn wrap_segments_char_breaks_at_width() {
1380 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1381 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1382 }
1383
1384 #[test]
1385 fn wrap_segments_word_backs_up_to_whitespace() {
1386 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1387 assert_eq!(segs[0], (0, 6));
1389 assert_eq!(segs[1], (6, 11));
1391 assert_eq!(segs[2], (11, 16));
1392 }
1393
1394 #[test]
1395 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1396 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1397 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1399 }
1400
1401 #[test]
1402 fn wrap_char_paints_continuation_rows() {
1403 let b = Buffer::from_str("abcdefghij");
1404 let v = Viewport {
1405 top_row: 0,
1406 top_col: 0,
1407 width: 4,
1408 height: 3,
1409 wrap: Wrap::Char,
1410 text_width: 4,
1411 tab_width: 0,
1412 };
1413 let r = no_styles as fn(u32) -> Style;
1414 let view = make_wrap_view(&b, &v, &r, None);
1415 let term = run_render(view, 4, 3);
1416 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1418 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1419 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1421 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1422 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1424 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1425 }
1426
1427 #[test]
1428 fn wrap_char_gutter_blank_on_continuation() {
1429 let b = Buffer::from_str("abcdefgh");
1430 let v = Viewport {
1431 top_row: 0,
1432 top_col: 0,
1433 width: 6,
1434 height: 3,
1435 wrap: Wrap::Char,
1436 text_width: 3,
1438 tab_width: 0,
1439 };
1440 let r = no_styles as fn(u32) -> Style;
1441 let gutter = Gutter {
1442 width: 3,
1443 style: Style::default().fg(Color::Yellow),
1444 line_offset: 0,
1445 ..Default::default()
1446 };
1447 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1448 let term = run_render(view, 6, 3);
1449 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1451 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1452 for x in 0..2 {
1454 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1455 }
1456 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1457 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1458 }
1459
1460 #[test]
1461 fn wrap_char_cursor_lands_on_correct_segment() {
1462 let mut b = Buffer::from_str("abcdefghij");
1463 let v = Viewport {
1464 top_row: 0,
1465 top_col: 0,
1466 width: 4,
1467 height: 3,
1468 wrap: Wrap::Char,
1469 text_width: 4,
1470 tab_width: 0,
1471 };
1472 b.set_cursor(crate::Position::new(0, 6));
1474 let r = no_styles as fn(u32) -> Style;
1475 let mut view = make_wrap_view(&b, &v, &r, None);
1476 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1477 let term = run_render(view, 4, 3);
1478 assert!(
1479 term.cell((2, 1))
1480 .unwrap()
1481 .modifier
1482 .contains(Modifier::REVERSED)
1483 );
1484 }
1485
1486 #[test]
1487 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1488 let mut b = Buffer::from_str("abcdef");
1489 let v = Viewport {
1490 top_row: 0,
1491 top_col: 0,
1492 width: 4,
1493 height: 3,
1494 wrap: Wrap::Char,
1495 text_width: 4,
1496 tab_width: 0,
1497 };
1498 b.set_cursor(crate::Position::new(0, 6));
1500 let r = no_styles as fn(u32) -> Style;
1501 let mut view = make_wrap_view(&b, &v, &r, None);
1502 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1503 let term = run_render(view, 4, 3);
1504 assert!(
1506 term.cell((2, 1))
1507 .unwrap()
1508 .modifier
1509 .contains(Modifier::REVERSED)
1510 );
1511 }
1512
1513 #[test]
1514 fn wrap_word_breaks_at_whitespace() {
1515 let b = Buffer::from_str("alpha beta gamma");
1516 let v = Viewport {
1517 top_row: 0,
1518 top_col: 0,
1519 width: 8,
1520 height: 3,
1521 wrap: Wrap::Word,
1522 text_width: 8,
1523 tab_width: 0,
1524 };
1525 let r = no_styles as fn(u32) -> Style;
1526 let view = make_wrap_view(&b, &v, &r, None);
1527 let term = run_render(view, 8, 3);
1528 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1530 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1531 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1533 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1534 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1536 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1537 }
1538
1539 fn view_with<'a>(
1545 b: &'a Buffer,
1546 viewport: &'a Viewport,
1547 resolver: &'a (impl StyleResolver + 'a),
1548 spans: &'a [Vec<Span>],
1549 search_pattern: Option<&'a regex::Regex>,
1550 ) -> BufferView<'a, impl StyleResolver + 'a> {
1551 BufferView {
1552 buffer: b,
1553 viewport,
1554 selection: None,
1555 resolver,
1556 cursor_line_bg: Style::default(),
1557 cursor_column_bg: Style::default(),
1558 selection_bg: Style::default().bg(Color::Blue),
1559 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1560 gutter: None,
1561 search_bg: Style::default().bg(Color::Magenta),
1562 signs: &[],
1563 conceals: &[],
1564 spans,
1565 search_pattern,
1566 non_text_style: Style::default(),
1567 diag_overlays: &[],
1568 }
1569 }
1570
1571 #[test]
1572 fn empty_spans_param_renders_default_style() {
1573 let b = Buffer::from_str("hello");
1574 let v = vp(10, 1);
1575 let r = no_styles as fn(u32) -> Style;
1576 let view = view_with(&b, &v, &r, &[], None);
1577 let term = run_render(view, 10, 1);
1578 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1579 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
1580 }
1581
1582 #[test]
1583 fn spans_param_paints_styled_byte_range() {
1584 let b = Buffer::from_str("abcdef");
1585 let v = vp(10, 1);
1586 let resolver = |id: u32| -> Style {
1587 if id == 3 {
1588 Style::default().fg(Color::Green)
1589 } else {
1590 Style::default()
1591 }
1592 };
1593 let spans = vec![vec![Span::new(0, 3, 3)]];
1594 let view = view_with(&b, &v, &resolver, &spans, None);
1595 let term = run_render(view, 10, 1);
1596 for x in 0..3 {
1597 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
1598 }
1599 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
1600 }
1601
1602 #[test]
1603 fn spans_param_handles_per_row_overlay() {
1604 let b = Buffer::from_str("abc\ndef");
1605 let v = vp(10, 2);
1606 let resolver = |id: u32| -> Style {
1607 if id == 1 {
1608 Style::default().fg(Color::Red)
1609 } else {
1610 Style::default().fg(Color::Green)
1611 }
1612 };
1613 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
1614 let view = view_with(&b, &v, &resolver, &spans, None);
1615 let term = run_render(view, 10, 2);
1616 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1617 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
1618 }
1619
1620 #[test]
1621 fn spans_param_rows_beyond_get_no_styling() {
1622 let b = Buffer::from_str("abc\ndef\nghi");
1623 let v = vp(10, 3);
1624 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1625 let spans = vec![vec![Span::new(0, 3, 0)]];
1627 let view = view_with(&b, &v, &resolver, &spans, None);
1628 let term = run_render(view, 10, 3);
1629 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1630 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
1631 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
1632 }
1633
1634 #[test]
1635 fn search_pattern_none_disables_hlsearch() {
1636 let b = Buffer::from_str("foo bar foo");
1637 let v = vp(20, 1);
1638 let r = no_styles as fn(u32) -> Style;
1639 let view = view_with(&b, &v, &r, &[], None);
1641 let term = run_render(view, 20, 1);
1642 for x in 0..11 {
1643 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1644 }
1645 }
1646
1647 #[test]
1648 fn search_pattern_regex_paints_match_bg() {
1649 use regex::Regex;
1650 let b = Buffer::from_str("xyz foo xyz");
1651 let v = vp(20, 1);
1652 let r = no_styles as fn(u32) -> Style;
1653 let pat = Regex::new("foo").unwrap();
1654 let view = view_with(&b, &v, &r, &[], Some(&pat));
1655 let term = run_render(view, 20, 1);
1656 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1658 for x in 4..7 {
1659 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1660 }
1661 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
1662 }
1663
1664 #[test]
1665 fn search_pattern_unicode_columns_are_charwise() {
1666 use regex::Regex;
1667 let b = Buffer::from_str("tablé foo");
1669 let v = vp(20, 1);
1670 let r = no_styles as fn(u32) -> Style;
1671 let pat = Regex::new("foo").unwrap();
1672 let view = view_with(&b, &v, &r, &[], Some(&pat));
1673 let term = run_render(view, 20, 1);
1674 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
1676 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
1677 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
1678 }
1679
1680 #[test]
1681 fn spans_param_clamps_short_row_overlay() {
1682 let b = Buffer::from_str("abc");
1684 let v = vp(10, 1);
1685 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1686 let spans = vec![vec![Span::new(0, 100, 0)]];
1687 let view = view_with(&b, &v, &resolver, &spans, None);
1688 let term = run_render(view, 10, 1);
1689 for x in 0..3 {
1690 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1691 }
1692 }
1693
1694 #[test]
1695 fn spans_and_search_pattern_compose() {
1696 use regex::Regex;
1698 let b = Buffer::from_str("foo");
1699 let v = vp(10, 1);
1700 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
1701 let spans = vec![vec![Span::new(0, 3, 0)]];
1702 let pat = Regex::new("foo").unwrap();
1703 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
1704 let term = run_render(view, 10, 1);
1705 let cell = term.cell((1, 0)).unwrap();
1706 assert_eq!(cell.fg, Color::Green);
1707 assert_eq!(cell.bg, Color::Magenta);
1708 }
1709
1710 #[test]
1714 fn tilde_marker_painted_past_eof() {
1715 let b = Buffer::from_str("a\nb\nc\nd\ne");
1717 let v = vp(10, 10);
1718 let r = no_styles as fn(u32) -> Style;
1719 let non_text_fg = Color::DarkGray;
1720 let view = BufferView {
1721 buffer: &b,
1722 viewport: &v,
1723 selection: None,
1724 resolver: &r,
1725 cursor_line_bg: Style::default(),
1726 cursor_column_bg: Style::default(),
1727 selection_bg: Style::default().bg(Color::Blue),
1728 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1729 gutter: None,
1730 search_bg: Style::default(),
1731 signs: &[],
1732 conceals: &[],
1733 spans: &[],
1734 search_pattern: None,
1735 non_text_style: Style::default().fg(non_text_fg),
1736 diag_overlays: &[],
1737 };
1738 let term = run_render(view, 10, 10);
1739 for row in 0..5u16 {
1741 assert_ne!(
1742 term.cell((0, row)).unwrap().symbol(),
1743 "~",
1744 "row {row} is a content row, expected no tilde"
1745 );
1746 }
1747 for row in 5..10u16 {
1749 let cell = term.cell((0, row)).unwrap();
1750 assert_eq!(cell.symbol(), "~", "row {row} is past EOF, expected tilde");
1751 assert_eq!(
1752 cell.fg, non_text_fg,
1753 "row {row} tilde should use non_text_style fg"
1754 );
1755 for x in 1..10u16 {
1757 assert_eq!(
1758 term.cell((x, row)).unwrap().symbol(),
1759 " ",
1760 "row {row} col {x} after tilde should be blank"
1761 );
1762 }
1763 }
1764 }
1765
1766 #[test]
1769 fn tilde_marker_with_gutter_past_eof() {
1770 let b = Buffer::from_str("a\nb");
1771 let v = vp(10, 5);
1772 let r = no_styles as fn(u32) -> Style;
1773 let non_text_fg = Color::DarkGray;
1774 let view = BufferView {
1775 buffer: &b,
1776 viewport: &v,
1777 selection: None,
1778 resolver: &r,
1779 cursor_line_bg: Style::default(),
1780 cursor_column_bg: Style::default(),
1781 selection_bg: Style::default().bg(Color::Blue),
1782 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1783 gutter: Some(Gutter {
1784 width: 4,
1785 style: Style::default().fg(Color::Yellow),
1786 line_offset: 0,
1787 numbers: GutterNumbers::Absolute,
1788 }),
1789 search_bg: Style::default(),
1790 signs: &[],
1791 conceals: &[],
1792 spans: &[],
1793 search_pattern: None,
1794 non_text_style: Style::default().fg(non_text_fg),
1795 diag_overlays: &[],
1796 };
1797 let term = run_render(view, 10, 5);
1798 for row in 2..5u16 {
1800 for x in 0..4u16 {
1802 assert_eq!(
1803 term.cell((x, row)).unwrap().symbol(),
1804 " ",
1805 "gutter col {x} on past-EOF row {row} should be blank"
1806 );
1807 }
1808 let cell = term.cell((4, row)).unwrap();
1810 assert_eq!(
1811 cell.symbol(),
1812 "~",
1813 "past-EOF row {row}: expected tilde at text column"
1814 );
1815 assert_eq!(cell.fg, non_text_fg);
1816 }
1817 }
1818
1819 #[test]
1820 fn diag_overlay_paints_underline_on_range() {
1821 let b = Buffer::from_str("hello world");
1825 let v = vp(20, 2);
1826 let overlay = DiagOverlay {
1827 row: 0,
1828 col_start: 6,
1829 col_end: 11,
1830 style: Style::default().add_modifier(Modifier::UNDERLINED),
1831 };
1832 let view = BufferView {
1833 buffer: &b,
1834 viewport: &v,
1835 selection: None,
1836 resolver: &(no_styles as fn(u32) -> Style),
1837 cursor_line_bg: Style::default(),
1838 cursor_column_bg: Style::default(),
1839 selection_bg: Style::default().bg(Color::Blue),
1840 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1841 gutter: None,
1842 search_bg: Style::default(),
1843 signs: &[],
1844 conceals: &[],
1845 spans: &[],
1846 search_pattern: None,
1847 non_text_style: Style::default(),
1848 diag_overlays: &[overlay],
1849 };
1850 let term = run_render(view, 20, 2);
1851
1852 for x in 0u16..6 {
1854 let cell = term.cell((x, 0)).unwrap();
1855 assert!(
1856 !cell.modifier.contains(Modifier::UNDERLINED),
1857 "col {x} must not be underlined (outside overlay)"
1858 );
1859 }
1860 for x in 6u16..11 {
1862 let cell = term.cell((x, 0)).unwrap();
1863 assert!(
1864 cell.modifier.contains(Modifier::UNDERLINED),
1865 "col {x} must be underlined (inside overlay)"
1866 );
1867 }
1868 let cell = term.cell((11, 0)).unwrap();
1870 assert!(
1871 !cell.modifier.contains(Modifier::UNDERLINED),
1872 "col 11 must not be underlined (past overlay end)"
1873 );
1874 }
1875
1876 #[test]
1877 fn diag_overlay_out_of_viewport_is_ignored() {
1878 let b = Buffer::from_str("a\nb\nc");
1880 let v = vp(10, 3);
1881 let overlay = DiagOverlay {
1882 row: 5,
1883 col_start: 0,
1884 col_end: 1,
1885 style: Style::default().add_modifier(Modifier::UNDERLINED),
1886 };
1887 let view = BufferView {
1888 buffer: &b,
1889 viewport: &v,
1890 selection: None,
1891 resolver: &(no_styles as fn(u32) -> Style),
1892 cursor_line_bg: Style::default(),
1893 cursor_column_bg: Style::default(),
1894 selection_bg: Style::default().bg(Color::Blue),
1895 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1896 gutter: None,
1897 search_bg: Style::default(),
1898 signs: &[],
1899 conceals: &[],
1900 spans: &[],
1901 search_pattern: None,
1902 non_text_style: Style::default(),
1903 diag_overlays: &[overlay],
1904 };
1905 let _term = run_render(view, 10, 3);
1907 }
1908}