diff --git a/src/prompt_toolkit/renderer.py b/src/prompt_toolkit/renderer.py index fcfde223e..b96db3f32 100644 --- a/src/prompt_toolkit/renderer.py +++ b/src/prompt_toolkit/renderer.py @@ -201,6 +201,16 @@ def get_max_column_index(row: dict[int, Char]) -> int: new_max_line_len = min(width - 1, get_max_column_index(new_row)) previous_max_line_len = min(width - 1, get_max_column_index(previous_row)) + # Whether this row has any real (non-blank) content at all. A row + # with nothing in it still gets a max index of 0 from + # `get_max_column_index`, same as a row whose only visible + # character happens to sit at column 0, so that can't be used here + # to tell the two apart. + new_row_has_content = any( + cell.char != " " or style_string_has_style[cell.style] + for cell in new_row.values() + ) + # Loop over the columns. c = 0 # Column counter. while c <= new_max_line_len: @@ -218,7 +228,12 @@ def get_max_column_index(row: dict[int, Char]) -> int: if c in zero_width_escapes_row: write_raw(zero_width_escapes_row[c]) - output_char(new_char) + # Don't bother drawing a blank row just because its style + # differs from whatever was there before (this is what + # happens the first time an otherwise-empty line renders). + # There's nothing worth repainting. + if new_row_has_content: + output_char(new_char) current_pos = Point(x=current_pos.x + char_width, y=current_pos.y) c += char_width diff --git a/tests/test_renderer.py b/tests/test_renderer.py new file mode 100644 index 000000000..13758e0ca --- /dev/null +++ b/tests/test_renderer.py @@ -0,0 +1,98 @@ +""" +Tests for `prompt_toolkit.renderer._output_screen_diff`. +""" + +from __future__ import annotations + +from prompt_toolkit.data_structures import Point, Size +from prompt_toolkit.layout.screen import Char, Screen +from prompt_toolkit.output import ColorDepth, DummyOutput +from prompt_toolkit.renderer import ( + _output_screen_diff, + _StyleStringHasStyleCache, + _StyleStringToAttrsCache, +) +from prompt_toolkit.styles import DummyStyleTransformation, Style + + +class _CapturingOutput(DummyOutput): + "Like `DummyOutput`, but remembers everything written to it." + + def __init__(self) -> None: + self.written: list[str] = [] + + def write(self, data: str) -> None: + self.written.append(data) + + +def _render(new_row_cells): + """ + Render a single ten-column, one-row screen against a blank previous + screen, given the (column, char, style) cells to put in the new row. + Returns the concatenation of everything that got written to the output. + """ + style = Style.from_dict({}) + attrs_for_style_string = _StyleStringToAttrsCache( + style.get_attrs_for_style_str, DummyStyleTransformation() + ) + style_string_has_style = _StyleStringHasStyleCache(attrs_for_style_string) + + previous_screen = Screen() + previous_screen.width = 10 + previous_screen.height = 1 + + screen = Screen() + screen.width = 10 + screen.height = 1 + for column, char, cell_style in new_row_cells: + screen.data_buffer[0][column] = Char(char, cell_style) + + output = _CapturingOutput() + + _output_screen_diff( + app=None, + output=output, + screen=screen, + current_pos=Point(x=0, y=0), + color_depth=ColorDepth.DEPTH_1_BIT, + previous_screen=previous_screen, + last_style=None, + is_done=True, + full_screen=False, + attrs_for_style_string=attrs_for_style_string, + style_string_has_style=style_string_has_style, + size=Size(rows=1, columns=10), + previous_width=10, + ) + return "".join(output.written) + + +def test_blank_row_does_not_redraw_a_space(): + """ + A line that never got any real content (just the default blank cell at + column 0) shouldn't have a space written for it, even on the first + render where its style differs from the previous screen's default. + """ + written = _render([(0, " ", "")]) + assert " " not in written + + +def test_content_at_column_zero_is_still_drawn(): + """ + A line whose only visible character happens to sit at column 0 must + still be drawn. This is the case a naive "skip column 0 when the row's + max index is 0" fix would break, since a lone character at column 0 + also has a max index of 0. + """ + written = _render([(0, "$", "")]) + assert "$" in written + + +def test_content_after_column_zero_is_still_drawn(): + """ + A line with real content beyond column 0 is unaffected by the blank-row + check either way, but is worth pinning down alongside the two cases + above. + """ + written = _render([(0, " ", ""), (1, "$", "")]) + assert "$" in written