fix(results): fix alignment for non unit width unicode characters (#442)

Fixes #439
This commit is contained in:
Alexandre Pasmantier 2025-04-01 01:30:18 +02:00 committed by GitHub
parent f9a49acccf
commit 6ba235fa11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 375 additions and 350 deletions

View File

@ -40,7 +40,7 @@ fn draw(c: &mut Criterion) {
let _ = tv.update_preview_state( let _ = tv.update_preview_state(
&tv.get_selected_entry(None).unwrap(), &tv.get_selected_entry(None).unwrap(),
); );
tv.update(&Action::Tick).unwrap(); let _ = tv.update(&Action::Tick);
(tv, terminal) (tv, terminal)
}, },
// Measurement // Measurement

View File

@ -4,7 +4,7 @@ use ratatui::layout::Alignment;
use ratatui::prelude::{Line, Style}; use ratatui::prelude::{Line, Style};
use ratatui::style::Color; use ratatui::style::Color;
use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding}; use ratatui::widgets::{Block, BorderType, Borders, ListDirection, Padding};
use television::channels::entry::merge_ranges; use television::channels::entry::into_ranges;
use television::channels::entry::{Entry, PreviewType}; use television::channels::entry::{Entry, PreviewType};
use television::screen::colors::ResultsColorscheme; use television::screen::colors::ResultsColorscheme;
use television::screen::results::build_results_list; use television::screen::results::build_results_list;
@ -17,12 +17,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/LICENSE".to_string(), name: "typeshed/LICENSE".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{f016}', icon: '\u{f016}',
@ -34,12 +29,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/README.md".to_string(), name: "typeshed/README.md".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{f48a}', icon: '\u{f48a}',
@ -51,12 +41,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/re.pyi".to_string(), name: "typeshed/stdlib/re.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -68,12 +53,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/io.pyi".to_string(), name: "typeshed/stdlib/io.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -85,12 +65,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/gc.pyi".to_string(), name: "typeshed/stdlib/gc.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -102,12 +77,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/uu.pyi".to_string(), name: "typeshed/stdlib/uu.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -119,12 +89,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/nt.pyi".to_string(), name: "typeshed/stdlib/nt.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -136,12 +101,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/dis.pyi".to_string(), name: "typeshed/stdlib/dis.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -153,12 +113,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/imp.pyi".to_string(), name: "typeshed/stdlib/imp.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -170,12 +125,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/bdb.pyi".to_string(), name: "typeshed/stdlib/bdb.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -187,12 +137,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/abc.pyi".to_string(), name: "typeshed/stdlib/abc.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -204,12 +149,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/cgi.pyi".to_string(), name: "typeshed/stdlib/cgi.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -221,12 +161,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/bz2.pyi".to_string(), name: "typeshed/stdlib/bz2.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -238,12 +173,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/grp.pyi".to_string(), name: "typeshed/stdlib/grp.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -255,12 +185,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/ast.pyi".to_string(), name: "typeshed/stdlib/ast.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -272,12 +197,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/csv.pyi".to_string(), name: "typeshed/stdlib/csv.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -289,12 +209,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/pdb.pyi".to_string(), name: "typeshed/stdlib/pdb.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -306,12 +221,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/pwd.pyi".to_string(), name: "typeshed/stdlib/pwd.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -323,12 +233,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/ssl.pyi".to_string(), name: "typeshed/stdlib/ssl.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -340,12 +245,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/tty.pyi".to_string(), name: "typeshed/stdlib/tty.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -357,12 +257,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/nis.pyi".to_string(), name: "typeshed/stdlib/nis.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -374,12 +269,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/pty.pyi".to_string(), name: "typeshed/stdlib/pty.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -391,12 +281,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/cmd.pyi".to_string(), name: "typeshed/stdlib/cmd.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -408,12 +293,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/tests/utils.py".to_string(), name: "typeshed/tests/utils.py".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -425,12 +305,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/pyproject.toml".to_string(), name: "typeshed/pyproject.toml".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e6b2}', icon: '\u{e6b2}',
@ -442,12 +317,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/MAINTAINERS.md".to_string(), name: "typeshed/MAINTAINERS.md".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{f48a}', icon: '\u{f48a}',
@ -459,12 +329,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/enum.pyi".to_string(), name: "typeshed/stdlib/enum.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -476,12 +341,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/hmac.pyi".to_string(), name: "typeshed/stdlib/hmac.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -493,12 +353,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/uuid.pyi".to_string(), name: "typeshed/stdlib/uuid.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -510,12 +365,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/glob.pyi".to_string(), name: "typeshed/stdlib/glob.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -527,12 +377,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/_ast.pyi".to_string(), name: "typeshed/stdlib/_ast.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -544,12 +389,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/_csv.pyi".to_string(), name: "typeshed/stdlib/_csv.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -561,12 +401,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/code.pyi".to_string(), name: "typeshed/stdlib/code.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -578,12 +413,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/spwd.pyi".to_string(), name: "typeshed/stdlib/spwd.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -595,12 +425,7 @@ pub fn draw_results_list(c: &mut Criterion) {
Entry { Entry {
name: "typeshed/stdlib/_msi.pyi".to_string(), name: "typeshed/stdlib/_msi.pyi".to_string(),
value: None, value: None,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
icon: Some(FileIcon { icon: Some(FileIcon {
icon: '\u{e606}', icon: '\u{e606}',
@ -619,12 +444,7 @@ pub fn draw_results_list(c: &mut Criterion) {
}), }),
line_number: None, line_number: None,
preview_type: PreviewType::Files, preview_type: PreviewType::Files,
name_match_ranges: Some(merge_ranges(&[ name_match_ranges: Some(into_ranges(&[0, 1, 2, 3])),
(0, 1),
(1, 2),
(2, 3),
(3, 4),
])),
value_match_ranges: None, value_match_ranges: None,
}, },
]; ];

