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)]
170pub struct Gutter {
171 pub width: u16,
174 pub style: Style,
175 pub line_offset: usize,
176 pub numbers: GutterNumbers,
178 pub sign_column_width: u16,
183}
184
185#[derive(Debug, Clone, Copy)]
190pub struct Sign {
191 pub row: usize,
192 pub ch: char,
193 pub style: Style,
194 pub priority: u8,
195}
196
197#[derive(Debug, Clone)]
202pub struct Conceal {
203 pub row: usize,
204 pub start_byte: usize,
205 pub end_byte: usize,
206 pub replacement: String,
207}
208
209#[derive(Debug, Clone, Copy)]
215pub struct DiagOverlay {
216 pub row: usize,
218 pub col_start: usize,
220 pub col_end: usize,
222 pub style: Style,
224}
225
226impl<R: StyleResolver> Widget for BufferView<'_, R> {
227 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
228 let viewport = *self.viewport;
229 let cursor = self.buffer.cursor();
230 let lines = self.buffer.lines();
231 let spans = self.spans;
232 let folds = self.buffer.folds();
233 let top_row = viewport.top_row;
234 let top_col = viewport.top_col;
235
236 let gutter_total = self
237 .gutter
238 .map(|g| g.sign_column_width + g.width)
239 .unwrap_or(0);
240 let text_area = Rect {
241 x: area.x.saturating_add(gutter_total),
242 y: area.y,
243 width: area.width.saturating_sub(gutter_total),
244 height: area.height,
245 };
246
247 let total_rows = lines.len();
248 let mut doc_row = top_row;
249 let mut screen_row: u16 = 0;
250 let wrap_mode = viewport.wrap;
251 let seg_width = if viewport.text_width > 0 {
252 viewport.text_width
253 } else {
254 text_area.width
255 };
256 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
262 while doc_row < total_rows && screen_row < area.height {
266 if folds.iter().any(|f| f.hides(doc_row)) {
269 doc_row += 1;
270 continue;
271 }
272 let folded_at_start = folds
273 .iter()
274 .find(|f| f.closed && f.start_row == doc_row)
275 .copied();
276 let line = &lines[doc_row];
277 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
278 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
279 let is_cursor_row = doc_row == cursor.row;
280 if let Some(fold) = folded_at_start {
281 if let Some(gutter) = self.gutter {
282 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
283 self.paint_signs(term_buf, area, screen_row, doc_row, gutter);
284 }
285 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
286 search_hit_at_cursor_col.push(false);
287 screen_row += 1;
288 doc_row = fold.end_row + 1;
289 continue;
290 }
291 let search_ranges = self.row_search_ranges(line);
292 let row_has_hit_at_cursor_col = search_ranges
293 .iter()
294 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
295 let row_conceals: Vec<&Conceal> = {
297 let mut v: Vec<&Conceal> =
298 self.conceals.iter().filter(|c| c.row == doc_row).collect();
299 v.sort_by_key(|c| c.start_byte);
300 v
301 };
302 let segments = match wrap_mode {
310 Wrap::None => vec![(top_col, usize::MAX)],
311 _ => wrap_segments(line, seg_width, wrap_mode),
312 };
313 let last_seg_idx = segments.len().saturating_sub(1);
314 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
315 if screen_row >= area.height {
316 break;
317 }
318 if let Some(gutter) = self.gutter {
319 if seg_idx == 0 {
320 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
321 self.paint_signs(term_buf, area, screen_row, doc_row, gutter);
322 } else {
323 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
324 }
325 }
326 self.paint_row(
327 term_buf,
328 text_area,
329 screen_row,
330 line,
331 row_spans,
332 sel_range,
333 &search_ranges,
334 is_cursor_row,
335 cursor.col,
336 seg_start,
337 seg_end,
338 seg_idx == last_seg_idx,
339 &row_conceals,
340 );
341 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
342 screen_row += 1;
343 }
344 doc_row += 1;
345 }
346 while screen_row < area.height {
349 if let Some(gutter) = self.gutter {
351 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
352 }
353 let y = text_area.y + screen_row;
355 if let Some(cell) = term_buf.cell_mut((text_area.x, y)) {
356 cell.set_char('~');
357 cell.set_style(self.non_text_style);
358 }
359 screen_row += 1;
360 }
361 if matches!(wrap_mode, Wrap::None)
368 && self.cursor_column_bg != Style::default()
369 && cursor.col >= top_col
370 && (cursor.col - top_col) < text_area.width as usize
371 {
372 let x = text_area.x + (cursor.col - top_col) as u16;
373 for sy in 0..screen_row {
374 if search_hit_at_cursor_col
378 .get(sy as usize)
379 .copied()
380 .unwrap_or(false)
381 {
382 continue;
383 }
384 let y = text_area.y + sy;
385 if let Some(cell) = term_buf.cell_mut((x, y)) {
386 cell.set_style(cell.style().patch(self.cursor_column_bg));
387 }
388 }
389 }
390
391 if matches!(wrap_mode, Wrap::None) && !self.colorcolumn_cols.is_empty() {
395 for &col_1based in self.colorcolumn_cols {
396 let col = col_1based as usize; if col == 0 || col < top_col + 1 {
398 continue; }
400 let screen_col = col - 1 - top_col; if screen_col >= text_area.width as usize {
402 continue; }
404 let x = text_area.x + screen_col as u16;
405 for sy in 0..screen_row {
406 let y = text_area.y + sy;
407 if let Some(cell) = term_buf.cell_mut((x, y)) {
408 cell.set_style(cell.style().patch(self.colorcolumn_style));
409 }
410 }
411 }
412 }
413
414 if matches!(wrap_mode, Wrap::None) && !self.diag_overlays.is_empty() {
419 let vp_top = top_row;
423 let vp_bot = vp_top + area.height as usize;
424 for overlay in self.diag_overlays {
425 if overlay.row < vp_top || overlay.row >= vp_bot {
426 continue;
427 }
428 let mut sr: u16 = 0;
431 let mut dr = vp_top;
432 while dr < overlay.row && sr < area.height {
433 if !folds.iter().any(|f| f.hides(dr)) {
434 sr += 1;
435 }
436 dr += 1;
437 }
438 if sr >= area.height {
439 continue;
440 }
441 let y = text_area.y + sr;
442 let col_start = overlay.col_start;
445 let col_end = overlay.col_end.max(col_start + 1);
446 for col in col_start..col_end {
447 if col < top_col {
448 continue;
449 }
450 let screen_col = col - top_col;
451 if screen_col >= text_area.width as usize {
452 break;
453 }
454 let x = text_area.x + screen_col as u16;
455 if let Some(cell) = term_buf.cell_mut((x, y)) {
456 cell.set_style(cell.style().patch(overlay.style));
457 }
458 }
459 }
460 }
461 }
462}
463
464impl<R: StyleResolver> BufferView<'_, R> {
465 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
469 let Some(re) = self.search_pattern else {
470 return Vec::new();
471 };
472 re.find_iter(line)
473 .map(|m| {
474 let start = line[..m.start()].chars().count();
475 let end = line[..m.end()].chars().count();
476 (start, end)
477 })
478 .collect()
479 }
480
481 fn paint_fold_marker(
482 &self,
483 term_buf: &mut TermBuffer,
484 area: Rect,
485 screen_row: u16,
486 fold: crate::Fold,
487 first_line: &str,
488 is_cursor_row: bool,
489 ) {
490 let y = area.y + screen_row;
491 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
492 self.cursor_line_bg
493 } else {
494 Style::default()
495 };
496 for x in area.x..(area.x + area.width) {
498 if let Some(cell) = term_buf.cell_mut((x, y)) {
499 cell.set_style(style);
500 }
501 }
502 let prefix = first_line.trim();
506 let count = fold.line_count();
507 let label = if prefix.is_empty() {
508 format!("▸ {count} lines folded")
509 } else {
510 const MAX_PREFIX: usize = 60;
511 let trimmed = if prefix.chars().count() > MAX_PREFIX {
512 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
513 format!("{head}…")
514 } else {
515 prefix.to_string()
516 };
517 format!("▸ {trimmed} ({count} lines)")
518 };
519 let mut x = area.x;
520 let row_end_x = area.x + area.width;
521 for ch in label.chars() {
522 if x >= row_end_x {
523 break;
524 }
525 let width = ch.width().unwrap_or(1) as u16;
526 if x + width > row_end_x {
527 break;
528 }
529 if let Some(cell) = term_buf.cell_mut((x, y)) {
530 cell.set_char(ch);
531 cell.set_style(style);
532 }
533 x = x.saturating_add(width);
534 }
535 }
536
537 fn paint_signs(
538 &self,
539 term_buf: &mut TermBuffer,
540 area: Rect,
541 screen_row: u16,
542 doc_row: usize,
543 gutter: Gutter,
544 ) {
545 if gutter.sign_column_width == 0 {
547 return;
548 }
549 let y = area.y + screen_row;
550 let sign_x = area.x;
551 for x in sign_x..sign_x + gutter.sign_column_width {
553 if let Some(cell) = term_buf.cell_mut((x, y)) {
554 cell.set_char(' ');
555 cell.set_style(gutter.style);
556 }
557 }
558 let Some(sign) = self
560 .signs
561 .iter()
562 .filter(|s| s.row == doc_row)
563 .max_by_key(|s| s.priority)
564 else {
565 return;
566 };
567 if let Some(cell) = term_buf.cell_mut((sign_x, y)) {
568 cell.set_char(sign.ch);
569 cell.set_style(sign.style);
570 }
571 }
572
573 fn paint_blank_gutter(
576 &self,
577 term_buf: &mut TermBuffer,
578 area: Rect,
579 screen_row: u16,
580 gutter: Gutter,
581 ) {
582 let y = area.y + screen_row;
583 let total = gutter.sign_column_width + gutter.width;
584 for x in area.x..(area.x + total) {
585 if let Some(cell) = term_buf.cell_mut((x, y)) {
586 cell.set_char(' ');
587 cell.set_style(gutter.style);
588 }
589 }
590 }
591
592 fn paint_gutter(
593 &self,
594 term_buf: &mut TermBuffer,
595 area: Rect,
596 screen_row: u16,
597 doc_row: usize,
598 gutter: Gutter,
599 ) {
600 let y = area.y + screen_row;
601 let num_start = area.x + gutter.sign_column_width;
603 let number_width = gutter.width.saturating_sub(1) as usize;
605
606 let label = match gutter.numbers {
608 GutterNumbers::None => {
609 for x in num_start..(num_start + gutter.width) {
611 if let Some(cell) = term_buf.cell_mut((x, y)) {
612 cell.set_char(' ');
613 cell.set_style(gutter.style);
614 }
615 }
616 return;
617 }
618 GutterNumbers::Absolute => {
619 format!(
620 "{:>width$}",
621 doc_row + 1 + gutter.line_offset,
622 width = number_width
623 )
624 }
625 GutterNumbers::Relative { cursor_row } => {
626 let n = if doc_row == cursor_row {
627 0
628 } else {
629 doc_row.abs_diff(cursor_row)
630 };
631 format!("{:>width$}", n, width = number_width)
632 }
633 GutterNumbers::Hybrid { cursor_row } => {
634 let n = if doc_row == cursor_row {
635 doc_row + 1 + gutter.line_offset
636 } else {
637 doc_row.abs_diff(cursor_row)
638 };
639 format!("{:>width$}", n, width = number_width)
640 }
641 };
642
643 let mut x = num_start;
644 for ch in label.chars() {
645 if x >= num_start + gutter.width.saturating_sub(1) {
646 break;
647 }
648 if let Some(cell) = term_buf.cell_mut((x, y)) {
649 cell.set_char(ch);
650 cell.set_style(gutter.style);
651 }
652 x = x.saturating_add(1);
653 }
654 let spacer_x = num_start + gutter.width.saturating_sub(1);
657 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
658 cell.set_char(' ');
659 cell.set_style(gutter.style);
660 }
661 }
662
663 #[allow(clippy::too_many_arguments)]
664 fn paint_row(
665 &self,
666 term_buf: &mut TermBuffer,
667 area: Rect,
668 screen_row: u16,
669 line: &str,
670 row_spans: &[crate::Span],
671 sel_range: crate::RowSpan,
672 search_ranges: &[(usize, usize)],
673 is_cursor_row: bool,
674 cursor_col: usize,
675 seg_start: usize,
676 seg_end: usize,
677 is_last_segment: bool,
678 conceals: &[&Conceal],
679 ) {
680 let y = area.y + screen_row;
681 let mut screen_x = area.x;
682 let row_end_x = area.x + area.width;
683
684 if is_cursor_row && self.cursor_line_bg != Style::default() {
688 for x in area.x..row_end_x {
689 if let Some(cell) = term_buf.cell_mut((x, y)) {
690 cell.set_style(self.cursor_line_bg);
691 }
692 }
693 }
694
695 let tab_width = self.viewport.effective_tab_width();
699 let mut byte_offset: usize = 0;
700 let mut line_col: usize = 0;
701 let mut chars_iter = line.chars().enumerate().peekable();
702 while let Some((col_idx, ch)) = chars_iter.next() {
703 let ch_byte_len = ch.len_utf8();
704 if col_idx >= seg_end {
705 break;
706 }
707 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
712 if col_idx >= seg_start {
713 let mut style = if is_cursor_row {
714 self.cursor_line_bg
715 } else {
716 Style::default()
717 };
718 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
719 style = style.patch(span_style);
720 }
721 for rch in conc.replacement.chars() {
722 let rwidth = rch.width().unwrap_or(1) as u16;
723 if screen_x + rwidth > row_end_x {
724 break;
725 }
726 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
727 cell.set_char(rch);
728 cell.set_style(style);
729 }
730 screen_x += rwidth;
731 }
732 }
733 let mut consumed = ch_byte_len;
736 byte_offset += ch_byte_len;
737 while byte_offset < conc.end_byte {
738 let Some((_, next_ch)) = chars_iter.next() else {
739 break;
740 };
741 consumed += next_ch.len_utf8();
742 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
743 }
744 let _ = consumed;
745 continue;
746 }
747 let visible_width = if ch == '\t' {
752 tab_width - (line_col % tab_width)
753 } else {
754 ch.width().unwrap_or(1)
755 };
756 if col_idx < seg_start {
759 line_col += visible_width;
760 byte_offset += ch_byte_len;
761 continue;
762 }
763 let width = visible_width as u16;
765 if screen_x + width > row_end_x {
766 break;
767 }
768
769 let mut style = if is_cursor_row {
771 self.cursor_line_bg
772 } else {
773 Style::default()
774 };
775 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
776 style = style.patch(span_style);
777 }
778 if self.search_bg != Style::default()
782 && search_ranges
783 .iter()
784 .any(|&(s, e)| col_idx >= s && col_idx < e)
785 {
786 style = style.patch(self.search_bg);
787 }
788 if let Some((lo, hi)) = sel_range
789 && col_idx >= lo
790 && col_idx <= hi
791 {
792 style = style.patch(self.selection_bg);
793 }
794 if is_cursor_row && col_idx == cursor_col {
795 style = style.patch(self.cursor_style);
796 }
797
798 if ch == '\t' {
799 for k in 0..width {
803 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
804 cell.set_char(' ');
805 cell.set_style(style);
806 }
807 }
808 } else if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
809 cell.set_char(ch);
810 cell.set_style(style);
811 }
812 screen_x += width;
813 line_col += visible_width;
814 byte_offset += ch_byte_len;
815 }
816
817 if let Some((lo, hi)) = sel_range
833 && is_last_segment
834 && line.chars().count() <= seg_start
835 {
836 let (start_col, end_col) = if hi == usize::MAX { (0, 0) } else { (lo, hi) };
837 for col in start_col..=end_col {
838 let pad_x = area.x + col as u16;
839 if pad_x >= row_end_x {
840 break;
841 }
842 if let Some(cell) = term_buf.cell_mut((pad_x, y)) {
843 let prev = cell.style();
844 cell.set_char(' ');
845 cell.set_style(prev.patch(self.selection_bg));
846 }
847 }
848 }
849
850 if is_cursor_row
855 && is_last_segment
856 && cursor_col >= line.chars().count()
857 && cursor_col >= seg_start
858 {
859 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
860 if pad_x < row_end_x
861 && let Some(cell) = term_buf.cell_mut((pad_x, y))
862 {
863 cell.set_char(' ');
864 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
865 }
866 }
867 }
868
869 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
886 let mut overlapping: Vec<&crate::Span> = row_spans
888 .iter()
889 .filter(|s| byte_offset >= s.start_byte && byte_offset < s.end_byte)
890 .collect();
891 if overlapping.is_empty() {
892 return None;
893 }
894 overlapping.sort_by_key(|s| std::cmp::Reverse(s.end_byte.saturating_sub(s.start_byte)));
895 let mut style = self.resolver.resolve(overlapping[0].style);
896 for s in &overlapping[1..] {
897 style = style.patch(self.resolver.resolve(s.style));
898 }
899 Some(style)
900 }
901}
902
903#[cfg(test)]
904mod tests {
905 use super::*;
906 use ratatui::style::{Color, Modifier};
907 use ratatui::widgets::Widget;
908
909 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
910 let area = Rect::new(0, 0, w, h);
911 let mut buf = TermBuffer::empty(area);
912 view.render(area, &mut buf);
913 buf
914 }
915
916 fn no_styles(_id: u32) -> Style {
917 Style::default()
918 }
919
920 fn vp(width: u16, height: u16) -> Viewport {
922 Viewport {
923 top_row: 0,
924 top_col: 0,
925 width,
926 height,
927 wrap: Wrap::None,
928 text_width: width,
929 tab_width: 0,
930 }
931 }
932
933 #[test]
934 fn renders_plain_chars_into_terminal_buffer() {
935 let b = Buffer::from_str("hello\nworld");
936 let v = vp(20, 5);
937 let view = BufferView {
938 buffer: &b,
939 viewport: &v,
940 selection: None,
941 resolver: &(no_styles as fn(u32) -> Style),
942 cursor_line_bg: Style::default(),
943 cursor_column_bg: Style::default(),
944 selection_bg: Style::default().bg(Color::Blue),
945 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
946 gutter: None,
947 search_bg: Style::default(),
948 signs: &[],
949 conceals: &[],
950 spans: &[],
951 search_pattern: None,
952 non_text_style: Style::default(),
953 diag_overlays: &[],
954 colorcolumn_cols: &[],
955 colorcolumn_style: Style::default(),
956 };
957 let term = run_render(view, 20, 5);
958 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
959 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
960 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
961 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
962 }
963
964 #[test]
965 fn cursor_cell_gets_reversed_style() {
966 let mut b = Buffer::from_str("abc");
967 let v = vp(10, 1);
968 b.set_cursor(crate::Position::new(0, 1));
969 let view = BufferView {
970 buffer: &b,
971 viewport: &v,
972 selection: None,
973 resolver: &(no_styles as fn(u32) -> Style),
974 cursor_line_bg: Style::default(),
975 cursor_column_bg: Style::default(),
976 selection_bg: Style::default().bg(Color::Blue),
977 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
978 gutter: None,
979 search_bg: Style::default(),
980 signs: &[],
981 conceals: &[],
982 spans: &[],
983 search_pattern: None,
984 non_text_style: Style::default(),
985 diag_overlays: &[],
986 colorcolumn_cols: &[],
987 colorcolumn_style: Style::default(),
988 };
989 let term = run_render(view, 10, 1);
990 let cursor_cell = term.cell((1, 0)).unwrap();
991 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
992 }
993
994 #[test]
995 fn selection_bg_applies_only_to_selected_cells() {
996 use crate::{Position, Selection};
997 let b = Buffer::from_str("abcdef");
998 let v = vp(10, 1);
999 let view = BufferView {
1000 buffer: &b,
1001 viewport: &v,
1002 selection: Some(Selection::Char {
1003 anchor: Position::new(0, 1),
1004 head: Position::new(0, 3),
1005 }),
1006 resolver: &(no_styles as fn(u32) -> Style),
1007 cursor_line_bg: Style::default(),
1008 cursor_column_bg: Style::default(),
1009 selection_bg: Style::default().bg(Color::Blue),
1010 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1011 gutter: None,
1012 search_bg: Style::default(),
1013 signs: &[],
1014 conceals: &[],
1015 spans: &[],
1016 search_pattern: None,
1017 non_text_style: Style::default(),
1018 diag_overlays: &[],
1019 colorcolumn_cols: &[],
1020 colorcolumn_style: Style::default(),
1021 };
1022 let term = run_render(view, 10, 1);
1023 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
1024 for x in 1..=3 {
1025 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
1026 }
1027 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
1028 }
1029
1030 #[test]
1031 fn selection_paints_placeholder_on_empty_line_charwise() {
1032 use crate::{Position, Selection};
1035 let b = Buffer::from_str("abc\n\nxyz");
1036 let v = vp(10, 3);
1037 let view = BufferView {
1038 buffer: &b,
1039 viewport: &v,
1040 selection: Some(Selection::Char {
1041 anchor: Position::new(0, 0),
1042 head: Position::new(2, 2),
1043 }),
1044 resolver: &(no_styles as fn(u32) -> Style),
1045 cursor_line_bg: Style::default(),
1046 cursor_column_bg: Style::default(),
1047 selection_bg: Style::default().bg(Color::Blue),
1048 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1049 gutter: None,
1050 search_bg: Style::default(),
1051 signs: &[],
1052 conceals: &[],
1053 spans: &[],
1054 search_pattern: None,
1055 non_text_style: Style::default(),
1056 diag_overlays: &[],
1057 colorcolumn_cols: &[],
1058 colorcolumn_style: Style::default(),
1059 };
1060 let term = run_render(view, 10, 3);
1061 assert_eq!(term.cell((0, 1)).unwrap().bg, Color::Blue);
1063 }
1064
1065 #[test]
1066 fn selection_paints_placeholder_on_empty_line_linewise() {
1067 use crate::Selection;
1068 let b = Buffer::from_str("abc\n\nxyz");
1069 let v = vp(10, 3);
1070 let view = BufferView {
1071 buffer: &b,
1072 viewport: &v,
1073 selection: Some(Selection::Line {
1074 anchor_row: 0,
1075 head_row: 2,
1076 }),
1077 resolver: &(no_styles as fn(u32) -> Style),
1078 cursor_line_bg: Style::default(),
1079 cursor_column_bg: Style::default(),
1080 selection_bg: Style::default().bg(Color::Blue),
1081 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1082 gutter: None,
1083 search_bg: Style::default(),
1084 signs: &[],
1085 conceals: &[],
1086 spans: &[],
1087 search_pattern: None,
1088 non_text_style: Style::default(),
1089 diag_overlays: &[],
1090 colorcolumn_cols: &[],
1091 colorcolumn_style: Style::default(),
1092 };
1093 let term = run_render(view, 10, 3);
1094 assert_eq!(term.cell((0, 1)).unwrap().bg, Color::Blue);
1095 }
1096
1097 #[test]
1098 fn selection_paints_placeholder_on_empty_line_blockwise() {
1099 use crate::{Position, Selection};
1104 let b = Buffer::from_str("abcdef\n\nuvwxyz");
1105 let v = vp(10, 3);
1106 let view = BufferView {
1107 buffer: &b,
1108 viewport: &v,
1109 selection: Some(Selection::Block {
1110 anchor: Position::new(0, 2),
1111 head: Position::new(2, 5),
1112 }),
1113 resolver: &(no_styles as fn(u32) -> Style),
1114 cursor_line_bg: Style::default(),
1115 cursor_column_bg: Style::default(),
1116 selection_bg: Style::default().bg(Color::Blue),
1117 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1118 gutter: None,
1119 search_bg: Style::default(),
1120 signs: &[],
1121 conceals: &[],
1122 spans: &[],
1123 search_pattern: None,
1124 non_text_style: Style::default(),
1125 diag_overlays: &[],
1126 colorcolumn_cols: &[],
1127 colorcolumn_style: Style::default(),
1128 };
1129 let term = run_render(view, 10, 3);
1130 for x in 2u16..=5 {
1132 assert_eq!(
1133 term.cell((x, 1)).unwrap().bg,
1134 Color::Blue,
1135 "empty row col {x} should carry block selection bg"
1136 );
1137 }
1138 assert!(term.cell((0, 1)).unwrap().bg != Color::Blue);
1141 assert!(term.cell((1, 1)).unwrap().bg != Color::Blue);
1142 assert!(term.cell((6, 1)).unwrap().bg != Color::Blue);
1144 for x in 2u16..=5 {
1146 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
1147 }
1148 }
1149
1150 #[test]
1151 fn selection_block_placeholder_clips_to_row_width() {
1152 use crate::{Position, Selection};
1154 let b = Buffer::from_str("abc\n\nxyz");
1155 let v = vp(5, 3);
1156 let view = BufferView {
1157 buffer: &b,
1158 viewport: &v,
1159 selection: Some(Selection::Block {
1160 anchor: Position::new(0, 1),
1161 head: Position::new(2, 20),
1162 }),
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(),
1168 gutter: None,
1169 search_bg: Style::default(),
1170 signs: &[],
1171 conceals: &[],
1172 spans: &[],
1173 search_pattern: None,
1174 non_text_style: Style::default(),
1175 diag_overlays: &[],
1176 colorcolumn_cols: &[],
1177 colorcolumn_style: Style::default(),
1178 };
1179 let term = run_render(view, 5, 3);
1181 for x in 1u16..=4 {
1182 assert_eq!(
1183 term.cell((x, 1)).unwrap().bg,
1184 Color::Blue,
1185 "col {x} clipped block placeholder"
1186 );
1187 }
1188 }
1190
1191 #[test]
1192 fn layered_spans_blend_broad_bg_with_narrow_fg() {
1193 use crate::Span;
1199 let b = Buffer::from_str("fn main() {}");
1200 let v = vp(20, 1);
1201 let spans = vec![vec![
1203 Span::new(0, 12, 1), Span::new(0, 2, 2), ]];
1206 let resolver = |id: u32| -> Style {
1207 match id {
1208 1 => Style::default().bg(Color::DarkGray),
1209 2 => Style::default().fg(Color::Magenta),
1210 _ => Style::default(),
1211 }
1212 };
1213 let view = BufferView {
1214 buffer: &b,
1215 viewport: &v,
1216 selection: None,
1217 resolver: &resolver,
1218 cursor_line_bg: Style::default(),
1219 cursor_column_bg: Style::default(),
1220 selection_bg: Style::default().bg(Color::Blue),
1221 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1222 gutter: None,
1223 search_bg: Style::default(),
1224 signs: &[],
1225 conceals: &[],
1226 spans: &spans,
1227 search_pattern: None,
1228 non_text_style: Style::default(),
1229 diag_overlays: &[],
1230 colorcolumn_cols: &[],
1231 colorcolumn_style: Style::default(),
1232 };
1233 let term = run_render(view, 20, 1);
1234 for x in 0u16..2 {
1236 let cell = term.cell((x, 0)).unwrap();
1237 assert_eq!(cell.fg, Color::Magenta, "col {x}: fg from narrow span");
1238 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1239 }
1240 for x in 2u16..12 {
1242 let cell = term.cell((x, 0)).unwrap();
1243 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1244 assert_eq!(
1245 cell.fg,
1246 Color::Reset,
1247 "col {x}: no fg set (broad span is bg-only)"
1248 );
1249 }
1250 }
1251
1252 #[test]
1253 fn narrow_span_with_explicit_bg_still_overrides_broad_bg() {
1254 use crate::Span;
1259 let b = Buffer::from_str("hello world");
1260 let v = vp(20, 1);
1261 let spans = vec![vec![
1262 Span::new(0, 11, 1), Span::new(6, 11, 2), ]];
1265 let resolver = |id: u32| -> Style {
1266 match id {
1267 1 => Style::default().bg(Color::DarkGray),
1268 2 => Style::default().bg(Color::Red),
1269 _ => Style::default(),
1270 }
1271 };
1272 let view = BufferView {
1273 buffer: &b,
1274 viewport: &v,
1275 selection: None,
1276 resolver: &resolver,
1277 cursor_line_bg: Style::default(),
1278 cursor_column_bg: Style::default(),
1279 selection_bg: Style::default().bg(Color::Blue),
1280 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1281 gutter: None,
1282 search_bg: Style::default(),
1283 signs: &[],
1284 conceals: &[],
1285 spans: &spans,
1286 search_pattern: None,
1287 non_text_style: Style::default(),
1288 diag_overlays: &[],
1289 colorcolumn_cols: &[],
1290 colorcolumn_style: Style::default(),
1291 };
1292 let term = run_render(view, 20, 1);
1293 for x in 0u16..6 {
1295 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::DarkGray);
1296 }
1297 for x in 6u16..11 {
1299 assert_eq!(
1300 term.cell((x, 0)).unwrap().bg,
1301 Color::Red,
1302 "col {x}: narrow span's bg overrides broad bg"
1303 );
1304 }
1305 }
1306
1307 #[test]
1308 fn syntax_span_fg_resolves_via_table() {
1309 use crate::Span;
1310 let b = Buffer::from_str("SELECT foo");
1311 let v = vp(20, 1);
1312 let spans = vec![vec![Span::new(0, 6, 7)]];
1313 let resolver = |id: u32| -> Style {
1314 if id == 7 {
1315 Style::default().fg(Color::Red)
1316 } else {
1317 Style::default()
1318 }
1319 };
1320 let view = BufferView {
1321 buffer: &b,
1322 viewport: &v,
1323 selection: None,
1324 resolver: &resolver,
1325 cursor_line_bg: Style::default(),
1326 cursor_column_bg: Style::default(),
1327 selection_bg: Style::default().bg(Color::Blue),
1328 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1329 gutter: None,
1330 search_bg: Style::default(),
1331 signs: &[],
1332 conceals: &[],
1333 spans: &spans,
1334 search_pattern: None,
1335 non_text_style: Style::default(),
1336 diag_overlays: &[],
1337 colorcolumn_cols: &[],
1338 colorcolumn_style: Style::default(),
1339 };
1340 let term = run_render(view, 20, 1);
1341 for x in 0..6 {
1342 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1343 }
1344 }
1345
1346 #[test]
1347 fn gutter_renders_right_aligned_line_numbers() {
1348 let b = Buffer::from_str("a\nb\nc");
1349 let v = vp(10, 3);
1350 let view = BufferView {
1351 buffer: &b,
1352 viewport: &v,
1353 selection: None,
1354 resolver: &(no_styles as fn(u32) -> Style),
1355 cursor_line_bg: Style::default(),
1356 cursor_column_bg: Style::default(),
1357 selection_bg: Style::default().bg(Color::Blue),
1358 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1359 gutter: Some(Gutter {
1360 width: 4,
1361 style: Style::default().fg(Color::Yellow),
1362 line_offset: 0,
1363 ..Default::default()
1364 }),
1365 search_bg: Style::default(),
1366 signs: &[],
1367 conceals: &[],
1368 spans: &[],
1369 search_pattern: None,
1370 non_text_style: Style::default(),
1371 diag_overlays: &[],
1372 colorcolumn_cols: &[],
1373 colorcolumn_style: Style::default(),
1374 };
1375 let term = run_render(view, 10, 3);
1376 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1378 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
1379 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1380 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
1381 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1383 }
1384
1385 #[test]
1386 fn gutter_renders_relative_with_cursor_at_zero() {
1387 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1389 b.set_cursor(crate::Position::new(2, 0));
1390 let v = vp(10, 5);
1391 let view = BufferView {
1392 buffer: &b,
1393 viewport: &v,
1394 selection: None,
1395 resolver: &(no_styles as fn(u32) -> Style),
1396 cursor_line_bg: Style::default(),
1397 cursor_column_bg: Style::default(),
1398 selection_bg: Style::default().bg(Color::Blue),
1399 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1400 gutter: Some(Gutter {
1401 width: 4,
1402 style: Style::default().fg(Color::Yellow),
1403 line_offset: 0,
1404 numbers: GutterNumbers::Relative { cursor_row: 2 },
1405 sign_column_width: 0,
1406 }),
1407 search_bg: Style::default(),
1408 signs: &[],
1409 conceals: &[],
1410 spans: &[],
1411 search_pattern: None,
1412 non_text_style: Style::default(),
1413 diag_overlays: &[],
1414 colorcolumn_cols: &[],
1415 colorcolumn_style: Style::default(),
1416 };
1417 let term = run_render(view, 10, 5);
1418 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "2");
1421 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "1");
1423 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "0");
1425 assert_eq!(term.cell((2, 3)).unwrap().symbol(), "1");
1427 assert_eq!(term.cell((2, 4)).unwrap().symbol(), "2");
1429 }
1430
1431 #[test]
1432 fn gutter_renders_hybrid_cursor_row_absolute() {
1433 let mut b = Buffer::from_str("a\nb\nc");
1436 b.set_cursor(crate::Position::new(1, 0));
1437 let v = vp(10, 3);
1438 let view = BufferView {
1439 buffer: &b,
1440 viewport: &v,
1441 selection: None,
1442 resolver: &(no_styles as fn(u32) -> Style),
1443 cursor_line_bg: Style::default(),
1444 cursor_column_bg: Style::default(),
1445 selection_bg: Style::default().bg(Color::Blue),
1446 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1447 gutter: Some(Gutter {
1448 width: 4,
1449 style: Style::default().fg(Color::Yellow),
1450 line_offset: 0,
1451 numbers: GutterNumbers::Hybrid { cursor_row: 1 },
1452 sign_column_width: 0,
1453 }),
1454 search_bg: Style::default(),
1455 signs: &[],
1456 conceals: &[],
1457 spans: &[],
1458 search_pattern: None,
1459 non_text_style: Style::default(),
1460 diag_overlays: &[],
1461 colorcolumn_cols: &[],
1462 colorcolumn_style: Style::default(),
1463 };
1464 let term = run_render(view, 10, 3);
1465 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1467 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1469 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "1");
1471 }
1472
1473 #[test]
1474 fn gutter_none_paints_blank_cells() {
1475 let b = Buffer::from_str("a\nb\nc");
1476 let v = vp(10, 3);
1477 let view = BufferView {
1478 buffer: &b,
1479 viewport: &v,
1480 selection: None,
1481 resolver: &(no_styles as fn(u32) -> Style),
1482 cursor_line_bg: Style::default(),
1483 cursor_column_bg: Style::default(),
1484 selection_bg: Style::default().bg(Color::Blue),
1485 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1486 gutter: Some(Gutter {
1487 width: 4,
1488 style: Style::default().fg(Color::Yellow),
1489 line_offset: 0,
1490 numbers: GutterNumbers::None,
1491 sign_column_width: 0,
1492 }),
1493 search_bg: Style::default(),
1494 signs: &[],
1495 conceals: &[],
1496 spans: &[],
1497 search_pattern: None,
1498 non_text_style: Style::default(),
1499 diag_overlays: &[],
1500 colorcolumn_cols: &[],
1501 colorcolumn_style: Style::default(),
1502 };
1503 let term = run_render(view, 10, 3);
1504 for row in 0..3u16 {
1506 for x in 0..4u16 {
1507 assert_eq!(
1508 term.cell((x, row)).unwrap().symbol(),
1509 " ",
1510 "expected blank at ({x}, {row})"
1511 );
1512 }
1513 }
1514 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1516 }
1517
1518 #[test]
1519 fn search_bg_paints_match_cells() {
1520 use regex::Regex;
1521 let b = Buffer::from_str("foo bar foo");
1522 let v = vp(20, 1);
1523 let pat = Regex::new("foo").unwrap();
1524 let view = BufferView {
1525 buffer: &b,
1526 viewport: &v,
1527 selection: None,
1528 resolver: &(no_styles as fn(u32) -> Style),
1529 cursor_line_bg: Style::default(),
1530 cursor_column_bg: Style::default(),
1531 selection_bg: Style::default().bg(Color::Blue),
1532 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1533 gutter: None,
1534 search_bg: Style::default().bg(Color::Magenta),
1535 signs: &[],
1536 conceals: &[],
1537 spans: &[],
1538 search_pattern: Some(&pat),
1539 non_text_style: Style::default(),
1540 diag_overlays: &[],
1541 colorcolumn_cols: &[],
1542 colorcolumn_style: Style::default(),
1543 };
1544 let term = run_render(view, 20, 1);
1545 for x in 0..3 {
1546 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1547 }
1548 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1550 for x in 8..11 {
1551 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1552 }
1553 }
1554
1555 #[test]
1556 fn search_bg_survives_cursorcolumn_overlay() {
1557 use regex::Regex;
1558 let mut b = Buffer::from_str("foo bar foo");
1562 let v = vp(20, 1);
1563 let pat = Regex::new("foo").unwrap();
1564 b.set_cursor(crate::Position::new(0, 1));
1566 let view = BufferView {
1567 buffer: &b,
1568 viewport: &v,
1569 selection: None,
1570 resolver: &(no_styles as fn(u32) -> Style),
1571 cursor_line_bg: Style::default(),
1572 cursor_column_bg: Style::default().bg(Color::DarkGray),
1573 selection_bg: Style::default().bg(Color::Blue),
1574 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1575 gutter: None,
1576 search_bg: Style::default().bg(Color::Magenta),
1577 signs: &[],
1578 conceals: &[],
1579 spans: &[],
1580 search_pattern: Some(&pat),
1581 non_text_style: Style::default(),
1582 diag_overlays: &[],
1583 colorcolumn_cols: &[],
1584 colorcolumn_style: Style::default(),
1585 };
1586 let term = run_render(view, 20, 1);
1587 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
1589 }
1590
1591 #[test]
1592 fn highest_priority_sign_wins_per_row_in_dedicated_sign_column() {
1593 let b = Buffer::from_str("a\nb\nc");
1596 let v = vp(10, 3);
1597 let signs = [
1598 Sign {
1599 row: 0,
1600 ch: 'W',
1601 style: Style::default().fg(Color::Yellow),
1602 priority: 1,
1603 },
1604 Sign {
1605 row: 0,
1606 ch: 'E',
1607 style: Style::default().fg(Color::Red),
1608 priority: 2,
1609 },
1610 ];
1611 let view = BufferView {
1612 buffer: &b,
1613 viewport: &v,
1614 selection: None,
1615 resolver: &(no_styles as fn(u32) -> Style),
1616 cursor_line_bg: Style::default(),
1617 cursor_column_bg: Style::default(),
1618 selection_bg: Style::default().bg(Color::Blue),
1619 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1620 gutter: Some(Gutter {
1621 width: 3,
1622 style: Style::default().fg(Color::DarkGray),
1623 line_offset: 0,
1624 sign_column_width: 1,
1625 ..Default::default()
1626 }),
1627 search_bg: Style::default(),
1628 signs: &signs,
1629 conceals: &[],
1630 spans: &[],
1631 search_pattern: None,
1632 non_text_style: Style::default(),
1633 diag_overlays: &[],
1634 colorcolumn_cols: &[],
1635 colorcolumn_style: Style::default(),
1636 };
1637 let term = run_render(view, 10, 3);
1638 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
1640 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1641 assert_ne!(term.cell((1, 0)).unwrap().symbol(), "E");
1643 assert_eq!(term.cell((0, 1)).unwrap().symbol(), " ");
1645 }
1646
1647 #[test]
1648 fn conceal_replaces_byte_range() {
1649 let b = Buffer::from_str("see https://example.com end");
1650 let v = vp(30, 1);
1651 let conceals = vec![Conceal {
1652 row: 0,
1653 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
1656 }];
1657 let view = BufferView {
1658 buffer: &b,
1659 viewport: &v,
1660 selection: None,
1661 resolver: &(no_styles as fn(u32) -> Style),
1662 cursor_line_bg: Style::default(),
1663 cursor_column_bg: Style::default(),
1664 selection_bg: Style::default(),
1665 cursor_style: Style::default(),
1666 gutter: None,
1667 search_bg: Style::default(),
1668 signs: &[],
1669 conceals: &conceals,
1670 spans: &[],
1671 search_pattern: None,
1672 non_text_style: Style::default(),
1673 diag_overlays: &[],
1674 colorcolumn_cols: &[],
1675 colorcolumn_style: Style::default(),
1676 };
1677 let term = run_render(view, 30, 1);
1678 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
1680 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
1681 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
1684 }
1685
1686 #[test]
1687 fn closed_fold_collapses_rows_and_paints_marker() {
1688 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1689 let v = vp(30, 5);
1690 b.add_fold(1, 3, true);
1692 let view = BufferView {
1693 buffer: &b,
1694 viewport: &v,
1695 selection: None,
1696 resolver: &(no_styles as fn(u32) -> Style),
1697 cursor_line_bg: Style::default(),
1698 cursor_column_bg: Style::default(),
1699 selection_bg: Style::default().bg(Color::Blue),
1700 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1701 gutter: None,
1702 search_bg: Style::default(),
1703 signs: &[],
1704 conceals: &[],
1705 spans: &[],
1706 search_pattern: None,
1707 non_text_style: Style::default(),
1708 diag_overlays: &[],
1709 colorcolumn_cols: &[],
1710 colorcolumn_style: Style::default(),
1711 };
1712 let term = run_render(view, 30, 5);
1713 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1715 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
1718 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
1720 }
1721
1722 #[test]
1723 fn open_fold_renders_normally() {
1724 let mut b = Buffer::from_str("a\nb\nc");
1725 let v = vp(5, 3);
1726 b.add_fold(0, 2, false); let view = BufferView {
1728 buffer: &b,
1729 viewport: &v,
1730 selection: None,
1731 resolver: &(no_styles as fn(u32) -> Style),
1732 cursor_line_bg: Style::default(),
1733 cursor_column_bg: Style::default(),
1734 selection_bg: Style::default().bg(Color::Blue),
1735 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1736 gutter: None,
1737 search_bg: Style::default(),
1738 signs: &[],
1739 conceals: &[],
1740 spans: &[],
1741 search_pattern: None,
1742 non_text_style: Style::default(),
1743 diag_overlays: &[],
1744 colorcolumn_cols: &[],
1745 colorcolumn_style: Style::default(),
1746 };
1747 let term = run_render(view, 5, 3);
1748 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1749 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1750 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
1751 }
1752
1753 #[test]
1754 fn horizontal_scroll_clips_left_chars() {
1755 let b = Buffer::from_str("abcdefgh");
1756 let mut v = vp(4, 1);
1757 v.top_col = 3;
1758 let view = BufferView {
1759 buffer: &b,
1760 viewport: &v,
1761 selection: None,
1762 resolver: &(no_styles as fn(u32) -> Style),
1763 cursor_line_bg: Style::default(),
1764 cursor_column_bg: Style::default(),
1765 selection_bg: Style::default().bg(Color::Blue),
1766 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1767 gutter: None,
1768 search_bg: Style::default(),
1769 signs: &[],
1770 conceals: &[],
1771 spans: &[],
1772 search_pattern: None,
1773 non_text_style: Style::default(),
1774 diag_overlays: &[],
1775 colorcolumn_cols: &[],
1776 colorcolumn_style: Style::default(),
1777 };
1778 let term = run_render(view, 4, 1);
1779 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
1780 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
1781 }
1782
1783 fn make_wrap_view<'a>(
1784 b: &'a Buffer,
1785 viewport: &'a Viewport,
1786 resolver: &'a (impl StyleResolver + 'a),
1787 gutter: Option<Gutter>,
1788 ) -> BufferView<'a, impl StyleResolver + 'a> {
1789 BufferView {
1790 buffer: b,
1791 viewport,
1792 selection: None,
1793 resolver,
1794 cursor_line_bg: Style::default(),
1795 cursor_column_bg: Style::default(),
1796 selection_bg: Style::default().bg(Color::Blue),
1797 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1798 gutter,
1799 search_bg: Style::default(),
1800 signs: &[],
1801 conceals: &[],
1802 spans: &[],
1803 search_pattern: None,
1804 non_text_style: Style::default(),
1805 diag_overlays: &[],
1806 colorcolumn_cols: &[],
1807 colorcolumn_style: Style::default(),
1808 }
1809 }
1810
1811 #[test]
1812 fn wrap_segments_char_breaks_at_width() {
1813 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1814 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1815 }
1816
1817 #[test]
1818 fn wrap_segments_word_backs_up_to_whitespace() {
1819 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1820 assert_eq!(segs[0], (0, 6));
1822 assert_eq!(segs[1], (6, 11));
1824 assert_eq!(segs[2], (11, 16));
1825 }
1826
1827 #[test]
1828 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1829 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1830 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1832 }
1833
1834 #[test]
1835 fn wrap_char_paints_continuation_rows() {
1836 let b = Buffer::from_str("abcdefghij");
1837 let v = Viewport {
1838 top_row: 0,
1839 top_col: 0,
1840 width: 4,
1841 height: 3,
1842 wrap: Wrap::Char,
1843 text_width: 4,
1844 tab_width: 0,
1845 };
1846 let r = no_styles as fn(u32) -> Style;
1847 let view = make_wrap_view(&b, &v, &r, None);
1848 let term = run_render(view, 4, 3);
1849 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1851 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1852 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1854 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1855 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1857 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1858 }
1859
1860 #[test]
1861 fn wrap_char_gutter_blank_on_continuation() {
1862 let b = Buffer::from_str("abcdefgh");
1863 let v = Viewport {
1864 top_row: 0,
1865 top_col: 0,
1866 width: 6,
1867 height: 3,
1868 wrap: Wrap::Char,
1869 text_width: 3,
1871 tab_width: 0,
1872 };
1873 let r = no_styles as fn(u32) -> Style;
1874 let gutter = Gutter {
1875 width: 3,
1876 style: Style::default().fg(Color::Yellow),
1877 line_offset: 0,
1878 ..Default::default()
1879 };
1880 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1881 let term = run_render(view, 6, 3);
1882 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1884 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1885 for x in 0..2 {
1887 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1888 }
1889 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1890 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1891 }
1892
1893 #[test]
1894 fn wrap_char_cursor_lands_on_correct_segment() {
1895 let mut b = Buffer::from_str("abcdefghij");
1896 let v = Viewport {
1897 top_row: 0,
1898 top_col: 0,
1899 width: 4,
1900 height: 3,
1901 wrap: Wrap::Char,
1902 text_width: 4,
1903 tab_width: 0,
1904 };
1905 b.set_cursor(crate::Position::new(0, 6));
1907 let r = no_styles as fn(u32) -> Style;
1908 let mut view = make_wrap_view(&b, &v, &r, None);
1909 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1910 let term = run_render(view, 4, 3);
1911 assert!(
1912 term.cell((2, 1))
1913 .unwrap()
1914 .modifier
1915 .contains(Modifier::REVERSED)
1916 );
1917 }
1918
1919 #[test]
1920 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1921 let mut b = Buffer::from_str("abcdef");
1922 let v = Viewport {
1923 top_row: 0,
1924 top_col: 0,
1925 width: 4,
1926 height: 3,
1927 wrap: Wrap::Char,
1928 text_width: 4,
1929 tab_width: 0,
1930 };
1931 b.set_cursor(crate::Position::new(0, 6));
1933 let r = no_styles as fn(u32) -> Style;
1934 let mut view = make_wrap_view(&b, &v, &r, None);
1935 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1936 let term = run_render(view, 4, 3);
1937 assert!(
1939 term.cell((2, 1))
1940 .unwrap()
1941 .modifier
1942 .contains(Modifier::REVERSED)
1943 );
1944 }
1945
1946 #[test]
1947 fn wrap_word_breaks_at_whitespace() {
1948 let b = Buffer::from_str("alpha beta gamma");
1949 let v = Viewport {
1950 top_row: 0,
1951 top_col: 0,
1952 width: 8,
1953 height: 3,
1954 wrap: Wrap::Word,
1955 text_width: 8,
1956 tab_width: 0,
1957 };
1958 let r = no_styles as fn(u32) -> Style;
1959 let view = make_wrap_view(&b, &v, &r, None);
1960 let term = run_render(view, 8, 3);
1961 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1963 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1964 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1966 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1967 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1969 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1970 }
1971
1972 fn view_with<'a>(
1978 b: &'a Buffer,
1979 viewport: &'a Viewport,
1980 resolver: &'a (impl StyleResolver + 'a),
1981 spans: &'a [Vec<Span>],
1982 search_pattern: Option<&'a regex::Regex>,
1983 ) -> BufferView<'a, impl StyleResolver + 'a> {
1984 BufferView {
1985 buffer: b,
1986 viewport,
1987 selection: None,
1988 resolver,
1989 cursor_line_bg: Style::default(),
1990 cursor_column_bg: Style::default(),
1991 selection_bg: Style::default().bg(Color::Blue),
1992 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1993 gutter: None,
1994 search_bg: Style::default().bg(Color::Magenta),
1995 signs: &[],
1996 conceals: &[],
1997 spans,
1998 search_pattern,
1999 non_text_style: Style::default(),
2000 diag_overlays: &[],
2001 colorcolumn_cols: &[],
2002 colorcolumn_style: Style::default(),
2003 }
2004 }
2005
2006 #[test]
2007 fn empty_spans_param_renders_default_style() {
2008 let b = Buffer::from_str("hello");
2009 let v = vp(10, 1);
2010 let r = no_styles as fn(u32) -> Style;
2011 let view = view_with(&b, &v, &r, &[], None);
2012 let term = run_render(view, 10, 1);
2013 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
2014 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
2015 }
2016
2017 #[test]
2018 fn spans_param_paints_styled_byte_range() {
2019 let b = Buffer::from_str("abcdef");
2020 let v = vp(10, 1);
2021 let resolver = |id: u32| -> Style {
2022 if id == 3 {
2023 Style::default().fg(Color::Green)
2024 } else {
2025 Style::default()
2026 }
2027 };
2028 let spans = vec![vec![Span::new(0, 3, 3)]];
2029 let view = view_with(&b, &v, &resolver, &spans, None);
2030 let term = run_render(view, 10, 1);
2031 for x in 0..3 {
2032 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
2033 }
2034 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
2035 }
2036
2037 #[test]
2038 fn spans_param_handles_per_row_overlay() {
2039 let b = Buffer::from_str("abc\ndef");
2040 let v = vp(10, 2);
2041 let resolver = |id: u32| -> Style {
2042 if id == 1 {
2043 Style::default().fg(Color::Red)
2044 } else {
2045 Style::default().fg(Color::Green)
2046 }
2047 };
2048 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
2049 let view = view_with(&b, &v, &resolver, &spans, None);
2050 let term = run_render(view, 10, 2);
2051 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
2052 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
2053 }
2054
2055 #[test]
2056 fn spans_param_rows_beyond_get_no_styling() {
2057 let b = Buffer::from_str("abc\ndef\nghi");
2058 let v = vp(10, 3);
2059 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
2060 let spans = vec![vec![Span::new(0, 3, 0)]];
2062 let view = view_with(&b, &v, &resolver, &spans, None);
2063 let term = run_render(view, 10, 3);
2064 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
2065 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
2066 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
2067 }
2068
2069 #[test]
2070 fn search_pattern_none_disables_hlsearch() {
2071 let b = Buffer::from_str("foo bar foo");
2072 let v = vp(20, 1);
2073 let r = no_styles as fn(u32) -> Style;
2074 let view = view_with(&b, &v, &r, &[], None);
2076 let term = run_render(view, 20, 1);
2077 for x in 0..11 {
2078 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
2079 }
2080 }
2081
2082 #[test]
2083 fn search_pattern_regex_paints_match_bg() {
2084 use regex::Regex;
2085 let b = Buffer::from_str("xyz foo xyz");
2086 let v = vp(20, 1);
2087 let r = no_styles as fn(u32) -> Style;
2088 let pat = Regex::new("foo").unwrap();
2089 let view = view_with(&b, &v, &r, &[], Some(&pat));
2090 let term = run_render(view, 20, 1);
2091 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
2093 for x in 4..7 {
2094 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
2095 }
2096 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
2097 }
2098
2099 #[test]
2100 fn search_pattern_unicode_columns_are_charwise() {
2101 use regex::Regex;
2102 let b = Buffer::from_str("tablé foo");
2104 let v = vp(20, 1);
2105 let r = no_styles as fn(u32) -> Style;
2106 let pat = Regex::new("foo").unwrap();
2107 let view = view_with(&b, &v, &r, &[], Some(&pat));
2108 let term = run_render(view, 20, 1);
2109 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
2111 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
2112 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
2113 }
2114
2115 #[test]
2116 fn spans_param_clamps_short_row_overlay() {
2117 let b = Buffer::from_str("abc");
2119 let v = vp(10, 1);
2120 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
2121 let spans = vec![vec![Span::new(0, 100, 0)]];
2122 let view = view_with(&b, &v, &resolver, &spans, None);
2123 let term = run_render(view, 10, 1);
2124 for x in 0..3 {
2125 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
2126 }
2127 }
2128
2129 #[test]
2130 fn spans_and_search_pattern_compose() {
2131 use regex::Regex;
2133 let b = Buffer::from_str("foo");
2134 let v = vp(10, 1);
2135 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
2136 let spans = vec![vec![Span::new(0, 3, 0)]];
2137 let pat = Regex::new("foo").unwrap();
2138 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
2139 let term = run_render(view, 10, 1);
2140 let cell = term.cell((1, 0)).unwrap();
2141 assert_eq!(cell.fg, Color::Green);
2142 assert_eq!(cell.bg, Color::Magenta);
2143 }
2144
2145 #[test]
2149 fn tilde_marker_painted_past_eof() {
2150 let b = Buffer::from_str("a\nb\nc\nd\ne");
2152 let v = vp(10, 10);
2153 let r = no_styles as fn(u32) -> Style;
2154 let non_text_fg = Color::DarkGray;
2155 let view = BufferView {
2156 buffer: &b,
2157 viewport: &v,
2158 selection: None,
2159 resolver: &r,
2160 cursor_line_bg: Style::default(),
2161 cursor_column_bg: Style::default(),
2162 selection_bg: Style::default().bg(Color::Blue),
2163 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2164 gutter: None,
2165 search_bg: Style::default(),
2166 signs: &[],
2167 conceals: &[],
2168 spans: &[],
2169 search_pattern: None,
2170 non_text_style: Style::default().fg(non_text_fg),
2171 diag_overlays: &[],
2172 colorcolumn_cols: &[],
2173 colorcolumn_style: Style::default(),
2174 };
2175 let term = run_render(view, 10, 10);
2176 for row in 0..5u16 {
2178 assert_ne!(
2179 term.cell((0, row)).unwrap().symbol(),
2180 "~",
2181 "row {row} is a content row, expected no tilde"
2182 );
2183 }
2184 for row in 5..10u16 {
2186 let cell = term.cell((0, row)).unwrap();
2187 assert_eq!(cell.symbol(), "~", "row {row} is past EOF, expected tilde");
2188 assert_eq!(
2189 cell.fg, non_text_fg,
2190 "row {row} tilde should use non_text_style fg"
2191 );
2192 for x in 1..10u16 {
2194 assert_eq!(
2195 term.cell((x, row)).unwrap().symbol(),
2196 " ",
2197 "row {row} col {x} after tilde should be blank"
2198 );
2199 }
2200 }
2201 }
2202
2203 #[test]
2206 fn tilde_marker_with_gutter_past_eof() {
2207 let b = Buffer::from_str("a\nb");
2208 let v = vp(10, 5);
2209 let r = no_styles as fn(u32) -> Style;
2210 let non_text_fg = Color::DarkGray;
2211 let view = BufferView {
2212 buffer: &b,
2213 viewport: &v,
2214 selection: None,
2215 resolver: &r,
2216 cursor_line_bg: Style::default(),
2217 cursor_column_bg: Style::default(),
2218 selection_bg: Style::default().bg(Color::Blue),
2219 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2220 gutter: Some(Gutter {
2221 width: 4,
2222 style: Style::default().fg(Color::Yellow),
2223 line_offset: 0,
2224 numbers: GutterNumbers::Absolute,
2225 sign_column_width: 0,
2226 }),
2227 search_bg: Style::default(),
2228 signs: &[],
2229 conceals: &[],
2230 spans: &[],
2231 search_pattern: None,
2232 non_text_style: Style::default().fg(non_text_fg),
2233 diag_overlays: &[],
2234 colorcolumn_cols: &[],
2235 colorcolumn_style: Style::default(),
2236 };
2237 let term = run_render(view, 10, 5);
2238 for row in 2..5u16 {
2240 for x in 0..4u16 {
2242 assert_eq!(
2243 term.cell((x, row)).unwrap().symbol(),
2244 " ",
2245 "gutter col {x} on past-EOF row {row} should be blank"
2246 );
2247 }
2248 let cell = term.cell((4, row)).unwrap();
2250 assert_eq!(
2251 cell.symbol(),
2252 "~",
2253 "past-EOF row {row}: expected tilde at text column"
2254 );
2255 assert_eq!(cell.fg, non_text_fg);
2256 }
2257 }
2258
2259 #[test]
2260 fn diag_overlay_paints_underline_on_range() {
2261 let b = Buffer::from_str("hello world");
2265 let v = vp(20, 2);
2266 let overlay = DiagOverlay {
2267 row: 0,
2268 col_start: 6,
2269 col_end: 11,
2270 style: Style::default().add_modifier(Modifier::UNDERLINED),
2271 };
2272 let view = BufferView {
2273 buffer: &b,
2274 viewport: &v,
2275 selection: None,
2276 resolver: &(no_styles as fn(u32) -> Style),
2277 cursor_line_bg: Style::default(),
2278 cursor_column_bg: Style::default(),
2279 selection_bg: Style::default().bg(Color::Blue),
2280 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2281 gutter: None,
2282 search_bg: Style::default(),
2283 signs: &[],
2284 conceals: &[],
2285 spans: &[],
2286 search_pattern: None,
2287 non_text_style: Style::default(),
2288 diag_overlays: &[overlay],
2289 colorcolumn_cols: &[],
2290 colorcolumn_style: Style::default(),
2291 };
2292 let term = run_render(view, 20, 2);
2293
2294 for x in 0u16..6 {
2296 let cell = term.cell((x, 0)).unwrap();
2297 assert!(
2298 !cell.modifier.contains(Modifier::UNDERLINED),
2299 "col {x} must not be underlined (outside overlay)"
2300 );
2301 }
2302 for x in 6u16..11 {
2304 let cell = term.cell((x, 0)).unwrap();
2305 assert!(
2306 cell.modifier.contains(Modifier::UNDERLINED),
2307 "col {x} must be underlined (inside overlay)"
2308 );
2309 }
2310 let cell = term.cell((11, 0)).unwrap();
2312 assert!(
2313 !cell.modifier.contains(Modifier::UNDERLINED),
2314 "col 11 must not be underlined (past overlay end)"
2315 );
2316 }
2317
2318 #[test]
2319 fn diag_overlay_out_of_viewport_is_ignored() {
2320 let b = Buffer::from_str("a\nb\nc");
2322 let v = vp(10, 3);
2323 let overlay = DiagOverlay {
2324 row: 5,
2325 col_start: 0,
2326 col_end: 1,
2327 style: Style::default().add_modifier(Modifier::UNDERLINED),
2328 };
2329 let view = BufferView {
2330 buffer: &b,
2331 viewport: &v,
2332 selection: None,
2333 resolver: &(no_styles as fn(u32) -> Style),
2334 cursor_line_bg: Style::default(),
2335 cursor_column_bg: Style::default(),
2336 selection_bg: Style::default().bg(Color::Blue),
2337 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2338 gutter: None,
2339 search_bg: Style::default(),
2340 signs: &[],
2341 conceals: &[],
2342 spans: &[],
2343 search_pattern: None,
2344 non_text_style: Style::default(),
2345 diag_overlays: &[overlay],
2346 colorcolumn_cols: &[],
2347 colorcolumn_style: Style::default(),
2348 };
2349 let _term = run_render(view, 10, 3);
2351 }
2352
2353 #[test]
2363 fn paint_signs_in_dedicated_column_does_not_overwrite_line_number() {
2364 let b = Buffer::from_str("a\nb");
2368 let v = vp(20, 2);
2370 let sign = Sign {
2371 row: 0,
2372 ch: '~',
2373 style: Style::default().fg(Color::Red),
2374 priority: 10,
2375 };
2376 let view = BufferView {
2377 buffer: &b,
2378 viewport: &v,
2379 selection: None,
2380 resolver: &(no_styles as fn(u32) -> Style),
2381 cursor_line_bg: Style::default(),
2382 cursor_column_bg: Style::default(),
2383 selection_bg: Style::default().bg(Color::Blue),
2384 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2385 gutter: Some(Gutter {
2386 width: 6, style: Style::default(),
2388 line_offset: 13108, sign_column_width: 1,
2390 ..Default::default()
2391 }),
2392 search_bg: Style::default(),
2393 signs: &[sign],
2394 conceals: &[],
2395 spans: &[],
2396 search_pattern: None,
2397 non_text_style: Style::default(),
2398 diag_overlays: &[],
2399 colorcolumn_cols: &[],
2400 colorcolumn_style: Style::default(),
2401 };
2402 let term = run_render(view, 20, 2);
2403 assert_eq!(
2405 term.cell((0, 0)).unwrap().symbol(),
2406 "~",
2407 "sign column (x=0) must hold the sign char"
2408 );
2409 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1", "x=1 must be '1'");
2411 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "3", "x=2 must be '3'");
2412 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "1", "x=3 must be '1'");
2413 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "0", "x=4 must be '0'");
2414 assert_eq!(term.cell((5, 0)).unwrap().symbol(), "9", "x=5 must be '9'");
2415 assert_eq!(
2417 term.cell((6, 0)).unwrap().symbol(),
2418 " ",
2419 "x=6 must be spacer"
2420 );
2421 assert_eq!(
2423 term.cell((7, 0)).unwrap().symbol(),
2424 "a",
2425 "text must start at x=sign_w+num_w=7"
2426 );
2427 }
2428
2429 #[test]
2432 fn paint_signs_zero_sign_column_width_layout_collapses() {
2433 let b = Buffer::from_str("abc");
2434 let v = vp(10, 1);
2435 let sign = Sign {
2436 row: 0,
2437 ch: 'E',
2438 style: Style::default().fg(Color::Red),
2439 priority: 10,
2440 };
2441 let view = BufferView {
2442 buffer: &b,
2443 viewport: &v,
2444 selection: None,
2445 resolver: &(no_styles as fn(u32) -> Style),
2446 cursor_line_bg: Style::default(),
2447 cursor_column_bg: Style::default(),
2448 selection_bg: Style::default().bg(Color::Blue),
2449 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2450 gutter: Some(Gutter {
2452 width: 3,
2453 style: Style::default(),
2454 line_offset: 0,
2455 sign_column_width: 0,
2456 ..Default::default()
2457 }),
2458 search_bg: Style::default(),
2459 signs: &[sign],
2460 conceals: &[],
2461 spans: &[],
2462 search_pattern: None,
2463 non_text_style: Style::default(),
2464 diag_overlays: &[],
2465 colorcolumn_cols: &[],
2466 colorcolumn_style: Style::default(),
2467 };
2468 let term = run_render(view, 10, 1);
2469 assert_ne!(
2471 term.cell((0, 0)).unwrap().symbol(),
2472 "E",
2473 "with sign_column_width=0, sign char must not appear in the gutter"
2474 );
2475 assert_eq!(
2477 term.cell((3, 0)).unwrap().symbol(),
2478 "a",
2479 "text must start at x=gutter.width when sign_column_width=0"
2480 );
2481 }
2482}