@@ -4,6 +4,8 @@ use std::{
4
4
str:: CharIndices ,
5
5
} ;
6
6
7
+ use crate :: utils:: char_width;
8
+
7
9
#[ derive( Debug , Clone , Copy ) ]
8
10
enum State {
9
11
Start ,
@@ -267,8 +269,63 @@ impl<'a> Iterator for AnsiCodeIterator<'a> {
267
269
268
270
impl < ' a > FusedIterator for AnsiCodeIterator < ' a > { }
269
271
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
+
270
325
#[ cfg( test) ]
271
326
mod tests {
327
+ use crate :: measure_text_width;
328
+
272
329
use super :: * ;
273
330
274
331
use lazy_static:: lazy_static;
@@ -435,4 +492,37 @@ mod tests {
435
492
assert_eq ! ( iter. rest_slice( ) , "" ) ;
436
493
assert_eq ! ( iter. next( ) , None ) ;
437
494
}
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
+ }
438
528
}
0 commit comments