@@ -485,39 +485,49 @@ def format_frame_summary(self, frame_summary):
485
485
stripped_line = frame_summary .line .strip ()
486
486
row .append (' {}\n ' .format (stripped_line ))
487
487
488
- orig_line_len = len (frame_summary ._original_line )
488
+ line = frame_summary ._original_line
489
+ orig_line_len = len (line )
489
490
frame_line_len = len (frame_summary .line .lstrip ())
490
491
stripped_characters = orig_line_len - frame_line_len
491
492
if (
492
493
frame_summary .colno is not None
493
494
and frame_summary .end_colno is not None
494
495
):
495
496
start_offset = _byte_offset_to_character_offset (
496
- frame_summary . _original_line , frame_summary .colno ) + 1
497
+ line , frame_summary .colno )
497
498
end_offset = _byte_offset_to_character_offset (
498
- frame_summary ._original_line , frame_summary .end_colno ) + 1
499
+ line , frame_summary .end_colno )
500
+ code_segment = line [start_offset :end_offset ]
499
501
500
502
anchors = None
501
503
if frame_summary .lineno == frame_summary .end_lineno :
502
504
with suppress (Exception ):
503
- anchors = _extract_caret_anchors_from_line_segment (
504
- frame_summary ._original_line [start_offset - 1 :end_offset - 1 ]
505
- )
505
+ anchors = _extract_caret_anchors_from_line_segment (code_segment )
506
506
else :
507
- end_offset = stripped_characters + len (stripped_line )
507
+ # Don't count the newline since the anchors only need to
508
+ # go up until the last character of the line.
509
+ end_offset = len (line .rstrip ())
508
510
509
511
# show indicators if primary char doesn't span the frame line
510
512
if end_offset - start_offset < len (stripped_line ) or (
511
513
anchors and anchors .right_start_offset - anchors .left_end_offset > 0 ):
514
+ # When showing this on a terminal, some of the non-ASCII characters
515
+ # might be rendered as double-width characters, so we need to take
516
+ # that into account when calculating the length of the line.
517
+ dp_start_offset = _display_width (line , start_offset ) + 1
518
+ dp_end_offset = _display_width (line , end_offset ) + 1
519
+
512
520
row .append (' ' )
513
- row .append (' ' * (start_offset - stripped_characters ))
521
+ row .append (' ' * (dp_start_offset - stripped_characters ))
514
522
515
523
if anchors :
516
- row .append (anchors .primary_char * (anchors .left_end_offset ))
517
- row .append (anchors .secondary_char * (anchors .right_start_offset - anchors .left_end_offset ))
518
- row .append (anchors .primary_char * (end_offset - start_offset - anchors .right_start_offset ))
524
+ dp_left_end_offset = _display_width (code_segment , anchors .left_end_offset )
525
+ dp_right_start_offset = _display_width (code_segment , anchors .right_start_offset )
526
+ row .append (anchors .primary_char * dp_left_end_offset )
527
+ row .append (anchors .secondary_char * (dp_right_start_offset - dp_left_end_offset ))
528
+ row .append (anchors .primary_char * (dp_end_offset - dp_start_offset - dp_right_start_offset ))
519
529
else :
520
- row .append ('^' * (end_offset - start_offset ))
530
+ row .append ('^' * (dp_end_offset - dp_start_offset ))
521
531
522
532
row .append ('\n ' )
523
533
@@ -638,6 +648,25 @@ def _extract_caret_anchors_from_line_segment(segment):
638
648
639
649
return None
640
650
651
+ _WIDE_CHAR_SPECIFIERS = "WF"
652
+
653
+ def _display_width (line , offset ):
654
+ """Calculate the extra amount of width space the given source
655
+ code segment might take if it were to be displayed on a fixed
656
+ width output device. Supports wide unicode characters and emojis."""
657
+
658
+ # Fast track for ASCII-only strings
659
+ if line .isascii ():
660
+ return offset
661
+
662
+ import unicodedata
663
+
664
+ return sum (
665
+ 2 if unicodedata .east_asian_width (char ) in _WIDE_CHAR_SPECIFIERS else 1
666
+ for char in line [:offset ]
667
+ )
668
+
669
+
641
670
642
671
class _ExceptionPrintContext :
643
672
def __init__ (self ):
0 commit comments