use std::borrow::Cow;
use std::sync::Arc;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Style;
use ratatui::widgets::{List, Widget};
use super::*;
use crate::item::RankBuilder;
use crate::{Rank, SkimItem};
fn renderer(theme: &ColorTheme) -> ItemRenderer<'_> {
ItemRenderer {
theme,
selector_icon: ">",
multi_select_icon: "*",
ellipsis: "..",
container_width: 6,
wrap: false,
multiline: None,
show_score: false,
show_index: false,
multi_select: false,
tabstop: 4,
no_hscroll: false,
keep_right: false,
manual_hscroll: 0,
skip_to_pattern: None,
reverse_sub_lines: false,
highlight_line: false,
}
}
fn matched_item(text: &str, matched_range: Option<MatchRange>) -> MatchedItem {
MatchedItem::new(
Arc::new(text.to_owned()) as Arc<dyn SkimItem>,
Rank::default(),
matched_range,
&RankBuilder::default(),
)
}
fn line_text(line: &Line<'_>) -> String {
line.spans.iter().map(|span| span.content.as_ref()).collect()
}
fn spans_text(spans: &[Span<'_>]) -> String {
spans.iter().map(|span| span.content.as_ref()).collect()
}
fn rendered_row_text(item: ListItem<'static>, width: u16) -> String {
let mut buf = Buffer::empty(Rect::new(0, 0, width, 1));
List::new(vec![item]).render(buf.area, &mut buf);
(0..width)
.map(|x| buf.cell((x, 0)).expect("cell should be inside buffer").symbol())
.collect::<String>()
}
struct DisplayLongerThanText;
impl SkimItem for DisplayLongerThanText {
fn text(&self) -> Cow<'_, str> {
Cow::Borrowed("ab")
}
fn display(&self, _context: DisplayContext) -> Line<'_> {
Line::from("abcdefghi")
}
}
#[test]
fn split_sub_lines_uses_configured_separator() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
assert_eq!(renderer.split_sub_lines("alpha|beta"), vec!["alpha|beta"]);
renderer.multiline = Some("|");
assert_eq!(
renderer.split_sub_lines("alpha|beta|gamma"),
vec!["alpha", "beta", "gamma"]
);
}
#[test]
fn matched_range_as_char_range_normalizes_supported_ranges() {
assert_eq!(
ItemRenderer::matched_range("aébc", Some(&MatchRange::ByteRange(1, 3))),
(1, 2)
);
assert_eq!(
ItemRenderer::matched_range("abcdef", Some(&MatchRange::Chars(vec![1, 3, 4]))),
(1, 5)
);
assert_eq!(
ItemRenderer::matched_range("abcdef", Some(&MatchRange::Chars(vec![]))),
(0, 0)
);
assert_eq!(
ItemRenderer::matched_range("abcdef", Some(&MatchRange::CharRange(2, 4))),
(2, 4)
);
assert_eq!(ItemRenderer::matched_range("abcdef", None), (0, 0));
}
#[test]
fn display_matches_preserves_original_match_kind() {
assert!(matches!(
ItemRenderer::display_matches(Some(&MatchRange::ByteRange(1, 3))),
crate::Matches::ByteRange(1, 3)
));
assert!(matches!(
ItemRenderer::display_matches(Some(&MatchRange::CharRange(2, 4))),
crate::Matches::CharRange(2, 4)
));
assert!(matches!(
ItemRenderer::display_matches(Some(&MatchRange::Chars(vec![0, 2]))),
crate::Matches::CharIndices(indices) if indices == vec![0, 2]
));
assert!(matches!(ItemRenderer::display_matches(None), crate::Matches::None));
}
#[test]
fn prefix_spans_uses_state_for_icons_and_first_line_fields() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.multi_select = true;
renderer.show_score = true;
renderer.show_index = true;
let mut item = matched_item("alpha", None);
item.rank.score = 42;
item.rank.index = 7;
let current_selected = SubLineState {
is_current: true,
is_selected: true,
is_first: true,
is_first_sub_line: true,
needs_ellipsis: false,
};
let continuation = SubLineState {
is_current: true,
is_selected: true,
is_first: false,
is_first_sub_line: false,
needs_ellipsis: false,
};
assert_eq!(
spans_text(&renderer.prefix_spans(&item, ¤t_selected)),
">*[42] [7] "
);
assert_eq!(spans_text(&renderer.prefix_spans(&item, &continuation)), " ");
}
#[test]
fn prefix_spans_with_highlight_line_resets_prefix_background() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.highlight_line = true;
renderer.multi_select = true;
let item = matched_item("alpha", None);
let current = SubLineState {
is_current: true,
is_selected: true,
is_first: true,
is_first_sub_line: true,
needs_ellipsis: false,
};
let spans = renderer.prefix_spans(&item, ¤t);
assert_eq!(spans_text(&spans), ">*");
assert_eq!(spans[0].style.bg, Some(ratatui::style::Color::Reset));
}
#[test]
fn trim_with_ellipsis_reserves_space_for_marker() {
let theme = ColorTheme::default();
let renderer = renderer(&theme);
let line = Line::from(vec![
Span::styled("abc", Style::default()),
Span::styled("def", Style::default()),
]);
let trimmed = renderer.trim_with_ellipsis(line, false);
assert_eq!(spans_text(&trimmed), "abcd..");
}
#[test]
fn continuation_sub_line_content_applies_hscroll() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.manual_hscroll = 2;
let line = renderer.continuation_sub_line_content("abcdefgh", false);
assert_eq!(line_text(&line), "..cdef");
}
#[test]
fn first_sub_line_content_keeps_display_longer_than_text() {
let theme = ColorTheme::default();
let renderer = renderer(&theme);
let item = MatchedItem::new(
Arc::new(DisplayLongerThanText) as Arc<dyn SkimItem>,
Rank::default(),
None,
&RankBuilder::default(),
);
let line = renderer.first_sub_line_content(&item, "ab", false, 0, 0);
assert_eq!(line_text(&line), "abcd..");
}
#[test]
fn calc_hscroll_container_narrower_than_ellipsis_returns_no_shift() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.container_width = 1;
let (shift, full_width, has_left, has_right) = renderer.calc_hscroll_for_width("hello world", 0, 0, 11);
assert_eq!(shift, 0);
assert_eq!(full_width, 11);
assert!(!has_left);
assert!(!has_right);
}
#[test]
fn calc_hscroll_keep_right_shifts_to_end() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.container_width = 6;
renderer.keep_right = true;
let (shift, _full, _l, _r) = renderer.calc_hscroll_for_width("hello world", 0, 0, 11);
assert_eq!(shift, 5);
}
#[test]
fn calc_hscroll_match_wider_than_available_anchors_at_match_start() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.container_width = 4;
let (shift, _full, _l, _r) = renderer.calc_hscroll_for_width("abcdefghijkl", 2, 10, 12);
assert_eq!(shift, 2);
}
#[test]
fn render_item_with_multiline_skip_starts_at_skipped_sub_line() {
let theme = ColorTheme::default();
let mut renderer = renderer(&theme);
renderer.multiline = Some("|");
renderer.container_width = 20;
let item = matched_item("first|second|third", None);
let mut out = Vec::new();
let added = renderer.render_item(&item, false, false, 1, 10, 0, &mut out);
assert_eq!(added, 2);
assert_eq!(rendered_row_text(out.remove(0), 8), " second");
assert_eq!(rendered_row_text(out.remove(0), 8), " third ");
assert!(out.is_empty());
}