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 pub colorcolumn_cols: &'a [u16],
131 pub colorcolumn_style: Style,
134}
135
136#[derive(Debug, Clone, Copy, Default)]
140pub enum GutterNumbers {
141 None,
143 #[default]
145 Absolute,
146 Relative { cursor_row: usize },
148 Hybrid { cursor_row: usize },
151}
152
153#[derive(Debug, Clone, Copy, Default)]
163pub struct Gutter {
164 pub width: u16,
165 pub style: Style,
166 pub line_offset: usize,
167 pub numbers: GutterNumbers,
169}
170
171#[derive(Debug, Clone, Copy)]
176pub struct Sign {
177 pub row: usize,
178 pub ch: char,
179 pub style: Style,
180 pub priority: u8,
181}
182
183#[derive(Debug, Clone)]
188pub struct Conceal {
189 pub row: usize,
190 pub start_byte: usize,
191 pub end_byte: usize,
192 pub replacement: String,
193}
194
195#[derive(Debug, Clone, Copy)]
201pub struct DiagOverlay {
202 pub row: usize,
204 pub col_start: usize,
206 pub col_end: usize,
208 pub style: Style,
210}
211
212impl<R: StyleResolver> Widget for BufferView<'_, R> {
213 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
214 let viewport = *self.viewport;
215 let cursor = self.buffer.cursor();
216 let lines = self.buffer.lines();
217 let spans = self.spans;
218 let folds = self.buffer.folds();
219 let top_row = viewport.top_row;
220 let top_col = viewport.top_col;
221
222 let gutter_width = self.gutter.map(|g| g.width).unwrap_or(0);
223 let text_area = Rect {
224 x: area.x.saturating_add(gutter_width),
225 y: area.y,
226 width: area.width.saturating_sub(gutter_width),
227 height: area.height,
228 };
229
230 let total_rows = lines.len();
231 let mut doc_row = top_row;
232 let mut screen_row: u16 = 0;
233 let wrap_mode = viewport.wrap;
234 let seg_width = if viewport.text_width > 0 {
235 viewport.text_width
236 } else {
237 text_area.width
238 };
239 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
245 while doc_row < total_rows && screen_row < area.height {
249 if folds.iter().any(|f| f.hides(doc_row)) {
252 doc_row += 1;
253 continue;
254 }
255 let folded_at_start = folds
256 .iter()
257 .find(|f| f.closed && f.start_row == doc_row)
258 .copied();
259 let line = &lines[doc_row];
260 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
261 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
262 let is_cursor_row = doc_row == cursor.row;
263 if let Some(fold) = folded_at_start {
264 if let Some(gutter) = self.gutter {
265 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
266 self.paint_signs(term_buf, area, screen_row, doc_row);
267 }
268 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
269 search_hit_at_cursor_col.push(false);
270 screen_row += 1;
271 doc_row = fold.end_row + 1;
272 continue;
273 }
274 let search_ranges = self.row_search_ranges(line);
275 let row_has_hit_at_cursor_col = search_ranges
276 .iter()
277 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
278 let row_conceals: Vec<&Conceal> = {
280 let mut v: Vec<&Conceal> =
281 self.conceals.iter().filter(|c| c.row == doc_row).collect();
282 v.sort_by_key(|c| c.start_byte);
283 v
284 };
285 let segments = match wrap_mode {
293 Wrap::None => vec![(top_col, usize::MAX)],
294 _ => wrap_segments(line, seg_width, wrap_mode),
295 };
296 let last_seg_idx = segments.len().saturating_sub(1);
297 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
298 if screen_row >= area.height {
299 break;
300 }
301 if let Some(gutter) = self.gutter {
302 if seg_idx == 0 {
303 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
304 self.paint_signs(term_buf, area, screen_row, doc_row);
305 } else {
306 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
307 }
308 }
309 self.paint_row(
310 term_buf,
311 text_area,
312 screen_row,
313 line,
314 row_spans,
315 sel_range,
316 &search_ranges,
317 is_cursor_row,
318 cursor.col,
319 seg_start,
320 seg_end,
321 seg_idx == last_seg_idx,
322 &row_conceals,
323 );
324 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
325 screen_row += 1;
326 }
327 doc_row += 1;
328 }
329 while screen_row < area.height {
332 if let Some(gutter) = self.gutter {
334 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
335 }
336 let y = text_area.y + screen_row;
338 if let Some(cell) = term_buf.cell_mut((text_area.x, y)) {
339 cell.set_char('~');
340 cell.set_style(self.non_text_style);
341 }
342 screen_row += 1;
343 }
344 if matches!(wrap_mode, Wrap::None)
351 && self.cursor_column_bg != Style::default()
352 && cursor.col >= top_col
353 && (cursor.col - top_col) < text_area.width as usize
354 {
355 let x = text_area.x + (cursor.col - top_col) as u16;
356 for sy in 0..screen_row {
357 if search_hit_at_cursor_col
361 .get(sy as usize)
362 .copied()
363 .unwrap_or(false)
364 {
365 continue;
366 }
367 let y = text_area.y + sy;
368 if let Some(cell) = term_buf.cell_mut((x, y)) {
369 cell.set_style(cell.style().patch(self.cursor_column_bg));
370 }
371 }
372 }
373
374 if matches!(wrap_mode, Wrap::None) && !self.colorcolumn_cols.is_empty() {
378 for &col_1based in self.colorcolumn_cols {
379 let col = col_1based as usize; if col == 0 || col < top_col + 1 {
381 continue; }
383 let screen_col = col - 1 - top_col; if screen_col >= text_area.width as usize {
385 continue; }
387 let x = text_area.x + screen_col as u16;
388 for sy in 0..screen_row {
389 let y = text_area.y + sy;
390 if let Some(cell) = term_buf.cell_mut((x, y)) {
391 cell.set_style(cell.style().patch(self.colorcolumn_style));
392 }
393 }
394 }
395 }
396
397 if matches!(wrap_mode, Wrap::None) && !self.diag_overlays.is_empty() {
402 let vp_top = top_row;
406 let vp_bot = vp_top + area.height as usize;
407 for overlay in self.diag_overlays {
408 if overlay.row < vp_top || overlay.row >= vp_bot {
409 continue;
410 }
411 let mut sr: u16 = 0;
414 let mut dr = vp_top;
415 while dr < overlay.row && sr < area.height {
416 if !folds.iter().any(|f| f.hides(dr)) {
417 sr += 1;
418 }
419 dr += 1;
420 }
421 if sr >= area.height {
422 continue;
423 }
424 let y = text_area.y + sr;
425 let col_start = overlay.col_start;
428 let col_end = overlay.col_end.max(col_start + 1);
429 for col in col_start..col_end {
430 if col < top_col {
431 continue;
432 }
433 let screen_col = col - top_col;
434 if screen_col >= text_area.width as usize {
435 break;
436 }
437 let x = text_area.x + screen_col as u16;
438 if let Some(cell) = term_buf.cell_mut((x, y)) {
439 cell.set_style(cell.style().patch(overlay.style));
440 }
441 }
442 }
443 }
444 }
445}
446
447impl<R: StyleResolver> BufferView<'_, R> {
448 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
452 let Some(re) = self.search_pattern else {
453 return Vec::new();
454 };
455 re.find_iter(line)
456 .map(|m| {
457 let start = line[..m.start()].chars().count();
458 let end = line[..m.end()].chars().count();
459 (start, end)
460 })
461 .collect()
462 }
463
464 fn paint_fold_marker(
465 &self,
466 term_buf: &mut TermBuffer,
467 area: Rect,
468 screen_row: u16,
469 fold: crate::Fold,
470 first_line: &str,
471 is_cursor_row: bool,
472 ) {
473 let y = area.y + screen_row;
474 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
475 self.cursor_line_bg
476 } else {
477 Style::default()
478 };
479 for x in area.x..(area.x + area.width) {
481 if let Some(cell) = term_buf.cell_mut((x, y)) {
482 cell.set_style(style);
483 }
484 }
485 let prefix = first_line.trim();
489 let count = fold.line_count();
490 let label = if prefix.is_empty() {
491 format!("▸ {count} lines folded")
492 } else {
493 const MAX_PREFIX: usize = 60;
494 let trimmed = if prefix.chars().count() > MAX_PREFIX {
495 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
496 format!("{head}…")
497 } else {
498 prefix.to_string()
499 };
500 format!("▸ {trimmed} ({count} lines)")
501 };
502 let mut x = area.x;
503 let row_end_x = area.x + area.width;
504 for ch in label.chars() {
505 if x >= row_end_x {
506 break;
507 }
508 let width = ch.width().unwrap_or(1) as u16;
509 if x + width > row_end_x {
510 break;
511 }
512 if let Some(cell) = term_buf.cell_mut((x, y)) {
513 cell.set_char(ch);
514 cell.set_style(style);
515 }
516 x = x.saturating_add(width);
517 }
518 }
519
520 fn paint_signs(&self, term_buf: &mut TermBuffer, area: Rect, screen_row: u16, doc_row: usize) {
521 let Some(sign) = self
522 .signs
523 .iter()
524 .filter(|s| s.row == doc_row)
525 .max_by_key(|s| s.priority)
526 else {
527 return;
528 };
529 let y = area.y + screen_row;
530 let x = area.x;
531 if let Some(cell) = term_buf.cell_mut((x, y)) {
532 cell.set_char(sign.ch);
533 cell.set_style(sign.style);
534 }
535 }
536
537 fn paint_blank_gutter(
540 &self,
541 term_buf: &mut TermBuffer,
542 area: Rect,
543 screen_row: u16,
544 gutter: Gutter,
545 ) {
546 let y = area.y + screen_row;
547 for x in area.x..(area.x + gutter.width) {
548 if let Some(cell) = term_buf.cell_mut((x, y)) {
549 cell.set_char(' ');
550 cell.set_style(gutter.style);
551 }
552 }
553 }
554
555 fn paint_gutter(
556 &self,
557 term_buf: &mut TermBuffer,
558 area: Rect,
559 screen_row: u16,
560 doc_row: usize,
561 gutter: Gutter,
562 ) {
563 let y = area.y + screen_row;
564 let number_width = gutter.width.saturating_sub(1) as usize;
566
567 let label = match gutter.numbers {
569 GutterNumbers::None => {
570 for x in area.x..(area.x + gutter.width) {
572 if let Some(cell) = term_buf.cell_mut((x, y)) {
573 cell.set_char(' ');
574 cell.set_style(gutter.style);
575 }
576 }
577 return;
578 }
579 GutterNumbers::Absolute => {
580 format!(
581 "{:>width$}",
582 doc_row + 1 + gutter.line_offset,
583 width = number_width
584 )
585 }
586 GutterNumbers::Relative { cursor_row } => {
587 let n = if doc_row == cursor_row {
588 0
589 } else {
590 doc_row.abs_diff(cursor_row)
591 };
592 format!("{:>width$}", n, width = number_width)
593 }
594 GutterNumbers::Hybrid { cursor_row } => {
595 let n = if doc_row == cursor_row {
596 doc_row + 1 + gutter.line_offset
597 } else {
598 doc_row.abs_diff(cursor_row)
599 };
600 format!("{:>width$}", n, width = number_width)
601 }
602 };
603
604 let mut x = area.x;
605 for ch in label.chars() {
606 if x >= area.x + gutter.width.saturating_sub(1) {
607 break;
608 }
609 if let Some(cell) = term_buf.cell_mut((x, y)) {
610 cell.set_char(ch);
611 cell.set_style(gutter.style);
612 }
613 x = x.saturating_add(1);
614 }
615 let spacer_x = area.x + gutter.width.saturating_sub(1);
618 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
619 cell.set_char(' ');
620 cell.set_style(gutter.style);
621 }
622 }
623
624 #[allow(clippy::too_many_arguments)]
625 fn paint_row(
626 &self,
627 term_buf: &mut TermBuffer,
628 area: Rect,
629 screen_row: u16,
630 line: &str,
631 row_spans: &[crate::Span],
632 sel_range: crate::RowSpan,
633 search_ranges: &[(usize, usize)],
634 is_cursor_row: bool,
635 cursor_col: usize,
636 seg_start: usize,
637 seg_end: usize,
638 is_last_segment: bool,
639 conceals: &[&Conceal],
640 ) {
641 let y = area.y + screen_row;
642 let mut screen_x = area.x;
643 let row_end_x = area.x + area.width;
644
645 if is_cursor_row && self.cursor_line_bg != Style::default() {
649 for x in area.x..row_end_x {
650 if let Some(cell) = term_buf.cell_mut((x, y)) {
651 cell.set_style(self.cursor_line_bg);
652 }
653 }
654 }
655
656 let tab_width = self.viewport.effective_tab_width();
660 let mut byte_offset: usize = 0;
661 let mut line_col: usize = 0;
662 let mut chars_iter = line.chars().enumerate().peekable();
663 while let Some((col_idx, ch)) = chars_iter.next() {
664 let ch_byte_len = ch.len_utf8();
665 if col_idx >= seg_end {
666 break;
667 }
668 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
673 if col_idx >= seg_start {
674 let mut style = if is_cursor_row {
675 self.cursor_line_bg
676 } else {
677 Style::default()
678 };
679 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
680 style = style.patch(span_style);
681 }
682 for rch in conc.replacement.chars() {
683 let rwidth = rch.width().unwrap_or(1) as u16;
684 if screen_x + rwidth > row_end_x {
685 break;
686 }
687 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
688 cell.set_char(rch);
689 cell.set_style(style);
690 }
691 screen_x += rwidth;
692 }
693 }
694 let mut consumed = ch_byte_len;
697 byte_offset += ch_byte_len;
698 while byte_offset < conc.end_byte {
699 let Some((_, next_ch)) = chars_iter.next() else {
700 break;
701 };
702 consumed += next_ch.len_utf8();
703 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
704 }
705 let _ = consumed;
706 continue;
707 }
708 let visible_width = if ch == '\t' {
713 tab_width - (line_col % tab_width)
714 } else {
715 ch.width().unwrap_or(1)
716 };
717 if col_idx < seg_start {
720 line_col += visible_width;
721 byte_offset += ch_byte_len;
722 continue;
723 }
724 let width = visible_width as u16;
726 if screen_x + width > row_end_x {
727 break;
728 }
729
730 let mut style = if is_cursor_row {
732 self.cursor_line_bg
733 } else {
734 Style::default()
735 };
736 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
737 style = style.patch(span_style);
738 }
739 if self.search_bg != Style::default()
743 && search_ranges
744 .iter()
745 .any(|&(s, e)| col_idx >= s && col_idx < e)
746 {
747 style = style.patch(self.search_bg);
748 }
749 if let Some((lo, hi)) = sel_range
750 && col_idx >= lo
751 && col_idx <= hi
752 {
753 style = style.patch(self.selection_bg);
754 }
755 if is_cursor_row && col_idx == cursor_col {
756 style = style.patch(self.cursor_style);
757 }
758
759 if ch == '\t' {
760 for k in 0..width {
764 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
765 cell.set_char(' ');
766 cell.set_style(style);
767 }
768 }
769 } else if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
770 cell.set_char(ch);
771 cell.set_style(style);
772 }
773 screen_x += width;
774 line_col += visible_width;
775 byte_offset += ch_byte_len;
776 }
777
778 if is_cursor_row
783 && is_last_segment
784 && cursor_col >= line.chars().count()
785 && cursor_col >= seg_start
786 {
787 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
788 if pad_x < row_end_x
789 && let Some(cell) = term_buf.cell_mut((pad_x, y))
790 {
791 cell.set_char(' ');
792 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
793 }
794 }
795 }
796
797 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
800 let mut best: Option<&crate::Span> = None;
805 for span in row_spans {
806 if byte_offset >= span.start_byte && byte_offset < span.end_byte {
807 let len = span.end_byte - span.start_byte;
808 match best {
809 Some(b) if (b.end_byte - b.start_byte) <= len => {}
810 _ => best = Some(span),
811 }
812 }
813 }
814 best.map(|s| self.resolver.resolve(s.style))
815 }
816}
817
818#[cfg(test)]
819mod tests {
820 use super::*;
821 use ratatui::style::{Color, Modifier};
822 use ratatui::widgets::Widget;
823
824 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
825 let area = Rect::new(0, 0, w, h);
826 let mut buf = TermBuffer::empty(area);
827 view.render(area, &mut buf);
828 buf
829 }
830
831 fn no_styles(_id: u32) -> Style {
832 Style::default()
833 }
834
835 fn vp(width: u16, height: u16) -> Viewport {
837 Viewport {
838 top_row: 0,
839 top_col: 0,
840 width,
841 height,
842 wrap: Wrap::None,
843 text_width: width,
844 tab_width: 0,
845 }
846 }
847
848 #[test]
849 fn renders_plain_chars_into_terminal_buffer() {
850 let b = Buffer::from_str("hello\nworld");
851 let v = vp(20, 5);
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 colorcolumn_cols: &[],
870 colorcolumn_style: Style::default(),
871 };
872 let term = run_render(view, 20, 5);
873 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
874 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
875 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
876 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
877 }
878
879 #[test]
880 fn cursor_cell_gets_reversed_style() {
881 let mut b = Buffer::from_str("abc");
882 let v = vp(10, 1);
883 b.set_cursor(crate::Position::new(0, 1));
884 let view = BufferView {
885 buffer: &b,
886 viewport: &v,
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 spans: &[],
898 search_pattern: None,
899 non_text_style: Style::default(),
900 diag_overlays: &[],
901 colorcolumn_cols: &[],
902 colorcolumn_style: Style::default(),
903 };
904 let term = run_render(view, 10, 1);
905 let cursor_cell = term.cell((1, 0)).unwrap();
906 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
907 }
908
909 #[test]
910 fn selection_bg_applies_only_to_selected_cells() {
911 use crate::{Position, Selection};
912 let b = Buffer::from_str("abcdef");
913 let v = vp(10, 1);
914 let view = BufferView {
915 buffer: &b,
916 viewport: &v,
917 selection: Some(Selection::Char {
918 anchor: Position::new(0, 1),
919 head: Position::new(0, 3),
920 }),
921 resolver: &(no_styles as fn(u32) -> Style),
922 cursor_line_bg: Style::default(),
923 cursor_column_bg: Style::default(),
924 selection_bg: Style::default().bg(Color::Blue),
925 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
926 gutter: None,
927 search_bg: Style::default(),
928 signs: &[],
929 conceals: &[],
930 spans: &[],
931 search_pattern: None,
932 non_text_style: Style::default(),
933 diag_overlays: &[],
934 colorcolumn_cols: &[],
935 colorcolumn_style: Style::default(),
936 };
937 let term = run_render(view, 10, 1);
938 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
939 for x in 1..=3 {
940 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
941 }
942 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
943 }
944
945 #[test]
946 fn syntax_span_fg_resolves_via_table() {
947 use crate::Span;
948 let b = Buffer::from_str("SELECT foo");
949 let v = vp(20, 1);
950 let spans = vec![vec![Span::new(0, 6, 7)]];
951 let resolver = |id: u32| -> Style {
952 if id == 7 {
953 Style::default().fg(Color::Red)
954 } else {
955 Style::default()
956 }
957 };
958 let view = BufferView {
959 buffer: &b,
960 viewport: &v,
961 selection: None,
962 resolver: &resolver,
963 cursor_line_bg: Style::default(),
964 cursor_column_bg: Style::default(),
965 selection_bg: Style::default().bg(Color::Blue),
966 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
967 gutter: None,
968 search_bg: Style::default(),
969 signs: &[],
970 conceals: &[],
971 spans: &spans,
972 search_pattern: None,
973 non_text_style: Style::default(),
974 diag_overlays: &[],
975 colorcolumn_cols: &[],
976 colorcolumn_style: Style::default(),
977 };
978 let term = run_render(view, 20, 1);
979 for x in 0..6 {
980 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
981 }
982 }
983
984 #[test]
985 fn gutter_renders_right_aligned_line_numbers() {
986 let b = Buffer::from_str("a\nb\nc");
987 let v = vp(10, 3);
988 let view = BufferView {
989 buffer: &b,
990 viewport: &v,
991 selection: None,
992 resolver: &(no_styles as fn(u32) -> Style),
993 cursor_line_bg: Style::default(),
994 cursor_column_bg: Style::default(),
995 selection_bg: Style::default().bg(Color::Blue),
996 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
997 gutter: Some(Gutter {
998 width: 4,
999 style: Style::default().fg(Color::Yellow),
1000 line_offset: 0,
1001 ..Default::default()
1002 }),
1003 search_bg: Style::default(),
1004 signs: &[],
1005 conceals: &[],
1006 spans: &[],
1007 search_pattern: None,
1008 non_text_style: Style::default(),
1009 diag_overlays: &[],
1010 colorcolumn_cols: &[],
1011 colorcolumn_style: Style::default(),
1012 };
1013 let term = run_render(view, 10, 3);
1014 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1016 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
1017 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1018 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
1019 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1021 }
1022
1023 #[test]
1024 fn gutter_renders_relative_with_cursor_at_zero() {
1025 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1027 b.set_cursor(crate::Position::new(2, 0));
1028 let v = vp(10, 5);
1029 let view = BufferView {
1030 buffer: &b,
1031 viewport: &v,
1032 selection: None,
1033 resolver: &(no_styles as fn(u32) -> Style),
1034 cursor_line_bg: Style::default(),
1035 cursor_column_bg: Style::default(),
1036 selection_bg: Style::default().bg(Color::Blue),
1037 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1038 gutter: Some(Gutter {
1039 width: 4,
1040 style: Style::default().fg(Color::Yellow),
1041 line_offset: 0,
1042 numbers: GutterNumbers::Relative { cursor_row: 2 },
1043 }),
1044 search_bg: Style::default(),
1045 signs: &[],
1046 conceals: &[],
1047 spans: &[],
1048 search_pattern: None,
1049 non_text_style: Style::default(),
1050 diag_overlays: &[],
1051 colorcolumn_cols: &[],
1052 colorcolumn_style: Style::default(),
1053 };
1054 let term = run_render(view, 10, 5);
1055 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "2");
1058 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "1");
1060 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "0");
1062 assert_eq!(term.cell((2, 3)).unwrap().symbol(), "1");
1064 assert_eq!(term.cell((2, 4)).unwrap().symbol(), "2");
1066 }
1067
1068 #[test]
1069 fn gutter_renders_hybrid_cursor_row_absolute() {
1070 let mut b = Buffer::from_str("a\nb\nc");
1073 b.set_cursor(crate::Position::new(1, 0));
1074 let v = vp(10, 3);
1075 let view = BufferView {
1076 buffer: &b,
1077 viewport: &v,
1078 selection: None,
1079 resolver: &(no_styles as fn(u32) -> Style),
1080 cursor_line_bg: Style::default(),
1081 cursor_column_bg: Style::default(),
1082 selection_bg: Style::default().bg(Color::Blue),
1083 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1084 gutter: Some(Gutter {
1085 width: 4,
1086 style: Style::default().fg(Color::Yellow),
1087 line_offset: 0,
1088 numbers: GutterNumbers::Hybrid { cursor_row: 1 },
1089 }),
1090 search_bg: Style::default(),
1091 signs: &[],
1092 conceals: &[],
1093 spans: &[],
1094 search_pattern: None,
1095 non_text_style: Style::default(),
1096 diag_overlays: &[],
1097 colorcolumn_cols: &[],
1098 colorcolumn_style: Style::default(),
1099 };
1100 let term = run_render(view, 10, 3);
1101 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1103 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1105 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "1");
1107 }
1108
1109 #[test]
1110 fn gutter_none_paints_blank_cells() {
1111 let b = Buffer::from_str("a\nb\nc");
1112 let v = vp(10, 3);
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: Some(Gutter {
1123 width: 4,
1124 style: Style::default().fg(Color::Yellow),
1125 line_offset: 0,
1126 numbers: GutterNumbers::None,
1127 }),
1128 search_bg: Style::default(),
1129 signs: &[],
1130 conceals: &[],
1131 spans: &[],
1132 search_pattern: None,
1133 non_text_style: Style::default(),
1134 diag_overlays: &[],
1135 colorcolumn_cols: &[],
1136 colorcolumn_style: Style::default(),
1137 };
1138 let term = run_render(view, 10, 3);
1139 for row in 0..3u16 {
1141 for x in 0..4u16 {
1142 assert_eq!(
1143 term.cell((x, row)).unwrap().symbol(),
1144 " ",
1145 "expected blank at ({x}, {row})"
1146 );
1147 }
1148 }
1149 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1151 }
1152
1153 #[test]
1154 fn search_bg_paints_match_cells() {
1155 use regex::Regex;
1156 let b = Buffer::from_str("foo bar foo");
1157 let v = vp(20, 1);
1158 let pat = Regex::new("foo").unwrap();
1159 let view = BufferView {
1160 buffer: &b,
1161 viewport: &v,
1162 selection: None,
1163 resolver: &(no_styles as fn(u32) -> Style),
1164 cursor_line_bg: Style::default(),
1165 cursor_column_bg: Style::default(),
1166 selection_bg: Style::default().bg(Color::Blue),
1167 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1168 gutter: None,
1169 search_bg: Style::default().bg(Color::Magenta),
1170 signs: &[],
1171 conceals: &[],
1172 spans: &[],
1173 search_pattern: Some(&pat),
1174 non_text_style: Style::default(),
1175 diag_overlays: &[],
1176 colorcolumn_cols: &[],
1177 colorcolumn_style: Style::default(),
1178 };
1179 let term = run_render(view, 20, 1);
1180 for x in 0..3 {
1181 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1182 }
1183 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1185 for x in 8..11 {
1186 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1187 }
1188 }
1189
1190 #[test]
1191 fn search_bg_survives_cursorcolumn_overlay() {
1192 use regex::Regex;
1193 let mut b = Buffer::from_str("foo bar foo");
1197 let v = vp(20, 1);
1198 let pat = Regex::new("foo").unwrap();
1199 b.set_cursor(crate::Position::new(0, 1));
1201 let view = BufferView {
1202 buffer: &b,
1203 viewport: &v,
1204 selection: None,
1205 resolver: &(no_styles as fn(u32) -> Style),
1206 cursor_line_bg: Style::default(),
1207 cursor_column_bg: Style::default().bg(Color::DarkGray),
1208 selection_bg: Style::default().bg(Color::Blue),
1209 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1210 gutter: None,
1211 search_bg: Style::default().bg(Color::Magenta),
1212 signs: &[],
1213 conceals: &[],
1214 spans: &[],
1215 search_pattern: Some(&pat),
1216 non_text_style: Style::default(),
1217 diag_overlays: &[],
1218 colorcolumn_cols: &[],
1219 colorcolumn_style: Style::default(),
1220 };
1221 let term = run_render(view, 20, 1);
1222 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
1224 }
1225
1226 #[test]
1227 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
1228 let b = Buffer::from_str("a\nb\nc");
1229 let v = vp(10, 3);
1230 let signs = [
1231 Sign {
1232 row: 0,
1233 ch: 'W',
1234 style: Style::default().fg(Color::Yellow),
1235 priority: 1,
1236 },
1237 Sign {
1238 row: 0,
1239 ch: 'E',
1240 style: Style::default().fg(Color::Red),
1241 priority: 2,
1242 },
1243 ];
1244 let view = BufferView {
1245 buffer: &b,
1246 viewport: &v,
1247 selection: None,
1248 resolver: &(no_styles as fn(u32) -> Style),
1249 cursor_line_bg: Style::default(),
1250 cursor_column_bg: Style::default(),
1251 selection_bg: Style::default().bg(Color::Blue),
1252 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1253 gutter: Some(Gutter {
1254 width: 3,
1255 style: Style::default().fg(Color::DarkGray),
1256 line_offset: 0,
1257 ..Default::default()
1258 }),
1259 search_bg: Style::default(),
1260 signs: &signs,
1261 conceals: &[],
1262 spans: &[],
1263 search_pattern: None,
1264 non_text_style: Style::default(),
1265 diag_overlays: &[],
1266 colorcolumn_cols: &[],
1267 colorcolumn_style: Style::default(),
1268 };
1269 let term = run_render(view, 10, 3);
1270 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
1271 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1272 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
1274 }
1275
1276 #[test]
1277 fn conceal_replaces_byte_range() {
1278 let b = Buffer::from_str("see https://example.com end");
1279 let v = vp(30, 1);
1280 let conceals = vec![Conceal {
1281 row: 0,
1282 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
1285 }];
1286 let view = BufferView {
1287 buffer: &b,
1288 viewport: &v,
1289 selection: None,
1290 resolver: &(no_styles as fn(u32) -> Style),
1291 cursor_line_bg: Style::default(),
1292 cursor_column_bg: Style::default(),
1293 selection_bg: Style::default(),
1294 cursor_style: Style::default(),
1295 gutter: None,
1296 search_bg: Style::default(),
1297 signs: &[],
1298 conceals: &conceals,
1299 spans: &[],
1300 search_pattern: None,
1301 non_text_style: Style::default(),
1302 diag_overlays: &[],
1303 colorcolumn_cols: &[],
1304 colorcolumn_style: Style::default(),
1305 };
1306 let term = run_render(view, 30, 1);
1307 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
1309 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
1310 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
1313 }
1314
1315 #[test]
1316 fn closed_fold_collapses_rows_and_paints_marker() {
1317 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1318 let v = vp(30, 5);
1319 b.add_fold(1, 3, true);
1321 let view = BufferView {
1322 buffer: &b,
1323 viewport: &v,
1324 selection: None,
1325 resolver: &(no_styles as fn(u32) -> Style),
1326 cursor_line_bg: Style::default(),
1327 cursor_column_bg: Style::default(),
1328 selection_bg: Style::default().bg(Color::Blue),
1329 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1330 gutter: None,
1331 search_bg: Style::default(),
1332 signs: &[],
1333 conceals: &[],
1334 spans: &[],
1335 search_pattern: None,
1336 non_text_style: Style::default(),
1337 diag_overlays: &[],
1338 colorcolumn_cols: &[],
1339 colorcolumn_style: Style::default(),
1340 };
1341 let term = run_render(view, 30, 5);
1342 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1344 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
1347 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
1349 }
1350
1351 #[test]
1352 fn open_fold_renders_normally() {
1353 let mut b = Buffer::from_str("a\nb\nc");
1354 let v = vp(5, 3);
1355 b.add_fold(0, 2, false); let view = BufferView {
1357 buffer: &b,
1358 viewport: &v,
1359 selection: None,
1360 resolver: &(no_styles as fn(u32) -> Style),
1361 cursor_line_bg: Style::default(),
1362 cursor_column_bg: Style::default(),
1363 selection_bg: Style::default().bg(Color::Blue),
1364 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1365 gutter: None,
1366 search_bg: Style::default(),
1367 signs: &[],
1368 conceals: &[],
1369 spans: &[],
1370 search_pattern: None,
1371 non_text_style: Style::default(),
1372 diag_overlays: &[],
1373 colorcolumn_cols: &[],
1374 colorcolumn_style: Style::default(),
1375 };
1376 let term = run_render(view, 5, 3);
1377 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1378 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1379 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
1380 }
1381
1382 #[test]
1383 fn horizontal_scroll_clips_left_chars() {
1384 let b = Buffer::from_str("abcdefgh");
1385 let mut v = vp(4, 1);
1386 v.top_col = 3;
1387 let view = BufferView {
1388 buffer: &b,
1389 viewport: &v,
1390 selection: None,
1391 resolver: &(no_styles as fn(u32) -> Style),
1392 cursor_line_bg: Style::default(),
1393 cursor_column_bg: Style::default(),
1394 selection_bg: Style::default().bg(Color::Blue),
1395 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1396 gutter: None,
1397 search_bg: Style::default(),
1398 signs: &[],
1399 conceals: &[],
1400 spans: &[],
1401 search_pattern: None,
1402 non_text_style: Style::default(),
1403 diag_overlays: &[],
1404 colorcolumn_cols: &[],
1405 colorcolumn_style: Style::default(),
1406 };
1407 let term = run_render(view, 4, 1);
1408 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
1409 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
1410 }
1411
1412 fn make_wrap_view<'a>(
1413 b: &'a Buffer,
1414 viewport: &'a Viewport,
1415 resolver: &'a (impl StyleResolver + 'a),
1416 gutter: Option<Gutter>,
1417 ) -> BufferView<'a, impl StyleResolver + 'a> {
1418 BufferView {
1419 buffer: b,
1420 viewport,
1421 selection: None,
1422 resolver,
1423 cursor_line_bg: Style::default(),
1424 cursor_column_bg: Style::default(),
1425 selection_bg: Style::default().bg(Color::Blue),
1426 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1427 gutter,
1428 search_bg: Style::default(),
1429 signs: &[],
1430 conceals: &[],
1431 spans: &[],
1432 search_pattern: None,
1433 non_text_style: Style::default(),
1434 diag_overlays: &[],
1435 colorcolumn_cols: &[],
1436 colorcolumn_style: Style::default(),
1437 }
1438 }
1439
1440 #[test]
1441 fn wrap_segments_char_breaks_at_width() {
1442 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1443 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1444 }
1445
1446 #[test]
1447 fn wrap_segments_word_backs_up_to_whitespace() {
1448 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1449 assert_eq!(segs[0], (0, 6));
1451 assert_eq!(segs[1], (6, 11));
1453 assert_eq!(segs[2], (11, 16));
1454 }
1455
1456 #[test]
1457 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1458 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1459 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1461 }
1462
1463 #[test]
1464 fn wrap_char_paints_continuation_rows() {
1465 let b = Buffer::from_str("abcdefghij");
1466 let v = Viewport {
1467 top_row: 0,
1468 top_col: 0,
1469 width: 4,
1470 height: 3,
1471 wrap: Wrap::Char,
1472 text_width: 4,
1473 tab_width: 0,
1474 };
1475 let r = no_styles as fn(u32) -> Style;
1476 let view = make_wrap_view(&b, &v, &r, None);
1477 let term = run_render(view, 4, 3);
1478 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1480 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1481 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1483 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1484 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1486 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1487 }
1488
1489 #[test]
1490 fn wrap_char_gutter_blank_on_continuation() {
1491 let b = Buffer::from_str("abcdefgh");
1492 let v = Viewport {
1493 top_row: 0,
1494 top_col: 0,
1495 width: 6,
1496 height: 3,
1497 wrap: Wrap::Char,
1498 text_width: 3,
1500 tab_width: 0,
1501 };
1502 let r = no_styles as fn(u32) -> Style;
1503 let gutter = Gutter {
1504 width: 3,
1505 style: Style::default().fg(Color::Yellow),
1506 line_offset: 0,
1507 ..Default::default()
1508 };
1509 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1510 let term = run_render(view, 6, 3);
1511 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1513 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1514 for x in 0..2 {
1516 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1517 }
1518 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1519 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1520 }
1521
1522 #[test]
1523 fn wrap_char_cursor_lands_on_correct_segment() {
1524 let mut b = Buffer::from_str("abcdefghij");
1525 let v = Viewport {
1526 top_row: 0,
1527 top_col: 0,
1528 width: 4,
1529 height: 3,
1530 wrap: Wrap::Char,
1531 text_width: 4,
1532 tab_width: 0,
1533 };
1534 b.set_cursor(crate::Position::new(0, 6));
1536 let r = no_styles as fn(u32) -> Style;
1537 let mut view = make_wrap_view(&b, &v, &r, None);
1538 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1539 let term = run_render(view, 4, 3);
1540 assert!(
1541 term.cell((2, 1))
1542 .unwrap()
1543 .modifier
1544 .contains(Modifier::REVERSED)
1545 );
1546 }
1547
1548 #[test]
1549 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1550 let mut b = Buffer::from_str("abcdef");
1551 let v = Viewport {
1552 top_row: 0,
1553 top_col: 0,
1554 width: 4,
1555 height: 3,
1556 wrap: Wrap::Char,
1557 text_width: 4,
1558 tab_width: 0,
1559 };
1560 b.set_cursor(crate::Position::new(0, 6));
1562 let r = no_styles as fn(u32) -> Style;
1563 let mut view = make_wrap_view(&b, &v, &r, None);
1564 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1565 let term = run_render(view, 4, 3);
1566 assert!(
1568 term.cell((2, 1))
1569 .unwrap()
1570 .modifier
1571 .contains(Modifier::REVERSED)
1572 );
1573 }
1574
1575 #[test]
1576 fn wrap_word_breaks_at_whitespace() {
1577 let b = Buffer::from_str("alpha beta gamma");
1578 let v = Viewport {
1579 top_row: 0,
1580 top_col: 0,
1581 width: 8,
1582 height: 3,
1583 wrap: Wrap::Word,
1584 text_width: 8,
1585 tab_width: 0,
1586 };
1587 let r = no_styles as fn(u32) -> Style;
1588 let view = make_wrap_view(&b, &v, &r, None);
1589 let term = run_render(view, 8, 3);
1590 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1592 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1593 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1595 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1596 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1598 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1599 }
1600
1601 fn view_with<'a>(
1607 b: &'a Buffer,
1608 viewport: &'a Viewport,
1609 resolver: &'a (impl StyleResolver + 'a),
1610 spans: &'a [Vec<Span>],
1611 search_pattern: Option<&'a regex::Regex>,
1612 ) -> BufferView<'a, impl StyleResolver + 'a> {
1613 BufferView {
1614 buffer: b,
1615 viewport,
1616 selection: None,
1617 resolver,
1618 cursor_line_bg: Style::default(),
1619 cursor_column_bg: Style::default(),
1620 selection_bg: Style::default().bg(Color::Blue),
1621 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1622 gutter: None,
1623 search_bg: Style::default().bg(Color::Magenta),
1624 signs: &[],
1625 conceals: &[],
1626 spans,
1627 search_pattern,
1628 non_text_style: Style::default(),
1629 diag_overlays: &[],
1630 colorcolumn_cols: &[],
1631 colorcolumn_style: Style::default(),
1632 }
1633 }
1634
1635 #[test]
1636 fn empty_spans_param_renders_default_style() {
1637 let b = Buffer::from_str("hello");
1638 let v = vp(10, 1);
1639 let r = no_styles as fn(u32) -> Style;
1640 let view = view_with(&b, &v, &r, &[], None);
1641 let term = run_render(view, 10, 1);
1642 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1643 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
1644 }
1645
1646 #[test]
1647 fn spans_param_paints_styled_byte_range() {
1648 let b = Buffer::from_str("abcdef");
1649 let v = vp(10, 1);
1650 let resolver = |id: u32| -> Style {
1651 if id == 3 {
1652 Style::default().fg(Color::Green)
1653 } else {
1654 Style::default()
1655 }
1656 };
1657 let spans = vec![vec![Span::new(0, 3, 3)]];
1658 let view = view_with(&b, &v, &resolver, &spans, None);
1659 let term = run_render(view, 10, 1);
1660 for x in 0..3 {
1661 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
1662 }
1663 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
1664 }
1665
1666 #[test]
1667 fn spans_param_handles_per_row_overlay() {
1668 let b = Buffer::from_str("abc\ndef");
1669 let v = vp(10, 2);
1670 let resolver = |id: u32| -> Style {
1671 if id == 1 {
1672 Style::default().fg(Color::Red)
1673 } else {
1674 Style::default().fg(Color::Green)
1675 }
1676 };
1677 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
1678 let view = view_with(&b, &v, &resolver, &spans, None);
1679 let term = run_render(view, 10, 2);
1680 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1681 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
1682 }
1683
1684 #[test]
1685 fn spans_param_rows_beyond_get_no_styling() {
1686 let b = Buffer::from_str("abc\ndef\nghi");
1687 let v = vp(10, 3);
1688 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1689 let spans = vec![vec![Span::new(0, 3, 0)]];
1691 let view = view_with(&b, &v, &resolver, &spans, None);
1692 let term = run_render(view, 10, 3);
1693 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1694 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
1695 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
1696 }
1697
1698 #[test]
1699 fn search_pattern_none_disables_hlsearch() {
1700 let b = Buffer::from_str("foo bar foo");
1701 let v = vp(20, 1);
1702 let r = no_styles as fn(u32) -> Style;
1703 let view = view_with(&b, &v, &r, &[], None);
1705 let term = run_render(view, 20, 1);
1706 for x in 0..11 {
1707 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1708 }
1709 }
1710
1711 #[test]
1712 fn search_pattern_regex_paints_match_bg() {
1713 use regex::Regex;
1714 let b = Buffer::from_str("xyz foo xyz");
1715 let v = vp(20, 1);
1716 let r = no_styles as fn(u32) -> Style;
1717 let pat = Regex::new("foo").unwrap();
1718 let view = view_with(&b, &v, &r, &[], Some(&pat));
1719 let term = run_render(view, 20, 1);
1720 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1722 for x in 4..7 {
1723 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1724 }
1725 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
1726 }
1727
1728 #[test]
1729 fn search_pattern_unicode_columns_are_charwise() {
1730 use regex::Regex;
1731 let b = Buffer::from_str("tablé foo");
1733 let v = vp(20, 1);
1734 let r = no_styles as fn(u32) -> Style;
1735 let pat = Regex::new("foo").unwrap();
1736 let view = view_with(&b, &v, &r, &[], Some(&pat));
1737 let term = run_render(view, 20, 1);
1738 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
1740 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
1741 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
1742 }
1743
1744 #[test]
1745 fn spans_param_clamps_short_row_overlay() {
1746 let b = Buffer::from_str("abc");
1748 let v = vp(10, 1);
1749 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1750 let spans = vec![vec![Span::new(0, 100, 0)]];
1751 let view = view_with(&b, &v, &resolver, &spans, None);
1752 let term = run_render(view, 10, 1);
1753 for x in 0..3 {
1754 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1755 }
1756 }
1757
1758 #[test]
1759 fn spans_and_search_pattern_compose() {
1760 use regex::Regex;
1762 let b = Buffer::from_str("foo");
1763 let v = vp(10, 1);
1764 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
1765 let spans = vec![vec![Span::new(0, 3, 0)]];
1766 let pat = Regex::new("foo").unwrap();
1767 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
1768 let term = run_render(view, 10, 1);
1769 let cell = term.cell((1, 0)).unwrap();
1770 assert_eq!(cell.fg, Color::Green);
1771 assert_eq!(cell.bg, Color::Magenta);
1772 }
1773
1774 #[test]
1778 fn tilde_marker_painted_past_eof() {
1779 let b = Buffer::from_str("a\nb\nc\nd\ne");
1781 let v = vp(10, 10);
1782 let r = no_styles as fn(u32) -> Style;
1783 let non_text_fg = Color::DarkGray;
1784 let view = BufferView {
1785 buffer: &b,
1786 viewport: &v,
1787 selection: None,
1788 resolver: &r,
1789 cursor_line_bg: Style::default(),
1790 cursor_column_bg: Style::default(),
1791 selection_bg: Style::default().bg(Color::Blue),
1792 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1793 gutter: None,
1794 search_bg: Style::default(),
1795 signs: &[],
1796 conceals: &[],
1797 spans: &[],
1798 search_pattern: None,
1799 non_text_style: Style::default().fg(non_text_fg),
1800 diag_overlays: &[],
1801 colorcolumn_cols: &[],
1802 colorcolumn_style: Style::default(),
1803 };
1804 let term = run_render(view, 10, 10);
1805 for row in 0..5u16 {
1807 assert_ne!(
1808 term.cell((0, row)).unwrap().symbol(),
1809 "~",
1810 "row {row} is a content row, expected no tilde"
1811 );
1812 }
1813 for row in 5..10u16 {
1815 let cell = term.cell((0, row)).unwrap();
1816 assert_eq!(cell.symbol(), "~", "row {row} is past EOF, expected tilde");
1817 assert_eq!(
1818 cell.fg, non_text_fg,
1819 "row {row} tilde should use non_text_style fg"
1820 );
1821 for x in 1..10u16 {
1823 assert_eq!(
1824 term.cell((x, row)).unwrap().symbol(),
1825 " ",
1826 "row {row} col {x} after tilde should be blank"
1827 );
1828 }
1829 }
1830 }
1831
1832 #[test]
1835 fn tilde_marker_with_gutter_past_eof() {
1836 let b = Buffer::from_str("a\nb");
1837 let v = vp(10, 5);
1838 let r = no_styles as fn(u32) -> Style;
1839 let non_text_fg = Color::DarkGray;
1840 let view = BufferView {
1841 buffer: &b,
1842 viewport: &v,
1843 selection: None,
1844 resolver: &r,
1845 cursor_line_bg: Style::default(),
1846 cursor_column_bg: Style::default(),
1847 selection_bg: Style::default().bg(Color::Blue),
1848 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1849 gutter: Some(Gutter {
1850 width: 4,
1851 style: Style::default().fg(Color::Yellow),
1852 line_offset: 0,
1853 numbers: GutterNumbers::Absolute,
1854 }),
1855 search_bg: Style::default(),
1856 signs: &[],
1857 conceals: &[],
1858 spans: &[],
1859 search_pattern: None,
1860 non_text_style: Style::default().fg(non_text_fg),
1861 diag_overlays: &[],
1862 colorcolumn_cols: &[],
1863 colorcolumn_style: Style::default(),
1864 };
1865 let term = run_render(view, 10, 5);
1866 for row in 2..5u16 {
1868 for x in 0..4u16 {
1870 assert_eq!(
1871 term.cell((x, row)).unwrap().symbol(),
1872 " ",
1873 "gutter col {x} on past-EOF row {row} should be blank"
1874 );
1875 }
1876 let cell = term.cell((4, row)).unwrap();
1878 assert_eq!(
1879 cell.symbol(),
1880 "~",
1881 "past-EOF row {row}: expected tilde at text column"
1882 );
1883 assert_eq!(cell.fg, non_text_fg);
1884 }
1885 }
1886
1887 #[test]
1888 fn diag_overlay_paints_underline_on_range() {
1889 let b = Buffer::from_str("hello world");
1893 let v = vp(20, 2);
1894 let overlay = DiagOverlay {
1895 row: 0,
1896 col_start: 6,
1897 col_end: 11,
1898 style: Style::default().add_modifier(Modifier::UNDERLINED),
1899 };
1900 let view = BufferView {
1901 buffer: &b,
1902 viewport: &v,
1903 selection: None,
1904 resolver: &(no_styles as fn(u32) -> Style),
1905 cursor_line_bg: Style::default(),
1906 cursor_column_bg: Style::default(),
1907 selection_bg: Style::default().bg(Color::Blue),
1908 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1909 gutter: None,
1910 search_bg: Style::default(),
1911 signs: &[],
1912 conceals: &[],
1913 spans: &[],
1914 search_pattern: None,
1915 non_text_style: Style::default(),
1916 diag_overlays: &[overlay],
1917 colorcolumn_cols: &[],
1918 colorcolumn_style: Style::default(),
1919 };
1920 let term = run_render(view, 20, 2);
1921
1922 for x in 0u16..6 {
1924 let cell = term.cell((x, 0)).unwrap();
1925 assert!(
1926 !cell.modifier.contains(Modifier::UNDERLINED),
1927 "col {x} must not be underlined (outside overlay)"
1928 );
1929 }
1930 for x in 6u16..11 {
1932 let cell = term.cell((x, 0)).unwrap();
1933 assert!(
1934 cell.modifier.contains(Modifier::UNDERLINED),
1935 "col {x} must be underlined (inside overlay)"
1936 );
1937 }
1938 let cell = term.cell((11, 0)).unwrap();
1940 assert!(
1941 !cell.modifier.contains(Modifier::UNDERLINED),
1942 "col 11 must not be underlined (past overlay end)"
1943 );
1944 }
1945
1946 #[test]
1947 fn diag_overlay_out_of_viewport_is_ignored() {
1948 let b = Buffer::from_str("a\nb\nc");
1950 let v = vp(10, 3);
1951 let overlay = DiagOverlay {
1952 row: 5,
1953 col_start: 0,
1954 col_end: 1,
1955 style: Style::default().add_modifier(Modifier::UNDERLINED),
1956 };
1957 let view = BufferView {
1958 buffer: &b,
1959 viewport: &v,
1960 selection: None,
1961 resolver: &(no_styles as fn(u32) -> Style),
1962 cursor_line_bg: Style::default(),
1963 cursor_column_bg: Style::default(),
1964 selection_bg: Style::default().bg(Color::Blue),
1965 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1966 gutter: None,
1967 search_bg: Style::default(),
1968 signs: &[],
1969 conceals: &[],
1970 spans: &[],
1971 search_pattern: None,
1972 non_text_style: Style::default(),
1973 diag_overlays: &[overlay],
1974 colorcolumn_cols: &[],
1975 colorcolumn_style: Style::default(),
1976 };
1977 let _term = run_render(view, 10, 3);
1979 }
1980}