@@ -2,6 +2,7 @@ use std::borrow::Cow;
22use std:: env;
33use std:: fmt;
44use std:: fmt:: { Debug , Formatter } ;
5+ use std:: ops:: Range ;
56use std:: sync:: atomic:: { AtomicBool , Ordering } ;
67
78use once_cell:: sync:: Lazy ;
@@ -800,80 +801,119 @@ pub(crate) fn char_width(_c: char) -> usize {
800801 1
801802}
802803
803- /// Truncates a string to a certain number of characters.
804+ /// Slice a `&str` in terms of text width. This means that only the text
805+ /// columns strictly between `start` and `stop` will be kept.
804806///
805- /// This ensures that escape codes are not screwed up in the process.
806- /// If the maximum length is hit the string will be truncated but
807- /// escapes code will still be honored. If truncation takes place
808- /// the tail string will be appended.
809- pub fn truncate_str < ' a > ( s : & ' a str , width : usize , tail : & str ) -> Cow < ' a , str > {
807+ /// If a multi-columns character overlaps with the end of the interval it will
808+ /// not be included. In such a case, the result will be less than `end - start`
809+ /// columns wide.
810+ ///
811+ /// This ensures that escape codes are not screwed up in the process. And if
812+ /// non-empty head and tail are specified, they are inserted between the ANSI
813+ /// codes from truncated bounds and the slice.
814+ pub fn slice_str < ' a > ( s : & ' a str , head : & str , bounds : Range < usize > , tail : & str ) -> Cow < ' a , str > {
810815 #[ cfg( feature = "ansi-parsing" ) ]
811816 {
812- use std:: cmp:: Ordering ;
813- let mut iter = AnsiCodeIterator :: new ( s) ;
814- let mut length = 0 ;
815- let mut rv = None ;
816-
817- while let Some ( item) = iter. next ( ) {
818- match item {
819- ( s, false ) => {
820- if rv. is_none ( ) {
821- if str_width ( s) + length > width - str_width ( tail) {
822- let ts = iter. current_slice ( ) ;
823-
824- let mut s_byte = 0 ;
825- let mut s_width = 0 ;
826- let rest_width = width - str_width ( tail) - length;
827- for c in s. chars ( ) {
828- s_byte += c. len_utf8 ( ) ;
829- s_width += char_width ( c) ;
830- match s_width. cmp ( & rest_width) {
831- Ordering :: Equal => break ,
832- Ordering :: Greater => {
833- s_byte -= c. len_utf8 ( ) ;
834- break ;
835- }
836- Ordering :: Less => continue ,
837- }
838- }
839-
840- let idx = ts. len ( ) - s. len ( ) + s_byte;
841- let mut buf = ts[ ..idx] . to_string ( ) ;
842- buf. push_str ( tail) ;
843- rv = Some ( buf) ;
844- }
845- length += str_width ( s) ;
846- }
847- }
848- ( s, true ) => {
849- if let Some ( ref mut rv) = rv {
850- rv. push_str ( s) ;
851- }
817+ let mut pos = 0 ;
818+ let mut code_iter = AnsiCodeIterator :: new ( s) . peekable ( ) ;
819+
820+ // Search for the begining of the slice while collecting heading ANSI
821+ // codes
822+ let mut slice_start = 0 ;
823+ let mut front_ansi = String :: new ( ) ;
824+
825+ while pos < bounds. start {
826+ let ( sub, is_ansi) = match code_iter. peek_mut ( ) {
827+ None => break ,
828+ Some ( x) => x,
829+ } ;
830+
831+ if * is_ansi {
832+ front_ansi. push_str ( sub) ;
833+ slice_start += sub. len ( ) ;
834+ } else if let Some ( c) = sub. chars ( ) . next ( ) {
835+ // Pop the head char of `sub` while keeping `sub` on top of
836+ // the iterator
837+ pos += char_width ( c) ;
838+ slice_start += c. len_utf8 ( ) ;
839+ * sub = & sub[ c. len_utf8 ( ) ..] ;
840+ continue ;
841+ }
842+
843+ code_iter. next ( ) ;
844+ }
845+
846+ // Search for the end of the slice
847+ let mut slice_end = slice_start;
848+
849+ ' search_slice_end: for ( sub, is_ansi) in & mut code_iter {
850+ if is_ansi {
851+ slice_end += sub. len ( ) ;
852+ continue ;
853+ }
854+
855+ for c in sub. chars ( ) {
856+ let c_width = char_width ( c) ;
857+
858+ if pos + c_width > bounds. end {
859+ // We will only search for ANSI codes after breaking this
860+ // loop, so we can safely drop the remaining of `sub`
861+ break ' search_slice_end;
852862 }
863+
864+ pos += c_width;
865+ slice_end += c. len_utf8 ( ) ;
853866 }
854867 }
855868
856- if let Some ( buf) = rv {
857- Cow :: Owned ( buf)
858- } else {
859- Cow :: Borrowed ( s)
869+ // Initialise the result, no allocation may have to be performed if
870+ // both head and front are empty
871+ let slice = & s[ slice_start..slice_end] ;
872+
873+ let mut result = {
874+ if front_ansi. is_empty ( ) && head. is_empty ( ) && tail. is_empty ( ) {
875+ Cow :: Borrowed ( slice)
876+ } else {
877+ Cow :: Owned ( front_ansi + head + slice + tail)
878+ }
879+ } ;
880+
881+ // Push back remaining ANSI codes to result
882+ for ( sub, is_ansi) in code_iter {
883+ if is_ansi {
884+ * result. to_mut ( ) += sub;
885+ }
860886 }
861- }
862887
888+ result
889+ }
863890 #[ cfg( not( feature = "ansi-parsing" ) ) ]
864891 {
865- if s. len ( ) <= width - tail. len ( ) {
866- Cow :: Borrowed ( s)
892+ let slice = s. get ( bounds) . unwrap_or ( "" ) ;
893+
894+ if head. is_empty ( ) && tail. is_empty ( ) {
895+ Cow :: Borrowed ( slice)
867896 } else {
868- Cow :: Owned ( format ! (
869- "{}{}" ,
870- s. get( ..width - tail. len( ) ) . unwrap_or_default( ) ,
871- tail
872- ) )
897+ Cow :: Owned ( format ! ( "{head}{slice}{tail}" ) )
873898 }
874899 }
875900}
876901
902+ /// Truncates a string to a certain number of characters.
903+ ///
904+ /// This ensures that escape codes are not screwed up in the process.
905+ /// If the maximum length is hit the string will be truncated but
906+ /// escapes code will still be honored. If truncation takes place
907+ /// the tail string will be appended.
908+ pub fn truncate_str < ' a > ( s : & ' a str , width : usize , tail : & str ) -> Cow < ' a , str > {
909+ if measure_text_width ( s) > width {
910+ let tail_width = measure_text_width ( tail) ;
911+ slice_str ( s, "" , 0 ..width. saturating_sub ( tail_width) , tail)
912+ } else {
913+ Cow :: Borrowed ( s)
914+ }
915+ }
916+
877917/// Pads a string to fill a certain number of characters.
878918///
879919/// This will honor ansi codes correctly and allows you to align a string
@@ -979,8 +1019,60 @@ fn test_truncate_str() {
9791019 ) ;
9801020}
9811021
1022+ #[ test]
1023+ fn test_slice_ansi_str ( ) {
1024+ // Note that 🐶 is two columns wide
1025+ let test_str = "Hello\x1b [31m🐶\x1b [1m🐶\x1b [0m world!" ;
1026+ assert_eq ! ( slice_str( test_str, "" , 0 ..test_str. len( ) , "" ) , test_str) ;
1027+
1028+ assert_eq ! (
1029+ slice_str( test_str, ">>>" , 0 ..test_str. len( ) , "<<<" ) ,
1030+ format!( ">>>{test_str}<<<" ) ,
1031+ ) ;
1032+
1033+ if cfg ! ( feature = "unicode-width" ) && cfg ! ( feature = "ansi-parsing" ) {
1034+ assert_eq ! ( measure_text_width( test_str) , 16 ) ;
1035+
1036+ assert_eq ! (
1037+ slice_str( test_str, "" , 5 ..5 , "" ) ,
1038+ "\u{1b} [31m\u{1b} [1m\u{1b} [0m"
1039+ ) ;
1040+
1041+ assert_eq ! (
1042+ slice_str( test_str, "" , 0 ..5 , "" ) ,
1043+ "Hello\x1b [31m\x1b [1m\x1b [0m"
1044+ ) ;
1045+
1046+ assert_eq ! (
1047+ slice_str( test_str, "" , 0 ..6 , "" ) ,
1048+ "Hello\x1b [31m\x1b [1m\x1b [0m"
1049+ ) ;
1050+
1051+ assert_eq ! (
1052+ slice_str( test_str, "" , 0 ..7 , "" ) ,
1053+ "Hello\x1b [31m🐶\x1b [1m\x1b [0m"
1054+ ) ;
1055+
1056+ assert_eq ! (
1057+ slice_str( test_str, "" , 4 ..9 , "" ) ,
1058+ "o\x1b [31m🐶\x1b [1m🐶\x1b [0m"
1059+ ) ;
1060+
1061+ assert_eq ! (
1062+ slice_str( test_str, "" , 7 ..21 , "" ) ,
1063+ "\x1b [31m\x1b [1m🐶\x1b [0m world!"
1064+ ) ;
1065+
1066+ assert_eq ! (
1067+ slice_str( test_str, ">>>" , 7 ..21 , "<<<" ) ,
1068+ "\x1b [31m>>>\x1b [1m🐶\x1b [0m world!<<<"
1069+ ) ;
1070+ }
1071+ }
1072+
9821073#[ test]
9831074fn test_truncate_str_no_ansi ( ) {
1075+ assert_eq ! ( & truncate_str( "foo bar" , 7 , "!" ) , "foo bar" ) ;
9841076 assert_eq ! ( & truncate_str( "foo bar" , 5 , "" ) , "foo b" ) ;
9851077 assert_eq ! ( & truncate_str( "foo bar" , 5 , "!" ) , "foo !" ) ;
9861078 assert_eq ! ( & truncate_str( "foo bar baz" , 10 , "..." ) , "foo bar..." ) ;
0 commit comments