From 047fea2d8de27001bc625178a47c5a8b4c4e07a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Wed, 7 Feb 2024 18:14:16 +0100 Subject: [PATCH 1/6] Add new function ansi::slice_ansi_str I also took my chance and suggested an non-allocating version of measure_text_width. --- Cargo.toml | 2 +- src/ansi.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/utils.rs | 33 +++++++++++++------ 4 files changed, 115 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d850f3f7..ef64bb25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "console" description = "A terminal and console abstraction for Rust" -version = "0.15.8" +version = "0.16.0" keywords = ["cli", "terminal", "colors", "console", "ansi"] authors = ["Armin Ronacher "] license = "MIT" diff --git a/src/ansi.rs b/src/ansi.rs index 3a3c96c3..eb7ccff0 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -4,6 +4,8 @@ use std::{ str::CharIndices, }; +use crate::utils::char_width; + #[derive(Debug, Clone, Copy)] enum State { Start, @@ -267,8 +269,63 @@ impl<'a> Iterator for AnsiCodeIterator<'a> { impl<'a> FusedIterator for AnsiCodeIterator<'a> {} +/// Slice a `&str` in terms of text width. This means that only the text +/// columns strictly between `start` and `stop` will be kept. +/// +/// If a multi-columns character overlaps with the end of the interval it will +/// not be included. In such a case, the result will be less than `end - start` +/// columns wide. +pub fn slice_ansi_str(s: &str, start: usize, end: usize) -> &str { + if end <= start { + return ""; + } + + let mut pos = 0; + let mut res_start = 0; + let mut res_end = 0; + + 'outer: for (sub, is_ansi) in AnsiCodeIterator::new(s) { + // As ansi symbols have a width of 0 we can safely early-interupt + // the outer for loop only if current pos strictly greater than + // `end`. + if pos > end { + break; + } + + if is_ansi { + if pos < start { + res_start += sub.len(); + res_end = res_start; + } else if pos <= end { + res_end += sub.len(); + } else { + break 'outer; + } + } else { + for c in sub.chars() { + let c_width = char_width(c); + + if pos < start { + res_start += c.len_utf8(); + res_end = res_start; + } else if pos + c_width <= end { + res_end += c.len_utf8(); + } else { + break 'outer; + } + + pos += char_width(c); + } + } + } + + &s[res_start..res_end] +} + #[cfg(test)] mod tests { + use crate::measure_text_width; + use super::*; use lazy_static::lazy_static; @@ -435,4 +492,37 @@ mod tests { assert_eq!(iter.rest_slice(), ""); assert_eq!(iter.next(), None); } + + #[test] + fn test_slice_ansi_str() { + // Note that ๐Ÿถ is two columns wide + let test_str = "Hello\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m world!"; + assert_eq!(slice_ansi_str(test_str, 5, 5), ""); + assert_eq!(slice_ansi_str(test_str, 0, test_str.len()), test_str); + + if cfg!(feature = "unicode-width") { + assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); + assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m"); + assert_eq!(measure_text_width(test_str), 16); + assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); + assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m"); + assert_eq!(slice_ansi_str(test_str, 0, 7), "Hello\x1b[31m๐Ÿถ\x1b[1m"); + assert_eq!(slice_ansi_str(test_str, 7, 21), "\x1b[1m๐Ÿถ\x1b[0m world!"); + assert_eq!(slice_ansi_str(test_str, 8, 21), "\x1b[0m world!"); + assert_eq!(slice_ansi_str(test_str, 9, 21), "\x1b[0m world!"); + + assert_eq!( + slice_ansi_str(test_str, 4, 9), + "o\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m" + ); + } else { + assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); + assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m๐Ÿถ\u{1b}[1m"); + + assert_eq!( + slice_ansi_str(test_str, 4, 9), + "o\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m w" + ); + } + } } diff --git a/src/lib.rs b/src/lib.rs index a1ac2275..f57e2c80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,7 +87,7 @@ pub use crate::utils::{ }; #[cfg(feature = "ansi-parsing")] -pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; +pub use crate::ansi::{slice_ansi_str, strip_ansi_codes, AnsiCodeIterator}; mod common_term; mod kb; diff --git a/src/utils.rs b/src/utils.rs index cfecc78f..b07f197f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -9,7 +9,7 @@ use lazy_static::lazy_static; use crate::term::{wants_emoji, Term}; #[cfg(feature = "ansi-parsing")] -use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; +use crate::ansi::AnsiCodeIterator; #[cfg(not(feature = "ansi-parsing"))] fn strip_ansi_codes(s: &str) -> &str { @@ -71,7 +71,17 @@ pub fn set_colors_enabled_stderr(val: bool) { /// Measure the width of a string in terminal characters. pub fn measure_text_width(s: &str) -> usize { - str_width(&strip_ansi_codes(s)) + #[cfg(feature = "ansi-parsing")] + { + AnsiCodeIterator::new(s) + .filter(|(_, is_ansi)| !is_ansi) + .map(|(sub, _)| str_width(sub)) + .sum() + } + #[cfg(not(feature = "ansi-parsing"))] + { + str_width(s) + } } /// A terminal color. @@ -719,7 +729,7 @@ fn str_width(s: &str) -> usize { } #[cfg(feature = "ansi-parsing")] -fn char_width(c: char) -> usize { +pub(crate) fn char_width(c: char) -> usize { #[cfg(feature = "unicode-width")] { use unicode_width::UnicodeWidthChar; @@ -868,15 +878,18 @@ fn test_text_width() { .on_black() .bold() .force_styling(true) - .to_string(); + .to_string() + + "๐Ÿถbar"; assert_eq!( measure_text_width(&s), - if cfg!(feature = "ansi-parsing") { - 3 - } else if cfg!(feature = "unicode-width") { - 17 - } else { - 21 + match ( + cfg!(feature = "ansi-parsing"), + cfg!(feature = "unicode-width") + ) { + (true, true) => 8, + (true, false) => 7, + (false, true) => 22, + (false, false) => 25, } ); } From cd1a6b41bbc4eed9407eeef8678785cb3a6255ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Wed, 7 Feb 2024 18:30:19 +0100 Subject: [PATCH 2/6] Fix clippy lints --- src/unix_term.rs | 2 +- src/utils.rs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/unix_term.rs b/src/unix_term.rs index 271709f2..52b28e33 100644 --- a/src/unix_term.rs +++ b/src/unix_term.rs @@ -56,7 +56,7 @@ pub fn terminal_size(out: &Term) -> Option<(u16, u16)> { #[allow(clippy::useless_conversion)] libc::ioctl(out.as_raw_fd(), libc::TIOCGWINSZ.into(), &mut winsize); if winsize.ws_row > 0 && winsize.ws_col > 0 { - Some((winsize.ws_row as u16, winsize.ws_col as u16)) + Some((winsize.ws_row, winsize.ws_col)) } else { None } diff --git a/src/utils.rs b/src/utils.rs index b07f197f..868c0b5c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -11,11 +11,6 @@ use crate::term::{wants_emoji, Term}; #[cfg(feature = "ansi-parsing")] use crate::ansi::AnsiCodeIterator; -#[cfg(not(feature = "ansi-parsing"))] -fn strip_ansi_codes(s: &str) -> &str { - s -} - fn default_colors_enabled(out: &Term) -> bool { (out.features().colors_supported() && &env::var("CLICOLOR").unwrap_or_else(|_| "1".into()) != "0") From ce77cc52968c553eb065bf287f51172d3c6f65f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Wed, 7 Feb 2024 19:16:50 +0100 Subject: [PATCH 3/6] Make `slice_str` similar to `truncate_str` --- src/ansi.rs | 90 --------------------------- src/lib.rs | 6 +- src/utils.rs | 173 ++++++++++++++++++++++++++++++++++----------------- 3 files changed, 120 insertions(+), 149 deletions(-) diff --git a/src/ansi.rs b/src/ansi.rs index eb7ccff0..3a3c96c3 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -4,8 +4,6 @@ use std::{ str::CharIndices, }; -use crate::utils::char_width; - #[derive(Debug, Clone, Copy)] enum State { Start, @@ -269,63 +267,8 @@ impl<'a> Iterator for AnsiCodeIterator<'a> { impl<'a> FusedIterator for AnsiCodeIterator<'a> {} -/// Slice a `&str` in terms of text width. This means that only the text -/// columns strictly between `start` and `stop` will be kept. -/// -/// If a multi-columns character overlaps with the end of the interval it will -/// not be included. In such a case, the result will be less than `end - start` -/// columns wide. -pub fn slice_ansi_str(s: &str, start: usize, end: usize) -> &str { - if end <= start { - return ""; - } - - let mut pos = 0; - let mut res_start = 0; - let mut res_end = 0; - - 'outer: for (sub, is_ansi) in AnsiCodeIterator::new(s) { - // As ansi symbols have a width of 0 we can safely early-interupt - // the outer for loop only if current pos strictly greater than - // `end`. - if pos > end { - break; - } - - if is_ansi { - if pos < start { - res_start += sub.len(); - res_end = res_start; - } else if pos <= end { - res_end += sub.len(); - } else { - break 'outer; - } - } else { - for c in sub.chars() { - let c_width = char_width(c); - - if pos < start { - res_start += c.len_utf8(); - res_end = res_start; - } else if pos + c_width <= end { - res_end += c.len_utf8(); - } else { - break 'outer; - } - - pos += char_width(c); - } - } - } - - &s[res_start..res_end] -} - #[cfg(test)] mod tests { - use crate::measure_text_width; - use super::*; use lazy_static::lazy_static; @@ -492,37 +435,4 @@ mod tests { assert_eq!(iter.rest_slice(), ""); assert_eq!(iter.next(), None); } - - #[test] - fn test_slice_ansi_str() { - // Note that ๐Ÿถ is two columns wide - let test_str = "Hello\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m world!"; - assert_eq!(slice_ansi_str(test_str, 5, 5), ""); - assert_eq!(slice_ansi_str(test_str, 0, test_str.len()), test_str); - - if cfg!(feature = "unicode-width") { - assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); - assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m"); - assert_eq!(measure_text_width(test_str), 16); - assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); - assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m"); - assert_eq!(slice_ansi_str(test_str, 0, 7), "Hello\x1b[31m๐Ÿถ\x1b[1m"); - assert_eq!(slice_ansi_str(test_str, 7, 21), "\x1b[1m๐Ÿถ\x1b[0m world!"); - assert_eq!(slice_ansi_str(test_str, 8, 21), "\x1b[0m world!"); - assert_eq!(slice_ansi_str(test_str, 9, 21), "\x1b[0m world!"); - - assert_eq!( - slice_ansi_str(test_str, 4, 9), - "o\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m" - ); - } else { - assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m"); - assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m๐Ÿถ\u{1b}[1m"); - - assert_eq!( - slice_ansi_str(test_str, 4, 9), - "o\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m w" - ); - } - } } diff --git a/src/lib.rs b/src/lib.rs index f57e2c80..a7fbb935 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,12 +82,12 @@ pub use crate::term::{ }; pub use crate::utils::{ colors_enabled, colors_enabled_stderr, measure_text_width, pad_str, pad_str_with, - set_colors_enabled, set_colors_enabled_stderr, style, truncate_str, Alignment, Attribute, - Color, Emoji, Style, StyledObject, + set_colors_enabled, set_colors_enabled_stderr, slice_str, style, truncate_str, Alignment, + Attribute, Color, Emoji, Style, StyledObject, }; #[cfg(feature = "ansi-parsing")] -pub use crate::ansi::{slice_ansi_str, strip_ansi_codes, AnsiCodeIterator}; +pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator}; mod common_term; mod kb; diff --git a/src/utils.rs b/src/utils.rs index 868c0b5c..da91691c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::collections::BTreeSet; use std::env; use std::fmt; +use std::ops::Range; use std::sync::atomic::{AtomicBool, Ordering}; use lazy_static::lazy_static; @@ -724,7 +725,7 @@ fn str_width(s: &str) -> usize { } #[cfg(feature = "ansi-parsing")] -pub(crate) fn char_width(c: char) -> usize { +fn char_width(c: char) -> usize { #[cfg(feature = "unicode-width")] { use unicode_width::UnicodeWidthChar; @@ -737,80 +738,98 @@ pub(crate) fn char_width(c: char) -> usize { } } -/// Truncates a string to a certain number of characters. +/// Slice a `&str` in terms of text width. This means that only the text +/// columns strictly between `start` and `stop` will be kept. /// -/// This ensures that escape codes are not screwed up in the process. -/// If the maximum length is hit the string will be truncated but -/// escapes code will still be honored. If truncation takes place -/// the tail string will be appended. -pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> { +/// If a multi-columns character overlaps with the end of the interval it will +/// not be included. In such a case, the result will be less than `end - start` +/// columns wide. +/// +/// This ensures that escape codes are not screwed up in the process. And if +/// non-empty head and tail are specified, they are inserted between the ANSI +/// symbols from truncated bounds and the slice. +pub fn slice_str<'a>(s: &'a str, head: &str, bounds: Range, tail: &str) -> Cow<'a, str> { #[cfg(feature = "ansi-parsing")] { - use std::cmp::Ordering; - let mut iter = AnsiCodeIterator::new(s); - let mut length = 0; - let mut rv = None; - - while let Some(item) = iter.next() { - match item { - (s, false) => { - if rv.is_none() { - if str_width(s) + length > width - str_width(tail) { - let ts = iter.current_slice(); - - let mut s_byte = 0; - let mut s_width = 0; - let rest_width = width - str_width(tail) - length; - for c in s.chars() { - s_byte += c.len_utf8(); - s_width += char_width(c); - match s_width.cmp(&rest_width) { - Ordering::Equal => break, - Ordering::Greater => { - s_byte -= c.len_utf8(); - break; - } - Ordering::Less => continue, - } - } - - let idx = ts.len() - s.len() + s_byte; - let mut buf = ts[..idx].to_string(); - buf.push_str(tail); - rv = Some(buf); - } - length += str_width(s); - } + let mut pos = 0; + let mut slice = 0..0; + + // ANSI symbols outside of the slice + let mut front_ansi = String::new(); + let mut back_ansi = String::new(); + + // Iterate through each ANSI symbol or unicode character while keeping + // track of: + // - pos: cumulated width of characters iterated so far + // - slice: char indices of the part of the string for which `pos` + // was inside bounds + for (sub, is_ansi) in AnsiCodeIterator::new(s) { + if is_ansi { + if pos < bounds.start { + // An ANSI symbol before the interval: keep for later + front_ansi.push_str(sub); + slice.start += sub.len(); + slice.end = slice.start; + } else if pos <= bounds.end { + // An ANSI symbol inside of the interval: extend the slice + slice.end += sub.len(); + } else { + // An ANSI symbol after the interval: keep for later + back_ansi.push_str(sub); } - (s, true) => { - if let Some(ref mut rv) = rv { - rv.push_str(s); + } else { + for c in sub.chars() { + let c_width = char_width(c); + + if pos < bounds.start { + // The char is before the interval: move the slice back + slice.start += c.len_utf8(); + slice.end = slice.start; + } else if pos + c_width <= bounds.end { + // The char fits into the interval: extend the slice + slice.end += c.len_utf8(); } + + pos += c_width; } } } - if let Some(buf) = rv { - Cow::Owned(buf) + let slice = &s[slice]; + + if front_ansi.is_empty() && back_ansi.is_empty() && head.is_empty() && tail.is_empty() { + Cow::Borrowed(slice) } else { - Cow::Borrowed(s) + Cow::Owned(front_ansi + head + slice + tail + &back_ansi) } } - #[cfg(not(feature = "ansi-parsing"))] { - if s.len() <= width - tail.len() { - Cow::Borrowed(s) + let slice = s.get(bounds).unwrap_or(""); + + if head.is_empty() && tail.is_empty() { + Cow::Borrowed(slice) } else { - Cow::Owned(format!( - "{}{}", - s.get(..width - tail.len()).unwrap_or_default(), - tail - )) + Cow::Owned(format!("{head}{slice}{tail}")) } } } +/// Truncates a string to a certain number of characters. +/// +/// This ensures that escape codes are not screwed up in the process. +/// If the maximum length is hit the string will be truncated but +/// escapes code will still be honored. If truncation takes place +/// the tail string will be appended. +pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> { + if measure_text_width(s) > width { + let tail_width = measure_text_width(tail); + slice_str(s, "", 0..width.saturating_sub(tail_width), tail) + } else { + Cow::Borrowed(s) + } +} + /// Pads a string to fill a certain number of characters. /// /// This will honor ansi codes correctly and allows you to align a string @@ -919,8 +938,50 @@ fn test_truncate_str() { ); } +#[test] +fn test_slice_ansi_str() { + // Note that ๐Ÿถ is two columns wide + let test_str = "Hello\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m world!"; + assert_eq!(slice_str(test_str, "", 0..test_str.len(), ""), test_str); + + if cfg!(feature = "unicode-width") && cfg!(feature = "ansi-parsing") { + assert_eq!(measure_text_width(test_str), 16); + + assert_eq!( + slice_str(test_str, "", 5..5, ""), + "\u{1b}[31m\u{1b}[1m\u{1b}[0m" + ); + + assert_eq!( + slice_str(test_str, "", 0..5, ""), + "Hello\x1b[31m\x1b[1m\x1b[0m" + ); + + assert_eq!( + slice_str(test_str, "", 0..6, ""), + "Hello\x1b[31m\x1b[1m\x1b[0m" + ); + + assert_eq!( + slice_str(test_str, "", 0..7, ""), + "Hello\x1b[31m๐Ÿถ\x1b[1m\x1b[0m" + ); + + assert_eq!( + slice_str(test_str, "", 4..9, ""), + "o\x1b[31m๐Ÿถ\x1b[1m๐Ÿถ\x1b[0m" + ); + + assert_eq!( + slice_str(test_str, "", 7..21, ""), + "\x1b[31m\x1b[1m๐Ÿถ\x1b[0m world!" + ); + } +} + #[test] fn test_truncate_str_no_ansi() { + assert_eq!(&truncate_str("foo bar", 7, "!"), "foo bar"); assert_eq!(&truncate_str("foo bar", 5, ""), "foo b"); assert_eq!(&truncate_str("foo bar", 5, "!"), "foo !"); assert_eq!(&truncate_str("foo bar baz", 10, "..."), "foo bar..."); From 7c6842687fe91c9a87318e7acafe4b12e1e797e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Thu, 8 Feb 2024 09:07:49 +0100 Subject: [PATCH 4/6] More verbose but more readable implementation of `slice_str` This new implementation also has the benefit of allocating at most once. --- src/utils.rs | 102 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index da91691c..1347772e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -747,61 +747,81 @@ fn char_width(c: char) -> usize { /// /// This ensures that escape codes are not screwed up in the process. And if /// non-empty head and tail are specified, they are inserted between the ANSI -/// symbols from truncated bounds and the slice. +/// codes from truncated bounds and the slice. pub fn slice_str<'a>(s: &'a str, head: &str, bounds: Range, tail: &str) -> Cow<'a, str> { #[cfg(feature = "ansi-parsing")] { let mut pos = 0; - let mut slice = 0..0; + let mut code_iter = AnsiCodeIterator::new(s).peekable(); - // ANSI symbols outside of the slice + // Search for the begining of the slice while collecting heading ANSI + // codes + let mut slice_start = 0; let mut front_ansi = String::new(); - let mut back_ansi = String::new(); - - // Iterate through each ANSI symbol or unicode character while keeping - // track of: - // - pos: cumulated width of characters iterated so far - // - slice: char indices of the part of the string for which `pos` - // was inside bounds - for (sub, is_ansi) in AnsiCodeIterator::new(s) { + + while pos < bounds.start { + let Some((sub, is_ansi)) = code_iter.peek_mut() else { + break; + }; + + if *is_ansi { + front_ansi.push_str(sub); + slice_start += sub.len(); + } else if let Some(c) = sub.chars().next() { + // Pop the head char of `sub` while keeping `sub` on top of + // the iterator + pos += char_width(c); + slice_start += c.len_utf8(); + *sub = &sub[c.len_utf8()..]; + continue; + } + + code_iter.next(); + } + + // Search for the end of the slice + let mut slice_end = slice_start; + + 'search_slice_end: for (sub, is_ansi) in &mut code_iter { if is_ansi { - if pos < bounds.start { - // An ANSI symbol before the interval: keep for later - front_ansi.push_str(sub); - slice.start += sub.len(); - slice.end = slice.start; - } else if pos <= bounds.end { - // An ANSI symbol inside of the interval: extend the slice - slice.end += sub.len(); - } else { - // An ANSI symbol after the interval: keep for later - back_ansi.push_str(sub); - } - } else { - for c in sub.chars() { - let c_width = char_width(c); - - if pos < bounds.start { - // The char is before the interval: move the slice back - slice.start += c.len_utf8(); - slice.end = slice.start; - } else if pos + c_width <= bounds.end { - // The char fits into the interval: extend the slice - slice.end += c.len_utf8(); - } + slice_end += sub.len(); + continue; + } - pos += c_width; + for c in sub.chars() { + let c_width = char_width(c); + + if pos + c_width > bounds.end { + // We will only search for ANSI codes after breaking this + // loop, so we can safely drop the remaining of `sub` + break 'search_slice_end; } + + pos += c_width; + slice_end += c.len_utf8(); } } - let slice = &s[slice]; + // Initialise the result, no allocation may have to be performed if + // both head and front are empty + let slice = &s[slice_start..slice_end]; - if front_ansi.is_empty() && back_ansi.is_empty() && head.is_empty() && tail.is_empty() { - Cow::Borrowed(slice) - } else { - Cow::Owned(front_ansi + head + slice + tail + &back_ansi) + let mut result = { + if front_ansi.is_empty() && head.is_empty() && tail.is_empty() { + Cow::Borrowed(slice) + } else { + Cow::Owned(front_ansi + head + slice + tail) + } + }; + + // Push back remaining ANSI codes to result + for (sub, is_ansi) in code_iter { + if is_ansi { + *result.to_mut() += sub; + } } + + result } #[cfg(not(feature = "ansi-parsing"))] { From 07a96f4019bc0e0b6f9f2d24dab70422a3840b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Sat, 10 Feb 2024 19:03:38 +0100 Subject: [PATCH 5/6] Remove rust >1.56 syntax --- src/utils.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 1347772e..ec28355d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -760,8 +760,9 @@ pub fn slice_str<'a>(s: &'a str, head: &str, bounds: Range, tail: &str) - let mut front_ansi = String::new(); while pos < bounds.start { - let Some((sub, is_ansi)) = code_iter.peek_mut() else { - break; + let (sub, is_ansi) = match code_iter.peek_mut() { + Some(x) => x, + None => break, }; if *is_ansi { From 51e4d4f0c0699113b78854376fe5f1b9cf833eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Dupr=C3=A9?= Date: Mon, 12 Feb 2024 11:04:50 +0100 Subject: [PATCH 6/6] Fix MSRV & update changelog --- CHANGELOG.md | 6 ++++++ src/utils.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05f735f5..7f505869 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.16.0 + +### Enhancements + +* Added `slice_str` util. + ## 0.15.8 ### Enhancements diff --git a/src/utils.rs b/src/utils.rs index ec28355d..8bf235da 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -831,7 +831,7 @@ pub fn slice_str<'a>(s: &'a str, head: &str, bounds: Range, tail: &str) - if head.is_empty() && tail.is_empty() { Cow::Borrowed(slice) } else { - Cow::Owned(format!("{head}{slice}{tail}")) + Cow::Owned(format!("{}{}{}", head, slice, tail)) } } }