@@ -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]
923969fn test_truncate_str_no_ansi ( ) {
924970 assert_eq ! ( & truncate_str( "foo bar" , 5 , "" ) , "foo b" ) ;
0 commit comments