View File

@ -85,7 +85,7 @@ impl OnAir for Channel {
should_add_name_indices, should_add_name_indices,
should_add_value_indices, should_add_value_indices,
) = sep_name_and_value_indices( ) = sep_name_and_value_indices(
&mut item.match_indices.iter().map(|i| i.0).collect(), item.match_indices,
u32::try_from(item.inner.name.len()).unwrap(), u32::try_from(item.inner.name.len()).unwrap(),
); );
@ -95,17 +95,11 @@ impl OnAir for Channel {
.with_icon(self.file_icon); .with_icon(self.file_icon);
if should_add_name_indices { if should_add_name_indices {
let name_indices: Vec<(u32, u32)> = entry = entry.with_name_match_indices(&name_indices);
name_indices.into_iter().map(|i| (i, i + 1)).collect();
entry = entry.with_name_match_ranges(&name_indices);
} }
if should_add_value_indices { if should_add_value_indices {
let value_indices: Vec<(u32, u32)> = value_indices entry = entry.with_value_match_indices(&value_indices);
.into_iter()
.map(|i| (i, i + 1))
.collect();
entry = entry.with_value_match_ranges(&value_indices);
} }
entry entry

View File

@ -167,7 +167,7 @@ impl OnAir for Channel {
PreviewKind::None => PreviewType::None, PreviewKind::None => PreviewType::None,
}, },
) )
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
}) })
.collect() .collect()
} }

View File

@ -95,7 +95,7 @@ impl OnAir for Channel {
" ", " ",
)), )),
) )
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
.with_icon(FileIcon::from(&path)) .with_icon(FileIcon::from(&path))
}) })
.collect() .collect()

View File

