@@ -724,7 +724,7 @@ fn str_width(s: &str) -> usize {
724
724
}
725
725
726
726
#[ cfg( feature = "ansi-parsing" ) ]
727
- pub ( crate ) fn char_width ( c : char ) -> usize {
727
+ fn char_width ( c : char ) -> usize {
728
728
#[ cfg( feature = "unicode-width" ) ]
729
729
{
730
730
use unicode_width:: UnicodeWidthChar ;
@@ -737,66 +737,91 @@ pub(crate) fn char_width(c: char) -> usize {
737
737
}
738
738
}
739
739
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.
741
746
///
742
747
/// 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 > {
747
749
#[ cfg( feature = "ansi-parsing" ) ]
748
750
{
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) ;
784
769
}
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 ( ) ;
788
779
}
780
+
781
+ pos += char_width ( c) ;
789
782
}
790
783
}
791
784
}
792
785
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)
795
790
} else {
796
- Cow :: Borrowed ( s )
791
+ Cow :: Owned ( front_ansi + slice + & back_ansi )
797
792
}
798
793
}
794
+ #[ cfg( not( feature = "ansi-parsing" ) ) ]
795
+ {
796
+ Cow :: Borrowed ( s. get ( start..end) . unwrap_or_default ( ) )
797
+ }
798
+ }
799
799
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
+ }
800
825
#[ cfg( not( feature = "ansi-parsing" ) ) ]
801
826
{
802
827
if s. len ( ) <= width - tail. len ( ) {
@@ -919,6 +944,27 @@ fn test_truncate_str() {
919
944
) ;
920
945
}
921
946
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
+
922
968
#[ test]
923
969
fn test_truncate_str_no_ansi ( ) {
924
970
assert_eq ! ( & truncate_str( "foo bar" , 5 , "" ) , "foo b" ) ;
0 commit comments