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> {
814 let mut overlapping: Vec<&crate::Span> = row_spans
816 .iter()
817 .filter(|s| byte_offset >= s.start_byte && byte_offset < s.end_byte)
818 .collect();
819 if overlapping.is_empty() {
820 return None;
821 }
822 overlapping.sort_by_key(|s| std::cmp::Reverse(s.end_byte.saturating_sub(s.start_byte)));
823 let mut style = self.resolver.resolve(overlapping[0].style);
824 for s in &overlapping[1..] {
825 style = style.patch(self.resolver.resolve(s.style));
826 }
827 Some(style)
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834 use ratatui::style::{Color, Modifier};
835 use ratatui::widgets::Widget;
836
837 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
838 let area = Rect::new(0, 0, w, h);
839 let mut buf = TermBuffer::empty(area);
840 view.render(area, &mut buf);
841 buf
842 }
843
844 fn no_styles(_id: u32) -> Style {
845 Style::default()
846 }
847
848 fn vp(width: u16, height: u16) -> Viewport {
850 Viewport {
851 top_row: 0,
852 top_col: 0,
853 width,
854 height,
855 wrap: Wrap::None,
856 text_width: width,
857 tab_width: 0,
858 }
859 }
860
861 #[test]
862 fn renders_plain_chars_into_terminal_buffer() {
863 let b = Buffer::from_str("hello\nworld");
864 let v = vp(20, 5);
865 let view = BufferView {
866 buffer: &b,
867 viewport: &v,
868 selection: None,
869 resolver: &(no_styles as fn(u32) -> Style),
870 cursor_line_bg: Style::default(),
871 cursor_column_bg: Style::default(),
872 selection_bg: Style::default().bg(Color::Blue),
873 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
874 gutter: None,
875 search_bg: Style::default(),
876 signs: &[],
877 conceals: &[],
878 spans: &[],
879 search_pattern: None,
880 non_text_style: Style::default(),
881 diag_overlays: &[],
882 colorcolumn_cols: &[],
883 colorcolumn_style: Style::default(),
884 };
885 let term = run_render(view, 20, 5);
886 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
887 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
888 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
889 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
890 }
891
892 #[test]
893 fn cursor_cell_gets_reversed_style() {
894 let mut b = Buffer::from_str("abc");
895 let v = vp(10, 1);
896 b.set_cursor(crate::Position::new(0, 1));
897 let view = BufferView {
898 buffer: &b,
899 viewport: &v,
900 selection: None,
901 resolver: &(no_styles as fn(u32) -> Style),
902 cursor_line_bg: Style::default(),
903 cursor_column_bg: Style::default(),
904 selection_bg: Style::default().bg(Color::Blue),
905 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
906 gutter: None,
907 search_bg: Style::default(),
908 signs: &[],
909 conceals: &[],
910 spans: &[],
911 search_pattern: None,
912 non_text_style: Style::default(),
913 diag_overlays: &[],
914 colorcolumn_cols: &[],
915 colorcolumn_style: Style::default(),
916 };
917 let term = run_render(view, 10, 1);
918 let cursor_cell = term.cell((1, 0)).unwrap();
919 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
920 }
921
922 #[test]
923 fn selection_bg_applies_only_to_selected_cells() {
924 use crate::{Position, Selection};
925 let b = Buffer::from_str("abcdef");
926 let v = vp(10, 1);
927 let view = BufferView {
928 buffer: &b,
929 viewport: &v,
930 selection: Some(Selection::Char {
931 anchor: Position::new(0, 1),
932 head: Position::new(0, 3),
933 }),
934 resolver: &(no_styles as fn(u32) -> Style),
935 cursor_line_bg: Style::default(),
936 cursor_column_bg: Style::default(),
937 selection_bg: Style::default().bg(Color::Blue),
938 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
939 gutter: None,
940 search_bg: Style::default(),
941 signs: &[],
942 conceals: &[],
943 spans: &[],
944 search_pattern: None,
945 non_text_style: Style::default(),
946 diag_overlays: &[],
947 colorcolumn_cols: &[],
948 colorcolumn_style: Style::default(),
949 };
950 let term = run_render(view, 10, 1);
951 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
952 for x in 1..=3 {
953 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
954 }
955 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
956 }
957
958 #[test]
959 fn layered_spans_blend_broad_bg_with_narrow_fg() {
960 use crate::Span;
966 let b = Buffer::from_str("fn main() {}");
967 let v = vp(20, 1);
968 let spans = vec![vec![
970 Span::new(0, 12, 1), Span::new(0, 2, 2), ]];
973 let resolver = |id: u32| -> Style {
974 match id {
975 1 => Style::default().bg(Color::DarkGray),
976 2 => Style::default().fg(Color::Magenta),
977 _ => Style::default(),
978 }
979 };
980 let view = BufferView {
981 buffer: &b,
982 viewport: &v,
983 selection: None,
984 resolver: &resolver,
985 cursor_line_bg: Style::default(),
986 cursor_column_bg: Style::default(),
987 selection_bg: Style::default().bg(Color::Blue),
988 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
989 gutter: None,
990 search_bg: Style::default(),
991 signs: &[],
992 conceals: &[],
993 spans: &spans,
994 search_pattern: None,
995 non_text_style: Style::default(),
996 diag_overlays: &[],
997 colorcolumn_cols: &[],
998 colorcolumn_style: Style::default(),
999 };
1000 let term = run_render(view, 20, 1);
1001 for x in 0u16..2 {
1003 let cell = term.cell((x, 0)).unwrap();
1004 assert_eq!(cell.fg, Color::Magenta, "col {x}: fg from narrow span");
1005 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1006 }
1007 for x in 2u16..12 {
1009 let cell = term.cell((x, 0)).unwrap();
1010 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1011 assert_eq!(
1012 cell.fg,
1013 Color::Reset,
1014 "col {x}: no fg set (broad span is bg-only)"
1015 );
1016 }
1017 }
1018
1019 #[test]
1020 fn narrow_span_with_explicit_bg_still_overrides_broad_bg() {
1021 use crate::Span;
1026 let b = Buffer::from_str("hello world");
1027 let v = vp(20, 1);
1028 let spans = vec![vec![
1029 Span::new(0, 11, 1), Span::new(6, 11, 2), ]];
1032 let resolver = |id: u32| -> Style {
1033 match id {
1034 1 => Style::default().bg(Color::DarkGray),
1035 2 => Style::default().bg(Color::Red),
1036 _ => Style::default(),
1037 }
1038 };
1039 let view = BufferView {
1040 buffer: &b,
1041 viewport: &v,
1042 selection: None,
1043 resolver: &resolver,
1044 cursor_line_bg: Style::default(),
1045 cursor_column_bg: Style::default(),
1046 selection_bg: Style::default().bg(Color::Blue),
1047 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1048 gutter: None,
1049 search_bg: Style::default(),
1050 signs: &[],
1051 conceals: &[],
1052 spans: &spans,
1053 search_pattern: None,
1054 non_text_style: Style::default(),
1055 diag_overlays: &[],
1056 colorcolumn_cols: &[],
1057 colorcolumn_style: Style::default(),
1058 };
1059 let term = run_render(view, 20, 1);
1060 for x in 0u16..6 {
1062 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::DarkGray);
1063 }
1064 for x in 6u16..11 {
1066 assert_eq!(
1067 term.cell((x, 0)).unwrap().bg,
1068 Color::Red,
1069 "col {x}: narrow span's bg overrides broad bg"
1070 );
1071 }
1072 }
1073
1074 #[test]
1075 fn syntax_span_fg_resolves_via_table() {
1076 use crate::Span;
1077 let b = Buffer::from_str("SELECT foo");
1078 let v = vp(20, 1);
1079 let spans = vec![vec![Span::new(0, 6, 7)]];
1080 let resolver = |id: u32| -> Style {
1081 if id == 7 {
1082 Style::default().fg(Color::Red)
1083 } else {
1084 Style::default()
1085 }
1086 };
1087 let view = BufferView {
1088 buffer: &b,
1089 viewport: &v,
1090 selection: None,
1091 resolver: &resolver,
1092 cursor_line_bg: Style::default(),
1093 cursor_column_bg: Style::default(),
1094 selection_bg: Style::default().bg(Color::Blue),
1095 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1096 gutter: None,
1097 search_bg: Style::default(),
1098 signs: &[],
1099 conceals: &[],
1100 spans: &spans,
1101 search_pattern: None,
1102 non_text_style: Style::default(),
1103 diag_overlays: &[],
1104 colorcolumn_cols: &[],
1105 colorcolumn_style: Style::default(),
1106 };
1107 let term = run_render(view, 20, 1);
1108 for x in 0..6 {
1109 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1110 }
1111 }
1112
1113 #[test]
1114 fn gutter_renders_right_aligned_line_numbers() {
1115 let b = Buffer::from_str("a\nb\nc");
1116 let v = vp(10, 3);
1117 let view = BufferView {
1118 buffer: &b,
1119 viewport: &v,
1120 selection: None,
1121 resolver: &(no_styles as fn(u32) -> Style),
1122 cursor_line_bg: Style::default(),
1123 cursor_column_bg: Style::default(),
1124 selection_bg: Style::default().bg(Color::Blue),
1125 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1126 gutter: Some(Gutter {
1127 width: 4,
1128 style: Style::default().fg(Color::Yellow),
1129 line_offset: 0,
1130 ..Default::default()
1131 }),
1132 search_bg: Style::default(),
1133 signs: &[],
1134 conceals: &[],
1135 spans: &[],
1136 search_pattern: None,
1137 non_text_style: Style::default(),
1138 diag_overlays: &[],
1139 colorcolumn_cols: &[],
1140 colorcolumn_style: Style::default(),
1141 };
1142 let term = run_render(view, 10, 3);
1143 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1145 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
1146 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1147 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
1148 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1150 }
1151
1152 #[test]
1153 fn gutter_renders_relative_with_cursor_at_zero() {
1154 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1156 b.set_cursor(crate::Position::new(2, 0));
1157 let v = vp(10, 5);
1158 let view = BufferView {
1159 buffer: &b,
1160 viewport: &v,
1161 selection: None,
1162 resolver: &(no_styles as fn(u32) -> Style),
1163 cursor_line_bg: Style::default(),
1164 cursor_column_bg: Style::default(),
1165 selection_bg: Style::default().bg(Color::Blue),
1166 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1167 gutter: Some(Gutter {
1168 width: 4,
1169 style: Style::default().fg(Color::Yellow),
1170 line_offset: 0,
1171 numbers: GutterNumbers::Relative { cursor_row: 2 },
1172 }),
1173 search_bg: Style::default(),
1174 signs: &[],
1175 conceals: &[],
1176 spans: &[],
1177 search_pattern: None,
1178 non_text_style: Style::default(),
1179 diag_overlays: &[],
1180 colorcolumn_cols: &[],
1181 colorcolumn_style: Style::default(),
1182 };
1183 let term = run_render(view, 10, 5);
1184 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "2");
1187 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "1");
1189 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "0");
1191 assert_eq!(term.cell((2, 3)).unwrap().symbol(), "1");
1193 assert_eq!(term.cell((2, 4)).unwrap().symbol(), "2");
1195 }
1196
1197 #[test]
1198 fn gutter_renders_hybrid_cursor_row_absolute() {
1199 let mut b = Buffer::from_str("a\nb\nc");
1202 b.set_cursor(crate::Position::new(1, 0));
1203 let v = vp(10, 3);
1204 let view = BufferView {
1205 buffer: &b,
1206 viewport: &v,
1207 selection: None,
1208 resolver: &(no_styles as fn(u32) -> Style),
1209 cursor_line_bg: Style::default(),
1210 cursor_column_bg: Style::default(),
1211 selection_bg: Style::default().bg(Color::Blue),
1212 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1213 gutter: Some(Gutter {
1214 width: 4,
1215 style: Style::default().fg(Color::Yellow),
1216 line_offset: 0,
1217 numbers: GutterNumbers::Hybrid { cursor_row: 1 },
1218 }),
1219 search_bg: Style::default(),
1220 signs: &[],
1221 conceals: &[],
1222 spans: &[],
1223 search_pattern: None,
1224 non_text_style: Style::default(),
1225 diag_overlays: &[],
1226 colorcolumn_cols: &[],
1227 colorcolumn_style: Style::default(),
1228 };
1229 let term = run_render(view, 10, 3);
1230 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1232 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1234 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "1");
1236 }
1237
1238 #[test]
1239 fn gutter_none_paints_blank_cells() {
1240 let b = Buffer::from_str("a\nb\nc");
1241 let v = vp(10, 3);
1242 let view = BufferView {
1243 buffer: &b,
1244 viewport: &v,
1245 selection: None,
1246 resolver: &(no_styles as fn(u32) -> Style),
1247 cursor_line_bg: Style::default(),
1248 cursor_column_bg: Style::default(),
1249 selection_bg: Style::default().bg(Color::Blue),
1250 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1251 gutter: Some(Gutter {
1252 width: 4,
1253 style: Style::default().fg(Color::Yellow),
1254 line_offset: 0,
1255 numbers: GutterNumbers::None,
1256 }),
1257 search_bg: Style::default(),
1258 signs: &[],
1259 conceals: &[],
1260 spans: &[],
1261 search_pattern: None,
1262 non_text_style: Style::default(),
1263 diag_overlays: &[],
1264 colorcolumn_cols: &[],
1265 colorcolumn_style: Style::default(),
1266 };
1267 let term = run_render(view, 10, 3);
1268 for row in 0..3u16 {
1270 for x in 0..4u16 {
1271 assert_eq!(
1272 term.cell((x, row)).unwrap().symbol(),
1273 " ",
1274 "expected blank at ({x}, {row})"
1275 );
1276 }
1277 }
1278 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1280 }
1281
1282 #[test]
1283 fn search_bg_paints_match_cells() {
1284 use regex::Regex;
1285 let b = Buffer::from_str("foo bar foo");
1286 let v = vp(20, 1);
1287 let pat = Regex::new("foo").unwrap();
1288 let view = BufferView {
1289 buffer: &b,
1290 viewport: &v,
1291 selection: None,
1292 resolver: &(no_styles as fn(u32) -> Style),
1293 cursor_line_bg: Style::default(),
1294 cursor_column_bg: Style::default(),
1295 selection_bg: Style::default().bg(Color::Blue),
1296 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1297 gutter: None,
1298 search_bg: Style::default().bg(Color::Magenta),
1299 signs: &[],
1300 conceals: &[],
1301 spans: &[],
1302 search_pattern: Some(&pat),
1303 non_text_style: Style::default(),
1304 diag_overlays: &[],
1305 colorcolumn_cols: &[],
1306 colorcolumn_style: Style::default(),
1307 };
1308 let term = run_render(view, 20, 1);
1309 for x in 0..3 {
1310 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1311 }
1312 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1314 for x in 8..11 {
1315 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1316 }
1317 }
1318
1319 #[test]
1320 fn search_bg_survives_cursorcolumn_overlay() {
1321 use regex::Regex;
1322 let mut b = Buffer::from_str("foo bar foo");
1326 let v = vp(20, 1);
1327 let pat = Regex::new("foo").unwrap();
1328 b.set_cursor(crate::Position::new(0, 1));
1330 let view = BufferView {
1331 buffer: &b,
1332 viewport: &v,
1333 selection: None,
1334 resolver: &(no_styles as fn(u32) -> Style),
1335 cursor_line_bg: Style::default(),
1336 cursor_column_bg: Style::default().bg(Color::DarkGray),
1337 selection_bg: Style::default().bg(Color::Blue),
1338 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1339 gutter: None,
1340 search_bg: Style::default().bg(Color::Magenta),
1341 signs: &[],
1342 conceals: &[],
1343 spans: &[],
1344 search_pattern: Some(&pat),
1345 non_text_style: Style::default(),
1346 diag_overlays: &[],
1347 colorcolumn_cols: &[],
1348 colorcolumn_style: Style::default(),
1349 };
1350 let term = run_render(view, 20, 1);
1351 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
1353 }
1354
1355 #[test]
1356 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
1357 let b = Buffer::from_str("a\nb\nc");
1358 let v = vp(10, 3);
1359 let signs = [
1360 Sign {
1361 row: 0,
1362 ch: 'W',
1363 style: Style::default().fg(Color::Yellow),
1364 priority: 1,
1365 },
1366 Sign {
1367 row: 0,
1368 ch: 'E',
1369 style: Style::default().fg(Color::Red),
1370 priority: 2,
1371 },
1372 ];
1373 let view = BufferView {
1374 buffer: &b,
1375 viewport: &v,
1376 selection: None,
1377 resolver: &(no_styles as fn(u32) -> Style),
1378 cursor_line_bg: Style::default(),
1379 cursor_column_bg: Style::default(),
1380 selection_bg: Style::default().bg(Color::Blue),
1381 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1382 gutter: Some(Gutter {
1383 width: 3,
1384 style: Style::default().fg(Color::DarkGray),
1385 line_offset: 0,
1386 ..Default::default()
1387 }),
1388 search_bg: Style::default(),
1389 signs: &signs,
1390 conceals: &[],
1391 spans: &[],
1392 search_pattern: None,
1393 non_text_style: Style::default(),
1394 diag_overlays: &[],
1395 colorcolumn_cols: &[],
1396 colorcolumn_style: Style::default(),
1397 };
1398 let term = run_render(view, 10, 3);
1399 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
1400 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1401 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
1403 }
1404
1405 #[test]
1406 fn conceal_replaces_byte_range() {
1407 let b = Buffer::from_str("see https://example.com end");
1408 let v = vp(30, 1);
1409 let conceals = vec![Conceal {
1410 row: 0,
1411 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
1414 }];
1415 let view = BufferView {
1416 buffer: &b,
1417 viewport: &v,
1418 selection: None,
1419 resolver: &(no_styles as fn(u32) -> Style),
1420 cursor_line_bg: Style::default(),
1421 cursor_column_bg: Style::default(),
1422 selection_bg: Style::default(),
1423 cursor_style: Style::default(),
1424 gutter: None,
1425 search_bg: Style::default(),
1426 signs: &[],
1427 conceals: &conceals,
1428 spans: &[],
1429 search_pattern: None,
1430 non_text_style: Style::default(),
1431 diag_overlays: &[],
1432 colorcolumn_cols: &[],
1433 colorcolumn_style: Style::default(),
1434 };
1435 let term = run_render(view, 30, 1);
1436 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
1438 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
1439 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
1442 }
1443
1444 #[test]
1445 fn closed_fold_collapses_rows_and_paints_marker() {
1446 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1447 let v = vp(30, 5);
1448 b.add_fold(1, 3, true);
1450 let view = BufferView {
1451 buffer: &b,
1452 viewport: &v,
1453 selection: None,
1454 resolver: &(no_styles as fn(u32) -> Style),
1455 cursor_line_bg: Style::default(),
1456 cursor_column_bg: Style::default(),
1457 selection_bg: Style::default().bg(Color::Blue),
1458 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1459 gutter: None,
1460 search_bg: Style::default(),
1461 signs: &[],
1462 conceals: &[],
1463 spans: &[],
1464 search_pattern: None,
1465 non_text_style: Style::default(),
1466 diag_overlays: &[],
1467 colorcolumn_cols: &[],
1468 colorcolumn_style: Style::default(),
1469 };
1470 let term = run_render(view, 30, 5);
1471 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1473 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
1476 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
1478 }
1479
1480 #[test]
1481 fn open_fold_renders_normally() {
1482 let mut b = Buffer::from_str("a\nb\nc");
1483 let v = vp(5, 3);
1484 b.add_fold(0, 2, false); let view = BufferView {
1486 buffer: &b,
1487 viewport: &v,
1488 selection: None,
1489 resolver: &(no_styles as fn(u32) -> Style),
1490 cursor_line_bg: Style::default(),
1491 cursor_column_bg: Style::default(),
1492 selection_bg: Style::default().bg(Color::Blue),
1493 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1494 gutter: None,
1495 search_bg: Style::default(),
1496 signs: &[],
1497 conceals: &[],
1498 spans: &[],
1499 search_pattern: None,
1500 non_text_style: Style::default(),
1501 diag_overlays: &[],
1502 colorcolumn_cols: &[],
1503 colorcolumn_style: Style::default(),
1504 };
1505 let term = run_render(view, 5, 3);
1506 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1507 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1508 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
1509 }
1510
1511 #[test]
1512 fn horizontal_scroll_clips_left_chars() {
1513 let b = Buffer::from_str("abcdefgh");
1514 let mut v = vp(4, 1);
1515 v.top_col = 3;
1516 let view = BufferView {
1517 buffer: &b,
1518 viewport: &v,
1519 selection: None,
1520 resolver: &(no_styles as fn(u32) -> Style),
1521 cursor_line_bg: Style::default(),
1522 cursor_column_bg: Style::default(),
1523 selection_bg: Style::default().bg(Color::Blue),
1524 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1525 gutter: None,
1526 search_bg: Style::default(),
1527 signs: &[],
1528 conceals: &[],
1529 spans: &[],
1530 search_pattern: None,
1531 non_text_style: Style::default(),
1532 diag_overlays: &[],
1533 colorcolumn_cols: &[],
1534 colorcolumn_style: Style::default(),
1535 };
1536 let term = run_render(view, 4, 1);
1537 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
1538 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
1539 }
1540
1541 fn make_wrap_view<'a>(
1542 b: &'a Buffer,
1543 viewport: &'a Viewport,
1544 resolver: &'a (impl StyleResolver + 'a),
1545 gutter: Option<Gutter>,
1546 ) -> BufferView<'a, impl StyleResolver + 'a> {
1547 BufferView {
1548 buffer: b,
1549 viewport,
1550 selection: None,
1551 resolver,
1552 cursor_line_bg: Style::default(),
1553 cursor_column_bg: Style::default(),
1554 selection_bg: Style::default().bg(Color::Blue),
1555 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1556 gutter,
1557 search_bg: Style::default(),
1558 signs: &[],
1559 conceals: &[],
1560 spans: &[],
1561 search_pattern: None,
1562 non_text_style: Style::default(),
1563 diag_overlays: &[],
1564 colorcolumn_cols: &[],
1565 colorcolumn_style: Style::default(),
1566 }
1567 }
1568
1569 #[test]
1570 fn wrap_segments_char_breaks_at_width() {
1571 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1572 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1573 }
1574
1575 #[test]
1576 fn wrap_segments_word_backs_up_to_whitespace() {
1577 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1578 assert_eq!(segs[0], (0, 6));
1580 assert_eq!(segs[1], (6, 11));
1582 assert_eq!(segs[2], (11, 16));
1583 }
1584
1585 #[test]
1586 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1587 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1588 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1590 }
1591
1592 #[test]
1593 fn wrap_char_paints_continuation_rows() {
1594 let b = Buffer::from_str("abcdefghij");
1595 let v = Viewport {
1596 top_row: 0,
1597 top_col: 0,
1598 width: 4,
1599 height: 3,
1600 wrap: Wrap::Char,
1601 text_width: 4,
1602 tab_width: 0,
1603 };
1604 let r = no_styles as fn(u32) -> Style;
1605 let view = make_wrap_view(&b, &v, &r, None);
1606 let term = run_render(view, 4, 3);
1607 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1609 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1610 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1612 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1613 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1615 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1616 }
1617
1618 #[test]
1619 fn wrap_char_gutter_blank_on_continuation() {
1620 let b = Buffer::from_str("abcdefgh");
1621 let v = Viewport {
1622 top_row: 0,
1623 top_col: 0,
1624 width: 6,
1625 height: 3,
1626 wrap: Wrap::Char,
1627 text_width: 3,
1629 tab_width: 0,
1630 };
1631 let r = no_styles as fn(u32) -> Style;
1632 let gutter = Gutter {
1633 width: 3,
1634 style: Style::default().fg(Color::Yellow),
1635 line_offset: 0,
1636 ..Default::default()
1637 };
1638 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1639 let term = run_render(view, 6, 3);
1640 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1642 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1643 for x in 0..2 {
1645 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1646 }
1647 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1648 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1649 }
1650
1651 #[test]
1652 fn wrap_char_cursor_lands_on_correct_segment() {
1653 let mut b = Buffer::from_str("abcdefghij");
1654 let v = Viewport {
1655 top_row: 0,
1656 top_col: 0,
1657 width: 4,
1658 height: 3,
1659 wrap: Wrap::Char,
1660 text_width: 4,
1661 tab_width: 0,
1662 };
1663 b.set_cursor(crate::Position::new(0, 6));
1665 let r = no_styles as fn(u32) -> Style;
1666 let mut view = make_wrap_view(&b, &v, &r, None);
1667 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1668 let term = run_render(view, 4, 3);
1669 assert!(
1670 term.cell((2, 1))
1671 .unwrap()
1672 .modifier
1673 .contains(Modifier::REVERSED)
1674 );
1675 }
1676
1677 #[test]
1678 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1679 let mut b = Buffer::from_str("abcdef");
1680 let v = Viewport {
1681 top_row: 0,
1682 top_col: 0,
1683 width: 4,
1684 height: 3,
1685 wrap: Wrap::Char,
1686 text_width: 4,
1687 tab_width: 0,
1688 };
1689 b.set_cursor(crate::Position::new(0, 6));
1691 let r = no_styles as fn(u32) -> Style;
1692 let mut view = make_wrap_view(&b, &v, &r, None);
1693 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1694 let term = run_render(view, 4, 3);
1695 assert!(
1697 term.cell((2, 1))
1698 .unwrap()
1699 .modifier
1700 .contains(Modifier::REVERSED)
1701 );
1702 }
1703
1704 #[test]
1705 fn wrap_word_breaks_at_whitespace() {
1706 let b = Buffer::from_str("alpha beta gamma");
1707 let v = Viewport {
1708 top_row: 0,
1709 top_col: 0,
1710 width: 8,
1711 height: 3,
1712 wrap: Wrap::Word,
1713 text_width: 8,
1714 tab_width: 0,
1715 };
1716 let r = no_styles as fn(u32) -> Style;
1717 let view = make_wrap_view(&b, &v, &r, None);
1718 let term = run_render(view, 8, 3);
1719 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1721 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1722 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1724 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1725 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1727 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1728 }
1729
1730 fn view_with<'a>(
1736 b: &'a Buffer,
1737 viewport: &'a Viewport,
1738 resolver: &'a (impl StyleResolver + 'a),
1739 spans: &'a [Vec<Span>],
1740 search_pattern: Option<&'a regex::Regex>,
1741 ) -> BufferView<'a, impl StyleResolver + 'a> {
1742 BufferView {
1743 buffer: b,
1744 viewport,
1745 selection: None,
1746 resolver,
1747 cursor_line_bg: Style::default(),
1748 cursor_column_bg: Style::default(),
1749 selection_bg: Style::default().bg(Color::Blue),
1750 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1751 gutter: None,
1752 search_bg: Style::default().bg(Color::Magenta),
1753 signs: &[],
1754 conceals: &[],
1755 spans,
1756 search_pattern,
1757 non_text_style: Style::default(),
1758 diag_overlays: &[],
1759 colorcolumn_cols: &[],
1760 colorcolumn_style: Style::default(),
1761 }
1762 }
1763
1764 #[test]
1765 fn empty_spans_param_renders_default_style() {
1766 let b = Buffer::from_str("hello");
1767 let v = vp(10, 1);
1768 let r = no_styles as fn(u32) -> Style;
1769 let view = view_with(&b, &v, &r, &[], None);
1770 let term = run_render(view, 10, 1);
1771 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1772 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
1773 }
1774
1775 #[test]
1776 fn spans_param_paints_styled_byte_range() {
1777 let b = Buffer::from_str("abcdef");
1778 let v = vp(10, 1);
1779 let resolver = |id: u32| -> Style {
1780 if id == 3 {
1781 Style::default().fg(Color::Green)
1782 } else {
1783 Style::default()
1784 }
1785 };
1786 let spans = vec![vec![Span::new(0, 3, 3)]];
1787 let view = view_with(&b, &v, &resolver, &spans, None);
1788 let term = run_render(view, 10, 1);
1789 for x in 0..3 {
1790 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
1791 }
1792 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
1793 }
1794
1795 #[test]
1796 fn spans_param_handles_per_row_overlay() {
1797 let b = Buffer::from_str("abc\ndef");
1798 let v = vp(10, 2);
1799 let resolver = |id: u32| -> Style {
1800 if id == 1 {
1801 Style::default().fg(Color::Red)
1802 } else {
1803 Style::default().fg(Color::Green)
1804 }
1805 };
1806 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
1807 let view = view_with(&b, &v, &resolver, &spans, None);
1808 let term = run_render(view, 10, 2);
1809 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1810 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
1811 }
1812
1813 #[test]
1814 fn spans_param_rows_beyond_get_no_styling() {
1815 let b = Buffer::from_str("abc\ndef\nghi");
1816 let v = vp(10, 3);
1817 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1818 let spans = vec![vec![Span::new(0, 3, 0)]];
1820 let view = view_with(&b, &v, &resolver, &spans, None);
1821 let term = run_render(view, 10, 3);
1822 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1823 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
1824 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
1825 }
1826
1827 #[test]
1828 fn search_pattern_none_disables_hlsearch() {
1829 let b = Buffer::from_str("foo bar foo");
1830 let v = vp(20, 1);
1831 let r = no_styles as fn(u32) -> Style;
1832 let view = view_with(&b, &v, &r, &[], None);
1834 let term = run_render(view, 20, 1);
1835 for x in 0..11 {
1836 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1837 }
1838 }
1839
1840 #[test]
1841 fn search_pattern_regex_paints_match_bg() {
1842 use regex::Regex;
1843 let b = Buffer::from_str("xyz foo xyz");
1844 let v = vp(20, 1);
1845 let r = no_styles as fn(u32) -> Style;
1846 let pat = Regex::new("foo").unwrap();
1847 let view = view_with(&b, &v, &r, &[], Some(&pat));
1848 let term = run_render(view, 20, 1);
1849 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1851 for x in 4..7 {
1852 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1853 }
1854 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
1855 }
1856
1857 #[test]
1858 fn search_pattern_unicode_columns_are_charwise() {
1859 use regex::Regex;
1860 let b = Buffer::from_str("tablé foo");
1862 let v = vp(20, 1);
1863 let r = no_styles as fn(u32) -> Style;
1864 let pat = Regex::new("foo").unwrap();
1865 let view = view_with(&b, &v, &r, &[], Some(&pat));
1866 let term = run_render(view, 20, 1);
1867 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
1869 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
1870 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
1871 }
1872
1873 #[test]
1874 fn spans_param_clamps_short_row_overlay() {
1875 let b = Buffer::from_str("abc");
1877 let v = vp(10, 1);
1878 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1879 let spans = vec![vec![Span::new(0, 100, 0)]];
1880 let view = view_with(&b, &v, &resolver, &spans, None);
1881 let term = run_render(view, 10, 1);
1882 for x in 0..3 {
1883 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1884 }
1885 }
1886
1887 #[test]
1888 fn spans_and_search_pattern_compose() {
1889 use regex::Regex;
1891 let b = Buffer::from_str("foo");
1892 let v = vp(10, 1);
1893 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
1894 let spans = vec![vec![Span::new(0, 3, 0)]];
1895 let pat = Regex::new("foo").unwrap();
1896 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
1897 let term = run_render(view, 10, 1);
1898 let cell = term.cell((1, 0)).unwrap();
1899 assert_eq!(cell.fg, Color::Green);
1900 assert_eq!(cell.bg, Color::Magenta);
1901 }
1902
1903 #[test]
1907 fn tilde_marker_painted_past_eof() {
1908 let b = Buffer::from_str("a\nb\nc\nd\ne");
1910 let v = vp(10, 10);
1911 let r = no_styles as fn(u32) -> Style;
1912 let non_text_fg = Color::DarkGray;
1913 let view = BufferView {
1914 buffer: &b,
1915 viewport: &v,
1916 selection: None,
1917 resolver: &r,
1918 cursor_line_bg: Style::default(),
1919 cursor_column_bg: Style::default(),
1920 selection_bg: Style::default().bg(Color::Blue),
1921 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1922 gutter: None,
1923 search_bg: Style::default(),
1924 signs: &[],
1925 conceals: &[],
1926 spans: &[],
1927 search_pattern: None,
1928 non_text_style: Style::default().fg(non_text_fg),
1929 diag_overlays: &[],
1930 colorcolumn_cols: &[],
1931 colorcolumn_style: Style::default(),
1932 };
1933 let term = run_render(view, 10, 10);
1934 for row in 0..5u16 {
1936 assert_ne!(
1937 term.cell((0, row)).unwrap().symbol(),
1938 "~",
1939 "row {row} is a content row, expected no tilde"
1940 );
1941 }
1942 for row in 5..10u16 {
1944 let cell = term.cell((0, row)).unwrap();
1945 assert_eq!(cell.symbol(), "~", "row {row} is past EOF, expected tilde");
1946 assert_eq!(
1947 cell.fg, non_text_fg,
1948 "row {row} tilde should use non_text_style fg"
1949 );
1950 for x in 1..10u16 {
1952 assert_eq!(
1953 term.cell((x, row)).unwrap().symbol(),
1954 " ",
1955 "row {row} col {x} after tilde should be blank"
1956 );
1957 }
1958 }
1959 }
1960
1961 #[test]
1964 fn tilde_marker_with_gutter_past_eof() {
1965 let b = Buffer::from_str("a\nb");
1966 let v = vp(10, 5);
1967 let r = no_styles as fn(u32) -> Style;
1968 let non_text_fg = Color::DarkGray;
1969 let view = BufferView {
1970 buffer: &b,
1971 viewport: &v,
1972 selection: None,
1973 resolver: &r,
1974 cursor_line_bg: Style::default(),
1975 cursor_column_bg: Style::default(),
1976 selection_bg: Style::default().bg(Color::Blue),
1977 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1978 gutter: Some(Gutter {
1979 width: 4,
1980 style: Style::default().fg(Color::Yellow),
1981 line_offset: 0,
1982 numbers: GutterNumbers::Absolute,
1983 }),
1984 search_bg: Style::default(),
1985 signs: &[],
1986 conceals: &[],
1987 spans: &[],
1988 search_pattern: None,
1989 non_text_style: Style::default().fg(non_text_fg),
1990 diag_overlays: &[],
1991 colorcolumn_cols: &[],
1992 colorcolumn_style: Style::default(),
1993 };
1994 let term = run_render(view, 10, 5);
1995 for row in 2..5u16 {
1997 for x in 0..4u16 {
1999 assert_eq!(
2000 term.cell((x, row)).unwrap().symbol(),
2001 " ",
2002 "gutter col {x} on past-EOF row {row} should be blank"
2003 );
2004 }
2005 let cell = term.cell((4, row)).unwrap();
2007 assert_eq!(
2008 cell.symbol(),
2009 "~",
2010 "past-EOF row {row}: expected tilde at text column"
2011 );
2012 assert_eq!(cell.fg, non_text_fg);
2013 }
2014 }
2015
2016 #[test]
2017 fn diag_overlay_paints_underline_on_range() {
2018 let b = Buffer::from_str("hello world");
2022 let v = vp(20, 2);
2023 let overlay = DiagOverlay {
2024 row: 0,
2025 col_start: 6,
2026 col_end: 11,
2027 style: Style::default().add_modifier(Modifier::UNDERLINED),
2028 };
2029 let view = BufferView {
2030 buffer: &b,
2031 viewport: &v,
2032 selection: None,
2033 resolver: &(no_styles as fn(u32) -> Style),
2034 cursor_line_bg: Style::default(),
2035 cursor_column_bg: Style::default(),
2036 selection_bg: Style::default().bg(Color::Blue),
2037 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2038 gutter: None,
2039 search_bg: Style::default(),
2040 signs: &[],
2041 conceals: &[],
2042 spans: &[],
2043 search_pattern: None,
2044 non_text_style: Style::default(),
2045 diag_overlays: &[overlay],
2046 colorcolumn_cols: &[],
2047 colorcolumn_style: Style::default(),
2048 };
2049 let term = run_render(view, 20, 2);
2050
2051 for x in 0u16..6 {
2053 let cell = term.cell((x, 0)).unwrap();
2054 assert!(
2055 !cell.modifier.contains(Modifier::UNDERLINED),
2056 "col {x} must not be underlined (outside overlay)"
2057 );
2058 }
2059 for x in 6u16..11 {
2061 let cell = term.cell((x, 0)).unwrap();
2062 assert!(
2063 cell.modifier.contains(Modifier::UNDERLINED),
2064 "col {x} must be underlined (inside overlay)"
2065 );
2066 }
2067 let cell = term.cell((11, 0)).unwrap();
2069 assert!(
2070 !cell.modifier.contains(Modifier::UNDERLINED),
2071 "col 11 must not be underlined (past overlay end)"
2072 );
2073 }
2074
2075 #[test]
2076 fn diag_overlay_out_of_viewport_is_ignored() {
2077 let b = Buffer::from_str("a\nb\nc");
2079 let v = vp(10, 3);
2080 let overlay = DiagOverlay {
2081 row: 5,
2082 col_start: 0,
2083 col_end: 1,
2084 style: Style::default().add_modifier(Modifier::UNDERLINED),
2085 };
2086 let view = BufferView {
2087 buffer: &b,
2088 viewport: &v,
2089 selection: None,
2090 resolver: &(no_styles as fn(u32) -> Style),
2091 cursor_line_bg: Style::default(),
2092 cursor_column_bg: Style::default(),
2093 selection_bg: Style::default().bg(Color::Blue),
2094 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2095 gutter: None,
2096 search_bg: Style::default(),
2097 signs: &[],
2098 conceals: &[],
2099 spans: &[],
2100 search_pattern: None,
2101 non_text_style: Style::default(),
2102 diag_overlays: &[overlay],
2103 colorcolumn_cols: &[],
2104 colorcolumn_style: Style::default(),
2105 };
2106 let _term = run_render(view, 10, 3);
2108 }
2109}