@ -56,22 +56,30 @@ impl PartialEq<Entry> for Entry {
} }
#[allow(clippy::needless_return)] #[allow(clippy::needless_return)]
pub fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> { /// Convert a list of indices into a list of ranges, merging contiguous ranges.
ranges.iter().fold( ///
Vec::new(), /// # Example
|mut acc: Vec<(u32, u32)>, x: &(u32, u32)| { /// ```
/// use television::channels::entry::into_ranges;
/// let indices = vec![1, 2, 7, 8];
/// let ranges = into_ranges(&indices);
/// assert_eq!(ranges, vec![(1, 3), (7, 9)]);
/// ```
pub fn into_ranges(indices: &[u32]) -> Vec<(u32, u32)> {
indices
.iter()
.fold(Vec::new(), |mut acc: Vec<(u32, u32)>, x| {
if let Some(last) = acc.last_mut() { if let Some(last) = acc.last_mut() {
if last.1 == x.0 { if last.1 == *x {
last.1 = x.1; last.1 = *x + 1;
} else { } else {
acc.push(*x); acc.push((*x, *x + 1));
} }
} else { } else {
acc.push(*x); acc.push((*x, *x + 1));
} }
return acc; return acc;
}, })
)
} }
impl Entry { impl Entry {
@ -84,8 +92,8 @@ impl Entry {
/// ///
/// let entry = Entry::new("name".to_string(), PreviewType::EnvVar) /// let entry = Entry::new("name".to_string(), PreviewType::EnvVar)
/// .with_value("value".to_string()) /// .with_value("value".to_string())
/// .with_name_match_ranges(&vec![(0, 1)]) /// .with_name_match_indices(&vec![0])
/// .with_value_match_ranges(&vec![(0, 1)]) /// .with_value_match_indices(&vec![0])
/// .with_icon(FileIcon::default()) /// .with_icon(FileIcon::default())
/// .with_line_number(0); /// .with_line_number(0);
/// ``` /// ```
@ -114,19 +122,13 @@ impl Entry {
self self
} }
pub fn with_name_match_ranges( pub fn with_name_match_indices(mut self, indices: &[u32]) -> Self {
mut self, self.name_match_ranges = Some(into_ranges(indices));
name_match_ranges: &[(u32, u32)],
) -> Self {
self.name_match_ranges = Some(merge_ranges(name_match_ranges));
self self
} }
pub fn with_value_match_ranges( pub fn with_value_match_indices(mut self, indices: &[u32]) -> Self {
mut self, self.value_match_ranges = Some(into_ranges(indices));
value_match_ranges: &[(u32, u32)],
) -> Self {
self.value_match_ranges = Some(merge_ranges(value_match_ranges));
self self
} }
@ -198,26 +200,26 @@ mod tests {
#[test] #[test]
fn test_empty_input() { fn test_empty_input() {
let ranges: Vec<(u32, u32)> = vec![]; let ranges: Vec<u32> = vec![];
assert_eq!(merge_ranges(&ranges), Vec::<(u32, u32)>::new()); assert_eq!(into_ranges(&ranges), Vec::<(u32, u32)>::new());
} }
#[test] #[test]
fn test_single_range() { fn test_single_range() {
let ranges = vec![(1, 3)]; let ranges = vec![1, 2];
assert_eq!(merge_ranges(&ranges), vec![(1, 3)]); assert_eq!(into_ranges(&ranges), vec![(1, 3)]);
} }
#[test] #[test]
fn test_contiguous_ranges() { fn test_contiguous_ranges() {
let ranges = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; let ranges = vec![1, 2, 3, 4];
assert_eq!(merge_ranges(&ranges), vec![(1, 5)]); assert_eq!(into_ranges(&ranges), vec![(1, 5)]);
} }
#[test] #[test]
fn test_non_contiguous_ranges() { fn test_non_contiguous_ranges() {
let ranges = vec![(1, 2), (3, 4), (5, 6)]; let ranges = vec![1, 3, 5];
assert_eq!(merge_ranges(&ranges), vec![(1, 2), (3, 4), (5, 6)]); assert_eq!(into_ranges(&ranges), vec![(1, 2), (3, 4), (5, 6)]);
} }
#[test] #[test]

View File

@ -70,7 +70,7 @@ impl OnAir for Channel {
should_add_name_indices, should_add_name_indices,
should_add_value_indices, should_add_value_indices,
) = sep_name_and_value_indices( ) = sep_name_and_value_indices(
&mut item.match_indices.iter().map(|i| i.0).collect(), item.match_indices,
u32::try_from(item.inner.name.len()).unwrap(), u32::try_from(item.inner.name.len()).unwrap(),
); );
@ -80,17 +80,11 @@ impl OnAir for Channel {
.with_icon(self.file_icon); .with_icon(self.file_icon);
if should_add_name_indices { if should_add_name_indices {
let name_indices: Vec<(u32, u32)> = entry = entry.with_name_match_indices(&name_indices);
name_indices.into_iter().map(|i| (i, i + 1)).collect();
entry = entry.with_name_match_ranges(&name_indices);
} }
if should_add_value_indices { if should_add_value_indices {
let value_indices: Vec<(u32, u32)> = value_indices entry = entry.with_value_match_indices(&value_indices);
.into_iter()
.map(|i| (i, i + 1))
.collect();
entry = entry.with_value_match_ranges(&value_indices);
} }
entry entry

View File

@ -107,7 +107,7 @@ impl OnAir for Channel {
.map(|item| { .map(|item| {
let path = item.matched_string; let path = item.matched_string;
Entry::new(path.clone(), PreviewType::Files) Entry::new(path.clone(), PreviewType::Files)
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
.with_icon(FileIcon::from(&path)) .with_icon(FileIcon::from(&path))
}) })
.collect() .collect()

View File

@ -68,7 +68,7 @@ impl OnAir for Channel {
path, path,
PreviewType::Command(self.preview_command.clone()), PreviewType::Command(self.preview_command.clone()),
) )
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
.with_icon(self.icon) .with_icon(self.icon)
}) })
.collect() .collect()

View File

