@@ -470,39 +470,49 @@ def format_frame_summary(self, frame_summary):
470
470
stripped_line = frame_summary .line .strip ()
471
471
row .append (' {}\n ' .format (stripped_line ))
472
472
473
- orig_line_len = len (frame_summary ._original_line )
473
+ line = frame_summary ._original_line
474
+ orig_line_len = len (line )
474
475
frame_line_len = len (frame_summary .line .lstrip ())
475
476
stripped_characters = orig_line_len - frame_line_len
476
477
if (
477
478
frame_summary .colno is not None
478
479
and frame_summary .end_colno is not None
479
480
):
480
481
start_offset = _byte_offset_to_character_offset (
481
- frame_summary . _original_line , frame_summary .colno ) + 1
482
+ line , frame_summary .colno )
482
483
end_offset = _byte_offset_to_character_offset (
483
- frame_summary ._original_line , frame_summary .end_colno ) + 1
484
+ line , frame_summary .end_colno )
485
+ code_segment = line [start_offset :end_offset ]
484
486
485
487
anchors = None
486
488
if frame_summary .lineno == frame_summary .end_lineno :
487
489
with suppress (Exception ):
488
- anchors = _extract_caret_anchors_from_line_segment (
489
- frame_summary ._original_line [start_offset - 1 :end_offset - 1 ]
490
- )
490
+ anchors = _extract_caret_anchors_from_line_segment (code_segment )
491
491
else :
492
- end_offset = stripped_characters + len (stripped_line )
492
+ # Don't count the newline since the anchors only need to
493
+ # go up until the last character of the line.
494
+ end_offset = len (line .rstrip ())
493
495
494
496
# show indicators if primary char doesn't span the frame line
495
497
if end_offset - start_offset < len (stripped_line ) or (
496
498
anchors and anchors .right_start_offset - anchors .left_end_offset > 0 ):
499
+ # When showing this on a terminal, some of the non-ASCII characters
500
+ # might be rendered as double-width characters, so we need to take
501
+ # that into account when calculating the length of the line.
502
+ dp_start_offset = _display_width (line , start_offset ) + 1
503
+ dp_end_offset = _display_width (line , end_offset ) + 1
504
+
497
505
row .append (' ' )
498
- row .append (' ' * (start_offset - stripped_characters ))
506
+ row .append (' ' * (dp_start_offset - stripped_characters ))
499
507
500
508
if anchors :
501
- row .append (anchors .primary_char * (anchors .left_end_offset ))
502
- row .append (anchors .secondary_char * (anchors .right_start_offset - anchors .left_end_offset ))
503
- row .append (anchors .primary_char * (end_offset - start_offset - anchors .right_start_offset ))
509
+ dp_left_end_offset = _display_width (code_segment , anchors .left_end_offset )
510
+ dp_right_start_offset = _display_width (code_segment , anchors .right_start_offset )
511
+ row .append (anchors .primary_char * dp_left_end_offset )
512
+ row .append (anchors .secondary_char * (dp_right_start_offset - dp_left_end_offset ))
513
+ row .append (anchors .primary_char * (dp_end_offset - dp_start_offset - dp_right_start_offset ))
504
514
else :
505
- row .append ('^' * (end_offset - start_offset ))
515
+ row .append ('^' * (dp_end_offset - dp_start_offset ))
506
516
507
517
row .append ('\n ' )
508
518
@@ -623,6 +633,25 @@ def _extract_caret_anchors_from_line_segment(segment):
623
633
624
634
return None
625
635
636
+ _WIDE_CHAR_SPECIFIERS = "WF"
637
+
638
+ def _display_width (line , offset ):
639
+ """Calculate the extra amount of width space the given source
640
+ code segment might take if it were to be displayed on a fixed
641
+ width output device. Supports wide unicode characters and emojis."""
642
+
643
+ # Fast track for ASCII-only strings
644
+ if line .isascii ():
645
+ return offset
646
+
647
+ import unicodedata
648
+
649
+ return sum (
650
+ 2 if unicodedata .east_asian_width (char ) in _WIDE_CHAR_SPECIFIERS else 1
651
+ for char in line [:offset ]
652
+ )
653
+
654
+
626
655
627
656
class _ExceptionPrintContext :
628
657
def __init__ (self ):
0 commit comments