@@ -2,6 +2,7 @@ use std::borrow::Cow;
2
2
use std:: env;
3
3
use std:: fmt;
4
4
use std:: fmt:: { Debug , Formatter } ;
5
+ use std:: ops:: Range ;
5
6
use std:: sync:: atomic:: { AtomicBool , Ordering } ;
6
7
7
8
use once_cell:: sync:: Lazy ;
@@ -800,80 +801,119 @@ pub(crate) fn char_width(_c: char) -> usize {
800
801
1
801
802
}
802
803
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.
804
806
///
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 > {
810
815
#[ cfg( feature = "ansi-parsing" ) ]
811
816
{
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;
852
862
}
863
+
864
+ pos += c_width;
865
+ slice_end += c. len_utf8 ( ) ;
853
866
}
854
867
}
855
868
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
+ }
860
886
}
861
- }
862
887
888
+ result
889
+ }
863
890
#[ cfg( not( feature = "ansi-parsing" ) ) ]
864
891
{
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)
867
896
} 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}" ) )
873
898
}
874
899
}
875
900
}
876
901
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
+
877
917
/// Pads a string to fill a certain number of characters.
878
918
///
879
919
/// This will honor ansi codes correctly and allows you to align a string
@@ -979,8 +1019,60 @@ fn test_truncate_str() {
979
1019
) ;
980
1020
}
981
1021
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
+
982
1073
#[ test]
983
1074
fn test_truncate_str_no_ansi ( ) {
1075
+ assert_eq ! ( & truncate_str( "foo bar" , 7 , "!" ) , "foo bar" ) ;
984
1076
assert_eq ! ( & truncate_str( "foo bar" , 5 , "" ) , "foo b" ) ;
985
1077
assert_eq ! ( & truncate_str( "foo bar" , 5 , "!" ) , "foo !" ) ;
986
1078
assert_eq ! ( & truncate_str( "foo bar baz" , 10 , "..." ) , "foo bar..." ) ;
0 commit comments