@ -145,7 +145,7 @@ impl OnAir for RemoteControl {
.map(|item| { .map(|item| {
let path = item.matched_string; let path = item.matched_string;
Entry::new(path, PreviewType::Basic) Entry::new(path, PreviewType::Basic)
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
.with_icon(match item.inner { .with_icon(match item.inner {
RCButton::Channel(_) => TV_ICON, RCButton::Channel(_) => TV_ICON,
RCButton::CableChannel(_) => CABLE_ICON, RCButton::CableChannel(_) => CABLE_ICON,

View File

@ -84,7 +84,7 @@ impl OnAir for Channel {
// NOTE: we're passing `PreviewType::Basic` here just as a placeholder // NOTE: we're passing `PreviewType::Basic` here just as a placeholder
// to avoid storing the preview command multiple times for each item. // to avoid storing the preview command multiple times for each item.
Entry::new(item.matched_string, PreviewType::Basic) Entry::new(item.matched_string, PreviewType::Basic)
.with_name_match_ranges(&item.match_indices) .with_name_match_indices(&item.match_indices)
}) })
.collect() .collect()
} }

View File

@ -202,7 +202,7 @@ impl OnAir for Channel {
item.inner.path.to_string_lossy().to_string(); item.inner.path.to_string_lossy().to_string();
Entry::new(display_path, PreviewType::Files) Entry::new(display_path, PreviewType::Files)
.with_value(line) .with_value(line)
.with_value_match_ranges(&item.match_indices) .with_value_match_indices(&item.match_indices)
.with_icon(FileIcon::from(item.inner.path.as_path())) .with_icon(FileIcon::from(item.inner.path.as_path()))
.with_line_number(item.inner.line_number) .with_line_number(item.inner.line_number)
}) })

View File

@ -16,5 +16,5 @@ where
/// The dimension against which the item was matched (as a string). /// The dimension against which the item was matched (as a string).
pub matched_string: String, pub matched_string: String,
/// The indices of the matched characters. /// The indices of the matched characters.
pub match_indices: Vec<(u32, u32)>, pub match_indices: Vec<u32>,
} }

View File

@ -181,7 +181,7 @@ where
matched_item::MatchedItem { matched_item::MatchedItem {
inner: item.data.clone(), inner: item.data.clone(),
matched_string, matched_string,
match_indices: indices.map(|i| (i, i + 1)).collect(), match_indices: indices.collect(),
} }
}) })
.collect() .collect()

View File

