Skip to content

Commit 3811bcc

Browse files
committed
Make slice_str similar to truncate_str
1 parent cd1a6b4 commit 3811bcc

File tree

3 files changed

+96
-140
lines changed

3 files changed

+96
-140
lines changed

src/ansi.rs

Lines changed: 0 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ use std::{
44
str::CharIndices,
55
};
66

7-
use crate::utils::char_width;
8-
97
#[derive(Debug, Clone, Copy)]
108
enum State {
119
Start,
@@ -269,63 +267,8 @@ impl<'a> Iterator for AnsiCodeIterator<'a> {
269267

270268
impl<'a> FusedIterator for AnsiCodeIterator<'a> {}
271269

272-
/// Slice a `&str` in terms of text width. This means that only the text
273-
/// columns strictly between `start` and `stop` will be kept.
274-
///
275-
/// If a multi-columns character overlaps with the end of the interval it will
276-
/// not be included. In such a case, the result will be less than `end - start`
277-
/// columns wide.
278-
pub fn slice_ansi_str(s: &str, start: usize, end: usize) -> &str {
279-
if end <= start {
280-
return "";
281-
}
282-
283-
let mut pos = 0;
284-
let mut res_start = 0;
285-
let mut res_end = 0;
286-
287-
'outer: for (sub, is_ansi) in AnsiCodeIterator::new(s) {
288-
// As ansi symbols have a width of 0 we can safely early-interupt
289-
// the outer for loop only if current pos strictly greater than
290-
// `end`.
291-
if pos > end {
292-
break;
293-
}
294-
295-
if is_ansi {
296-
if pos < start {
297-
res_start += sub.len();
298-
res_end = res_start;
299-
} else if pos <= end {
300-
res_end += sub.len();
301-
} else {
302-
break 'outer;
303-
}
304-
} else {
305-
for c in sub.chars() {
306-
let c_width = char_width(c);
307-
308-
if pos < start {
309-
res_start += c.len_utf8();
310-
res_end = res_start;
311-
} else if pos + c_width <= end {
312-
res_end += c.len_utf8();
313-
} else {
314-
break 'outer;
315-
}
316-
317-
pos += char_width(c);
318-
}
319-
}
320-
}
321-
322-
&s[res_start..res_end]
323-
}
324-
325270
#[cfg(test)]
326271
mod tests {
327-
use crate::measure_text_width;
328-
329272
use super::*;
330273

331274
use lazy_static::lazy_static;
@@ -492,37 +435,4 @@ mod tests {
492435
assert_eq!(iter.rest_slice(), "");
493436
assert_eq!(iter.next(), None);
494437
}
495-
496-
#[test]
497-
fn test_slice_ansi_str() {
498-
// Note that 🐶 is two columns wide
499-
let test_str = "Hello\x1b[31m🐶\x1b[1m🐶\x1b[0m world!";
500-
assert_eq!(slice_ansi_str(test_str, 5, 5), "");
501-
assert_eq!(slice_ansi_str(test_str, 0, test_str.len()), test_str);
502-
503-
if cfg!(feature = "unicode-width") {
504-
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
505-
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m");
506-
assert_eq!(measure_text_width(test_str), 16);
507-
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
508-
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m");
509-
assert_eq!(slice_ansi_str(test_str, 0, 7), "Hello\x1b[31m🐶\x1b[1m");
510-
assert_eq!(slice_ansi_str(test_str, 7, 21), "\x1b[1m🐶\x1b[0m world!");
511-
assert_eq!(slice_ansi_str(test_str, 8, 21), "\x1b[0m world!");
512-
assert_eq!(slice_ansi_str(test_str, 9, 21), "\x1b[0m world!");
513-
514-
assert_eq!(
515-
slice_ansi_str(test_str, 4, 9),
516-
"o\x1b[31m🐶\x1b[1m🐶\x1b[0m"
517-
);
518-
} else {
519-
assert_eq!(slice_ansi_str(test_str, 0, 5), "Hello\x1b[31m");
520-
assert_eq!(slice_ansi_str(test_str, 0, 6), "Hello\x1b[31m🐶\u{1b}[1m");
521-
522-
assert_eq!(
523-
slice_ansi_str(test_str, 4, 9),
524-
"o\x1b[31m🐶\x1b[1m🐶\x1b[0m w"
525-
);
526-
}
527-
}
528438
}

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,12 @@ pub use crate::term::{
8282
};
8383
pub use crate::utils::{
8484
colors_enabled, colors_enabled_stderr, measure_text_width, pad_str, pad_str_with,
85-
set_colors_enabled, set_colors_enabled_stderr, style, truncate_str, Alignment, Attribute,
86-
Color, Emoji, Style, StyledObject,
85+
set_colors_enabled, set_colors_enabled_stderr, slice_str, style, truncate_str, Alignment,
86+
Attribute, Color, Emoji, Style, StyledObject,
8787
};
8888

8989
#[cfg(feature = "ansi-parsing")]
90-
pub use crate::ansi::{slice_ansi_str, strip_ansi_codes, AnsiCodeIterator};
90+
pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator};
9191

