1use ratatui::buffer::Buffer as TermBuffer;
15use ratatui::layout::Rect;
16use ratatui::style::Style;
17use ratatui::widgets::Widget;
18use unicode_width::UnicodeWidthChar;
19
20use crate::wrap::wrap_segments;
21use crate::{Buffer, Selection, Span, Viewport, Wrap};
22
23pub trait StyleResolver {
27 fn resolve(&self, style_id: u32) -> Style;
28}
29
30impl<F: Fn(u32) -> Style> StyleResolver for F {
32 fn resolve(&self, style_id: u32) -> Style {
33 self(style_id)
34 }
35}
36
37pub struct BufferView<'a, R: StyleResolver> {
52 pub buffer: &'a Buffer,
53 pub viewport: &'a Viewport,
57 pub selection: Option<Selection>,
58 pub resolver: &'a R,
59 pub cursor_line_bg: Style,
62 pub cursor_column_bg: Style,
65 pub selection_bg: Style,
67 pub cursor_style: Style,
70 pub gutter: Option<Gutter>,
74 pub search_bg: Style,
77 pub signs: &'a [Sign],
82 pub conceals: &'a [Conceal],
86 pub spans: &'a [Vec<Span>],
95 pub search_pattern: Option<&'a regex::Regex>,
103}
104
105#[derive(Debug, Clone, Copy, Default)]
115pub struct Gutter {
116 pub width: u16,
117 pub style: Style,
118 pub line_offset: usize,
119}
120
121#[derive(Debug, Clone, Copy)]
126pub struct Sign {
127 pub row: usize,
128 pub ch: char,
129 pub style: Style,
130 pub priority: u8,
131}
132
133#[derive(Debug, Clone)]
138pub struct Conceal {
139 pub row: usize,
140 pub start_byte: usize,
141 pub end_byte: usize,
142 pub replacement: String,
143}
144
145impl<R: StyleResolver> Widget for BufferView<'_, R> {
146 fn render(self, area: Rect, term_buf: &mut TermBuffer) {
147 let viewport = *self.viewport;
148 let cursor = self.buffer.cursor();
149 let lines = self.buffer.lines();
150 let spans = self.spans;
151 let folds = self.buffer.folds();
152 let top_row = viewport.top_row;
153 let top_col = viewport.top_col;
154
155 let gutter_width = self.gutter.map(|g| g.width).unwrap_or(0);
156 let text_area = Rect {
157 x: area.x.saturating_add(gutter_width),
158 y: area.y,
159 width: area.width.saturating_sub(gutter_width),
160 height: area.height,
161 };
162
163 let total_rows = lines.len();
164 let mut doc_row = top_row;
165 let mut screen_row: u16 = 0;
166 let wrap_mode = viewport.wrap;
167 let seg_width = if viewport.text_width > 0 {
168 viewport.text_width
169 } else {
170 text_area.width
171 };
172 let mut search_hit_at_cursor_col: Vec<bool> = Vec::new();
178 while doc_row < total_rows && screen_row < area.height {
182 if folds.iter().any(|f| f.hides(doc_row)) {
185 doc_row += 1;
186 continue;
187 }
188 let folded_at_start = folds
189 .iter()
190 .find(|f| f.closed && f.start_row == doc_row)
191 .copied();
192 let line = &lines[doc_row];
193 let row_spans = spans.get(doc_row).map(Vec::as_slice).unwrap_or(&[]);
194 let sel_range = self.selection.and_then(|s| s.row_span(doc_row));
195 let is_cursor_row = doc_row == cursor.row;
196 if let Some(fold) = folded_at_start {
197 if let Some(gutter) = self.gutter {
198 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
199 self.paint_signs(term_buf, area, screen_row, doc_row);
200 }
201 self.paint_fold_marker(term_buf, text_area, screen_row, fold, line, is_cursor_row);
202 search_hit_at_cursor_col.push(false);
203 screen_row += 1;
204 doc_row = fold.end_row + 1;
205 continue;
206 }
207 let search_ranges = self.row_search_ranges(line);
208 let row_has_hit_at_cursor_col = search_ranges
209 .iter()
210 .any(|&(s, e)| cursor.col >= s && cursor.col < e);
211 let row_conceals: Vec<&Conceal> = {
213 let mut v: Vec<&Conceal> =
214 self.conceals.iter().filter(|c| c.row == doc_row).collect();
215 v.sort_by_key(|c| c.start_byte);
216 v
217 };
218 let segments = match wrap_mode {
226 Wrap::None => vec![(top_col, usize::MAX)],
227 _ => wrap_segments(line, seg_width, wrap_mode),
228 };
229 let last_seg_idx = segments.len().saturating_sub(1);
230 for (seg_idx, &(seg_start, seg_end)) in segments.iter().enumerate() {
231 if screen_row >= area.height {
232 break;
233 }
234 if let Some(gutter) = self.gutter {
235 if seg_idx == 0 {
236 self.paint_gutter(term_buf, area, screen_row, doc_row, gutter);
237 self.paint_signs(term_buf, area, screen_row, doc_row);
238 } else {
239 self.paint_blank_gutter(term_buf, area, screen_row, gutter);
240 }
241 }
242 self.paint_row(
243 term_buf,
244 text_area,
245 screen_row,
246 line,
247 row_spans,
248 sel_range,
249 &search_ranges,
250 is_cursor_row,
251 cursor.col,
252 seg_start,
253 seg_end,
254 seg_idx == last_seg_idx,
255 &row_conceals,
256 );
257 search_hit_at_cursor_col.push(row_has_hit_at_cursor_col);
258 screen_row += 1;
259 }
260 doc_row += 1;
261 }
262 if matches!(wrap_mode, Wrap::None)
269 && self.cursor_column_bg != Style::default()
270 && cursor.col >= top_col
271 && (cursor.col - top_col) < text_area.width as usize
272 {
273 let x = text_area.x + (cursor.col - top_col) as u16;
274 for sy in 0..screen_row {
275 if search_hit_at_cursor_col
279 .get(sy as usize)
280 .copied()
281 .unwrap_or(false)
282 {
283 continue;
284 }
285 let y = text_area.y + sy;
286 if let Some(cell) = term_buf.cell_mut((x, y)) {
287 cell.set_style(cell.style().patch(self.cursor_column_bg));
288 }
289 }
290 }
291 }
292}
293
294impl<R: StyleResolver> BufferView<'_, R> {
295 fn row_search_ranges(&self, line: &str) -> Vec<(usize, usize)> {
299 let Some(re) = self.search_pattern else {
300 return Vec::new();
301 };
302 re.find_iter(line)
303 .map(|m| {
304 let start = line[..m.start()].chars().count();
305 let end = line[..m.end()].chars().count();
306 (start, end)
307 })
308 .collect()
309 }
310
311 fn paint_fold_marker(
312 &self,
313 term_buf: &mut TermBuffer,
314 area: Rect,
315 screen_row: u16,
316 fold: crate::Fold,
317 first_line: &str,
318 is_cursor_row: bool,
319 ) {
320 let y = area.y + screen_row;
321 let style = if is_cursor_row && self.cursor_line_bg != Style::default() {
322 self.cursor_line_bg
323 } else {
324 Style::default()
325 };
326 for x in area.x..(area.x + area.width) {
328 if let Some(cell) = term_buf.cell_mut((x, y)) {
329 cell.set_style(style);
330 }
331 }
332 let prefix = first_line.trim();
336 let count = fold.line_count();
337 let label = if prefix.is_empty() {
338 format!("▸ {count} lines folded")
339 } else {
340 const MAX_PREFIX: usize = 60;
341 let trimmed = if prefix.chars().count() > MAX_PREFIX {
342 let head: String = prefix.chars().take(MAX_PREFIX - 1).collect();
343 format!("{head}…")
344 } else {
345 prefix.to_string()
346 };
347 format!("▸ {trimmed} ({count} lines)")
348 };
349 let mut x = area.x;
350 let row_end_x = area.x + area.width;
351 for ch in label.chars() {
352 if x >= row_end_x {
353 break;
354 }
355 let width = ch.width().unwrap_or(1) as u16;
356 if x + width > row_end_x {
357 break;
358 }
359 if let Some(cell) = term_buf.cell_mut((x, y)) {
360 cell.set_char(ch);
361 cell.set_style(style);
362 }
363 x = x.saturating_add(width);
364 }
365 }
366
367 fn paint_signs(&self, term_buf: &mut TermBuffer, area: Rect, screen_row: u16, doc_row: usize) {
368 let Some(sign) = self
369 .signs
370 .iter()
371 .filter(|s| s.row == doc_row)
372 .max_by_key(|s| s.priority)
373 else {
374 return;
375 };
376 let y = area.y + screen_row;
377 let x = area.x;
378 if let Some(cell) = term_buf.cell_mut((x, y)) {
379 cell.set_char(sign.ch);
380 cell.set_style(sign.style);
381 }
382 }
383
384 fn paint_blank_gutter(
387 &self,
388 term_buf: &mut TermBuffer,
389 area: Rect,
390 screen_row: u16,
391 gutter: Gutter,
392 ) {
393 let y = area.y + screen_row;
394 for x in area.x..(area.x + gutter.width) {
395 if let Some(cell) = term_buf.cell_mut((x, y)) {
396 cell.set_char(' ');
397 cell.set_style(gutter.style);
398 }
399 }
400 }
401
402 fn paint_gutter(
403 &self,
404 term_buf: &mut TermBuffer,
405 area: Rect,
406 screen_row: u16,
407 doc_row: usize,
408 gutter: Gutter,
409 ) {
410 let y = area.y + screen_row;
411 let number_width = gutter.width.saturating_sub(1) as usize;
413 let label = format!(
414 "{:>width$}",
415 doc_row + 1 + gutter.line_offset,
416 width = number_width
417 );
418 let mut x = area.x;
419 for ch in label.chars() {
420 if x >= area.x + gutter.width.saturating_sub(1) {
421 break;
422 }
423 if let Some(cell) = term_buf.cell_mut((x, y)) {
424 cell.set_char(ch);
425 cell.set_style(gutter.style);
426 }
427 x = x.saturating_add(1);
428 }
429 let spacer_x = area.x + gutter.width.saturating_sub(1);
432 if let Some(cell) = term_buf.cell_mut((spacer_x, y)) {
433 cell.set_char(' ');
434 cell.set_style(gutter.style);
435 }
436 }
437
438 #[allow(clippy::too_many_arguments)]
439 fn paint_row(
440 &self,
441 term_buf: &mut TermBuffer,
442 area: Rect,
443 screen_row: u16,
444 line: &str,
445 row_spans: &[crate::Span],
446 sel_range: crate::RowSpan,
447 search_ranges: &[(usize, usize)],
448 is_cursor_row: bool,
449 cursor_col: usize,
450 seg_start: usize,
451 seg_end: usize,
452 is_last_segment: bool,
453 conceals: &[&Conceal],
454 ) {
455 let y = area.y + screen_row;
456 let mut screen_x = area.x;
457 let row_end_x = area.x + area.width;
458
459 if is_cursor_row && self.cursor_line_bg != Style::default() {
463 for x in area.x..row_end_x {
464 if let Some(cell) = term_buf.cell_mut((x, y)) {
465 cell.set_style(self.cursor_line_bg);
466 }
467 }
468 }
469
470 let tab_width = self.viewport.effective_tab_width();
474 let mut byte_offset: usize = 0;
475 let mut line_col: usize = 0;
476 let mut chars_iter = line.chars().enumerate().peekable();
477 while let Some((col_idx, ch)) = chars_iter.next() {
478 let ch_byte_len = ch.len_utf8();
479 if col_idx >= seg_end {
480 break;
481 }
482 if let Some(conc) = conceals.iter().find(|c| c.start_byte == byte_offset) {
487 if col_idx >= seg_start {
488 let mut style = if is_cursor_row {
489 self.cursor_line_bg
490 } else {
491 Style::default()
492 };
493 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
494 style = style.patch(span_style);
495 }
496 for rch in conc.replacement.chars() {
497 let rwidth = rch.width().unwrap_or(1) as u16;
498 if screen_x + rwidth > row_end_x {
499 break;
500 }
501 if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
502 cell.set_char(rch);
503 cell.set_style(style);
504 }
505 screen_x += rwidth;
506 }
507 }
508 let mut consumed = ch_byte_len;
511 byte_offset += ch_byte_len;
512 while byte_offset < conc.end_byte {
513 let Some((_, next_ch)) = chars_iter.next() else {
514 break;
515 };
516 consumed += next_ch.len_utf8();
517 byte_offset = byte_offset.saturating_add(next_ch.len_utf8());
518 }
519 let _ = consumed;
520 continue;
521 }
522 let visible_width = if ch == '\t' {
527 tab_width - (line_col % tab_width)
528 } else {
529 ch.width().unwrap_or(1)
530 };
531 if col_idx < seg_start {
534 line_col += visible_width;
535 byte_offset += ch_byte_len;
536 continue;
537 }
538 let width = visible_width as u16;
540 if screen_x + width > row_end_x {
541 break;
542 }
543
544 let mut style = if is_cursor_row {
546 self.cursor_line_bg
547 } else {
548 Style::default()
549 };
550 if let Some(span_style) = self.resolve_span_style(row_spans, byte_offset) {
551 style = style.patch(span_style);
552 }
553 if self.search_bg != Style::default()
557 && search_ranges
558 .iter()
559 .any(|&(s, e)| col_idx >= s && col_idx < e)
560 {
561 style = style.patch(self.search_bg);
562 }
563 if let Some((lo, hi)) = sel_range
564 && col_idx >= lo
565 && col_idx <= hi
566 {
567 style = style.patch(self.selection_bg);
568 }
569 if is_cursor_row && col_idx == cursor_col {
570 style = style.patch(self.cursor_style);
571 }
572
573 if ch == '\t' {
574 for k in 0..width {
578 if let Some(cell) = term_buf.cell_mut((screen_x + k, y)) {
579 cell.set_char(' ');
580 cell.set_style(style);
581 }
582 }
583 } else if let Some(cell) = term_buf.cell_mut((screen_x, y)) {
584 cell.set_char(ch);
585 cell.set_style(style);
586 }
587 screen_x += width;
588 line_col += visible_width;
589 byte_offset += ch_byte_len;
590 }
591
592 if is_cursor_row
597 && is_last_segment
598 && cursor_col >= line.chars().count()
599 && cursor_col >= seg_start
600 {
601 let pad_x = area.x + (cursor_col.saturating_sub(seg_start)) as u16;
602 if pad_x < row_end_x
603 && let Some(cell) = term_buf.cell_mut((pad_x, y))
604 {
605 cell.set_char(' ');
606 cell.set_style(self.cursor_line_bg.patch(self.cursor_style));
607 }
608 }
609 }
610
611 fn resolve_span_style(&self, row_spans: &[crate::Span], byte_offset: usize) -> Option<Style> {
614 let mut best: Option<&crate::Span> = None;
619 for span in row_spans {
620 if byte_offset >= span.start_byte && byte_offset < span.end_byte {
621 let len = span.end_byte - span.start_byte;
622 match best {
623 Some(b) if (b.end_byte - b.start_byte) <= len => {}
624 _ => best = Some(span),
625 }
626 }
627 }
628 best.map(|s| self.resolver.resolve(s.style))
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 use super::*;
635 use ratatui::style::{Color, Modifier};
636 use ratatui::widgets::Widget;
637
638 fn run_render<R: StyleResolver>(view: BufferView<'_, R>, w: u16, h: u16) -> TermBuffer {
639 let area = Rect::new(0, 0, w, h);
640 let mut buf = TermBuffer::empty(area);
641 view.render(area, &mut buf);
642 buf
643 }
644
645 fn no_styles(_id: u32) -> Style {
646 Style::default()
647 }
648
649 fn vp(width: u16, height: u16) -> Viewport {
651 Viewport {
652 top_row: 0,
653 top_col: 0,
654 width,
655 height,
656 wrap: Wrap::None,
657 text_width: width,
658 tab_width: 0,
659 }
660 }
661
662 #[test]
663 fn renders_plain_chars_into_terminal_buffer() {
664 let b = Buffer::from_str("hello\nworld");
665 let v = vp(20, 5);
666 let view = BufferView {
667 buffer: &b,
668 viewport: &v,
669 selection: None,
670 resolver: &(no_styles as fn(u32) -> Style),
671 cursor_line_bg: Style::default(),
672 cursor_column_bg: Style::default(),
673 selection_bg: Style::default().bg(Color::Blue),
674 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
675 gutter: None,
676 search_bg: Style::default(),
677 signs: &[],
678 conceals: &[],
679 spans: &[],
680 search_pattern: None,
681 };
682 let term = run_render(view, 20, 5);
683 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
684 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "o");
685 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "w");
686 assert_eq!(term.cell((4, 1)).unwrap().symbol(), "d");
687 }
688
689 #[test]
690 fn cursor_cell_gets_reversed_style() {
691 let mut b = Buffer::from_str("abc");
692 let v = vp(10, 1);
693 b.set_cursor(crate::Position::new(0, 1));
694 let view = BufferView {
695 buffer: &b,
696 viewport: &v,
697 selection: None,
698 resolver: &(no_styles as fn(u32) -> Style),
699 cursor_line_bg: Style::default(),
700 cursor_column_bg: Style::default(),
701 selection_bg: Style::default().bg(Color::Blue),
702 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
703 gutter: None,
704 search_bg: Style::default(),
705 signs: &[],
706 conceals: &[],
707 spans: &[],
708 search_pattern: None,
709 };
710 let term = run_render(view, 10, 1);
711 let cursor_cell = term.cell((1, 0)).unwrap();
712 assert!(cursor_cell.modifier.contains(Modifier::REVERSED));
713 }
714
715 #[test]
716 fn selection_bg_applies_only_to_selected_cells() {
717 use crate::{Position, Selection};
718 let b = Buffer::from_str("abcdef");
719 let v = vp(10, 1);
720 let view = BufferView {
721 buffer: &b,
722 viewport: &v,
723 selection: Some(Selection::Char {
724 anchor: Position::new(0, 1),
725 head: Position::new(0, 3),
726 }),
727 resolver: &(no_styles as fn(u32) -> Style),
728 cursor_line_bg: Style::default(),
729 cursor_column_bg: Style::default(),
730 selection_bg: Style::default().bg(Color::Blue),
731 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
732 gutter: None,
733 search_bg: Style::default(),
734 signs: &[],
735 conceals: &[],
736 spans: &[],
737 search_pattern: None,
738 };
739 let term = run_render(view, 10, 1);
740 assert!(term.cell((0, 0)).unwrap().bg != Color::Blue);
741 for x in 1..=3 {
742 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Blue);
743 }
744 assert!(term.cell((4, 0)).unwrap().bg != Color::Blue);
745 }
746
747 #[test]
748 fn syntax_span_fg_resolves_via_table() {
749 use crate::Span;
750 let b = Buffer::from_str("SELECT foo");
751 let v = vp(20, 1);
752 let spans = vec![vec![Span::new(0, 6, 7)]];
753 let resolver = |id: u32| -> Style {
754 if id == 7 {
755 Style::default().fg(Color::Red)
756 } else {
757 Style::default()
758 }
759 };
760 let view = BufferView {
761 buffer: &b,
762 viewport: &v,
763 selection: None,
764 resolver: &resolver,
765 cursor_line_bg: Style::default(),
766 cursor_column_bg: Style::default(),
767 selection_bg: Style::default().bg(Color::Blue),
768 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
769 gutter: None,
770 search_bg: Style::default(),
771 signs: &[],
772 conceals: &[],
773 spans: &spans,
774 search_pattern: None,
775 };
776 let term = run_render(view, 20, 1);
777 for x in 0..6 {
778 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
779 }
780 }
781
782 #[test]
783 fn gutter_renders_right_aligned_line_numbers() {
784 let b = Buffer::from_str("a\nb\nc");
785 let v = vp(10, 3);
786 let view = BufferView {
787 buffer: &b,
788 viewport: &v,
789 selection: None,
790 resolver: &(no_styles as fn(u32) -> Style),
791 cursor_line_bg: Style::default(),
792 cursor_column_bg: Style::default(),
793 selection_bg: Style::default().bg(Color::Blue),
794 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
795 gutter: Some(Gutter {
796 width: 4,
797 style: Style::default().fg(Color::Yellow),
798 line_offset: 0,
799 }),
800 search_bg: Style::default(),
801 signs: &[],
802 conceals: &[],
803 spans: &[],
804 search_pattern: None,
805 };
806 let term = run_render(view, 10, 3);
807 assert_eq!(term.cell((2, 0)).unwrap().symbol(), "1");
809 assert_eq!(term.cell((2, 0)).unwrap().fg, Color::Yellow);
810 assert_eq!(term.cell((2, 1)).unwrap().symbol(), "2");
811 assert_eq!(term.cell((2, 2)).unwrap().symbol(), "3");
812 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
814 }
815
816 #[test]
817 fn search_bg_paints_match_cells() {
818 use regex::Regex;
819 let b = Buffer::from_str("foo bar foo");
820 let v = vp(20, 1);
821 let pat = Regex::new("foo").unwrap();
822 let view = BufferView {
823 buffer: &b,
824 viewport: &v,
825 selection: None,
826 resolver: &(no_styles as fn(u32) -> Style),
827 cursor_line_bg: Style::default(),
828 cursor_column_bg: Style::default(),
829 selection_bg: Style::default().bg(Color::Blue),
830 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
831 gutter: None,
832 search_bg: Style::default().bg(Color::Magenta),
833 signs: &[],
834 conceals: &[],
835 spans: &[],
836 search_pattern: Some(&pat),
837 };
838 let term = run_render(view, 20, 1);
839 for x in 0..3 {
840 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
841 }
842 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
844 for x in 8..11 {
845 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
846 }
847 }
848
849 #[test]
850 fn search_bg_survives_cursorcolumn_overlay() {
851 use regex::Regex;
852 let mut b = Buffer::from_str("foo bar foo");
856 let v = vp(20, 1);
857 let pat = Regex::new("foo").unwrap();
858 b.set_cursor(crate::Position::new(0, 1));
860 let view = BufferView {
861 buffer: &b,
862 viewport: &v,
863 selection: None,
864 resolver: &(no_styles as fn(u32) -> Style),
865 cursor_line_bg: Style::default(),
866 cursor_column_bg: Style::default().bg(Color::DarkGray),
867 selection_bg: Style::default().bg(Color::Blue),
868 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
869 gutter: None,
870 search_bg: Style::default().bg(Color::Magenta),
871 signs: &[],
872 conceals: &[],
873 spans: &[],
874 search_pattern: Some(&pat),
875 };
876 let term = run_render(view, 20, 1);
877 assert_eq!(term.cell((1, 0)).unwrap().bg, Color::Magenta);
879 }
880
881 #[test]
882 fn highest_priority_sign_wins_per_row_and_overwrites_gutter() {
883 let b = Buffer::from_str("a\nb\nc");
884 let v = vp(10, 3);
885 let signs = [
886 Sign {
887 row: 0,
888 ch: 'W',
889 style: Style::default().fg(Color::Yellow),
890 priority: 1,
891 },
892 Sign {
893 row: 0,
894 ch: 'E',
895 style: Style::default().fg(Color::Red),
896 priority: 2,
897 },
898 ];
899 let view = BufferView {
900 buffer: &b,
901 viewport: &v,
902 selection: None,
903 resolver: &(no_styles as fn(u32) -> Style),
904 cursor_line_bg: Style::default(),
905 cursor_column_bg: Style::default(),
906 selection_bg: Style::default().bg(Color::Blue),
907 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
908 gutter: Some(Gutter {
909 width: 3,
910 style: Style::default().fg(Color::DarkGray),
911 line_offset: 0,
912 }),
913 search_bg: Style::default(),
914 signs: &signs,
915 conceals: &[],
916 spans: &[],
917 search_pattern: None,
918 };
919 let term = run_render(view, 10, 3);
920 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "E");
921 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
922 assert_ne!(term.cell((0, 1)).unwrap().symbol(), "E");
924 }
925
926 #[test]
927 fn conceal_replaces_byte_range() {
928 let b = Buffer::from_str("see https://example.com end");
929 let v = vp(30, 1);
930 let conceals = vec![Conceal {
931 row: 0,
932 start_byte: 4, end_byte: 4 + "https://example.com".len(), replacement: "🔗".to_string(),
935 }];
936 let view = BufferView {
937 buffer: &b,
938 viewport: &v,
939 selection: None,
940 resolver: &(no_styles as fn(u32) -> Style),
941 cursor_line_bg: Style::default(),
942 cursor_column_bg: Style::default(),
943 selection_bg: Style::default(),
944 cursor_style: Style::default(),
945 gutter: None,
946 search_bg: Style::default(),
947 signs: &[],
948 conceals: &conceals,
949 spans: &[],
950 search_pattern: None,
951 };
952 let term = run_render(view, 30, 1);
953 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "s");
955 assert_eq!(term.cell((3, 0)).unwrap().symbol(), " ");
956 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "🔗");
959 }
960
961 #[test]
962 fn closed_fold_collapses_rows_and_paints_marker() {
963 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
964 let v = vp(30, 5);
965 b.add_fold(1, 3, true);
967 let view = BufferView {
968 buffer: &b,
969 viewport: &v,
970 selection: None,
971 resolver: &(no_styles as fn(u32) -> Style),
972 cursor_line_bg: Style::default(),
973 cursor_column_bg: Style::default(),
974 selection_bg: Style::default().bg(Color::Blue),
975 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
976 gutter: None,
977 search_bg: Style::default(),
978 signs: &[],
979 conceals: &[],
980 spans: &[],
981 search_pattern: None,
982 };
983 let term = run_render(view, 30, 5);
984 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
986 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "▸");
989 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "e");
991 }
992
993 #[test]
994 fn open_fold_renders_normally() {
995 let mut b = Buffer::from_str("a\nb\nc");
996 let v = vp(5, 3);
997 b.add_fold(0, 2, false); let view = BufferView {
999 buffer: &b,
1000 viewport: &v,
1001 selection: None,
1002 resolver: &(no_styles as fn(u32) -> Style),
1003 cursor_line_bg: Style::default(),
1004 cursor_column_bg: Style::default(),
1005 selection_bg: Style::default().bg(Color::Blue),
1006 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1007 gutter: None,
1008 search_bg: Style::default(),
1009 signs: &[],
1010 conceals: &[],
1011 spans: &[],
1012 search_pattern: None,
1013 };
1014 let term = run_render(view, 5, 3);
1015 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1016 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1017 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "c");
1018 }
1019
1020 #[test]
1021 fn horizontal_scroll_clips_left_chars() {
1022 let b = Buffer::from_str("abcdefgh");
1023 let mut v = vp(4, 1);
1024 v.top_col = 3;
1025 let view = BufferView {
1026 buffer: &b,
1027 viewport: &v,
1028 selection: None,
1029 resolver: &(no_styles as fn(u32) -> Style),
1030 cursor_line_bg: Style::default(),
1031 cursor_column_bg: Style::default(),
1032 selection_bg: Style::default().bg(Color::Blue),
1033 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1034 gutter: None,
1035 search_bg: Style::default(),
1036 signs: &[],
1037 conceals: &[],
1038 spans: &[],
1039 search_pattern: None,
1040 };
1041 let term = run_render(view, 4, 1);
1042 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "d");
1043 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "g");
1044 }
1045
1046 fn make_wrap_view<'a>(
1047 b: &'a Buffer,
1048 viewport: &'a Viewport,
1049 resolver: &'a (impl StyleResolver + 'a),
1050 gutter: Option<Gutter>,
1051 ) -> BufferView<'a, impl StyleResolver + 'a> {
1052 BufferView {
1053 buffer: b,
1054 viewport,
1055 selection: None,
1056 resolver,
1057 cursor_line_bg: Style::default(),
1058 cursor_column_bg: Style::default(),
1059 selection_bg: Style::default().bg(Color::Blue),
1060 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1061 gutter,
1062 search_bg: Style::default(),
1063 signs: &[],
1064 conceals: &[],
1065 spans: &[],
1066 search_pattern: None,
1067 }
1068 }
1069
1070 #[test]
1071 fn wrap_segments_char_breaks_at_width() {
1072 let segs = wrap_segments("abcdefghij", 4, Wrap::Char);
1073 assert_eq!(segs, vec![(0, 4), (4, 8), (8, 10)]);
1074 }
1075
1076 #[test]
1077 fn wrap_segments_word_backs_up_to_whitespace() {
1078 let segs = wrap_segments("alpha beta gamma", 8, Wrap::Word);
1079 assert_eq!(segs[0], (0, 6));
1081 assert_eq!(segs[1], (6, 11));
1083 assert_eq!(segs[2], (11, 16));
1084 }
1085
1086 #[test]
1087 fn wrap_segments_word_falls_back_to_char_for_long_runs() {
1088 let segs = wrap_segments("supercalifragilistic", 5, Wrap::Word);
1089 assert_eq!(segs, vec![(0, 5), (5, 10), (10, 15), (15, 20)]);
1091 }
1092
1093 #[test]
1094 fn wrap_char_paints_continuation_rows() {
1095 let b = Buffer::from_str("abcdefghij");
1096 let v = Viewport {
1097 top_row: 0,
1098 top_col: 0,
1099 width: 4,
1100 height: 3,
1101 wrap: Wrap::Char,
1102 text_width: 4,
1103 tab_width: 0,
1104 };
1105 let r = no_styles as fn(u32) -> Style;
1106 let view = make_wrap_view(&b, &v, &r, None);
1107 let term = run_render(view, 4, 3);
1108 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1110 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "d");
1111 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "e");
1113 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "h");
1114 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "i");
1116 assert_eq!(term.cell((1, 2)).unwrap().symbol(), "j");
1117 }
1118
1119 #[test]
1120 fn wrap_char_gutter_blank_on_continuation() {
1121 let b = Buffer::from_str("abcdefgh");
1122 let v = Viewport {
1123 top_row: 0,
1124 top_col: 0,
1125 width: 6,
1126 height: 3,
1127 wrap: Wrap::Char,
1128 text_width: 3,
1130 tab_width: 0,
1131 };
1132 let r = no_styles as fn(u32) -> Style;
1133 let gutter = Gutter {
1134 width: 3,
1135 style: Style::default().fg(Color::Yellow),
1136 line_offset: 0,
1137 };
1138 let view = make_wrap_view(&b, &v, &r, Some(gutter));
1139 let term = run_render(view, 6, 3);
1140 assert_eq!(term.cell((1, 0)).unwrap().symbol(), "1");
1142 assert_eq!(term.cell((3, 0)).unwrap().symbol(), "a");
1143 for x in 0..2 {
1145 assert_eq!(term.cell((x, 1)).unwrap().symbol(), " ");
1146 }
1147 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "d");
1148 assert_eq!(term.cell((5, 1)).unwrap().symbol(), "f");
1149 }
1150
1151 #[test]
1152 fn wrap_char_cursor_lands_on_correct_segment() {
1153 let mut b = Buffer::from_str("abcdefghij");
1154 let v = Viewport {
1155 top_row: 0,
1156 top_col: 0,
1157 width: 4,
1158 height: 3,
1159 wrap: Wrap::Char,
1160 text_width: 4,
1161 tab_width: 0,
1162 };
1163 b.set_cursor(crate::Position::new(0, 6));
1165 let r = no_styles as fn(u32) -> Style;
1166 let mut view = make_wrap_view(&b, &v, &r, None);
1167 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1168 let term = run_render(view, 4, 3);
1169 assert!(
1170 term.cell((2, 1))
1171 .unwrap()
1172 .modifier
1173 .contains(Modifier::REVERSED)
1174 );
1175 }
1176
1177 #[test]
1178 fn wrap_char_eol_cursor_placeholder_on_last_segment() {
1179 let mut b = Buffer::from_str("abcdef");
1180 let v = Viewport {
1181 top_row: 0,
1182 top_col: 0,
1183 width: 4,
1184 height: 3,
1185 wrap: Wrap::Char,
1186 text_width: 4,
1187 tab_width: 0,
1188 };
1189 b.set_cursor(crate::Position::new(0, 6));
1191 let r = no_styles as fn(u32) -> Style;
1192 let mut view = make_wrap_view(&b, &v, &r, None);
1193 view.cursor_style = Style::default().add_modifier(Modifier::REVERSED);
1194 let term = run_render(view, 4, 3);
1195 assert!(
1197 term.cell((2, 1))
1198 .unwrap()
1199 .modifier
1200 .contains(Modifier::REVERSED)
1201 );
1202 }
1203
1204 #[test]
1205 fn wrap_word_breaks_at_whitespace() {
1206 let b = Buffer::from_str("alpha beta gamma");
1207 let v = Viewport {
1208 top_row: 0,
1209 top_col: 0,
1210 width: 8,
1211 height: 3,
1212 wrap: Wrap::Word,
1213 text_width: 8,
1214 tab_width: 0,
1215 };
1216 let r = no_styles as fn(u32) -> Style;
1217 let view = make_wrap_view(&b, &v, &r, None);
1218 let term = run_render(view, 8, 3);
1219 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "a");
1221 assert_eq!(term.cell((4, 0)).unwrap().symbol(), "a");
1222 assert_eq!(term.cell((0, 1)).unwrap().symbol(), "b");
1224 assert_eq!(term.cell((3, 1)).unwrap().symbol(), "a");
1225 assert_eq!(term.cell((0, 2)).unwrap().symbol(), "g");
1227 assert_eq!(term.cell((4, 2)).unwrap().symbol(), "a");
1228 }
1229
1230 fn view_with<'a>(
1236 b: &'a Buffer,
1237 viewport: &'a Viewport,
1238 resolver: &'a (impl StyleResolver + 'a),
1239 spans: &'a [Vec<Span>],
1240 search_pattern: Option<&'a regex::Regex>,
1241 ) -> BufferView<'a, impl StyleResolver + 'a> {
1242 BufferView {
1243 buffer: b,
1244 viewport,
1245 selection: None,
1246 resolver,
1247 cursor_line_bg: Style::default(),
1248 cursor_column_bg: Style::default(),
1249 selection_bg: Style::default().bg(Color::Blue),
1250 cursor_style: Style::default().add_modifier(Modifier::REVERSED),
1251 gutter: None,
1252 search_bg: Style::default().bg(Color::Magenta),
1253 signs: &[],
1254 conceals: &[],
1255 spans,
1256 search_pattern,
1257 }
1258 }
1259
1260 #[test]
1261 fn empty_spans_param_renders_default_style() {
1262 let b = Buffer::from_str("hello");
1263 let v = vp(10, 1);
1264 let r = no_styles as fn(u32) -> Style;
1265 let view = view_with(&b, &v, &r, &[], None);
1266 let term = run_render(view, 10, 1);
1267 assert_eq!(term.cell((0, 0)).unwrap().symbol(), "h");
1268 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Reset);
1269 }
1270
1271 #[test]
1272 fn spans_param_paints_styled_byte_range() {
1273 let b = Buffer::from_str("abcdef");
1274 let v = vp(10, 1);
1275 let resolver = |id: u32| -> Style {
1276 if id == 3 {
1277 Style::default().fg(Color::Green)
1278 } else {
1279 Style::default()
1280 }
1281 };
1282 let spans = vec![vec![Span::new(0, 3, 3)]];
1283 let view = view_with(&b, &v, &resolver, &spans, None);
1284 let term = run_render(view, 10, 1);
1285 for x in 0..3 {
1286 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Green);
1287 }
1288 assert_ne!(term.cell((3, 0)).unwrap().fg, Color::Green);
1289 }
1290
1291 #[test]
1292 fn spans_param_handles_per_row_overlay() {
1293 let b = Buffer::from_str("abc\ndef");
1294 let v = vp(10, 2);
1295 let resolver = |id: u32| -> Style {
1296 if id == 1 {
1297 Style::default().fg(Color::Red)
1298 } else {
1299 Style::default().fg(Color::Green)
1300 }
1301 };
1302 let spans = vec![vec![Span::new(0, 3, 1)], vec![Span::new(0, 3, 2)]];
1303 let view = view_with(&b, &v, &resolver, &spans, None);
1304 let term = run_render(view, 10, 2);
1305 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1306 assert_eq!(term.cell((0, 1)).unwrap().fg, Color::Green);
1307 }
1308
1309 #[test]
1310 fn spans_param_rows_beyond_get_no_styling() {
1311 let b = Buffer::from_str("abc\ndef\nghi");
1312 let v = vp(10, 3);
1313 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1314 let spans = vec![vec![Span::new(0, 3, 0)]];
1316 let view = view_with(&b, &v, &resolver, &spans, None);
1317 let term = run_render(view, 10, 3);
1318 assert_eq!(term.cell((0, 0)).unwrap().fg, Color::Red);
1319 assert_ne!(term.cell((0, 1)).unwrap().fg, Color::Red);
1320 assert_ne!(term.cell((0, 2)).unwrap().fg, Color::Red);
1321 }
1322
1323 #[test]
1324 fn search_pattern_none_disables_hlsearch() {
1325 let b = Buffer::from_str("foo bar foo");
1326 let v = vp(20, 1);
1327 let r = no_styles as fn(u32) -> Style;
1328 let view = view_with(&b, &v, &r, &[], None);
1330 let term = run_render(view, 20, 1);
1331 for x in 0..11 {
1332 assert_ne!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1333 }
1334 }
1335
1336 #[test]
1337 fn search_pattern_regex_paints_match_bg() {
1338 use regex::Regex;
1339 let b = Buffer::from_str("xyz foo xyz");
1340 let v = vp(20, 1);
1341 let r = no_styles as fn(u32) -> Style;
1342 let pat = Regex::new("foo").unwrap();
1343 let view = view_with(&b, &v, &r, &[], Some(&pat));
1344 let term = run_render(view, 20, 1);
1345 assert_ne!(term.cell((3, 0)).unwrap().bg, Color::Magenta);
1347 for x in 4..7 {
1348 assert_eq!(term.cell((x, 0)).unwrap().bg, Color::Magenta);
1349 }
1350 assert_ne!(term.cell((7, 0)).unwrap().bg, Color::Magenta);
1351 }
1352
1353 #[test]
1354 fn search_pattern_unicode_columns_are_charwise() {
1355 use regex::Regex;
1356 let b = Buffer::from_str("tablé foo");
1358 let v = vp(20, 1);
1359 let r = no_styles as fn(u32) -> Style;
1360 let pat = Regex::new("foo").unwrap();
1361 let view = view_with(&b, &v, &r, &[], Some(&pat));
1362 let term = run_render(view, 20, 1);
1363 assert_eq!(term.cell((6, 0)).unwrap().bg, Color::Magenta);
1365 assert_eq!(term.cell((8, 0)).unwrap().bg, Color::Magenta);
1366 assert_ne!(term.cell((5, 0)).unwrap().bg, Color::Magenta);
1367 }
1368
1369 #[test]
1370 fn spans_param_clamps_short_row_overlay() {
1371 let b = Buffer::from_str("abc");
1373 let v = vp(10, 1);
1374 let resolver = |_: u32| -> Style { Style::default().fg(Color::Red) };
1375 let spans = vec![vec![Span::new(0, 100, 0)]];
1376 let view = view_with(&b, &v, &resolver, &spans, None);
1377 let term = run_render(view, 10, 1);
1378 for x in 0..3 {
1379 assert_eq!(term.cell((x, 0)).unwrap().fg, Color::Red);
1380 }
1381 }
1382
1383 #[test]
1384 fn spans_and_search_pattern_compose() {
1385 use regex::Regex;
1387 let b = Buffer::from_str("foo");
1388 let v = vp(10, 1);
1389 let resolver = |_: u32| -> Style { Style::default().fg(Color::Green) };
1390 let spans = vec![vec![Span::new(0, 3, 0)]];
1391 let pat = Regex::new("foo").unwrap();
1392 let view = view_with(&b, &v, &resolver, &spans, Some(&pat));
1393 let term = run_render(view, 10, 1);
1394 let cell = term.cell((1, 0)).unwrap();
1395 assert_eq!(cell.fg, Color::Green);
1396 assert_eq!(cell.bg, Color::Magenta);
1397 }
1398}