1use ratatui::buffer::Buffer as TermBuffer;
25use ratatui::layout::Rect;
26use ratatui::style::Style;
27use ratatui::widgets::Widget;
28use unicode_width::UnicodeWidthChar;
29
30use hjkl_buffer::wrap::wrap_segments;
31use hjkl_buffer::{Buffer, Selection, Span, Viewport, Wrap};
32
33pub trait StyleResolver {
37 fn resolve(&self, style_id: u32) -> Style;
38}
39
40impl<F: Fn(u32) -> Style> StyleResolver for F {
42 fn resolve(&self, style_id: u32) -> Style {
43 self(style_id)
44 }
45}
46
47pub struct BufferView<'a, R: StyleResolver> {
62 pub buffer: &'a Buffer,
63 pub viewport: &'a Viewport,
67 pub selection: Option<Selection>,
68 pub resolver: &'a R,
69 pub cursor_line_bg: Style,
72 pub cursor_column_bg: Style,
75 pub selection_bg: Style,
77 pub cursor_style: Style,
80 pub gutter: Option<Gutter>,
84 pub search_bg: Style,
87 pub signs: &'a [Sign],
92 pub conceals: &'a [Conceal],
96 pub spans: &'a [Vec<Span>],
105 pub search_pattern: Option<&'a regex::Regex>,
113 pub non_text_style: Style,
120 pub diag_overlays: &'a [DiagOverlay],
125 pub colorcolumn_cols: &'a [u16],
129 pub colorcolumn_style: Style,
132 pub listchars: Option<&'a hjkl_buffer::ListChars>,
136 pub indent_guides_enabled: bool,
140 pub indent_guide_char: char,
142 pub indent_guide_shiftwidth: usize,
146 pub indent_guide_fg: ratatui::style::Color,
148 pub indent_guide_active_fg: ratatui::style::Color,
150 pub indent_guide_active_col: Option<usize>,
154}
155
156#[derive(Debug, Clone, Copy, Default)]
160pub enum GutterNumbers {
161 None,
163 #[default]
165 Absolute,
166 Relative { cursor_row: usize },
168 Hybrid { cursor_row: usize },
171}
172
173#[derive(Debug, Clone, Copy, Default)]
190pub struct Gutter {
191 pub width: u16,
194 pub style: Style,
195 pub line_offset: usize,
196 pub numbers: GutterNumbers,
198 pub sign_column_width: u16,
203}
204
205#[derive(Debug, Clone, Copy)]
210pub struct Sign {
211 pub row: usize,
212 pub ch: char,
213 pub style: Style,
214 pub priority: u8,
215}
216
217#[derive(Debug, Clone)]
222pub struct Conceal {
223 pub row: usize,
224 pub start_byte: usize,
225 pub end_byte: usize,
226 pub replacement: String,
227}
228
229#[derive(Debug, Clone, Copy)]
235pub struct DiagOverlay {
236 pub row: usize,
238 pub col_start: usize,
240 pub col_end: usize,
242 pub style: Style,
244}
245
246impl<R: StyleResolver> Widget for BufferView<'_, R> {
247 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
248 let viewport = *self.viewport;
249 let cursor = self.buffer.cursor();
250 let spans = self.spans;
251 let folds = self.buffer.folds();
252 let top_row = viewport.top_row;
253 let top_col = viewport.top_col;
254 let total_rows = self.buffer.row_count();
260 let prefetch_end = top_row.saturating_add(area.height as usize).min(total_rows);
261 let rope = self.buffer.rope();
262 let lines_prefetch: Vec<String> = (top_row..prefetch_end)
263 .map(|i| hjkl_buffer::rope_line_str(&rope, i))
264 .collect();
265 let prefetch_base = top_row;
266 let prefetch_end_idx = prefetch_end;
267 let line_at = |row: usize| -> String {
268 if row >= prefetch_base && row < prefetch_end_idx {
269 lines_prefetch[row - prefetch_base].clone()
270 } else {
271 hjkl_buffer::rope_line_str(&rope, row)
272 }
273 };
274
275 let gutter_total = self
276 .gutter
277 .map(|g| g.sign_column_width + g.width)
278 .unwrap_or(0);
279 let text_area = Rect {
280 x: area.x.saturating_add(gutter_total),
281 y: area.y,
282 width: area.width.saturating_sub(gutter_total),
283 height: area.height,
284 };
285
286 let mut doc_row = top_row;
288 let mut screen_row: u16 = 0;
289 let wrap_mode = viewport.wrap;
290 let seg_width = if viewport.text_width > 0 {
291 viewport.text_width
292 } else {
293 text_area.width
294 };
295 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
301 while doc_row < total_rows && screen_row < area.height {
305 if folds.iter().any(|f| f.hides(doc_row)) {
308 doc_row += 1;
309 continue;
310 }
311 let folded_at_start = folds
312 .iter()
313 .find(|f| f.closed && f.start_row == doc_row)
314 .copied();
315 let line_owned = line_at(doc_row);
316 let line: &str = line_owned.as_str();
317 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
318 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
319 let is_cursor_row = doc_row == cursor.row;
320 if let Some(fold) = folded_at_start {
321 if let Some(gutter) = self.gutter {
322 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
323 self.paint_signs(term_buf, area, screen_row, doc_row, gutter);
324 }
325 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
326 search_hit_at_cursor_col.push(false);
327 screen_row += 1;
328 doc_row = fold.end_row + 1;
329 continue;
330 }
331 let search_ranges = self.row_search_ranges(line);
332 let row_has_hit_at_cursor_col = search_ranges
333 .iter()
334 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
335 let row_conceals: Vec<&Conceal> = {
337 let mut v: Vec<&Conceal> =
338 self.conceals.iter().filter(|c| c.row == doc_row).collect();
339 v.sort_by_key(|c| c.start_byte);
340 v
341 };
342 let segments = match wrap_mode {
350 Wrap::None => vec![(top_col, usize::MAX)],
351 _ => wrap_segments(line, seg_width, wrap_mode),
352 };
353 let last_seg_idx = segments.len().saturating_sub(1);
354 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
355 if screen_row >= area.height {
356 break;
357 }
358 if let Some(gutter) = self.gutter {
359 if seg_idx == 0 {
360 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
361 self.paint_signs(term_buf, area, screen_row, doc_row, gutter);
362 } else {
363 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
364 }
365 }
366 self.paint_row(
367 term_buf,
368 text_area,
369 screen_row,
370 line,
371 row_spans,
372 sel_range,
373 &search_ranges,
374 is_cursor_row,
375 cursor.col,
376 seg_start,
377 seg_end,
378 seg_idx == last_seg_idx,
379 &row_conceals,
380 );
381 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
382 screen_row += 1;
383 }
384 doc_row += 1;
385 }
386 while screen_row < area.height {
389 if let Some(gutter) = self.gutter {
391 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
392 }
393 let y = text_area.y + screen_row;
395 if let Some(cell) = term_buf.cell_mut((text_area.x, y)) {
396 cell.set_char('~');
397 cell.set_style(self.non_text_style);
398 }
399 screen_row += 1;
400 }
401 if matches!(wrap_mode, Wrap::None)
408 && self.cursor_column_bg != Style::default()
409 && cursor.col >= top_col
410 && (cursor.col - top_col) < text_area.width as usize
411 {
412 let x = text_area.x + (cursor.col - top_col) as u16;
413 for sy in 0..screen_row {
414 if search_hit_at_cursor_col
418 .get(sy as usize)
419 .copied()
420 .unwrap_or(false)
421 {
422 continue;
423 }
424 let y = text_area.y + sy;
425 if let Some(cell) = term_buf.cell_mut((x, y)) {
426 cell.set_style(cell.style().patch(self.cursor_column_bg));
427 }
428 }
429 }
430
431 if matches!(wrap_mode, Wrap::None) && !self.colorcolumn_cols.is_empty() {
435 for &col_1based in self.colorcolumn_cols {
436 let col = col_1based as usize; if col == 0 || col < top_col + 1 {
438 continue; }
440 let screen_col = col - 1 - top_col; if screen_col >= text_area.width as usize {
442 continue; }
444 let x = text_area.x + screen_col as u16;
445 for sy in 0..screen_row {
446 let y = text_area.y + sy;
447 if let Some(cell) = term_buf.cell_mut((x, y)) {
448 cell.set_style(cell.style().patch(self.colorcolumn_style));
449 }
450 }
451 }
452 }
453
454 if matches!(wrap_mode, Wrap::None)
459 && self.indent_guides_enabled
460 && self.indent_guide_shiftwidth > 0
461 {
462 let sw = self.indent_guide_shiftwidth;
463 let tab_width = self.viewport.effective_tab_width();
464 let mut ig_doc_row = top_row;
466 let mut ig_screen_row: u16 = 0;
467 while ig_doc_row < total_rows && ig_screen_row < area.height {
468 if folds.iter().any(|f| f.hides(ig_doc_row)) {
469 ig_doc_row += 1;
470 continue;
471 }
472 if let Some(fold) = folds
474 .iter()
475 .find(|f| f.closed && f.start_row == ig_doc_row)
476 .copied()
477 {
478 ig_screen_row += 1;
479 ig_doc_row = fold.end_row + 1;
480 continue;
481 }
482 let line_owned = line_at(ig_doc_row);
483 let line: &str = line_owned.as_str();
484 let mut leading_vcols: usize = 0;
486 for ch in line.chars() {
487 match ch {
488 ' ' => leading_vcols += 1,
489 '\t' => {
490 leading_vcols += tab_width - (leading_vcols % tab_width);
491 }
492 _ => break,
493 }
494 }
495 let y = text_area.y + ig_screen_row;
497 let mut guide_col = sw;
498 while guide_col < leading_vcols {
499 if guide_col >= top_col {
501 let screen_col = guide_col - top_col;
502 if screen_col < text_area.width as usize {
503 let x = text_area.x + screen_col as u16;
504 let is_active = Some(guide_col) == self.indent_guide_active_col;
505 let fg = if is_active {
506 self.indent_guide_active_fg
507 } else {
508 self.indent_guide_fg
509 };
510 if let Some(cell) = term_buf.cell_mut((x, y)) {
511 if cell.symbol() == " " {
514 cell.set_char(self.indent_guide_char);
515 let existing = cell.style();
517 cell.set_style(existing.fg(fg));
518 }
519 }
520 }
521 }
522 guide_col += sw;
523 }
524 ig_screen_row += 1;
525 ig_doc_row += 1;
526 }
527 }
528
529 if matches!(wrap_mode, Wrap::None) && !self.diag_overlays.is_empty() {
534 let vp_top = top_row;
538 let vp_bot = vp_top + area.height as usize;
539 for overlay in self.diag_overlays {
540 if overlay.row < vp_top || overlay.row >= vp_bot {
541 continue;
542 }
543 let mut sr: u16 = 0;
546 let mut dr = vp_top;
547 while dr < overlay.row && sr < area.height {
548 if !folds.iter().any(|f| f.hides(dr)) {
549 sr += 1;
550 }
551 dr += 1;
552 }
553 if sr >= area.height {
554 continue;
555 }
556 let y = text_area.y + sr;
557 let col_start = overlay.col_start;
560 let col_end = overlay.col_end.max(col_start + 1);
561 for col in col_start..col_end {
562 if col < top_col {
563 continue;
564 }
565 let screen_col = col - top_col;
566 if screen_col >= text_area.width as usize {
567 break;
568 }
569 let x = text_area.x + screen_col as u16;
570 if let Some(cell) = term_buf.cell_mut((x, y)) {
571 cell.set_style(cell.style().patch(overlay.style));
572 }
573 }
574 }
575 }
576 }
577}
578
579impl<R: StyleResolver> BufferView<'_, R> {
580 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
584 let Some(re) = self.search_pattern else {
585 return Vec::new();
586 };
587 re.find_iter(line)
588 .map(|m| {
589 let start = line[..m.start()].chars().count();
590 let end = line[..m.end()].chars().count();
591 (start, end)
592 })
593 .collect()
594 }
595
596 fn paint_fold_marker(
597 &self,
598 term_buf: &mut TermBuffer,
599 area: Rect,
600 screen_row: u16,
601 fold: hjkl_buffer::Fold,
602 first_line: &str,
603 is_cursor_row: bool,
604 ) {
605 let y = area.y + screen_row;
606 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
607 self.cursor_line_bg
608 } else {
609 Style::default()
610 };
611 for x in area.x..(area.x + area.width) {
613 if let Some(cell) = term_buf.cell_mut((x, y)) {
614 cell.set_style(style);
615 }
616 }
617 let prefix = first_line.trim();
621 let count = fold.line_count();
622 let label = if prefix.is_empty() {
623 format!("▸ {count} lines folded")
624 } else {
625 const MAX_PREFIX: usize = 60;
626 let trimmed = if prefix.chars().count() > MAX_PREFIX {
627 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
628 format!("{head}…")
629 } else {
630 prefix.to_string()
631 };
632 format!("▸ {trimmed} ({count} lines)")
633 };
634 let mut x = area.x;
635 let row_end_x = area.x + area.width;
636 for ch in label.chars() {
637 if x >= row_end_x {
638 break;
639 }
640 let width = ch.width().unwrap_or(1) as u16;
641 if x + width > row_end_x {
642 break;
643 }
644 if let Some(cell) = term_buf.cell_mut((x, y)) {
645 cell.set_char(ch);
646 cell.set_style(style);
647 }
648 x = x.saturating_add(width);
649 }
650 }
651
652 fn paint_signs(
653 &self,
654 term_buf: &mut TermBuffer,
655 area: Rect,
656 screen_row: u16,
657 doc_row: usize,
658 gutter: Gutter,
659 ) {
660 if gutter.sign_column_width == 0 {
662 return;
663 }
664 let y = area.y + screen_row;
665 let sign_x = area.x;
666 for x in sign_x..sign_x + gutter.sign_column_width {
668 if let Some(cell) = term_buf.cell_mut((x, y)) {
669 cell.set_char(' ');
670 cell.set_style(gutter.style);
671 }
672 }
673 let Some(sign) = self
675 .signs
676 .iter()
677 .filter(|s| s.row == doc_row)
678 .max_by_key(|s| s.priority)
679 else {
680 return;
681 };
682 if let Some(cell) = term_buf.cell_mut((sign_x, y)) {
683 cell.set_char(sign.ch);
684 cell.set_style(sign.style);
685 }
686 }
687
688 fn paint_blank_gutter(
691 &self,
692 term_buf: &mut TermBuffer,
693 area: Rect,
694 screen_row: u16,
695 gutter: Gutter,
696 ) {
697 let y = area.y + screen_row;
698 let total = gutter.sign_column_width + gutter.width;
699 for x in area.x..(area.x + total) {
700 if let Some(cell) = term_buf.cell_mut((x, y)) {
701 cell.set_char(' ');
702 cell.set_style(gutter.style);
703 }
704 }
705 }
706
707 fn paint_gutter(
708 &self,
709 term_buf: &mut TermBuffer,
710 area: Rect,
711 screen_row: u16,
712 doc_row: usize,
713 gutter: Gutter,
714 ) {
715 let y = area.y + screen_row;
716 let num_start = area.x + gutter.sign_column_width;
718 let number_width = gutter.width.saturating_sub(1) as usize;
720
721 let label = match gutter.numbers {
723 GutterNumbers::None => {
724 for x in num_start..(num_start + gutter.width) {
726 if let Some(cell) = term_buf.cell_mut((x, y)) {
727 cell.set_char(' ');
728 cell.set_style(gutter.style);
729 }
730 }
731 return;
732 }
733 GutterNumbers::Absolute => {
734 format!(
735 "{:>width$}",
736 doc_row + 1 + gutter.line_offset,
737 width = number_width
738 )
739 }
740 GutterNumbers::Relative { cursor_row } => {
741 let n = if doc_row == cursor_row {
742 0
743 } else {
744 doc_row.abs_diff(cursor_row)
745 };
746 format!("{:>width$}", n, width = number_width)
747 }
748 GutterNumbers::Hybrid { cursor_row } => {
749 let n = if doc_row == cursor_row {
750 doc_row + 1 + gutter.line_offset
751 } else {
752 doc_row.abs_diff(cursor_row)
753 };
754 format!("{:>width$}", n, width = number_width)
755 }
756 };
757
758 let mut x = num_start;
759 for ch in label.chars() {
760 if x >= num_start + gutter.width.saturating_sub(1) {
761 break;
762 }
763 if let Some(cell) = term_buf.cell_mut((x, y)) {
764 cell.set_char(ch);
765 cell.set_style(gutter.style);
766 }
767 x = x.saturating_add(1);
768 }
769 let spacer_x = num_start + gutter.width.saturating_sub(1);
772 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
773 cell.set_char(' ');
774 cell.set_style(gutter.style);
775 }
776 }
777
778 #[allow(clippy::too_many_arguments)]
779 fn paint_row(
780 &self,
781 term_buf: &mut TermBuffer,
782 area: Rect,
783 screen_row: u16,
784 line: &str,
785 row_spans: &[hjkl_buffer::Span],
786 sel_range: hjkl_buffer::RowSpan,
787 search_ranges: &[(usize, usize)],
788 is_cursor_row: bool,
789 cursor_col: usize,
790 seg_start: usize,
791 seg_end: usize,
792 is_last_segment: bool,
793 conceals: &[&Conceal],
794 ) {
795 let y = area.y + screen_row;
796 let mut screen_x = area.x;
797 let row_end_x = area.x + area.width;
798
799 if is_cursor_row && self.cursor_line_bg != Style::default() {
803 for x in area.x..row_end_x {
804 if let Some(cell) = term_buf.cell_mut((x, y)) {
805 cell.set_style(self.cursor_line_bg);
806 }
807 }
808 }
809
810 let tab_width = self.viewport.effective_tab_width();
814
815 let trail_byte_start: usize = if self.listchars.is_some() {
819 line.trim_end_matches([' ', '\t']).len()
820 } else {
821 line.len()
822 };
823
824 let mut byte_offset: usize = 0;
825 let mut line_col: usize = 0;
826 let mut chars_iter = line.chars().enumerate().peekable();
827 while let Some((col_idx, ch)) = chars_iter.next() {
828 let ch_byte_len = ch.len_utf8();
829 if col_idx >= seg_end {
830 break;
831 }
832 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
837 if col_idx >= seg_start {
838 let mut style = if is_cursor_row {
839 self.cursor_line_bg
840 } else {
841 Style::default()
842 };
843 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
844 style = style.patch(span_style);
845 }
846 for rch in conc.replacement.chars() {
847 let rwidth = rch.width().unwrap_or(1) as u16;
848 if screen_x + rwidth > row_end_x {
849 break;
850 }
851 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
852 cell.set_char(rch);
853 cell.set_style(style);
854 }
855 screen_x += rwidth;
856 }
857 }
858 let mut consumed = ch_byte_len;
861 byte_offset += ch_byte_len;
862 while byte_offset < conc.end_byte {
863 let Some((_, next_ch)) = chars_iter.next() else {
864 break;
865 };
866 consumed += next_ch.len_utf8();
867 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
868 }
869 let _ = consumed;
870 continue;
871 }
872 let visible_width = if ch == '\t' {
877 tab_width - (line_col % tab_width)
878 } else {
879 ch.width().unwrap_or(1)
880 };
881 if col_idx < seg_start {
884 line_col += visible_width;
885 byte_offset += ch_byte_len;
886 continue;
887 }
888 let width = visible_width as u16;
890 if screen_x + width > row_end_x {
891 break;
892 }
893
894 let mut style = if is_cursor_row {
896 self.cursor_line_bg
897 } else {
898 Style::default()
899 };
900 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
901 style = style.patch(span_style);
902 }
903 if self.search_bg != Style::default()
907 && search_ranges
908 .iter()
909 .any(|&(s, e)| col_idx >= s && col_idx < e)
910 {
911 style = style.patch(self.search_bg);
912 }
913 if let Some((lo, hi)) = sel_range
914 && col_idx >= lo
915 && col_idx <= hi
916 {
917 style = style.patch(self.selection_bg);
918 }
919 if is_cursor_row && col_idx == cursor_col {
920 style = style.patch(self.cursor_style);
921 }
922
923 if ch == '\t' {
924 if let Some(lc) = self.listchars {
928 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
930 cell.set_char(lc.tab_lead);
931 cell.set_style(style);
932 }
933 let fill_ch = lc.tab_fill.unwrap_or(' ');
934 for k in 1..width {
935 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
936 cell.set_char(fill_ch);
937 cell.set_style(style);
938 }
939 }
940 } else {
941 for k in 0..width {
945 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
946 cell.set_char(' ');
947 cell.set_style(style);
948 }
949 }
950 }
951 } else if let Some(lc) = self.listchars {
952 let display_ch = if ch == '\u{00a0}' {
954 lc.nbsp.unwrap_or(ch)
956 } else if ch == ' ' {
957 let is_trailing = byte_offset >= trail_byte_start;
958 if is_trailing {
959 lc.trail.or(lc.space).unwrap_or(ch)
960 } else {
961 lc.space.unwrap_or(ch)
962 }
963 } else {
964 ch
965 };
966 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
967 cell.set_char(display_ch);
968 cell.set_style(style);
969 }
970 } else if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
971 cell.set_char(ch);
972 cell.set_style(style);
973 }
974 screen_x += width;
975 line_col += visible_width;
976 byte_offset += ch_byte_len;
977 }
978
979 if is_last_segment
982 && let Some(lc) = self.listchars
983 && let Some(eol_ch) = lc.eol
984 && screen_x < row_end_x
985 && let Some(cell) = term_buf.cell_mut((screen_x, y))
986 {
987 cell.set_char(eol_ch);
988 cell.set_style(Style::default());
989 screen_x += 1;
990 }
991 let _ = screen_x; if let Some((lo, hi)) = sel_range
1009 && is_last_segment
1010 && line.chars().count() <= seg_start
1011 {
1012 let (start_col, end_col) = if hi == usize::MAX { (0, 0) } else { (lo, hi) };
1013 for col in start_col..=end_col {
1014 let pad_x = area.x + col as u16;
1015 if pad_x >= row_end_x {
1016 break;
1017 }
1018 if let Some(cell) = term_buf.cell_mut((pad_x, y)) {
1019 let prev = cell.style();
1020 cell.set_char(' ');
1021 cell.set_style(prev.patch(self.selection_bg));
1022 }
1023 }
1024 }
1025
1026 if is_cursor_row
1031 && is_last_segment
1032 && cursor_col >= line.chars().count()
1033 && cursor_col >= seg_start
1034 {
1035 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
1036 if pad_x < row_end_x
1037 && let Some(cell) = term_buf.cell_mut((pad_x, y))
1038 {
1039 cell.set_char(' ');
1040 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
1041 }
1042 }
1043 }
1044
1045 fn resolve_span_style(
1062 &self,
1063 row_spans: &[hjkl_buffer::Span],
1064 byte_offset: usize,
1065 ) -> Option<Style> {
1066 let mut overlapping: Vec<&hjkl_buffer::Span> = row_spans
1068 .iter()
1069 .filter(|s| byte_offset >= s.start_byte && byte_offset < s.end_byte)
1070 .collect();
1071 if overlapping.is_empty() {
1072 return None;
1073 }
1074 overlapping.sort_by_key(|s| std::cmp::Reverse(s.end_byte.saturating_sub(s.start_byte)));
1075 let mut style = self.resolver.resolve(overlapping[0].style);
1076 for s in &overlapping[1..] {
1077 style = style.patch(self.resolver.resolve(s.style));
1078 }
1079 Some(style)
1080 }
1081}
1082
1083#[cfg(test)]
1084mod tests {
1085 use super::*;
1086 use ratatui::style::{Color, Modifier};
1087 use ratatui::widgets::Widget;
1088
1089 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
1090 let area = Rect::new(0, 0, w, h);
1091 let mut buf = TermBuffer::empty(area);
1092 view.render(area, &mut buf);
1093 buf
1094 }
1095
1096 fn no_styles(_id: u32) -> Style {
1097 Style::default()
1098 }
1099
1100 fn vp(width: u16, height: u16) -> Viewport {
1102 Viewport {
1103 top_row: 0,
1104 top_col: 0,
1105 width,
1106 height,
1107 wrap: Wrap::None,
1108 text_width: width,
1109 tab_width: 0,
1110 }
1111 }
1112
1113 #[test]
1114 fn renders_plain_chars_into_terminal_buffer() {
1115 let b = Buffer::from_str("hello\nworld");
1116 let v = vp(20, 5);
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: None,
1127 search_bg: Style::default(),
1128 signs: &[],
1129 conceals: &[],
1130 spans: &[],
1131 search_pattern: None,
1132 non_text_style: Style::default(),
1133 diag_overlays: &[],
1134 colorcolumn_cols: &[],
1135 colorcolumn_style: Style::default(),
1136 listchars: None,
1137 indent_guides_enabled: false,
1138 indent_guide_char: '│',
1139 indent_guide_shiftwidth: 4,
1140 indent_guide_fg: Color::Reset,
1141 indent_guide_active_fg: Color::Reset,
1142 indent_guide_active_col: None,
1143 };
1144 let term = run_render(view, 20, 5);
1145 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1146 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
1147 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
1148 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
1149 }
1150
1151 #[test]
1152 fn cursor_cell_gets_reversed_style() {
1153 let mut b = Buffer::from_str("abc");
1154 let v = vp(10, 1);
1155 b.set_cursor(hjkl_buffer::Position::new(0, 1));
1156 let view = BufferView {
1157 buffer: &b,
1158 viewport: &v,
1159 selection: None,
1160 resolver: &(no_styles as fn(u32) -> Style),
1161 cursor_line_bg: Style::default(),
1162 cursor_column_bg: Style::default(),
1163 selection_bg: Style::default().bg(Color::Blue),
1164 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1165 gutter: None,
1166 search_bg: Style::default(),
1167 signs: &[],
1168 conceals: &[],
1169 spans: &[],
1170 search_pattern: None,
1171 non_text_style: Style::default(),
1172 diag_overlays: &[],
1173 colorcolumn_cols: &[],
1174 colorcolumn_style: Style::default(),
1175 listchars: None,
1176 indent_guides_enabled: false,
1177 indent_guide_char: '│',
1178 indent_guide_shiftwidth: 4,
1179 indent_guide_fg: Color::Reset,
1180 indent_guide_active_fg: Color::Reset,
1181 indent_guide_active_col: None,
1182 };
1183 let term = run_render(view, 10, 1);
1184 let cursor_cell = term.cell((1, 0)).unwrap();
1185 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
1186 }
1187
1188 #[test]
1189 fn selection_bg_applies_only_to_selected_cells() {
1190 use hjkl_buffer::{Position, Selection};
1191 let b = Buffer::from_str("abcdef");
1192 let v = vp(10, 1);
1193 let view = BufferView {
1194 buffer: &b,
1195 viewport: &v,
1196 selection: Some(Selection::Char {
1197 anchor: Position::new(0, 1),
1198 head: Position::new(0, 3),
1199 }),
1200 resolver: &(no_styles as fn(u32) -> Style),
1201 cursor_line_bg: Style::default(),
1202 cursor_column_bg: Style::default(),
1203 selection_bg: Style::default().bg(Color::Blue),
1204 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1205 gutter: None,
1206 search_bg: Style::default(),
1207 signs: &[],
1208 conceals: &[],
1209 spans: &[],
1210 search_pattern: None,
1211 non_text_style: Style::default(),
1212 diag_overlays: &[],
1213 colorcolumn_cols: &[],
1214 colorcolumn_style: Style::default(),
1215 listchars: None,
1216 indent_guides_enabled: false,
1217 indent_guide_char: '│',
1218 indent_guide_shiftwidth: 4,
1219 indent_guide_fg: Color::Reset,
1220 indent_guide_active_fg: Color::Reset,
1221 indent_guide_active_col: None,
1222 };
1223 let term = run_render(view, 10, 1);
1224 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
1225 for x in 1..=3 {
1226 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
1227 }
1228 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
1229 }
1230
1231 #[test]
1232 fn selection_paints_placeholder_on_empty_line_charwise() {
1233 use hjkl_buffer::{Position, Selection};
1236 let b = Buffer::from_str("abc\n\nxyz");
1237 let v = vp(10, 3);
1238 let view = BufferView {
1239 buffer: &b,
1240 viewport: &v,
1241 selection: Some(Selection::Char {
1242 anchor: Position::new(0, 0),
1243 head: Position::new(2, 2),
1244 }),
1245 resolver: &(no_styles as fn(u32) -> Style),
1246 cursor_line_bg: Style::default(),
1247 cursor_column_bg: Style::default(),
1248 selection_bg: Style::default().bg(Color::Blue),
1249 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1250 gutter: None,
1251 search_bg: Style::default(),
1252 signs: &[],
1253 conceals: &[],
1254 spans: &[],
1255 search_pattern: None,
1256 non_text_style: Style::default(),
1257 diag_overlays: &[],
1258 colorcolumn_cols: &[],
1259 colorcolumn_style: Style::default(),
1260 listchars: None,
1261 indent_guides_enabled: false,
1262 indent_guide_char: '│',
1263 indent_guide_shiftwidth: 4,
1264 indent_guide_fg: Color::Reset,
1265 indent_guide_active_fg: Color::Reset,
1266 indent_guide_active_col: None,
1267 };
1268 let term = run_render(view, 10, 3);
1269 assert_eq!(term.cell((0, 1)).unwrap().bg, Color::Blue);
1271 }
1272
1273 #[test]
1274 fn selection_paints_placeholder_on_empty_line_linewise() {
1275 use hjkl_buffer::Selection;
1276 let b = Buffer::from_str("abc\n\nxyz");
1277 let v = vp(10, 3);
1278 let view = BufferView {
1279 buffer: &b,
1280 viewport: &v,
1281 selection: Some(Selection::Line {
1282 anchor_row: 0,
1283 head_row: 2,
1284 }),
1285 resolver: &(no_styles as fn(u32) -> Style),
1286 cursor_line_bg: Style::default(),
1287 cursor_column_bg: Style::default(),
1288 selection_bg: Style::default().bg(Color::Blue),
1289 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1290 gutter: None,
1291 search_bg: Style::default(),
1292 signs: &[],
1293 conceals: &[],
1294 spans: &[],
1295 search_pattern: None,
1296 non_text_style: Style::default(),
1297 diag_overlays: &[],
1298 colorcolumn_cols: &[],
1299 colorcolumn_style: Style::default(),
1300 listchars: None,
1301 indent_guides_enabled: false,
1302 indent_guide_char: '│',
1303 indent_guide_shiftwidth: 4,
1304 indent_guide_fg: Color::Reset,
1305 indent_guide_active_fg: Color::Reset,
1306 indent_guide_active_col: None,
1307 };
1308 let term = run_render(view, 10, 3);
1309 assert_eq!(term.cell((0, 1)).unwrap().bg, Color::Blue);
1310 }
1311
1312 #[test]
1313 fn selection_paints_placeholder_on_empty_line_blockwise() {
1314 use hjkl_buffer::{Position, Selection};
1319 let b = Buffer::from_str("abcdef\n\nuvwxyz");
1320 let v = vp(10, 3);
1321 let view = BufferView {
1322 buffer: &b,
1323 viewport: &v,
1324 selection: Some(Selection::Block {
1325 anchor: Position::new(0, 2),
1326 head: Position::new(2, 5),
1327 }),
1328 resolver: &(no_styles as fn(u32) -> Style),
1329 cursor_line_bg: Style::default(),
1330 cursor_column_bg: Style::default(),
1331 selection_bg: Style::default().bg(Color::Blue),
1332 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1333 gutter: None,
1334 search_bg: Style::default(),
1335 signs: &[],
1336 conceals: &[],
1337 spans: &[],
1338 search_pattern: None,
1339 non_text_style: Style::default(),
1340 diag_overlays: &[],
1341 colorcolumn_cols: &[],
1342 colorcolumn_style: Style::default(),
1343 listchars: None,
1344 indent_guides_enabled: false,
1345 indent_guide_char: '│',
1346 indent_guide_shiftwidth: 4,
1347 indent_guide_fg: Color::Reset,
1348 indent_guide_active_fg: Color::Reset,
1349 indent_guide_active_col: None,
1350 };
1351 let term = run_render(view, 10, 3);
1352 for x in 2u16..=5 {
1354 assert_eq!(
1355 term.cell((x, 1)).unwrap().bg,
1356 Color::Blue,
1357 "empty row col {x} should carry block selection bg"
1358 );
1359 }
1360 assert!(term.cell((0, 1)).unwrap().bg != Color::Blue);
1363 assert!(term.cell((1, 1)).unwrap().bg != Color::Blue);
1364 assert!(term.cell((6, 1)).unwrap().bg != Color::Blue);
1366 for x in 2u16..=5 {
1368 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
1369 }
1370 }
1371
1372 #[test]
1373 fn selection_block_placeholder_clips_to_row_width() {
1374 use hjkl_buffer::{Position, Selection};
1376 let b = Buffer::from_str("abc\n\nxyz");
1377 let v = vp(5, 3);
1378 let view = BufferView {
1379 buffer: &b,
1380 viewport: &v,
1381 selection: Some(Selection::Block {
1382 anchor: Position::new(0, 1),
1383 head: Position::new(2, 20),
1384 }),
1385 resolver: &(no_styles as fn(u32) -> Style),
1386 cursor_line_bg: Style::default(),
1387 cursor_column_bg: Style::default(),
1388 selection_bg: Style::default().bg(Color::Blue),
1389 cursor_style: Style::default(),
1390 gutter: None,
1391 search_bg: Style::default(),
1392 signs: &[],
1393 conceals: &[],
1394 spans: &[],
1395 search_pattern: None,
1396 non_text_style: Style::default(),
1397 diag_overlays: &[],
1398 colorcolumn_cols: &[],
1399 colorcolumn_style: Style::default(),
1400 listchars: None,
1401 indent_guides_enabled: false,
1402 indent_guide_char: '│',
1403 indent_guide_shiftwidth: 4,
1404 indent_guide_fg: Color::Reset,
1405 indent_guide_active_fg: Color::Reset,
1406 indent_guide_active_col: None,
1407 };
1408 let term = run_render(view, 5, 3);
1410 for x in 1u16..=4 {
1411 assert_eq!(
1412 term.cell((x, 1)).unwrap().bg,
1413 Color::Blue,
1414 "col {x} clipped block placeholder"
1415 );
1416 }
1417 }
1419
1420 #[test]
1421 fn layered_spans_blend_broad_bg_with_narrow_fg() {
1422 use hjkl_buffer::Span;
1428 let b = Buffer::from_str("fn main() {}");
1429 let v = vp(20, 1);
1430 let spans = vec![vec![
1432 Span::new(0, 12, 1), Span::new(0, 2, 2), ]];
1435 let resolver = |id: u32| -> Style {
1436 match id {
1437 1 => Style::default().bg(Color::DarkGray),
1438 2 => Style::default().fg(Color::Magenta),
1439 _ => Style::default(),
1440 }
1441 };
1442 let view = BufferView {
1443 buffer: &b,
1444 viewport: &v,
1445 selection: None,
1446 resolver: &resolver,
1447 cursor_line_bg: Style::default(),
1448 cursor_column_bg: Style::default(),
1449 selection_bg: Style::default().bg(Color::Blue),
1450 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1451 gutter: None,
1452 search_bg: Style::default(),
1453 signs: &[],
1454 conceals: &[],
1455 spans: &spans,
1456 search_pattern: None,
1457 non_text_style: Style::default(),
1458 diag_overlays: &[],
1459 colorcolumn_cols: &[],
1460 colorcolumn_style: Style::default(),
1461 listchars: None,
1462 indent_guides_enabled: false,
1463 indent_guide_char: '│',
1464 indent_guide_shiftwidth: 4,
1465 indent_guide_fg: Color::Reset,
1466 indent_guide_active_fg: Color::Reset,
1467 indent_guide_active_col: None,
1468 };
1469 let term = run_render(view, 20, 1);
1470 for x in 0u16..2 {
1472 let cell = term.cell((x, 0)).unwrap();
1473 assert_eq!(cell.fg, Color::Magenta, "col {x}: fg from narrow span");
1474 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1475 }
1476 for x in 2u16..12 {
1478 let cell = term.cell((x, 0)).unwrap();
1479 assert_eq!(cell.bg, Color::DarkGray, "col {x}: bg from broad span");
1480 assert_eq!(
1481 cell.fg,
1482 Color::Reset,
1483 "col {x}: no fg set (broad span is bg-only)"
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn narrow_span_with_explicit_bg_still_overrides_broad_bg() {
1490 use hjkl_buffer::Span;
1495 let b = Buffer::from_str("hello world");
1496 let v = vp(20, 1);
1497 let spans = vec![vec![
1498 Span::new(0, 11, 1), Span::new(6, 11, 2), ]];
1501 let resolver = |id: u32| -> Style {
1502 match id {
1503 1 => Style::default().bg(Color::DarkGray),
1504 2 => Style::default().bg(Color::Red),
1505 _ => Style::default(),
1506 }
1507 };
1508 let view = BufferView {
1509 buffer: &b,
1510 viewport: &v,
1511 selection: None,
1512 resolver: &resolver,
1513 cursor_line_bg: Style::default(),
1514 cursor_column_bg: Style::default(),
1515 selection_bg: Style::default().bg(Color::Blue),
1516 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1517 gutter: None,
1518 search_bg: Style::default(),
1519 signs: &[],
1520 conceals: &[],
1521 spans: &spans,
1522 search_pattern: None,
1523 non_text_style: Style::default(),
1524 diag_overlays: &[],
1525 colorcolumn_cols: &[],
1526 colorcolumn_style: Style::default(),
1527 listchars: None,
1528 indent_guides_enabled: false,
1529 indent_guide_char: '│',
1530 indent_guide_shiftwidth: 4,
1531 indent_guide_fg: Color::Reset,
1532 indent_guide_active_fg: Color::Reset,
1533 indent_guide_active_col: None,
1534 };
1535 let term = run_render(view, 20, 1);
1536 for x in 0u16..6 {
1538 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::DarkGray);
1539 }
1540 for x in 6u16..11 {
1542 assert_eq!(
1543 term.cell((x, 0)).unwrap().bg,
1544 Color::Red,
1545 "col {x}: narrow span's bg overrides broad bg"
1546 );
1547 }
1548 }
1549
1550 #[test]
1551 fn syntax_span_fg_resolves_via_table() {
1552 use hjkl_buffer::Span;
1553 let b = Buffer::from_str("SELECT foo");
1554 let v = vp(20, 1);
1555 let spans = vec![vec![Span::new(0, 6, 7)]];
1556 let resolver = |id: u32| -> Style {
1557 if id == 7 {
1558 Style::default().fg(Color::Red)
1559 } else {
1560 Style::default()
1561 }
1562 };
1563 let view = BufferView {
1564 buffer: &b,
1565 viewport: &v,
1566 selection: None,
1567 resolver: &resolver,
1568 cursor_line_bg: Style::default(),
1569 cursor_column_bg: Style::default(),
1570 selection_bg: Style::default().bg(Color::Blue),
1571 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1572 gutter: None,
1573 search_bg: Style::default(),
1574 signs: &[],
1575 conceals: &[],
1576 spans: &spans,
1577 search_pattern: None,
1578 non_text_style: Style::default(),
1579 diag_overlays: &[],
1580 colorcolumn_cols: &[],
1581 colorcolumn_style: Style::default(),
1582 listchars: None,
1583 indent_guides_enabled: false,
1584 indent_guide_char: '│',
1585 indent_guide_shiftwidth: 4,
1586 indent_guide_fg: Color::Reset,
1587 indent_guide_active_fg: Color::Reset,
1588 indent_guide_active_col: None,
1589 };
1590 let term = run_render(view, 20, 1);
1591 for x in 0..6 {
1592 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1593 }
1594 }
1595
1596 #[test]
1597 fn gutter_renders_right_aligned_line_numbers() {
1598 let b = Buffer::from_str("a\nb\nc");
1599 let v = vp(10, 3);
1600 let view = BufferView {
1601 buffer: &b,
1602 viewport: &v,
1603 selection: None,
1604 resolver: &(no_styles as fn(u32) -> Style),
1605 cursor_line_bg: Style::default(),
1606 cursor_column_bg: Style::default(),
1607 selection_bg: Style::default().bg(Color::Blue),
1608 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1609 gutter: Some(Gutter {
1610 width: 4,
1611 style: Style::default().fg(Color::Yellow),
1612 line_offset: 0,
1613 ..Default::default()
1614 }),
1615 search_bg: Style::default(),
1616 signs: &[],
1617 conceals: &[],
1618 spans: &[],
1619 search_pattern: None,
1620 non_text_style: Style::default(),
1621 diag_overlays: &[],
1622 colorcolumn_cols: &[],
1623 colorcolumn_style: Style::default(),
1624 listchars: None,
1625 indent_guides_enabled: false,
1626 indent_guide_char: '│',
1627 indent_guide_shiftwidth: 4,
1628 indent_guide_fg: Color::Reset,
1629 indent_guide_active_fg: Color::Reset,
1630 indent_guide_active_col: None,
1631 };
1632 let term = run_render(view, 10, 3);
1633 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1635 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
1636 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1637 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
1638 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1640 }
1641
1642 #[test]
1643 fn gutter_renders_relative_with_cursor_at_zero() {
1644 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1646 b.set_cursor(hjkl_buffer::Position::new(2, 0));
1647 let v = vp(10, 5);
1648 let view = BufferView {
1649 buffer: &b,
1650 viewport: &v,
1651 selection: None,
1652 resolver: &(no_styles as fn(u32) -> Style),
1653 cursor_line_bg: Style::default(),
1654 cursor_column_bg: Style::default(),
1655 selection_bg: Style::default().bg(Color::Blue),
1656 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1657 gutter: Some(Gutter {
1658 width: 4,
1659 style: Style::default().fg(Color::Yellow),
1660 line_offset: 0,
1661 numbers: GutterNumbers::Relative { cursor_row: 2 },
1662 sign_column_width: 0,
1663 }),
1664 search_bg: Style::default(),
1665 signs: &[],
1666 conceals: &[],
1667 spans: &[],
1668 search_pattern: None,
1669 non_text_style: Style::default(),
1670 diag_overlays: &[],
1671 colorcolumn_cols: &[],
1672 colorcolumn_style: Style::default(),
1673 listchars: None,
1674 indent_guides_enabled: false,
1675 indent_guide_char: '│',
1676 indent_guide_shiftwidth: 4,
1677 indent_guide_fg: Color::Reset,
1678 indent_guide_active_fg: Color::Reset,
1679 indent_guide_active_col: None,
1680 };
1681 let term = run_render(view, 10, 5);
1682 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "2");
1685 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "1");
1687 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "0");
1689 assert_eq!(term.cell((2, 3)).unwrap().symbol(), "1");
1691 assert_eq!(term.cell((2, 4)).unwrap().symbol(), "2");
1693 }
1694
1695 #[test]
1696 fn gutter_renders_hybrid_cursor_row_absolute() {
1697 let mut b = Buffer::from_str("a\nb\nc");
1700 b.set_cursor(hjkl_buffer::Position::new(1, 0));
1701 let v = vp(10, 3);
1702 let view = BufferView {
1703 buffer: &b,
1704 viewport: &v,
1705 selection: None,
1706 resolver: &(no_styles as fn(u32) -> Style),
1707 cursor_line_bg: Style::default(),
1708 cursor_column_bg: Style::default(),
1709 selection_bg: Style::default().bg(Color::Blue),
1710 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1711 gutter: Some(Gutter {
1712 width: 4,
1713 style: Style::default().fg(Color::Yellow),
1714 line_offset: 0,
1715 numbers: GutterNumbers::Hybrid { cursor_row: 1 },
1716 sign_column_width: 0,
1717 }),
1718 search_bg: Style::default(),
1719 signs: &[],
1720 conceals: &[],
1721 spans: &[],
1722 search_pattern: None,
1723 non_text_style: Style::default(),
1724 diag_overlays: &[],
1725 colorcolumn_cols: &[],
1726 colorcolumn_style: Style::default(),
1727 listchars: None,
1728 indent_guides_enabled: false,
1729 indent_guide_char: '│',
1730 indent_guide_shiftwidth: 4,
1731 indent_guide_fg: Color::Reset,
1732 indent_guide_active_fg: Color::Reset,
1733 indent_guide_active_col: None,
1734 };
1735 let term = run_render(view, 10, 3);
1736 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
1738 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
1740 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "1");
1742 }
1743
1744 #[test]
1745 fn gutter_none_paints_blank_cells() {
1746 let b = Buffer::from_str("a\nb\nc");
1747 let v = vp(10, 3);
1748 let view = BufferView {
1749 buffer: &b,
1750 viewport: &v,
1751 selection: None,
1752 resolver: &(no_styles as fn(u32) -> Style),
1753 cursor_line_bg: Style::default(),
1754 cursor_column_bg: Style::default(),
1755 selection_bg: Style::default().bg(Color::Blue),
1756 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1757 gutter: Some(Gutter {
1758 width: 4,
1759 style: Style::default().fg(Color::Yellow),
1760 line_offset: 0,
1761 numbers: GutterNumbers::None,
1762 sign_column_width: 0,
1763 }),
1764 search_bg: Style::default(),
1765 signs: &[],
1766 conceals: &[],
1767 spans: &[],
1768 search_pattern: None,
1769 non_text_style: Style::default(),
1770 diag_overlays: &[],
1771 colorcolumn_cols: &[],
1772 colorcolumn_style: Style::default(),
1773 listchars: None,
1774 indent_guides_enabled: false,
1775 indent_guide_char: '│',
1776 indent_guide_shiftwidth: 4,
1777 indent_guide_fg: Color::Reset,
1778 indent_guide_active_fg: Color::Reset,
1779 indent_guide_active_col: None,
1780 };
1781 let term = run_render(view, 10, 3);
1782 for row in 0..3u16 {
1784 for x in 0..4u16 {
1785 assert_eq!(
1786 term.cell((x, row)).unwrap().symbol(),
1787 " ",
1788 "expected blank at ({x}, {row})"
1789 );
1790 }
1791 }
1792 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1794 }
1795
1796 #[test]
1797 fn search_bg_paints_match_cells() {
1798 use regex::Regex;
1799 let b = Buffer::from_str("foo bar foo");
1800 let v = vp(20, 1);
1801 let pat = Regex::new("foo").unwrap();
1802 let view = BufferView {
1803 buffer: &b,
1804 viewport: &v,
1805 selection: None,
1806 resolver: &(no_styles as fn(u32) -> Style),
1807 cursor_line_bg: Style::default(),
1808 cursor_column_bg: Style::default(),
1809 selection_bg: Style::default().bg(Color::Blue),
1810 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1811 gutter: None,
1812 search_bg: Style::default().bg(Color::Magenta),
1813 signs: &[],
1814 conceals: &[],
1815 spans: &[],
1816 search_pattern: Some(&pat),
1817 non_text_style: Style::default(),
1818 diag_overlays: &[],
1819 colorcolumn_cols: &[],
1820 colorcolumn_style: Style::default(),
1821 listchars: None,
1822 indent_guides_enabled: false,
1823 indent_guide_char: '│',
1824 indent_guide_shiftwidth: 4,
1825 indent_guide_fg: Color::Reset,
1826 indent_guide_active_fg: Color::Reset,
1827 indent_guide_active_col: None,
1828 };
1829 let term = run_render(view, 20, 1);
1830 for x in 0..3 {
1831 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1832 }
1833 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1835 for x in 8..11 {
1836 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1837 }
1838 }
1839
1840 #[test]
1841 fn search_bg_survives_cursorcolumn_overlay() {
1842 use regex::Regex;
1843 let mut b = Buffer::from_str("foo bar foo");
1847 let v = vp(20, 1);
1848 let pat = Regex::new("foo").unwrap();
1849 b.set_cursor(hjkl_buffer::Position::new(0, 1));
1851 let view = BufferView {
1852 buffer: &b,
1853 viewport: &v,
1854 selection: None,
1855 resolver: &(no_styles as fn(u32) -> Style),
1856 cursor_line_bg: Style::default(),
1857 cursor_column_bg: Style::default().bg(Color::DarkGray),
1858 selection_bg: Style::default().bg(Color::Blue),
1859 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1860 gutter: None,
1861 search_bg: Style::default().bg(Color::Magenta),
1862 signs: &[],
1863 conceals: &[],
1864 spans: &[],
1865 search_pattern: Some(&pat),
1866 non_text_style: Style::default(),
1867 diag_overlays: &[],
1868 colorcolumn_cols: &[],
1869 colorcolumn_style: Style::default(),
1870 listchars: None,
1871 indent_guides_enabled: false,
1872 indent_guide_char: '│',
1873 indent_guide_shiftwidth: 4,
1874 indent_guide_fg: Color::Reset,
1875 indent_guide_active_fg: Color::Reset,
1876 indent_guide_active_col: None,
1877 };
1878 let term = run_render(view, 20, 1);
1879 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
1881 }
1882
1883 #[test]
1884 fn highest_priority_sign_wins_per_row_in_dedicated_sign_column() {
1885 let b = Buffer::from_str("a\nb\nc");
1888 let v = vp(10, 3);
1889 let signs = [
1890 Sign {
1891 row: 0,
1892 ch: 'W',
1893 style: Style::default().fg(Color::Yellow),
1894 priority: 1,
1895 },
1896 Sign {
1897 row: 0,
1898 ch: 'E',
1899 style: Style::default().fg(Color::Red),
1900 priority: 2,
1901 },
1902 ];
1903 let view = BufferView {
1904 buffer: &b,
1905 viewport: &v,
1906 selection: None,
1907 resolver: &(no_styles as fn(u32) -> Style),
1908 cursor_line_bg: Style::default(),
1909 cursor_column_bg: Style::default(),
1910 selection_bg: Style::default().bg(Color::Blue),
1911 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1912 gutter: Some(Gutter {
1913 width: 3,
1914 style: Style::default().fg(Color::DarkGray),
1915 line_offset: 0,
1916 sign_column_width: 1,
1917 ..Default::default()
1918 }),
1919 search_bg: Style::default(),
1920 signs: &signs,
1921 conceals: &[],
1922 spans: &[],
1923 search_pattern: None,
1924 non_text_style: Style::default(),
1925 diag_overlays: &[],
1926 colorcolumn_cols: &[],
1927 colorcolumn_style: Style::default(),
1928 listchars: None,
1929 indent_guides_enabled: false,
1930 indent_guide_char: '│',
1931 indent_guide_shiftwidth: 4,
1932 indent_guide_fg: Color::Reset,
1933 indent_guide_active_fg: Color::Reset,
1934 indent_guide_active_col: None,
1935 };
1936 let term = run_render(view, 10, 3);
1937 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
1939 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1940 assert_ne!(term.cell((1, 0)).unwrap().symbol(), "E");
1942 assert_eq!(term.cell((0, 1)).unwrap().symbol(), " ");
1944 }
1945
1946 #[test]
1947 fn conceal_replaces_byte_range() {
1948 let b = Buffer::from_str("see https://example.com end");
1949 let v = vp(30, 1);
1950 let conceals = vec![Conceal {
1951 row: 0,
1952 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
1955 }];
1956 let view = BufferView {
1957 buffer: &b,
1958 viewport: &v,
1959 selection: None,
1960 resolver: &(no_styles as fn(u32) -> Style),
1961 cursor_line_bg: Style::default(),
1962 cursor_column_bg: Style::default(),
1963 selection_bg: Style::default(),
1964 cursor_style: Style::default(),
1965 gutter: None,
1966 search_bg: Style::default(),
1967 signs: &[],
1968 conceals: &conceals,
1969 spans: &[],
1970 search_pattern: None,
1971 non_text_style: Style::default(),
1972 diag_overlays: &[],
1973 colorcolumn_cols: &[],
1974 colorcolumn_style: Style::default(),
1975 listchars: None,
1976 indent_guides_enabled: false,
1977 indent_guide_char: '│',
1978 indent_guide_shiftwidth: 4,
1979 indent_guide_fg: Color::Reset,
1980 indent_guide_active_fg: Color::Reset,
1981 indent_guide_active_col: None,
1982 };
1983 let term = run_render(view, 30, 1);
1984 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
1986 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
1987 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
1990 }
1991
1992 #[test]
1993 fn closed_fold_collapses_rows_and_paints_marker() {
1994 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
1995 let v = vp(30, 5);
1996 b.add_fold(1, 3, true);
1998 let view = BufferView {
1999 buffer: &b,
2000 viewport: &v,
2001 selection: None,
2002 resolver: &(no_styles as fn(u32) -> Style),
2003 cursor_line_bg: Style::default(),
2004 cursor_column_bg: Style::default(),
2005 selection_bg: Style::default().bg(Color::Blue),
2006 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2007 gutter: None,
2008 search_bg: Style::default(),
2009 signs: &[],
2010 conceals: &[],
2011 spans: &[],
2012 search_pattern: None,
2013 non_text_style: Style::default(),
2014 diag_overlays: &[],
2015 colorcolumn_cols: &[],
2016 colorcolumn_style: Style::default(),
2017 listchars: None,
2018 indent_guides_enabled: false,
2019 indent_guide_char: '│',
2020 indent_guide_shiftwidth: 4,
2021 indent_guide_fg: Color::Reset,
2022 indent_guide_active_fg: Color::Reset,
2023 indent_guide_active_col: None,
2024 };
2025 let term = run_render(view, 30, 5);
2026 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
2028 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
2031 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
2033 }
2034
2035 #[test]
2036 fn open_fold_renders_normally() {
2037 let mut b = Buffer::from_str("a\nb\nc");
2038 let v = vp(5, 3);
2039 b.add_fold(0, 2, false); let view = BufferView {
2041 buffer: &b,
2042 viewport: &v,
2043 selection: None,
2044 resolver: &(no_styles as fn(u32) -> Style),
2045 cursor_line_bg: Style::default(),
2046 cursor_column_bg: Style::default(),
2047 selection_bg: Style::default().bg(Color::Blue),
2048 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2049 gutter: None,
2050 search_bg: Style::default(),
2051 signs: &[],
2052 conceals: &[],
2053 spans: &[],
2054 search_pattern: None,
2055 non_text_style: Style::default(),
2056 diag_overlays: &[],
2057 colorcolumn_cols: &[],
2058 colorcolumn_style: Style::default(),
2059 listchars: None,
2060 indent_guides_enabled: false,
2061 indent_guide_char: '│',
2062 indent_guide_shiftwidth: 4,
2063 indent_guide_fg: Color::Reset,
2064 indent_guide_active_fg: Color::Reset,
2065 indent_guide_active_col: None,
2066 };
2067 let term = run_render(view, 5, 3);
2068 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
2069 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
2070 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
2071 }
2072
2073 #[test]
2074 fn horizontal_scroll_clips_left_chars() {
2075 let b = Buffer::from_str("abcdefgh");
2076 let mut v = vp(4, 1);
2077 v.top_col = 3;
2078 let view = BufferView {
2079 buffer: &b,
2080 viewport: &v,
2081 selection: None,
2082 resolver: &(no_styles as fn(u32) -> Style),
2083 cursor_line_bg: Style::default(),
2084 cursor_column_bg: Style::default(),
2085 selection_bg: Style::default().bg(Color::Blue),
2086 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2087 gutter: None,
2088 search_bg: Style::default(),
2089 signs: &[],
2090 conceals: &[],
2091 spans: &[],
2092 search_pattern: None,
2093 non_text_style: Style::default(),
2094 diag_overlays: &[],
2095 colorcolumn_cols: &[],
2096 colorcolumn_style: Style::default(),
2097 listchars: None,
2098 indent_guides_enabled: false,
2099 indent_guide_char: '│',
2100 indent_guide_shiftwidth: 4,
2101 indent_guide_fg: Color::Reset,
2102 indent_guide_active_fg: Color::Reset,
2103 indent_guide_active_col: None,
2104 };
2105 let term = run_render(view, 4, 1);
2106 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
2107 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
2108 }
2109
2110 fn make_wrap_view<'a>(
2111 b: &'a Buffer,
2112 viewport: &'a Viewport,
2113 resolver: &'a (impl StyleResolver + 'a),
2114 gutter: Option<Gutter>,
2115 ) -> BufferView<'a, impl StyleResolver + 'a> {
2116 BufferView {
2117 buffer: b,
2118 viewport,
2119 selection: None,
2120 resolver,
2121 cursor_line_bg: Style::default(),
2122 cursor_column_bg: Style::default(),
2123 selection_bg: Style::default().bg(Color::Blue),
2124 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2125 gutter,
2126 search_bg: Style::default(),
2127 signs: &[],
2128 conceals: &[],
2129 spans: &[],
2130 search_pattern: None,
2131 non_text_style: Style::default(),
2132 diag_overlays: &[],
2133 colorcolumn_cols: &[],
2134 colorcolumn_style: Style::default(),
2135 listchars: None,
2136 indent_guides_enabled: false,
2137 indent_guide_char: '│',
2138 indent_guide_shiftwidth: 4,
2139 indent_guide_fg: Color::Reset,
2140 indent_guide_active_fg: Color::Reset,
2141 indent_guide_active_col: None,
2142 }
2143 }
2144
2145 #[test]
2146 fn wrap_segments_char_breaks_at_width() {
2147 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
2148 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
2149 }
2150
2151 #[test]
2152 fn wrap_segments_word_backs_up_to_whitespace() {
2153 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
2154 assert_eq!(segs[0], (0, 6));
2156 assert_eq!(segs[1], (6, 11));
2158 assert_eq!(segs[2], (11, 16));
2159 }
2160
2161 #[test]
2162 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
2163 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
2164 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
2166 }
2167
2168 #[test]
2169 fn wrap_char_paints_continuation_rows() {
2170 let b = Buffer::from_str("abcdefghij");
2171 let v = Viewport {
2172 top_row: 0,
2173 top_col: 0,
2174 width: 4,
2175 height: 3,
2176 wrap: Wrap::Char,
2177 text_width: 4,
2178 tab_width: 0,
2179 };
2180 let r = no_styles as fn(u32) -> Style;
2181 let view = make_wrap_view(&b, &v, &r, None);
2182 let term = run_render(view, 4, 3);
2183 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
2185 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
2186 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
2188 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
2189 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
2191 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
2192 }
2193
2194 #[test]
2195 fn wrap_char_gutter_blank_on_continuation() {
2196 let b = Buffer::from_str("abcdefgh");
2197 let v = Viewport {
2198 top_row: 0,
2199 top_col: 0,
2200 width: 6,
2201 height: 3,
2202 wrap: Wrap::Char,
2203 text_width: 3,
2205 tab_width: 0,
2206 };
2207 let r = no_styles as fn(u32) -> Style;
2208 let gutter = Gutter {
2209 width: 3,
2210 style: Style::default().fg(Color::Yellow),
2211 line_offset: 0,
2212 ..Default::default()
2213 };
2214 let view = make_wrap_view(&b, &v, &r, Some(gutter));
2215 let term = run_render(view, 6, 3);
2216 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
2218 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
2219 for x in 0..2 {
2221 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
2222 }
2223 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
2224 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
2225 }
2226
2227 #[test]
2228 fn wrap_char_cursor_lands_on_correct_segment() {
2229 let mut b = Buffer::from_str("abcdefghij");
2230 let v = Viewport {
2231 top_row: 0,
2232 top_col: 0,
2233 width: 4,
2234 height: 3,
2235 wrap: Wrap::Char,
2236 text_width: 4,
2237 tab_width: 0,
2238 };
2239 b.set_cursor(hjkl_buffer::Position::new(0, 6));
2241 let r = no_styles as fn(u32) -> Style;
2242 let mut view = make_wrap_view(&b, &v, &r, None);
2243 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
2244 let term = run_render(view, 4, 3);
2245 assert!(
2246 term.cell((2, 1))
2247 .unwrap()
2248 .modifier
2249 .contains(Modifier::REVERSED)
2250 );
2251 }
2252
2253 #[test]
2254 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
2255 let mut b = Buffer::from_str("abcdef");
2256 let v = Viewport {
2257 top_row: 0,
2258 top_col: 0,
2259 width: 4,
2260 height: 3,
2261 wrap: Wrap::Char,
2262 text_width: 4,
2263 tab_width: 0,
2264 };
2265 b.set_cursor(hjkl_buffer::Position::new(0, 6));
2267 let r = no_styles as fn(u32) -> Style;
2268 let mut view = make_wrap_view(&b, &v, &r, None);
2269 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
2270 let term = run_render(view, 4, 3);
2271 assert!(
2273 term.cell((2, 1))
2274 .unwrap()
2275 .modifier
2276 .contains(Modifier::REVERSED)
2277 );
2278 }
2279
2280 #[test]
2281 fn wrap_word_breaks_at_whitespace() {
2282 let b = Buffer::from_str("alpha beta gamma");
2283 let v = Viewport {
2284 top_row: 0,
2285 top_col: 0,
2286 width: 8,
2287 height: 3,
2288 wrap: Wrap::Word,
2289 text_width: 8,
2290 tab_width: 0,
2291 };
2292 let r = no_styles as fn(u32) -> Style;
2293 let view = make_wrap_view(&b, &v, &r, None);
2294 let term = run_render(view, 8, 3);
2295 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
2297 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
2298 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
2300 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
2301 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
2303 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
2304 }
2305
2306 fn view_with<'a>(
2312 b: &'a Buffer,
2313 viewport: &'a Viewport,
2314 resolver: &'a (impl StyleResolver + 'a),
2315 spans: &'a [Vec<Span>],
2316 search_pattern: Option<&'a regex::Regex>,
2317 ) -> BufferView<'a, impl StyleResolver + 'a> {
2318 BufferView {
2319 buffer: b,
2320 viewport,
2321 selection: None,
2322 resolver,
2323 cursor_line_bg: Style::default(),
2324 cursor_column_bg: Style::default(),
2325 selection_bg: Style::default().bg(Color::Blue),
2326 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2327 gutter: None,
2328 search_bg: Style::default().bg(Color::Magenta),
2329 signs: &[],
2330 conceals: &[],
2331 spans,
2332 search_pattern,
2333 non_text_style: Style::default(),
2334 diag_overlays: &[],
2335 colorcolumn_cols: &[],
2336 colorcolumn_style: Style::default(),
2337 listchars: None,
2338 indent_guides_enabled: false,
2339 indent_guide_char: '│',
2340 indent_guide_shiftwidth: 4,
2341 indent_guide_fg: Color::Reset,
2342 indent_guide_active_fg: Color::Reset,
2343 indent_guide_active_col: None,
2344 }
2345 }
2346
2347 #[test]
2348 fn empty_spans_param_renders_default_style() {
2349 let b = Buffer::from_str("hello");
2350 let v = vp(10, 1);
2351 let r = no_styles as fn(u32) -> Style;
2352 let view = view_with(&b, &v, &r, &[], None);
2353 let term = run_render(view, 10, 1);
2354 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
2355 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
2356 }
2357
2358 #[test]
2359 fn spans_param_paints_styled_byte_range() {
2360 let b = Buffer::from_str("abcdef");
2361 let v = vp(10, 1);
2362 let resolver = |id: u32| -> Style {
2363 if id == 3 {
2364 Style::default().fg(Color::Green)
2365 } else {
2366 Style::default()
2367 }
2368 };
2369 let spans = vec![vec![Span::new(0, 3, 3)]];
2370 let view = view_with(&b, &v, &resolver, &spans, None);
2371 let term = run_render(view, 10, 1);
2372 for x in 0..3 {
2373 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
2374 }
2375 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
2376 }
2377
2378 #[test]
2379 fn spans_param_handles_per_row_overlay() {
2380 let b = Buffer::from_str("abc\ndef");
2381 let v = vp(10, 2);
2382 let resolver = |id: u32| -> Style {
2383 if id == 1 {
2384 Style::default().fg(Color::Red)
2385 } else {
2386 Style::default().fg(Color::Green)
2387 }
2388 };
2389 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
2390 let view = view_with(&b, &v, &resolver, &spans, None);
2391 let term = run_render(view, 10, 2);
2392 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
2393 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
2394 }
2395
2396 #[test]
2397 fn spans_param_rows_beyond_get_no_styling() {
2398 let b = Buffer::from_str("abc\ndef\nghi");
2399 let v = vp(10, 3);
2400 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
2401 let spans = vec![vec![Span::new(0, 3, 0)]];
2403 let view = view_with(&b, &v, &resolver, &spans, None);
2404 let term = run_render(view, 10, 3);
2405 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
2406 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
2407 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
2408 }
2409
2410 #[test]
2411 fn search_pattern_none_disables_hlsearch() {
2412 let b = Buffer::from_str("foo bar foo");
2413 let v = vp(20, 1);
2414 let r = no_styles as fn(u32) -> Style;
2415 let view = view_with(&b, &v, &r, &[], None);
2417 let term = run_render(view, 20, 1);
2418 for x in 0..11 {
2419 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
2420 }
2421 }
2422
2423 #[test]
2424 fn search_pattern_regex_paints_match_bg() {
2425 use regex::Regex;
2426 let b = Buffer::from_str("xyz foo xyz");
2427 let v = vp(20, 1);
2428 let r = no_styles as fn(u32) -> Style;
2429 let pat = Regex::new("foo").unwrap();
2430 let view = view_with(&b, &v, &r, &[], Some(&pat));
2431 let term = run_render(view, 20, 1);
2432 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
2434 for x in 4..7 {
2435 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
2436 }
2437 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
2438 }
2439
2440 #[test]
2441 fn search_pattern_unicode_columns_are_charwise() {
2442 use regex::Regex;
2443 let b = Buffer::from_str("tablé foo");
2445 let v = vp(20, 1);
2446 let r = no_styles as fn(u32) -> Style;
2447 let pat = Regex::new("foo").unwrap();
2448 let view = view_with(&b, &v, &r, &[], Some(&pat));
2449 let term = run_render(view, 20, 1);
2450 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
2452 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
2453 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
2454 }
2455
2456 #[test]
2457 fn spans_param_clamps_short_row_overlay() {
2458 let b = Buffer::from_str("abc");
2460 let v = vp(10, 1);
2461 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
2462 let spans = vec![vec![Span::new(0, 100, 0)]];
2463 let view = view_with(&b, &v, &resolver, &spans, None);
2464 let term = run_render(view, 10, 1);
2465 for x in 0..3 {
2466 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
2467 }
2468 }
2469
2470 #[test]
2471 fn spans_and_search_pattern_compose() {
2472 use regex::Regex;
2474 let b = Buffer::from_str("foo");
2475 let v = vp(10, 1);
2476 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
2477 let spans = vec![vec![Span::new(0, 3, 0)]];
2478 let pat = Regex::new("foo").unwrap();
2479 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
2480 let term = run_render(view, 10, 1);
2481 let cell = term.cell((1, 0)).unwrap();
2482 assert_eq!(cell.fg, Color::Green);
2483 assert_eq!(cell.bg, Color::Magenta);
2484 }
2485
2486 #[test]
2490 fn tilde_marker_painted_past_eof() {
2491 let b = Buffer::from_str("a\nb\nc\nd\ne");
2493 let v = vp(10, 10);
2494 let r = no_styles as fn(u32) -> Style;
2495 let non_text_fg = Color::DarkGray;
2496 let view = BufferView {
2497 buffer: &b,
2498 viewport: &v,
2499 selection: None,
2500 resolver: &r,
2501 cursor_line_bg: Style::default(),
2502 cursor_column_bg: Style::default(),
2503 selection_bg: Style::default().bg(Color::Blue),
2504 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2505 gutter: None,
2506 search_bg: Style::default(),
2507 signs: &[],
2508 conceals: &[],
2509 spans: &[],
2510 search_pattern: None,
2511 non_text_style: Style::default().fg(non_text_fg),
2512 diag_overlays: &[],
2513 colorcolumn_cols: &[],
2514 colorcolumn_style: Style::default(),
2515 listchars: None,
2516 indent_guides_enabled: false,
2517 indent_guide_char: '│',
2518 indent_guide_shiftwidth: 4,
2519 indent_guide_fg: Color::Reset,
2520 indent_guide_active_fg: Color::Reset,
2521 indent_guide_active_col: None,
2522 };
2523 let term = run_render(view, 10, 10);
2524 for row in 0..5u16 {
2526 assert_ne!(
2527 term.cell((0, row)).unwrap().symbol(),
2528 "~",
2529 "row {row} is a content row, expected no tilde"
2530 );
2531 }
2532 for row in 5..10u16 {
2534 let cell = term.cell((0, row)).unwrap();
2535 assert_eq!(cell.symbol(), "~", "row {row} is past EOF, expected tilde");
2536 assert_eq!(
2537 cell.fg, non_text_fg,
2538 "row {row} tilde should use non_text_style fg"
2539 );
2540 for x in 1..10u16 {
2542 assert_eq!(
2543 term.cell((x, row)).unwrap().symbol(),
2544 " ",
2545 "row {row} col {x} after tilde should be blank"
2546 );
2547 }
2548 }
2549 }
2550
2551 #[test]
2554 fn tilde_marker_with_gutter_past_eof() {
2555 let b = Buffer::from_str("a\nb");
2556 let v = vp(10, 5);
2557 let r = no_styles as fn(u32) -> Style;
2558 let non_text_fg = Color::DarkGray;
2559 let view = BufferView {
2560 buffer: &b,
2561 viewport: &v,
2562 selection: None,
2563 resolver: &r,
2564 cursor_line_bg: Style::default(),
2565 cursor_column_bg: Style::default(),
2566 selection_bg: Style::default().bg(Color::Blue),
2567 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2568 gutter: Some(Gutter {
2569 width: 4,
2570 style: Style::default().fg(Color::Yellow),
2571 line_offset: 0,
2572 numbers: GutterNumbers::Absolute,
2573 sign_column_width: 0,
2574 }),
2575 search_bg: Style::default(),
2576 signs: &[],
2577 conceals: &[],
2578 spans: &[],
2579 search_pattern: None,
2580 non_text_style: Style::default().fg(non_text_fg),
2581 diag_overlays: &[],
2582 colorcolumn_cols: &[],
2583 colorcolumn_style: Style::default(),
2584 listchars: None,
2585 indent_guides_enabled: false,
2586 indent_guide_char: '│',
2587 indent_guide_shiftwidth: 4,
2588 indent_guide_fg: Color::Reset,
2589 indent_guide_active_fg: Color::Reset,
2590 indent_guide_active_col: None,
2591 };
2592 let term = run_render(view, 10, 5);
2593 for row in 2..5u16 {
2595 for x in 0..4u16 {
2597 assert_eq!(
2598 term.cell((x, row)).unwrap().symbol(),
2599 " ",
2600 "gutter col {x} on past-EOF row {row} should be blank"
2601 );
2602 }
2603 let cell = term.cell((4, row)).unwrap();
2605 assert_eq!(
2606 cell.symbol(),
2607 "~",
2608 "past-EOF row {row}: expected tilde at text column"
2609 );
2610 assert_eq!(cell.fg, non_text_fg);
2611 }
2612 }
2613
2614 #[test]
2615 fn diag_overlay_paints_underline_on_range() {
2616 let b = Buffer::from_str("hello world");
2620 let v = vp(20, 2);
2621 let overlay = DiagOverlay {
2622 row: 0,
2623 col_start: 6,
2624 col_end: 11,
2625 style: Style::default().add_modifier(Modifier::UNDERLINED),
2626 };
2627 let view = BufferView {
2628 buffer: &b,
2629 viewport: &v,
2630 selection: None,
2631 resolver: &(no_styles as fn(u32) -> Style),
2632 cursor_line_bg: Style::default(),
2633 cursor_column_bg: Style::default(),
2634 selection_bg: Style::default().bg(Color::Blue),
2635 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2636 gutter: None,
2637 search_bg: Style::default(),
2638 signs: &[],
2639 conceals: &[],
2640 spans: &[],
2641 search_pattern: None,
2642 non_text_style: Style::default(),
2643 diag_overlays: &[overlay],
2644 colorcolumn_cols: &[],
2645 colorcolumn_style: Style::default(),
2646 listchars: None,
2647 indent_guides_enabled: false,
2648 indent_guide_char: '│',
2649 indent_guide_shiftwidth: 4,
2650 indent_guide_fg: Color::Reset,
2651 indent_guide_active_fg: Color::Reset,
2652 indent_guide_active_col: None,
2653 };
2654 let term = run_render(view, 20, 2);
2655
2656 for x in 0u16..6 {
2658 let cell = term.cell((x, 0)).unwrap();
2659 assert!(
2660 !cell.modifier.contains(Modifier::UNDERLINED),
2661 "col {x} must not be underlined (outside overlay)"
2662 );
2663 }
2664 for x in 6u16..11 {
2666 let cell = term.cell((x, 0)).unwrap();
2667 assert!(
2668 cell.modifier.contains(Modifier::UNDERLINED),
2669 "col {x} must be underlined (inside overlay)"
2670 );
2671 }
2672 let cell = term.cell((11, 0)).unwrap();
2674 assert!(
2675 !cell.modifier.contains(Modifier::UNDERLINED),
2676 "col 11 must not be underlined (past overlay end)"
2677 );
2678 }
2679
2680 #[test]
2681 fn diag_overlay_out_of_viewport_is_ignored() {
2682 let b = Buffer::from_str("a\nb\nc");
2684 let v = vp(10, 3);
2685 let overlay = DiagOverlay {
2686 row: 5,
2687 col_start: 0,
2688 col_end: 1,
2689 style: Style::default().add_modifier(Modifier::UNDERLINED),
2690 };
2691 let view = BufferView {
2692 buffer: &b,
2693 viewport: &v,
2694 selection: None,
2695 resolver: &(no_styles as fn(u32) -> Style),
2696 cursor_line_bg: Style::default(),
2697 cursor_column_bg: Style::default(),
2698 selection_bg: Style::default().bg(Color::Blue),
2699 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2700 gutter: None,
2701 search_bg: Style::default(),
2702 signs: &[],
2703 conceals: &[],
2704 spans: &[],
2705 search_pattern: None,
2706 non_text_style: Style::default(),
2707 diag_overlays: &[overlay],
2708 colorcolumn_cols: &[],
2709 colorcolumn_style: Style::default(),
2710 listchars: None,
2711 indent_guides_enabled: false,
2712 indent_guide_char: '│',
2713 indent_guide_shiftwidth: 4,
2714 indent_guide_fg: Color::Reset,
2715 indent_guide_active_fg: Color::Reset,
2716 indent_guide_active_col: None,
2717 };
2718 let _term = run_render(view, 10, 3);
2720 }
2721
2722 #[test]
2732 fn paint_signs_in_dedicated_column_does_not_overwrite_line_number() {
2733 let b = Buffer::from_str("a\nb");
2737 let v = vp(20, 2);
2739 let sign = Sign {
2740 row: 0,
2741 ch: '~',
2742 style: Style::default().fg(Color::Red),
2743 priority: 10,
2744 };
2745 let view = BufferView {
2746 buffer: &b,
2747 viewport: &v,
2748 selection: None,
2749 resolver: &(no_styles as fn(u32) -> Style),
2750 cursor_line_bg: Style::default(),
2751 cursor_column_bg: Style::default(),
2752 selection_bg: Style::default().bg(Color::Blue),
2753 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2754 gutter: Some(Gutter {
2755 width: 6, style: Style::default(),
2757 line_offset: 13108, sign_column_width: 1,
2759 ..Default::default()
2760 }),
2761 search_bg: Style::default(),
2762 signs: &[sign],
2763 conceals: &[],
2764 spans: &[],
2765 search_pattern: None,
2766 non_text_style: Style::default(),
2767 diag_overlays: &[],
2768 colorcolumn_cols: &[],
2769 colorcolumn_style: Style::default(),
2770 listchars: None,
2771 indent_guides_enabled: false,
2772 indent_guide_char: '│',
2773 indent_guide_shiftwidth: 4,
2774 indent_guide_fg: Color::Reset,
2775 indent_guide_active_fg: Color::Reset,
2776 indent_guide_active_col: None,
2777 };
2778 let term = run_render(view, 20, 2);
2779 assert_eq!(
2781 term.cell((0, 0)).unwrap().symbol(),
2782 "~",
2783 "sign column (x=0) must hold the sign char"
2784 );
2785 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1", "x=1 must be '1'");
2787 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "3", "x=2 must be '3'");
2788 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "1", "x=3 must be '1'");
2789 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "0", "x=4 must be '0'");
2790 assert_eq!(term.cell((5, 0)).unwrap().symbol(), "9", "x=5 must be '9'");
2791 assert_eq!(
2793 term.cell((6, 0)).unwrap().symbol(),
2794 " ",
2795 "x=6 must be spacer"
2796 );
2797 assert_eq!(
2799 term.cell((7, 0)).unwrap().symbol(),
2800 "a",
2801 "text must start at x=sign_w+num_w=7"
2802 );
2803 }
2804
2805 #[test]
2808 fn paint_signs_zero_sign_column_width_layout_collapses() {
2809 let b = Buffer::from_str("abc");
2810 let v = vp(10, 1);
2811 let sign = Sign {
2812 row: 0,
2813 ch: 'E',
2814 style: Style::default().fg(Color::Red),
2815 priority: 10,
2816 };
2817 let view = BufferView {
2818 buffer: &b,
2819 viewport: &v,
2820 selection: None,
2821 resolver: &(no_styles as fn(u32) -> Style),
2822 cursor_line_bg: Style::default(),
2823 cursor_column_bg: Style::default(),
2824 selection_bg: Style::default().bg(Color::Blue),
2825 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
2826 gutter: Some(Gutter {
2828 width: 3,
2829 style: Style::default(),
2830 line_offset: 0,
2831 sign_column_width: 0,
2832 ..Default::default()
2833 }),
2834 search_bg: Style::default(),
2835 signs: &[sign],
2836 conceals: &[],
2837 spans: &[],
2838 search_pattern: None,
2839 non_text_style: Style::default(),
2840 diag_overlays: &[],
2841 colorcolumn_cols: &[],
2842 colorcolumn_style: Style::default(),
2843 listchars: None,
2844 indent_guides_enabled: false,
2845 indent_guide_char: '│',
2846 indent_guide_shiftwidth: 4,
2847 indent_guide_fg: Color::Reset,
2848 indent_guide_active_fg: Color::Reset,
2849 indent_guide_active_col: None,
2850 };
2851 let term = run_render(view, 10, 1);
2852 assert_ne!(
2854 term.cell((0, 0)).unwrap().symbol(),
2855 "E",
2856 "with sign_column_width=0, sign char must not appear in the gutter"
2857 );
2858 assert_eq!(
2860 term.cell((3, 0)).unwrap().symbol(),
2861 "a",
2862 "text must start at x=gutter.width when sign_column_width=0"
2863 );
2864 }
2865
2866 fn indent_guide_view<'a>(
2870 b: &'a Buffer,
2871 viewport: &'a Viewport,
2872 shiftwidth: usize,
2873 guide_char: char,
2874 guide_fg: Color,
2875 active_fg: Color,
2876 active_col: Option<usize>,
2877 ) -> BufferView<'a, impl StyleResolver + 'a> {
2878 BufferView {
2879 buffer: b,
2880 viewport,
2881 selection: None,
2882 resolver: &(no_styles as fn(u32) -> Style),
2883 cursor_line_bg: Style::default(),
2884 cursor_column_bg: Style::default(),
2885 selection_bg: Style::default(),
2886 cursor_style: Style::default(),
2887 gutter: None,
2888 search_bg: Style::default(),
2889 signs: &[],
2890 conceals: &[],
2891 spans: &[],
2892 search_pattern: None,
2893 non_text_style: Style::default(),
2894 diag_overlays: &[],
2895 colorcolumn_cols: &[],
2896 colorcolumn_style: Style::default(),
2897 listchars: None,
2898 indent_guides_enabled: true,
2899 indent_guide_char: guide_char,
2900 indent_guide_shiftwidth: shiftwidth,
2901 indent_guide_fg: guide_fg,
2902 indent_guide_active_fg: active_fg,
2903 indent_guide_active_col: active_col,
2904 }
2905 }
2906
2907 #[test]
2908 fn indent_guides_disabled_paints_nothing() {
2909 let b = Buffer::from_str(" foo\n bar");
2911 let v = vp(20, 2);
2912 let view = BufferView {
2913 buffer: &b,
2914 viewport: &v,
2915 selection: None,
2916 resolver: &(no_styles as fn(u32) -> Style),
2917 cursor_line_bg: Style::default(),
2918 cursor_column_bg: Style::default(),
2919 selection_bg: Style::default(),
2920 cursor_style: Style::default(),
2921 gutter: None,
2922 search_bg: Style::default(),
2923 signs: &[],
2924 conceals: &[],
2925 spans: &[],
2926 search_pattern: None,
2927 non_text_style: Style::default(),
2928 diag_overlays: &[],
2929 colorcolumn_cols: &[],
2930 colorcolumn_style: Style::default(),
2931 listchars: None,
2932 indent_guides_enabled: false,
2933 indent_guide_char: '│',
2934 indent_guide_shiftwidth: 4,
2935 indent_guide_fg: Color::DarkGray,
2936 indent_guide_active_fg: Color::Gray,
2937 indent_guide_active_col: None,
2938 };
2939 let term = run_render(view, 20, 2);
2940 for y in 0..2u16 {
2942 for x in 0..20u16 {
2943 assert_ne!(
2944 term.cell((x, y)).unwrap().symbol(),
2945 "│",
2946 "no guide expected at ({x}, {y}) when disabled"
2947 );
2948 }
2949 }
2950 }
2951
2952 #[test]
2953 fn indent_guides_basic_two_levels() {
2954 let b = Buffer::from_str("fn() {\n if foo {\n bar();\n }");
2957 let v = vp(20, 4);
2958 let view = indent_guide_view(&b, &v, 4, '│', Color::DarkGray, Color::Gray, None);
2960 let term = run_render(view, 20, 4);
2961 assert_ne!(term.cell((4, 0)).unwrap().symbol(), "│");
2963 assert_ne!(
2977 term.cell((4, 1)).unwrap().symbol(),
2978 "│",
2979 "row1: no guide (leading_vcols=4, sw=4, 4<4=false)"
2980 );
2981 assert_eq!(
2982 term.cell((4, 2)).unwrap().symbol(),
2983 "│",
2984 "row2: guide at col 4 (leading_vcols=8)"
2985 );
2986 assert_ne!(
2987 term.cell((8, 2)).unwrap().symbol(),
2988 "│",
2989 "row2: no guide at col 8 (8<8=false)"
2990 );
2991 assert_ne!(
2992 term.cell((4, 3)).unwrap().symbol(),
2993 "│",
2994 "row3: no guide (leading_vcols=4, 4<4=false)"
2995 );
2996 }
2997
2998 #[test]
2999 fn indent_guides_skip_when_no_indent() {
3000 let b = Buffer::from_str("no_indent\nstill_none");
3001 let v = vp(20, 2);
3002 let view = indent_guide_view(&b, &v, 4, '│', Color::DarkGray, Color::Gray, None);
3003 let term = run_render(view, 20, 2);
3004 for y in 0..2u16 {
3005 for x in 0..20u16 {
3006 assert_ne!(
3007 term.cell((x, y)).unwrap().symbol(),
3008 "│",
3009 "no guide expected on non-indented rows"
3010 );
3011 }
3012 }
3013 }
3014
3015 #[test]
3016 fn indent_guides_respects_tabs() {
3017 let b = Buffer::from_str("\t\tfoo");
3021 let mut v = vp(20, 1);
3022 v.tab_width = 4;
3023 let view = indent_guide_view(&b, &v, 4, '│', Color::DarkGray, Color::Gray, None);
3024 let term = run_render(view, 20, 1);
3025 assert_eq!(
3028 term.cell((4, 0)).unwrap().symbol(),
3029 "│",
3030 "guide at visual col 4 inside second tab"
3031 );
3032 assert_eq!(term.cell((8, 0)).unwrap().symbol(), "f");
3034 }
3035
3036 #[test]
3037 fn indent_guides_active_col_uses_active_fg() {
3038 let b = Buffer::from_str(" code");
3041 let v = vp(20, 1);
3042 let active_col = Some(4usize);
3043 let view = indent_guide_view(&b, &v, 4, '│', Color::DarkGray, Color::Gray, active_col);
3044 let term = run_render(view, 20, 1);
3045 let cell = term.cell((4, 0)).unwrap();
3046 assert_eq!(cell.symbol(), "│", "guide painted at col 4");
3047 assert_eq!(cell.fg, Color::Gray, "active col uses active_fg (Gray)");
3048 }
3049
3050 #[test]
3051 fn indent_guides_inactive_col_uses_inactive_fg() {
3052 let b = Buffer::from_str(" code");
3055 let v = vp(20, 1);
3056 let view = indent_guide_view(&b, &v, 4, '│', Color::DarkGray, Color::Gray, Some(8));
3057 let term = run_render(view, 20, 1);
3058 let cell4 = term.cell((4, 0)).unwrap();
3060 assert_eq!(cell4.symbol(), "│", "guide at col 4");
3061 assert_eq!(cell4.fg, Color::DarkGray, "col 4 uses inactive fg");
3062 let cell8 = term.cell((8, 0)).unwrap();
3064 assert_eq!(cell8.symbol(), "│", "guide at col 8");
3065 assert_eq!(cell8.fg, Color::Gray, "col 8 uses active fg");
3066 }
3067
3068 #[test]
3069 fn indent_guides_custom_char_paints_that_char() {
3070 let b = Buffer::from_str(" code");
3072 let v = vp(20, 1);
3073 let view = indent_guide_view(&b, &v, 4, ':', Color::DarkGray, Color::Gray, None);
3074 let term = run_render(view, 20, 1);
3075 assert_eq!(
3076 term.cell((4, 0)).unwrap().symbol(),
3077 ":",
3078 "custom guide char ':' at col 4"
3079 );
3080 }
3081}