@@ -465,39 +465,49 @@ def format_frame_summary(self, frame_summary):
465465 stripped_line = frame_summary .line .strip ()
466466 row .append (' {}\n ' .format (stripped_line ))
467467
468- orig_line_len = len (frame_summary ._original_line )
468+ line = frame_summary ._original_line
469+ orig_line_len = len (line )
469470 frame_line_len = len (frame_summary .line .lstrip ())
470471 stripped_characters = orig_line_len - frame_line_len
471472 if (
472473 frame_summary .colno is not None
473474 and frame_summary .end_colno is not None
474475 ):
475476 start_offset = _byte_offset_to_character_offset (
476- frame_summary . _original_line , frame_summary .colno ) + 1
477+ line , frame_summary .colno )
477478 end_offset = _byte_offset_to_character_offset (
478- frame_summary ._original_line , frame_summary .end_colno ) + 1
479+ line , frame_summary .end_colno )
480+ code_segment = line [start_offset :end_offset ]
479481
480482 anchors = None
481483 if frame_summary .lineno == frame_summary .end_lineno :
482484 with suppress (Exception ):
483- anchors = _extract_caret_anchors_from_line_segment (
484- frame_summary ._original_line [start_offset - 1 :end_offset - 1 ]
485- )
485+ anchors = _extract_caret_anchors_from_line_segment (code_segment )
486486 else :
487- end_offset = stripped_characters + len (stripped_line )
487+ # Don't count the newline since the anchors only need to
488+ # go up until the last character of the line.
489+ end_offset = len (line .rstrip ())
488490
489491 # show indicators if primary char doesn't span the frame line
490492 if end_offset - start_offset < len (stripped_line ) or (
491493 anchors and anchors .right_start_offset - anchors .left_end_offset > 0 ):
494+ # When showing this on a terminal, some of the non-ASCII characters
495+ # might be rendered as double-width characters, so we need to take
496+ # that into account when calculating the length of the line.
497+ dp_start_offset = _display_width (line , start_offset ) + 1
498+ dp_end_offset = _display_width (line , end_offset ) + 1
499+
492500 row .append (' ' )
493- row .append (' ' * (start_offset - stripped_characters ))
501+ row .append (' ' * (dp_start_offset - stripped_characters ))
494502
495503 if anchors :
496- row .append (anchors .primary_char * (anchors .left_end_offset ))
497- row .append (anchors .secondary_char * (anchors .right_start_offset - anchors .left_end_offset ))
498- row .append (anchors .primary_char * (end_offset - start_offset - anchors .right_start_offset ))
504+ dp_left_end_offset = _display_width (code_segment , anchors .left_end_offset )
505+ dp_right_start_offset = _display_width (code_segment , anchors .right_start_offset )
506+ row .append (anchors .primary_char * dp_left_end_offset )
507+ row .append (anchors .secondary_char * (dp_right_start_offset - dp_left_end_offset ))
508+ row .append (anchors .primary_char * (dp_end_offset - dp_start_offset - dp_right_start_offset ))
499509 else :
500- row .append ('^' * (end_offset - start_offset ))
510+ row .append ('^' * (dp_end_offset - dp_start_offset ))
501511
502512 row .append ('\n ' )
503513
@@ -618,6 +628,25 @@ def _extract_caret_anchors_from_line_segment(segment):
618628
619629 return None
620630
631+ _WIDE_CHAR_SPECIFIERS = "WF"
632+
633+ def _display_width (line , offset ):
634+ """Calculate the extra amount of width space the given source
635+ code segment might take if it were to be displayed on a fixed
636+ width output device. Supports wide unicode characters and emojis."""
637+
638+ # Fast track for ASCII-only strings
639+ if line .isascii ():
640+ return offset
641+
642+ import unicodedata
643+
644+ return sum (
645+ 2 if unicodedata .east_asian_width (char ) in _WIDE_CHAR_SPECIFIERS else 1
646+ for char in line [:offset ]
647+ )
648+
649+
621650
622651class _ExceptionPrintContext :
623652 def __init__ (self ):
0 commit comments