@ -22,7 +22,7 @@ pub struct HelpColorscheme {
pub metadata_field_value_fg: Color, pub metadata_field_value_fg: Color,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct ResultsColorscheme { pub struct ResultsColorscheme {
pub result_name_fg: Color, pub result_name_fg: Color,
pub result_preview_fg: Color, pub result_preview_fg: Color,

View File

@ -2,10 +2,7 @@ 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::indices::truncate_highlighted_string;
use crate::utils::strings::{ use crate::utils::strings::make_matched_string_printable;
make_matched_string_printable, next_char_boundary,
slice_at_char_boundaries,
};
use anyhow::Result; use anyhow::Result;
use ratatui::layout::{Alignment, Rect}; use ratatui::layout::{Alignment, Rect};
use ratatui::prelude::{Color, Line, Span, Style}; use ratatui::prelude::{Color, Line, Span, Style};
@ -16,15 +13,13 @@ use ratatui::widgets::{
use ratatui::Frame; use ratatui::Frame;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use std::str::FromStr; use std::str::FromStr;
use unicode_width::UnicodeWidthStr;
const POINTER_SYMBOL: &str = "> "; 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. /// 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( fn max_widths(
entry: &Entry, entry: &Entry,
available_width: u16, available_width: u16,
@ -35,6 +30,7 @@ fn max_widths(
2 // pointer and space 2 // pointer and space
+ 2 * (u16::from(use_icons)) + 2 * (u16::from(use_icons))
+ 2 * (u16::from(is_selected)) + 2 * (u16::from(is_selected))
+ 2 // borders
+ entry + entry
.line_number .line_number
// ":{line_number}: " // ":{line_number}: "
@ -54,14 +50,17 @@ fn max_widths(
.map_or(0, |v| u16::try_from(v.chars().count()).unwrap_or(u16::MAX)); .map_or(0, |v| u16::try_from(v.chars().count()).unwrap_or(u16::MAX));
if name_len < available_width / 2 { if name_len < available_width / 2 {
(name_len, available_width - name_len - 2) (name_len, available_width - name_len)
} else if value_len < available_width / 2 { } else if value_len < available_width / 2 {
(available_width - value_len, value_len - 2) (available_width - value_len, value_len)
} else { } else {
(available_width / 2, available_width / 2 - 2) (available_width / 2, available_width / 2)
} }
} }
// TODO: could we not just iterate on chars here instead of using the indices?
// that would avoid quite some computation during the rendering and might fix multibyte char
// issues (nucleo's indices are actually char-based)
fn build_result_line<'a>( fn build_result_line<'a>(
entry: &'a Entry, entry: &'a Entry,
selected_entries: Option<&FxHashSet<Entry>>, selected_entries: Option<&FxHashSet<Entry>>,
@ -101,42 +100,52 @@ fn build_result_line<'a>(
} }
} }
// entry name // entry name
let (mut entry_name, mut value_match_ranges) = let (mut entry_name, mut name_match_ranges) =
make_matched_string_printable( make_matched_string_printable(
&entry.name, &entry.name,
entry.name_match_ranges.as_deref(), entry.name_match_ranges.as_deref(),
); );
// if the name is too long, we need to truncate it and add an ellipsis // if the name is too long, we need to truncate it and add an ellipsis
if entry_name.len() > name_max_width as usize { if entry_name.as_str().width() > name_max_width as usize {
(entry_name, value_match_ranges) = truncate_highlighted_string( (entry_name, name_match_ranges) = truncate_highlighted_string(
&entry_name, &entry_name,
&value_match_ranges, &name_match_ranges,
name_max_width, name_max_width,
); );
} }
let mut last_match_end = 0; let mut last_match_end = 0;
for (start, end) in value_match_ranges let name_chars = entry_name.chars();
let name_len = entry_name.as_str().width();
for (start, end) in name_match_ranges
.iter() .iter()
.map(|(s, e)| (*s as usize, *e as usize)) .map(|(s, e)| (*s as usize, *e as usize))
{ {
// from the end of the last match to the start of the current one // from the end of the last match to the start of the current one
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(&entry_name, last_match_end, start) name_chars
.to_string(), .clone()
.skip(last_match_end)
.take(start - last_match_end)
.collect::<String>(),
//entry_name[last_match_end..start].to_string(),
Style::default().fg(colorscheme.result_name_fg), Style::default().fg(colorscheme.result_name_fg),
)); ));
// the current match // the current match
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(&entry_name, start, end).to_string(), name_chars
.clone()
.skip(start)
.take(end - start)
.collect::<String>(),
Style::default().fg(colorscheme.match_foreground_color), Style::default().fg(colorscheme.match_foreground_color),
)); ));
last_match_end = end; last_match_end = end;
} }
// we need to push a span for the remainder of the entry name // we need to push a span for the remainder of the entry name
// but only if there's something left // but only if there's something left
let next_boundary = next_char_boundary(&entry_name, last_match_end); if last_match_end < name_len {
if next_boundary < entry_name.len() { let remainder = name_chars.skip(last_match_end).collect::<String>();
let remainder = entry_name[next_boundary..].to_string();
spans.push(Span::styled( spans.push(Span::styled(
remainder, remainder,
Style::default().fg(colorscheme.result_name_fg), Style::default().fg(colorscheme.result_name_fg),
@ -159,7 +168,7 @@ fn build_result_line<'a>(
entry.value_match_ranges.as_deref(), entry.value_match_ranges.as_deref(),
); );
// if the value is too long, we need to truncate it and add an ellipsis // if the value is too long, we need to truncate it and add an ellipsis
if value.len() > value_max_width as usize { if value.as_str().width() > value_max_width as usize {
(value, value_match_ranges) = truncate_highlighted_string( (value, value_match_ranges) = truncate_highlighted_string(
&value, &value,
&value_match_ranges, &value_match_ranges,
@ -168,25 +177,33 @@ fn build_result_line<'a>(
} }
let mut last_match_end = 0; let mut last_match_end = 0;
let value_chars = value.chars();
let value_len = value.chars().count();
for (start, end) in value_match_ranges for (start, end) in value_match_ranges
.iter() .iter()
.map(|(s, e)| (*s as usize, *e as usize)) .map(|(s, e)| (*s as usize, *e as usize))
{ {
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(&value, last_match_end, start) value_chars
.to_string(), .clone()
.skip(last_match_end)
.take(start - last_match_end)
.collect::<String>(),
Style::default().fg(colorscheme.result_preview_fg), Style::default().fg(colorscheme.result_preview_fg),
)); ));
spans.push(Span::styled( spans.push(Span::styled(
slice_at_char_boundaries(&value, start, end).to_string(), value_chars
.clone()
.skip(start)
.take(end - start)
.collect::<String>(),
Style::default().fg(colorscheme.match_foreground_color), Style::default().fg(colorscheme.match_foreground_color),
)); ));
last_match_end = end; last_match_end = end;
} }
let next_boundary = next_char_boundary(&value, last_match_end); if last_match_end < value_len {
if next_boundary < value.len() {
spans.push(Span::styled( spans.push(Span::styled(
value[next_boundary..].to_string(), value_chars.skip(last_match_end).collect::<String>(),
Style::default().fg(colorscheme.result_preview_fg), Style::default().fg(colorscheme.result_preview_fg),
)); ));
} }
@ -270,3 +287,63 @@ pub fn draw_results_list(
f.render_stateful_widget(results_list, rect, relative_picker_state); f.render_stateful_widget(results_list, rect, relative_picker_state);
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use crate::channels::entry::PreviewType;
use super::*;
#[test]
fn test_build_result_line() {
let entry =
Entry::new(String::from("something nice"), PreviewType::None)
.with_name_match_indices(
// something nice
// 012345678901234
// om ni
&[1, 2, 10, 11],
);
let result_line = build_result_line(
&entry,
None,
false,
&ResultsColorscheme::default(),
200,
);
let expected_line = Line::from(vec![
Span::raw("s").fg(Color::Reset),
Span::raw("om").fg(Color::Reset),
Span::raw("ething ").fg(Color::Reset),
Span::raw("ni").fg(Color::Reset),
Span::raw("ce").fg(Color::Reset),
]);
assert_eq!(result_line, expected_line);
}
#[test]
fn test_build_result_line_multibyte_chars() {
let entry =
// See https://github.com/alexpasmantier/television/issues/439
Entry::new(String::from("ジェイムス下地 - REDLINE Original Soundtrack - 06 - ROBOWORLD TV.mp3"), PreviewType::None)
.with_name_match_indices(&[27, 28, 29, 30, 31]);
let result_line = build_result_line(
&entry,
None,
false,
&ResultsColorscheme::default(),
// 16 + (borders + (pointer & space))
16 + 2 + 2,
);
let expected_line = Line::from(vec![
Span::raw("…Original ").fg(Color::Reset),
Span::raw("Sound").fg(Color::Reset),
Span::raw("").fg(Color::Reset),
]);
assert_eq!(result_line, expected_line);
}
}

View File

@ -1,7 +1,7 @@
use super::strings::prev_char_boundary; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
pub fn sep_name_and_value_indices( pub fn sep_name_and_value_indices(
indices: &mut Vec<u32>, indices: Vec<u32>,
name_len: u32, name_len: u32,
) -> (Vec<u32>, Vec<u32>, bool, bool) { ) -> (Vec<u32>, Vec<u32>, bool, bool) {
let mut name_indices = Vec::new(); let mut name_indices = Vec::new();
@ -9,7 +9,7 @@ pub fn sep_name_and_value_indices(
let mut should_add_name_indices = false; let mut should_add_name_indices = false;
let mut should_add_value_indices = false; let mut should_add_value_indices = false;
for i in indices.drain(..) { for i in indices {
if i < name_len { if i < name_len {
name_indices.push(i); name_indices.push(i);
should_add_name_indices = true; should_add_name_indices = true;
@ -34,7 +34,8 @@ pub fn sep_name_and_value_indices(
const ELLIPSIS: &str = ""; const ELLIPSIS: &str = "";
const ELLIPSIS_CHAR_WIDTH_U16: u16 = 1; const ELLIPSIS_CHAR_WIDTH_U16: u16 = 1;
const ELLIPSIS_BYTE_LEN_U32: u32 = 3; const ELLIPSIS_CHAR_WIDTH_U32: u32 = 1;
const ELLIPSIS_CHAR_WIDTH_USIZE: usize = 1;
/// Truncate a string to fit within a certain width, while keeping track of the /// Truncate a string to fit within a certain width, while keeping track of the
/// indices of the highlighted characters. /// indices of the highlighted characters.
@ -42,59 +43,116 @@ const ELLIPSIS_BYTE_LEN_U32: u32 = 3;
/// This will either truncate from the start or the end of the string, depending /// This will either truncate from the start or the end of the string, depending
/// on where the highlighted characters are. /// on where the highlighted characters are.
/// ///
/// NOTE: This function assumes that the highlighted ranges are sorted and non-overlapping. /// This will take care of non-unit width characters such as emojis, or certain
/// CJK characters that are wider than a single character.
///
/// # Note
/// This function assumes that the highlighted ranges are sorted and non-overlapping.
///
/// # Examples
/// ```
/// use television::utils::indices::truncate_highlighted_string;
///
/// let s = "hello world";
/// let highlighted_ranges = vec![(0, 2), (4, 8), (10, 11)];
/// let max_width = 6;
/// let (truncated, ranges) = truncate_highlighted_string(
/// s,
/// &highlighted_ranges,
/// max_width,
/// );
///
/// assert_eq!(truncated, "…world");
/// assert_eq!(ranges, vec![(1, 3), (5, 6)]);
///
/// let s = "下地.mp3";
/// let highlighted_ranges = vec![(3, 5)];
/// let max_width = 5;
/// let (truncated, ranges) = truncate_highlighted_string(
/// s,
/// &highlighted_ranges,
/// max_width,
/// );
/// assert_eq!(truncated, "….mp3");
/// assert_eq!(ranges, vec![(2, 4)]);
/// ```
///
/// See unit tests for more examples.
pub fn truncate_highlighted_string<'a>( pub fn truncate_highlighted_string<'a>(
s: &'a str, s: &'a str,
highlighted_ranges: &'a [(u32, u32)], highlighted_ranges: &'a [(u32, u32)],
max_width: u16, max_width: u16,
) -> (String, Vec<(u32, u32)>) { ) -> (String, Vec<(u32, u32)>) {
let (byte_positions, chars) = let str_width = s.width();
s.char_indices().unzip::<_, _, Vec<_>, Vec<_>>();
if chars.len() <= max_width as usize { if str_width <= max_width as usize {
return (s.to_string(), highlighted_ranges.to_vec()); return (s.to_string(), highlighted_ranges.to_vec());
} }
let max_byte_index = byte_positions[usize::from(max_width)]; let last_highlighted_char_index =
(highlighted_ranges.last().unwrap_or(&(0, 0)).1 as usize)
// ranges are exclusive on the right
.saturating_sub(1);
let width_to_last_highlighted_char = s
.chars()
.take(last_highlighted_char_index + 1)
.fold(0, |acc, c| acc + c.width().unwrap_or(0));
let last_highlighted_byte_index = // if the string isn't highlighted, or all highlighted characters are within the max 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 // simply truncate it from the right and add an ellipsis
if highlighted_ranges.is_empty() if highlighted_ranges.is_empty()
// is the last highlighted byte index within the max byte index? // is the last highlighted char index within the first "`max_width` of" characters?
|| last_highlighted_byte_index < max_byte_index - 1 || width_to_last_highlighted_char < max_width as usize
{ {
let mut cumulative_width = 0;
return ( return (
s.chars() s.chars()
.take( .take_while(|c| {
max_width.saturating_sub(ELLIPSIS_CHAR_WIDTH_U16) as usize cumulative_width += c.width().unwrap_or(0);
) cumulative_width
<= max_width.saturating_sub(ELLIPSIS_CHAR_WIDTH_U16)
as usize
})
.collect::<String>() .collect::<String>()
+ ELLIPSIS, + ELLIPSIS,
highlighted_ranges.to_vec(), highlighted_ranges.to_vec(),
); );
} }
// otherwise, if the last highlighted byte index is within the last "max width" bytes of the // otherwise, if the last highlighted char index is within the last "max width" chars of the
// string, truncate it from the left and add an ellipsis at the beginning // 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); // |<------- str_width ------->|
let byte_offset = byte_positions[start_offset]; // |<-- max_width -->|
if last_highlighted_byte_index > byte_offset { // |--------> start_width_offset - 1 (for the ellipsis)
let start_width_offset = str_width.saturating_sub(max_width as usize)
+ ELLIPSIS_CHAR_WIDTH_USIZE;
if width_to_last_highlighted_char > start_width_offset {
let mut truncated_width = str_width;
let chars_to_skip = s
.chars()
.take_while(|c| {
if truncated_width >= max_width as usize {
truncated_width -= c.width().unwrap_or(0);
true
} else {
false
}
})
.count();
let truncated_string =
s.chars().skip(chars_to_skip).collect::<String>();
return ( return (
ELLIPSIS.to_string() ELLIPSIS.to_string() + &truncated_string,
+ &s.chars().skip(start_offset).collect::<String>(),
highlighted_ranges highlighted_ranges
.iter() .iter()
.map(|(start, end)| { .map(|(start, end)| {
( (
start.saturating_sub( start.saturating_sub(
u32::try_from(byte_offset).unwrap(), u32::try_from(chars_to_skip).unwrap(),
) + ELLIPSIS_BYTE_LEN_U32, ) + ELLIPSIS_CHAR_WIDTH_U32,
end.saturating_sub( end.saturating_sub(
u32::try_from(byte_offset).unwrap(), u32::try_from(chars_to_skip).unwrap(),
) + ELLIPSIS_BYTE_LEN_U32, ) + ELLIPSIS_CHAR_WIDTH_U32,
) )
}) })
.filter(|(start, end)| start != end) .filter(|(start, end)| start != end)
@ -104,15 +162,33 @@ pub fn truncate_highlighted_string<'a>(
// otherwise, try to put the last highlighted character towards the end of the truncated string and // 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 // truncate from both sides to fit the max width
let byte_offset = let start_width_offset =
// note that we're using `max_width` here as a rough estimate to avoid more complex calculations // 0123456789012
// and then finding the closest character boundary // ^^ ^ highlights
prev_char_boundary(s, last_highlighted_byte_index.saturating_sub(max_width.saturating_sub(2) as usize)); // a long string
// -------x width to last highlighted char: 7
// max width = 4
// … s… truncated string
// <--> 4
width_to_last_highlighted_char.saturating_sub(max_width.saturating_sub(2*ELLIPSIS_CHAR_WIDTH_U16) as usize);
let mut cumulated_width = 0;
let chars_to_skip = s
.chars()
.take_while(|c| {
if cumulated_width < start_width_offset {
cumulated_width += c.width().unwrap_or(0);
true
} else {
false
}
})
.count();
( (
ELLIPSIS.to_string() ELLIPSIS.to_string()
+ &s[byte_offset..] + &s.chars()
.chars() .skip(chars_to_skip)
.take(max_width.saturating_sub(2 * ELLIPSIS_CHAR_WIDTH_U16) .take(max_width.saturating_sub(2 * ELLIPSIS_CHAR_WIDTH_U16)
as usize) as usize)
.collect::<String>() .collect::<String>()
@ -121,10 +197,11 @@ pub fn truncate_highlighted_string<'a>(
.iter() .iter()
.map(|(start, end)| { .map(|(start, end)| {
( (
start.saturating_sub(u32::try_from(byte_offset).unwrap()) start
+ ELLIPSIS_BYTE_LEN_U32, .saturating_sub(u32::try_from(chars_to_skip).unwrap())
end.saturating_sub(u32::try_from(byte_offset).unwrap()) + ELLIPSIS_CHAR_WIDTH_U32,
+ ELLIPSIS_BYTE_LEN_U32, end.saturating_sub(u32::try_from(chars_to_skip).unwrap())
+ ELLIPSIS_CHAR_WIDTH_U32,
) )
}) })
.filter(|(start, end)| start != end) .filter(|(start, end)| start != end)
@ -192,6 +269,7 @@ mod tests {
#[test] #[test]
/// string: hello world /// string: hello world
/// highlights: -- ---- - /// highlights: -- ---- -
/// he o wo d
/// max width: ------ /// max width: ------
/// ------ /// ------
/// result: …world /// result: …world
@ -206,8 +284,25 @@ mod tests {
); );
assert_eq!(truncated, "…world"); assert_eq!(truncated, "…world");
// the ellipsis is 3 bytes long assert_eq!(ranges, vec![(1, 3), (5, 6)]);
assert_eq!(ranges, vec![(3, 5), (7, 8)]); }
#[test]
/// string: 下地.mp3
/// highlights: ---
/// max width: -----
/// result: ….mp3
fn test_truncate_hightlighted_string_highlights_right_wide_chars() {
let s = "下地.mp3";
let highlighted_ranges = vec![(3, 5)];
let max_width = 5;
let (truncated, ranges) = super::truncate_highlighted_string(
s,
&highlighted_ranges,
max_width,
);
assert_eq!(truncated, "….mp3");
assert_eq!(ranges, vec![(2, 4)]);
} }
#[test] #[test]
@ -227,7 +322,7 @@ mod tests {
); );
assert_eq!(truncated, "…emes/solarized-light.toml"); assert_eq!(truncated, "…emes/solarized-light.toml");
assert_eq!(ranges, vec![(24, 28)]); assert_eq!(ranges, vec![(22, 26)]);
} }
#[test] #[test]
@ -236,10 +331,6 @@ mod tests {
super::ELLIPSIS.chars().count(), super::ELLIPSIS.chars().count(),
super::ELLIPSIS_CHAR_WIDTH_U16 as usize super::ELLIPSIS_CHAR_WIDTH_U16 as usize
); );
assert_eq!(
super::ELLIPSIS.len(),
super::ELLIPSIS_BYTE_LEN_U32 as usize
);
} }
#[test] #[test]
@ -259,6 +350,46 @@ mod tests {
); );
assert_eq!(truncated, "…lariz…"); assert_eq!(truncated, "…lariz…");
assert_eq!(ranges, vec![(5, 8)]); assert_eq!(ranges, vec![(3, 6)]);
}
#[test]
// 0123456789012
// ^^ ^ highlights
// a long string
// ---------x last highlighted char index: 9
// max width = 4
// … s… truncated string
// <--> 4
fn test_truncate_highlighted_string_truncate_both_ends() {
let s = "a long string";
let highlighted_ranges = vec![(3, 5), (7, 8)];
let max_width = 4;
let (truncated, ranges) = super::truncate_highlighted_string(
s,
&highlighted_ranges,
max_width,
);
assert_eq!(truncated, "… s…");
assert_eq!(ranges, vec![(2, 3)]);
}
#[test]
/// string: 下地下地abc下地下地
/// highlights: ---
/// max width: -----
/// result: …abc…
fn test_truncate_hightlighted_string_truncate_both_ends_wide_chars() {
let s = "下地下地abc下地下地";
let highlighted_ranges = vec![(4, 7)];
let max_width = 5;
let (truncated, ranges) = super::truncate_highlighted_string(
s,
&highlighted_ranges,
max_width,
);
assert_eq!(truncated, "…abc…");
assert_eq!(ranges, vec![(1, 4)]);
} }
} }

View File

@ -477,6 +477,13 @@ pub fn preprocess_line(line: &str) -> (String, Vec<i16>) {
/// let (printable, match_indices) = make_matched_string_printable(&matched_string, match_ranges); /// let (printable, match_indices) = make_matched_string_printable(&matched_string, match_ranges);
/// assert_eq!(printable.len(), 480); /// assert_eq!(printable.len(), 480);
/// assert_eq!(match_indices, vec![(0, 1)]); /// assert_eq!(match_indices, vec![(0, 1)]);
///
/// let matched_string = "ジェ abc";
/// let match_ranges = vec![(0, 1), (2, 3)];
/// let match_ranges = Some(match_ranges.as_slice());
/// let (printable, match_indices) = make_matched_string_printable(matched_string, match_ranges);
/// assert_eq!(printable, "ジェ abc");
/// assert_eq!(match_indices, vec![(0, 1), (2, 3)]);
/// ``` /// ```
/// ///
/// # Panics /// # Panics