mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 19:45:23 +00:00
refactor: nicer results alignment to improve discoverability of matched patterns (#435)
<img width="1018" alt="Screenshot 2025-03-23 at 16 13 54" src="https://github.com/user-attachments/assets/89777043-e98f-464c-b508-0c0dce9d4b26" /> Fixes #373
This commit is contained in:
parent
b81e0df791
commit
8e17ef694e
@ -655,6 +655,7 @@ pub fn draw_results_list(c: &mut Criterion) {
|
|||||||
ListDirection::BottomToTop,
|
ListDirection::BottomToTop,
|
||||||
false,
|
false,
|
||||||
&colorscheme,
|
&colorscheme,
|
||||||
|
100,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -81,6 +81,7 @@ fn draw_rc_channels(
|
|||||||
ListDirection::TopToBottom,
|
ListDirection::TopToBottom,
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
&colorscheme.results,
|
&colorscheme.results,
|
||||||
|
area.width,
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_stateful_widget(channel_list, area, picker_state);
|
f.render_stateful_widget(channel_list, area, picker_state);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::channels::entry::Entry;
|
use crate::channels::entry::Entry;
|
||||||
use crate::screen::colors::{Colorscheme, ResultsColorscheme};
|
use crate::screen::colors::{Colorscheme, ResultsColorscheme};
|
||||||
use crate::screen::layout::InputPosition;
|
use crate::screen::layout::InputPosition;
|
||||||
|
use crate::utils::indices::truncate_highlighted_string;
|
||||||
use crate::utils::strings::{
|
use crate::utils::strings::{
|
||||||
make_matched_string_printable, next_char_boundary,
|
make_matched_string_printable, next_char_boundary,
|
||||||
slice_at_char_boundaries,
|
slice_at_char_boundaries,
|
||||||
@ -20,6 +21,179 @@ const POINTER_SYMBOL: &str = "> ";
|
|||||||
const SELECTED_SYMBOL: &str = "● ";
|
const SELECTED_SYMBOL: &str = "● ";
|
||||||
const DESELECTED_SYMBOL: &str = " ";
|
const DESELECTED_SYMBOL: &str = " ";
|
||||||
|
|
||||||
|
/// The max width for each part of the entry (name and value) depending on various factors.
|
||||||
|
///
|
||||||
|
/// - name only: `available_width - 2 * (use_icons as u16) - 2 * (is_selected as u16) - line_number_width`
|
||||||
|
/// - name and value: `(available_width - 2 * (use_icons as u16) - 2 * (is_selected as u16) - line_number_width) / 2`
|
||||||
|
fn max_widths(
|
||||||
|
entry: &Entry,
|
||||||
|
available_width: u16,
|
||||||
|
use_icons: bool,
|
||||||
|
is_selected: bool,
|
||||||
|
) -> (u16, u16) {
|
||||||
|
let available_width = available_width.saturating_sub(
|
||||||
|
2 // pointer and space
|
||||||
|
+ 2 * (u16::from(use_icons))
|
||||||
|
+ 2 * (u16::from(is_selected))
|
||||||
|
+ entry
|
||||||
|
.line_number
|
||||||
|
// ":{line_number}: "
|
||||||
|
.map_or(0, |l| 1 + u16::try_from(l.checked_ilog10().unwrap_or(0)).unwrap() + 3),
|
||||||
|
);
|
||||||
|
|
||||||
|
if entry.value.is_none() {
|
||||||
|
return (available_width, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, use up the available space for both name and value as nicely as possible
|
||||||
|
let name_len =
|
||||||
|
u16::try_from(entry.name.chars().count()).unwrap_or(u16::MAX);
|
||||||
|
let value_len = entry
|
||||||
|
.value
|
||||||
|
.as_ref()
|
||||||
|
.map_or(0, |v| u16::try_from(v.chars().count()).unwrap_or(u16::MAX));
|
||||||
|
|
||||||
|
if name_len < available_width / 2 {
|
||||||
|
(name_len, available_width - name_len - 2)
|
||||||
|
} else if value_len < available_width / 2 {
|
||||||
|
(available_width - value_len, value_len - 2)
|
||||||
|
} else {
|
||||||
|
(available_width / 2, available_width / 2 - 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_result_line<'a>(
|
||||||
|
entry: &'a Entry,
|
||||||
|
selected_entries: Option<&FxHashSet<Entry>>,
|
||||||
|
use_icons: bool,
|
||||||
|
colorscheme: &ResultsColorscheme,
|
||||||
|
area_width: u16,
|
||||||
|
) -> Line<'a> {
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let (name_max_width, value_max_width) = max_widths(
|
||||||
|
entry,
|
||||||
|
area_width,
|
||||||
|
use_icons,
|
||||||
|
selected_entries.map_or(false, |selected| selected.contains(entry)),
|
||||||
|
);
|
||||||
|
// optional selection symbol
|
||||||
|
if let Some(selected_entries) = selected_entries {
|
||||||
|
if !selected_entries.is_empty() {
|
||||||
|
spans.push(if selected_entries.contains(entry) {
|
||||||
|
Span::styled(
|
||||||
|
SELECTED_SYMBOL,
|
||||||
|
Style::default().fg(colorscheme.result_selected_fg),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Span::from(DESELECTED_SYMBOL)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// optional icon
|
||||||
|
if let Some(icon) = entry.icon.as_ref() {
|
||||||
|
if use_icons {
|
||||||
|
spans.push(Span::styled(
|
||||||
|
icon.to_string(),
|
||||||
|
Style::default().fg(Color::from_str(icon.color).unwrap()),
|
||||||
|
));
|
||||||
|
|
||||||
|
spans.push(Span::raw(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// entry name
|
||||||
|
let (mut entry_name, mut value_match_ranges) =
|
||||||
|
make_matched_string_printable(
|
||||||
|
&entry.name,
|
||||||
|
entry.name_match_ranges.as_deref(),
|
||||||
|
);
|
||||||
|
// if the name is too long, we need to truncate it and add an ellipsis
|
||||||
|
if entry_name.len() > name_max_width as usize {
|
||||||
|
(entry_name, value_match_ranges) = truncate_highlighted_string(
|
||||||
|
&entry_name,
|
||||||
|
&value_match_ranges,
|
||||||
|
name_max_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let mut last_match_end = 0;
|
||||||
|
for (start, end) in value_match_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|(s, e)| (*s as usize, *e as usize))
|
||||||
|
{
|
||||||
|
// from the end of the last match to the start of the current one
|
||||||
|
spans.push(Span::styled(
|
||||||
|
slice_at_char_boundaries(&entry_name, last_match_end, start)
|
||||||
|
.to_string(),
|
||||||
|
Style::default().fg(colorscheme.result_name_fg),
|
||||||
|
));
|
||||||
|
// the current match
|
||||||
|
spans.push(Span::styled(
|
||||||
|
slice_at_char_boundaries(&entry_name, start, end).to_string(),
|
||||||
|
Style::default().fg(colorscheme.match_foreground_color),
|
||||||
|
));
|
||||||
|
last_match_end = end;
|
||||||
|
}
|
||||||
|
// we need to push a span for the remainder of the entry name
|
||||||
|
// but only if there's something left
|
||||||
|
let next_boundary = next_char_boundary(&entry_name, last_match_end);
|
||||||
|
if next_boundary < entry_name.len() {
|
||||||
|
let remainder = entry_name[next_boundary..].to_string();
|
||||||
|
spans.push(Span::styled(
|
||||||
|
remainder,
|
||||||
|
Style::default().fg(colorscheme.result_name_fg),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// optional line number
|
||||||
|
if let Some(line_number) = entry.line_number {
|
||||||
|
spans.push(Span::styled(
|
||||||
|
format!(":{line_number}"),
|
||||||
|
Style::default().fg(colorscheme.result_line_number_fg),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// optional value
|
||||||
|
if let Some(value) = &entry.value {
|
||||||
|
spans.push(Span::raw(": "));
|
||||||
|
|
||||||
|
let (mut value, mut value_match_ranges) =
|
||||||
|
make_matched_string_printable(
|
||||||
|
value,
|
||||||
|
entry.value_match_ranges.as_deref(),
|
||||||
|
);
|
||||||
|
// if the value is too long, we need to truncate it and add an ellipsis
|
||||||
|
if value.len() > value_max_width as usize {
|
||||||
|
(value, value_match_ranges) = truncate_highlighted_string(
|
||||||
|
&value,
|
||||||
|
&value_match_ranges,
|
||||||
|
value_max_width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut last_match_end = 0;
|
||||||
|
for (start, end) in value_match_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|(s, e)| (*s as usize, *e as usize))
|
||||||
|
{
|
||||||
|
spans.push(Span::styled(
|
||||||
|
slice_at_char_boundaries(&value, last_match_end, start)
|
||||||
|
.to_string(),
|
||||||
|
Style::default().fg(colorscheme.result_preview_fg),
|
||||||
|
));
|
||||||
|
spans.push(Span::styled(
|
||||||
|
slice_at_char_boundaries(&value, start, end).to_string(),
|
||||||
|
Style::default().fg(colorscheme.match_foreground_color),
|
||||||
|
));
|
||||||
|
last_match_end = end;
|
||||||
|
}
|
||||||
|
let next_boundary = next_char_boundary(&value, last_match_end);
|
||||||
|
if next_boundary < value.len() {
|
||||||
|
spans.push(Span::styled(
|
||||||
|
value[next_boundary..].to_string(),
|
||||||
|
Style::default().fg(colorscheme.result_preview_fg),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Line::from(spans)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn build_results_list<'a, 'b>(
|
pub fn build_results_list<'a, 'b>(
|
||||||
results_block: Block<'b>,
|
results_block: Block<'b>,
|
||||||
entries: &'a [Entry],
|
entries: &'a [Entry],
|
||||||
@ -27,110 +201,19 @@ pub fn build_results_list<'a, 'b>(
|
|||||||
list_direction: ListDirection,
|
list_direction: ListDirection,
|
||||||
use_icons: bool,
|
use_icons: bool,
|
||||||
colorscheme: &ResultsColorscheme,
|
colorscheme: &ResultsColorscheme,
|
||||||
|
area_width: u16,
|
||||||
) -> List<'a>
|
) -> List<'a>
|
||||||
where
|
where
|
||||||
'b: 'a,
|
'b: 'a,
|
||||||
{
|
{
|
||||||
List::new(entries.iter().map(|entry| {
|
List::new(entries.iter().map(|entry| {
|
||||||
let mut spans = Vec::new();
|
build_result_line(
|
||||||
// optional selection symbol
|
entry,
|
||||||
if let Some(selected_entries) = selected_entries {
|
selected_entries,
|
||||||
if !selected_entries.is_empty() {
|
use_icons,
|
||||||
spans.push(if selected_entries.contains(entry) {
|
colorscheme,
|
||||||
Span::styled(
|
area_width,
|
||||||
SELECTED_SYMBOL,
|
)
|
||||||
Style::default().fg(colorscheme.result_selected_fg),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Span::from(DESELECTED_SYMBOL)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// optional icon
|
|
||||||
if let Some(icon) = entry.icon.as_ref() {
|
|
||||||
if use_icons {
|
|
||||||
spans.push(Span::styled(
|
|
||||||
icon.to_string(),
|
|
||||||
Style::default().fg(Color::from_str(icon.color).unwrap()),
|
|
||||||
));
|
|
||||||
|
|
||||||
spans.push(Span::raw(" "));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// entry name
|
|
||||||
let (entry_name, name_match_ranges) = make_matched_string_printable(
|
|
||||||
&entry.name,
|
|
||||||
entry.name_match_ranges.as_deref(),
|
|
||||||
);
|
|
||||||
let mut last_match_end = 0;
|
|
||||||
for (start, end) in name_match_ranges
|
|
||||||
.iter()
|
|
||||||
.map(|(s, e)| (*s as usize, *e as usize))
|
|
||||||
{
|
|
||||||
// from the end of the last match to the start of the current one
|
|
||||||
spans.push(Span::styled(
|
|
||||||
slice_at_char_boundaries(&entry_name, last_match_end, start)
|
|
||||||
.to_string(),
|
|
||||||
Style::default().fg(colorscheme.result_name_fg),
|
|
||||||
));
|
|
||||||
// the current match
|
|
||||||
spans.push(Span::styled(
|
|
||||||
slice_at_char_boundaries(&entry_name, start, end).to_string(),
|
|
||||||
Style::default().fg(colorscheme.match_foreground_color),
|
|
||||||
));
|
|
||||||
last_match_end = end;
|
|
||||||
}
|
|
||||||
// we need to push a span for the remainder of the entry name
|
|
||||||
// but only if there's something left
|
|
||||||
let next_boundary = next_char_boundary(&entry_name, last_match_end);
|
|
||||||
if next_boundary < entry_name.len() {
|
|
||||||
let remainder = entry_name[next_boundary..].to_string();
|
|
||||||
spans.push(Span::styled(
|
|
||||||
remainder,
|
|
||||||
Style::default().fg(colorscheme.result_name_fg),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// optional line number
|
|
||||||
if let Some(line_number) = entry.line_number {
|
|
||||||
spans.push(Span::styled(
|
|
||||||
format!(":{line_number}"),
|
|
||||||
Style::default().fg(colorscheme.result_line_number_fg),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
// optional preview
|
|
||||||
if let Some(preview) = &entry.value {
|
|
||||||
spans.push(Span::raw(": "));
|
|
||||||
|
|
||||||
let (preview, preview_match_ranges) =
|
|
||||||
make_matched_string_printable(
|
|
||||||
preview,
|
|
||||||
entry.value_match_ranges.as_deref(),
|
|
||||||
);
|
|
||||||
let mut last_match_end = 0;
|
|
||||||
for (start, end) in preview_match_ranges
|
|
||||||
.iter()
|
|
||||||
.map(|(s, e)| (*s as usize, *e as usize))
|
|
||||||
{
|
|
||||||
spans.push(Span::styled(
|
|
||||||
slice_at_char_boundaries(&preview, last_match_end, start)
|
|
||||||
.to_string(),
|
|
||||||
Style::default().fg(colorscheme.result_preview_fg),
|
|
||||||
));
|
|
||||||
spans.push(Span::styled(
|
|
||||||
slice_at_char_boundaries(&preview, start, end).to_string(),
|
|
||||||
Style::default().fg(colorscheme.match_foreground_color),
|
|
||||||
));
|
|
||||||
last_match_end = end;
|
|
||||||
}
|
|
||||||
let next_boundary = next_char_boundary(&preview, last_match_end);
|
|
||||||
if next_boundary < preview.len() {
|
|
||||||
spans.push(Span::styled(
|
|
||||||
preview[next_boundary..].to_string(),
|
|
||||||
Style::default().fg(colorscheme.result_preview_fg),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Line::from(spans)
|
|
||||||
}))
|
}))
|
||||||
.direction(list_direction)
|
.direction(list_direction)
|
||||||
.highlight_style(
|
.highlight_style(
|
||||||
@ -154,15 +237,9 @@ pub fn draw_results_list(
|
|||||||
preview_keybinding: &str,
|
preview_keybinding: &str,
|
||||||
preview_togglable: bool,
|
preview_togglable: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut toggle_hints = format!(
|
let mut toggle_hints = format!(" help: <{help_keybinding}> ",);
|
||||||
" help: <{help_keybinding}> ",
|
|
||||||
help_keybinding = help_keybinding,
|
|
||||||
);
|
|
||||||
if preview_togglable {
|
if preview_togglable {
|
||||||
toggle_hints.push_str(&format!(
|
toggle_hints.push_str(&format!(" preview: <{preview_keybinding}> ",));
|
||||||
" preview: <{preview_keybinding}> ",
|
|
||||||
preview_keybinding = preview_keybinding,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let results_block = Block::default()
|
let results_block = Block::default()
|
||||||
@ -187,6 +264,7 @@ pub fn draw_results_list(
|
|||||||
},
|
},
|
||||||
use_nerd_font_icons,
|
use_nerd_font_icons,
|
||||||
&colorscheme.results,
|
&colorscheme.results,
|
||||||
|
rect.width - 1, // right padding
|
||||||
);
|
);
|
||||||
|
|
||||||
f.render_stateful_widget(results_list, rect, relative_picker_state);
|
f.render_stateful_widget(results_list, rect, relative_picker_state);
|
||||||
|
@ -334,7 +334,10 @@ impl Television {
|
|||||||
/ 2)
|
/ 2)
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
.try_into()?,
|
.try_into()
|
||||||
|
// if the scroll doesn't fit in a u16, just scroll to the top
|
||||||
|
// this is a current limitation of ratatui
|
||||||
|
.unwrap_or(0),
|
||||||
selected_entry
|
selected_entry
|
||||||
.line_number
|
.line_number
|
||||||
.and_then(|l| l.try_into().ok()),
|
.and_then(|l| l.try_into().ok()),
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use super::strings::prev_char_boundary;
|
||||||
|
|
||||||
pub fn sep_name_and_value_indices(
|
pub fn sep_name_and_value_indices(
|
||||||
indices: &mut Vec<u32>,
|
indices: &mut Vec<u32>,
|
||||||
name_len: u32,
|
name_len: u32,
|
||||||
@ -29,3 +31,234 @@ pub fn sep_name_and_value_indices(
|
|||||||
should_add_value_indices,
|
should_add_value_indices,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ELLIPSIS: &str = "…";
|
||||||
|
const ELLIPSIS_CHAR_WIDTH_U16: u16 = 1;
|
||||||
|
const ELLIPSIS_BYTE_LEN_U32: u32 = 3;
|
||||||
|
|
||||||
|
/// Truncate a string to fit within a certain width, while keeping track of the
|
||||||
|
/// indices of the highlighted characters.
|
||||||
|
///
|
||||||
|
/// This will either truncate from the start or the end of the string, depending
|
||||||
|
/// on where the highlighted characters are.
|
||||||
|
///
|
||||||
|
/// NOTE: This function assumes that the highlighted ranges are sorted and non-overlapping.
|
||||||
|
pub fn truncate_highlighted_string<'a>(
|
||||||
|
s: &'a str,
|
||||||
|
highlighted_ranges: &'a [(u32, u32)],
|
||||||
|
max_width: u16,
|
||||||
|
) -> (String, Vec<(u32, u32)>) {
|
||||||
|
let (byte_positions, chars) =
|
||||||
|
s.char_indices().unzip::<_, _, Vec<_>, Vec<_>>();
|
||||||
|
|
||||||
|
if chars.len() <= max_width as usize {
|
||||||
|
return (s.to_string(), highlighted_ranges.to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_byte_index = byte_positions[usize::from(max_width)];
|
||||||
|
|
||||||
|
let last_highlighted_byte_index =
|
||||||
|
highlighted_ranges.last().unwrap_or(&(0, 0)).1 as usize;
|
||||||
|
|
||||||
|
// if the string isn't highlighted, or all highlighted characters are within the max byte index,
|
||||||
|
// simply truncate it from the right and add an ellipsis
|
||||||
|
if highlighted_ranges.is_empty()
|
||||||
|
// is the last highlighted byte index within the max byte index?
|
||||||
|
|| last_highlighted_byte_index < max_byte_index - 1
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
s.chars()
|
||||||
|
.take(
|
||||||
|
max_width.saturating_sub(ELLIPSIS_CHAR_WIDTH_U16) as usize
|
||||||
|
)
|
||||||
|
.collect::<String>()
|
||||||
|
+ ELLIPSIS,
|
||||||
|
highlighted_ranges.to_vec(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, if the last highlighted byte index is within the last "max width" bytes of the
|
||||||
|
// string, truncate it from the left and add an ellipsis at the beginning
|
||||||
|
let start_offset = (chars.len() + 1).saturating_sub(max_width as usize);
|
||||||
|
let byte_offset = byte_positions[start_offset];
|
||||||
|
if last_highlighted_byte_index > byte_offset {
|
||||||
|
return (
|
||||||
|
ELLIPSIS.to_string()
|
||||||
|
+ &s.chars().skip(start_offset).collect::<String>(),
|
||||||
|
highlighted_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|(start, end)| {
|
||||||
|
(
|
||||||
|
start.saturating_sub(
|
||||||
|
u32::try_from(byte_offset).unwrap(),
|
||||||
|
) + ELLIPSIS_BYTE_LEN_U32,
|
||||||
|
end.saturating_sub(
|
||||||
|
u32::try_from(byte_offset).unwrap(),
|
||||||
|
) + ELLIPSIS_BYTE_LEN_U32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|(start, end)| start != end)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, try to put the last highlighted character towards the end of the truncated string and
|
||||||
|
// truncate from both sides to fit the max width
|
||||||
|
let byte_offset =
|
||||||
|
// note that we're using `max_width` here as a rough estimate to avoid more complex calculations
|
||||||
|
// and then finding the closest character boundary
|
||||||
|
prev_char_boundary(s, last_highlighted_byte_index.saturating_sub(max_width.saturating_sub(2) as usize));
|
||||||
|
|
||||||
|
(
|
||||||
|
ELLIPSIS.to_string()
|
||||||
|
+ &s[byte_offset..]
|
||||||
|
.chars()
|
||||||
|
.take(max_width.saturating_sub(2 * ELLIPSIS_CHAR_WIDTH_U16)
|
||||||
|
as usize)
|
||||||
|
.collect::<String>()
|
||||||
|
+ ELLIPSIS,
|
||||||
|
highlighted_ranges
|
||||||
|
.iter()
|
||||||
|
.map(|(start, end)| {
|
||||||
|
(
|
||||||
|
start.saturating_sub(u32::try_from(byte_offset).unwrap())
|
||||||
|
+ ELLIPSIS_BYTE_LEN_U32,
|
||||||
|
end.saturating_sub(u32::try_from(byte_offset).unwrap())
|
||||||
|
+ ELLIPSIS_BYTE_LEN_U32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.filter(|(start, end)| start != end)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
/// string: themes/solarized-light.toml
|
||||||
|
/// highlights: ----
|
||||||
|
/// max width: ---------------------------
|
||||||
|
/// result: themes/solarized-light.toml
|
||||||
|
/// expected: ----
|
||||||
|
fn test_truncate_hightlighted_string_no_op() {
|
||||||
|
let s = "themes/solarized-light.toml";
|
||||||
|
let highlighted_ranges = vec![(23, 27)];
|
||||||
|
let max_width = 27;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
assert_eq!(truncated, s);
|
||||||
|
assert_eq!(ranges, highlighted_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// string: hello world
|
||||||
|
/// highlights:
|
||||||
|
/// max width: -----
|
||||||
|
/// result: hell…
|
||||||
|
fn test_truncate_hightlighted_string_no_highlight() {
|
||||||
|
let s = "hello world";
|
||||||
|
let highlighted_ranges = vec![];
|
||||||
|
let max_width = 5;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
assert_eq!(truncated, "hell…");
|
||||||
|
assert_eq!(ranges, highlighted_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// string: hello world
|
||||||
|
/// highlights: -----
|
||||||
|
/// max width: ----------
|
||||||
|
/// result: hello wor…
|
||||||
|
fn test_truncate_hightlighted_string_highlights_fit_left() {
|
||||||
|
let s = "hello world";
|
||||||
|
let highlighted_ranges = vec![(0, 5)];
|
||||||
|
let max_width = 10;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
assert_eq!(truncated, "hello wor…");
|
||||||
|
assert_eq!(ranges, highlighted_ranges);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// string: hello world
|
||||||
|
/// highlights: -- ---- -
|
||||||
|
/// max width: ------
|
||||||
|
/// ------
|
||||||
|
/// result: …world
|
||||||
|
fn test_truncate_highlighted_string_highlights_right() {
|
||||||
|
let s = "hello world";
|
||||||
|
let highlighted_ranges = vec![(0, 2), (4, 8), (10, 11)];
|
||||||
|
let max_width = 6;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(truncated, "…world");
|
||||||
|
// the ellipsis is 3 bytes long
|
||||||
|
assert_eq!(ranges, vec![(3, 5), (7, 8)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// string: themes/solarized-light.toml
|
||||||
|
/// highlights: ----
|
||||||
|
/// max width: --------------------------
|
||||||
|
/// result: …emes/solarized-light.toml
|
||||||
|
/// expected: ----
|
||||||
|
fn test_truncate_highlighted_string_truncate_left() {
|
||||||
|
let s = "themes/solarized-light.toml";
|
||||||
|
let highlighted_ranges = vec![(23, 27)];
|
||||||
|
let max_width = 26;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(truncated, "…emes/solarized-light.toml");
|
||||||
|
assert_eq!(ranges, vec![(24, 28)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ellipsis_len() {
|
||||||
|
assert_eq!(
|
||||||
|
super::ELLIPSIS.chars().count(),
|
||||||
|
super::ELLIPSIS_CHAR_WIDTH_U16 as usize
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
super::ELLIPSIS.len(),
|
||||||
|
super::ELLIPSIS_BYTE_LEN_U32 as usize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// string: themes/solarized-light.toml
|
||||||
|
/// highlights: ---
|
||||||
|
/// max width: ------- 7
|
||||||
|
/// result: …lariz…
|
||||||
|
/// expected: ---
|
||||||
|
fn test_truncate_highlighted_string_truncate_middle() {
|
||||||
|
let s = "themes/solarized-light.toml";
|
||||||
|
let highlighted_ranges = vec![(11, 14)];
|
||||||
|
let max_width = 7;
|
||||||
|
let (truncated, ranges) = super::truncate_highlighted_string(
|
||||||
|
s,
|
||||||
|
&highlighted_ranges,
|
||||||
|
max_width,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(truncated, "…lariz…");
|
||||||
|
assert_eq!(ranges, vec![(5, 8)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user