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