diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 1eccf2ec6..a448f0724 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -402,7 +402,7 @@ def __init__( # so we're just using the same object self.interact = self.status_bar - # line currently being edited, without ps1 (usually '>>> ') + # logical line currently being edited, without ps1 (usually '>>> ') self._current_line = "" # current line of output - stdout and stdin go here @@ -744,8 +744,9 @@ def process_key_event(self, e): ) and self.config.curtsies_right_arrow_completion and self.cursor_offset == len(self.current_line) + # if at end of current line and user presses RIGHT (to autocomplete) ): - + # then autocomplete self.current_line += self.current_suggestion self.cursor_offset = len(self.current_line) elif e in ("",) + key_dispatch[self.config.up_one_line_key]: @@ -1435,6 +1436,23 @@ def current_output_line(self, value): self.current_stdouterr_line = "" self.stdin.current_line = "\n" + def number_of_padding_chars_on_current_cursor_line(self): + """ To avoid cutting off two-column characters at the end of lines where + there's only one column left, curtsies adds a padding char (u' '). + It's important to know about these for cursor positioning. + + Should return zero unless there are fullwidth characters. """ + full_line = self.current_cursor_line_without_suggestion + line_with_padding = "".join( + line.s + for line in paint.display_linize( + self.current_cursor_line_without_suggestion.s, self.width + ) + ) + + # the difference in length here is how much padding there is + return len(line_with_padding) - len(full_line) + def paint( self, about_to_exit=False, @@ -1620,7 +1638,8 @@ def move_screen_up(current_line_start_row): wcswidth(self.current_cursor_line_without_suggestion.s) - wcswidth(self.current_line) + wcswidth(self.current_line[: self.cursor_offset]) - ), + ) + + self.number_of_padding_chars_on_current_cursor_line(), width, ) assert cursor_column >= 0, ( diff --git a/bpython/curtsiesfrontend/replpainter.py b/bpython/curtsiesfrontend/replpainter.py index b1fdf3a81..8a4c94b62 100644 --- a/bpython/curtsiesfrontend/replpainter.py +++ b/bpython/curtsiesfrontend/replpainter.py @@ -26,20 +26,24 @@ def display_linize(msg, columns, blank_line=False): """Returns lines obtained by splitting msg over multiple lines. Warning: if msg is empty, returns an empty list of lines""" - display_lines = ( - [ - msg[start:end] - for start, end in zip( - range(0, len(msg), columns), - range(columns, len(msg) + columns, columns), - ) - ] - if msg - else ([""] if blank_line else []) - ) + if not msg: + return [''] if blank_line else [] + msg = fmtstr(msg) + try: + display_lines = list(msg.width_aware_splitlines(columns)) + # use old method if wcwidth can't determine width of msg + except ValueError: + display_lines = ( + [ + msg[start:end] + for start, end in zip( + range(0, len(msg), columns), + range(columns, len(msg) + columns, columns), + ) + ] + ) return display_lines - def paint_history(rows, columns, display_lines): lines = [] for r, line in zip(range(rows), display_lines[-rows:]): diff --git a/bpython/test/test_curtsies_painting.py b/bpython/test/test_curtsies_painting.py index 15424c662..21d96521e 100644 --- a/bpython/test/test_curtsies_painting.py +++ b/bpython/test/test_curtsies_painting.py @@ -271,6 +271,32 @@ def send_key(self, key): self.repl.process_event("" if key == " " else key) self.repl.paint() # has some side effects we need to be wary of +class TestWidthAwareness(HigherLevelCurtsiesPaintingTest): + + def test_cursor_position_with_fullwidth_char(self): + self.repl.add_normal_character("間") + + cursor_pos = self.repl.paint()[1] + self.assertEqual(cursor_pos, (0,6)) + + def test_cursor_position_with_padding_char(self): + # odd numbered so fullwidth chars don't wrap evenly + self.repl.width = 11 + [self.repl.add_normal_character(c) for c in "width"] + + cursor_pos = self.repl.paint()[1] + self.assertEqual(cursor_pos, (1,4)) + + def test_display_of_padding_chars(self): + self.repl.width = 11 + [self.repl.add_normal_character(c) for c in "width"] + + self.enter() + expected = [ + '>>> wid ', # <--- note the added trailing space + 'th'] + result = [d.s for d in self.repl.display_lines[0:2]] + self.assertEqual(result, expected) class TestCurtsiesRewindRedraw(HigherLevelCurtsiesPaintingTest): def test_rewind(self):