9292
mod common_term;
9393
mod kb;

src/utils.rs

Lines changed: 93 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ fn str_width(s: &str) -> usize {
724724
}
725725

726726
#[cfg(feature = "ansi-parsing")]
727-
pub(crate) fn char_width(c: char) -> usize {
727+
fn char_width(c: char) -> usize {
728728
#[cfg(feature = "unicode-width")]
729729
{
730730
use unicode_width::UnicodeWidthChar;
@@ -737,66 +737,91 @@ pub(crate) fn char_width(c: char) -> usize {
737737
}
738738
}
739739

740-
/// Truncates a string to a certain number of characters.
740+
/// Slice a `&str` in terms of text width. This means that only the text
741+
/// columns strictly between `start` and `stop` will be kept.
742+
///
743+
/// If a multi-columns character overlaps with the end of the interval it will
744+
/// not be included. In such a case, the result will be less than `end - start`
745+
/// columns wide.
741746
///
742747
/// This ensures that escape codes are not screwed up in the process.
743-
/// If the maximum length is hit the string will be truncated but
744-
/// escapes code will still be honored. If truncation takes place
745-
/// the tail string will be appended.
746-
pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> {
748+
pub fn slice_str(s: &str, start: usize, end: usize) -> Cow<'_, str> {
747749
#[cfg(feature = "ansi-parsing")]
748750
{
749-
use std::cmp::Ordering;
750-
let mut iter = AnsiCodeIterator::new(s);
751-
let mut length = 0;
752-
let mut rv = None;
753-
754-
while let Some(item) = iter.next() {
755-
match item {
756-
(s, false) => {
757-
if rv.is_none() {
758-
if str_width(s) + length > width - str_width(tail) {
759-
let ts = iter.current_slice();
760-
761-
let mut s_byte = 0;
762-
let mut s_width = 0;
763-
let rest_width = width - str_width(tail) - length;
764-
for c in s.chars() {
765-
s_byte += c.len_utf8();
766-
s_width += char_width(c);
767-
match s_width.cmp(&rest_width) {
768-
Ordering::Equal => break,
769-
Ordering::Greater => {
770-
s_byte -= c.len_utf8();
771-
break;
772-
}
773-
Ordering::Less => continue,
774-
}
775-
}
776-
777-
let idx = ts.len() - s.len() + s_byte;
778-
let mut buf = ts[..idx].to_string();
779-
buf.push_str(tail);
780-
rv = Some(buf);
781-
}
782-
length += str_width(s);
783-
}
751+
let mut pos = 0;
752+
let mut slice_start = 0;
753+
let mut slice_end = 0;
754+
755+
// ANSI symbols outside of the slice
756+
let mut front_ansi = String::new();
757+
let mut back_ansi = String::new();
758+
759+
for (sub, is_ansi) in AnsiCodeIterator::new(s) {
760+
if is_ansi {
761+
if pos < start {
762+
front_ansi.push_str(sub);
763+
slice_start += sub.len();
764+
slice_end = slice_start;
765+
} else if pos <= end {
766+
slice_end += sub.len();
767+
} else {
768+
back_ansi.push_str(sub);
784769
}
785-
(s, true) => {
786-
if let Some(ref mut rv) = rv {
787-
rv.push_str(s);
770+
} else {
771+
for c in sub.chars() {
772+
let c_width = char_width(c);
773+
774+
if pos < start {
775+
slice_start += c.len_utf8();
776+
slice_end = slice_start;
777+
} else if pos + c_width <= end {
778+
slice_end += c.len_utf8();
788779
}
780+
781+
pos += char_width(c);
789782
}
790783
}
791784
}
792785

793-
if let Some(buf) = rv {
794-
Cow::Owned(buf)
786+
let slice = &s[slice_start..slice_end];
787+
788+
if front_ansi.is_empty() && back_ansi.is_empty() {
789+
Cow::Borrowed(slice)
795790
} else {
796-
Cow::Borrowed(s)
791+
Cow::Owned(front_ansi + slice + &back_ansi)
797792
}
798793
}
794+
#[cfg(not(feature = "ansi-parsing"))]
795+
{
796+
Cow::Borrowed(s.get(start..end).unwrap_or_default())
797+
}
798+
}
799799

800+
/// Truncates a string to a certain number of characters.
801+
///
802+
/// This ensures that escape codes are not screwed up in the process.
803+
/// If the maximum length is hit the string will be truncated but
804+
/// escapes code will still be honored. If truncation takes place
805+
/// the tail string will be appended.
806+
pub fn truncate_str<'a>(s: &'a str, width: usize, tail: &str) -> Cow<'a, str> {
807+
#[cfg(feature = "ansi-parsing")]
808+
{
809+
let s_width = measure_text_width(s);
810+
811+
if s_width <= width {
812+
return Cow::Borrowed(s);
813+
}
814+
815+
let tail_width = measure_text_width(tail);
816+
let mut sliced = slice_str(s, 0, width.saturating_sub(tail_width));
817+
818+
if tail.is_empty() {
819+
sliced
820+
} else {
821+
sliced.to_mut().push_str(tail);
822+
sliced
823+
}
824+
}
800825
#[cfg(not(feature = "ansi-parsing"))]
801826
{
802827
if s.len() <= width - tail.len() {
@@ -919,6 +944,27 @@ fn test_truncate_str() {
919944
);
920945
}
921946

947+
#[test]
948+
fn test_slice_ansi_str() {
949+
// Note that 🐶 is two columns wide
950+
let test_str = "Hello\x1b[31m🐶\x1b[1m🐶\x1b[0m world!";
951+
assert_eq!(slice_str(test_str, 0, test_str.len()), test_str);
952+
953+
if cfg!(feature = "unicode-width") && cfg!(feature = "ansi-parsing") {
954+
assert_eq!(slice_str(test_str, 5, 5), "\u{1b}[31m\u{1b}[1m\u{1b}[0m");
955+
assert_eq!(measure_text_width(test_str), 16);
956+
assert_eq!(slice_str(test_str, 0, 5), "Hello\x1b[31m\x1b[1m\x1b[0m");
957+
assert_eq!(slice_str(test_str, 0, 6), "Hello\x1b[31m\x1b[1m\x1b[0m");
958+
assert_eq!(slice_str(test_str, 0, 7), "Hello\x1b[31m🐶\x1b[1m\x1b[0m");
959+
assert_eq!(slice_str(test_str, 4, 9), "o\x1b[31m🐶\x1b[1m🐶\x1b[0m");
960+
961+
assert_eq!(
962+
slice_str(test_str, 7, 21),
963+
"\x1b[31m\x1b[1m🐶\x1b[0m world!"
964+
);
965+
}
966+
}
967+
922968
#[test]
923969
fn test_truncate_str_no_ansi() {
924970
assert_eq!(&truncate_str("foo bar", 5, ""), "foo b");

0 commit comments

Comments
 (0)