diff --git a/Lib/_pyrepl/console.py b/Lib/_pyrepl/console.py index 8956fb1242e52a..0f079bdadb5d7b 100644 --- a/Lib/_pyrepl/console.py +++ b/Lib/_pyrepl/console.py @@ -82,6 +82,9 @@ def restore(self) -> None: ... @abstractmethod def move_cursor(self, x: int, y: int) -> None: ... + @abstractmethod + def sync_screen(self) -> None: ... + @abstractmethod def set_cursor_vis(self, visible: bool) -> None: ... diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 7fc2422dac9c3f..19d5b5804cbf39 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -716,6 +716,7 @@ def handle1(self, block: bool = True) -> bool: elif event.evt == "scroll": self.refresh() elif event.evt == "resize": + self.console.sync_screen() self.refresh() else: translate = False diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 96379bc20f3357..eabddb9162ff24 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -233,6 +233,7 @@ def refresh(self, screen, c_xy): Parameters: - screen (list): List of strings representing the screen contents. - c_xy (tuple): Cursor position (x, y) on the screen. + - repaint (bool): If True, overwrite the old screen and not reuse. """ cx, cy = c_xy if not self.__gone_tall: @@ -324,6 +325,62 @@ def move_cursor(self, x, y): self.posxy = x, y self.flushoutput() + def sync_screen(self): + """ + Synchronize self.posxy, self.screen, self.width and self.height. + Assuming that the content of the screen doesn't change, only the width changes. + """ + if not self.screen: + self.posxy = 0, 0 + return + + px, py = self.posxy + old_height, old_width = self.height, self.width + new_height, new_width = self.getheightwidth() + + groups = [] + x, y = 0, 0 + new_line = True + for i, line in enumerate(self.screen): + l = wlen(line) + if i == py: + if new_line: + y = sum(wlen(g) // new_width for g in groups) + len(groups) - 1 + x = px + else: + y = sum(wlen(g) // new_width for g in groups[:-1]) + len(groups) - 1 + x = px + wlen(groups[-1]) + if x >= new_width: + y += x // new_width + x %= new_width + + if new_line: + groups.append(line) + new_line = False + else: + groups[-1] += line + if l != old_width: + new_line = True + + new_screen = [] + for group in groups: + l = 0 + line = "" + for c in group: + cw = wlen(c) + if l + cw > new_width: + new_screen.append(line) + line = c + l = cw + else: + line += c + l += cw + if line: + new_screen.append(line) + + self.posxy = x, y + self.height, self.width = new_height, new_width + def prepare(self): """ Prepare the console for input/output operations. @@ -757,7 +814,6 @@ def __move_tall(self, x, y): self.__write_code(self._cup, y - self.__offset, x) def __sigwinch(self, signum, frame): - self.height, self.width = self.getheightwidth() self.event_queue.insert(Event("resize", None)) def __hide_cursor(self): diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 47fd3fd8f8909b..844ac56cbb06a3 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -165,6 +165,7 @@ def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: Parameters: - screen (list): List of strings representing the screen contents. - c_xy (tuple): Cursor position (x, y) on the screen. + - repaint (bool): If True, overwrite the old screen and not reuse. """ cx, cy = c_xy @@ -384,6 +385,62 @@ def move_cursor(self, x: int, y: int) -> None: self._move_relative(x, y) self.posxy = x, y + def sync_screen(self): + """ + Synchronize self.posxy, self.screen, self.width and self.height. + Assuming that the content of the screen doesn't change, only the width changes. + """ + if not self.screen: + self.posxy = 0, 0 + return + + px, py = self.posxy + old_height, old_width = self.height, self.width + new_height, new_width = self.getheightwidth() + + groups = [] + x, y = 0, 0 + new_line = True + for i, line in enumerate(self.screen): + l = wlen(line) + if i == py: + if new_line: + y = sum(wlen(g) // new_width for g in groups) + len(groups) - 1 + x = px + else: + y = sum(wlen(g) // new_width for g in groups[:-1]) + len(groups) - 1 + x = px + wlen(groups[-1]) + if x >= new_width: + y += x // new_width + x %= new_width + + if new_line: + groups.append(line) + new_line = False + else: + groups[-1] += line + if l != old_width: + new_line = True + + new_screen = [] + for group in groups: + l = 0 + line = "" + for c in group: + cw = wlen(c) + if l + cw > new_width: + new_screen.append(line) + line = c + l = cw + else: + line += c + l += cw + if line: + new_screen.append(line) + + self.posxy = x, y + self.height, self.width = new_height, new_width + def set_cursor_vis(self, visible: bool) -> None: if visible: self._show_cursor() diff --git a/Lib/test/test_pyrepl/support.py b/Lib/test/test_pyrepl/support.py index 3692e164cb9254..bd02e8fd142538 100644 --- a/Lib/test/test_pyrepl/support.py +++ b/Lib/test/test_pyrepl/support.py @@ -146,6 +146,9 @@ def restore(self) -> None: def move_cursor(self, x: int, y: int) -> None: pass + def sync_screen(self) -> None: + pass + def set_cursor_vis(self, visible: bool) -> None: pass diff --git a/Lib/test/test_pyrepl/test_unix_console.py b/Lib/test/test_pyrepl/test_unix_console.py index 057cdd112852dc..283ac87586256c 100644 --- a/Lib/test/test_pyrepl/test_unix_console.py +++ b/Lib/test/test_pyrepl/test_unix_console.py @@ -257,7 +257,7 @@ def test_resize_bigger_on_multiline_function(self, _os_write): ) console.height = 2 - console.getheightwidth = MagicMock(lambda _: (2, 80)) + console.getheightwidth = MagicMock(return_value=(2, 80)) def same_reader(_): return reader @@ -293,7 +293,7 @@ def test_resize_smaller_on_multiline_function(self, _os_write): reader, console = handle_events_unix_console_height_3(events) console.height = 1 - console.getheightwidth = MagicMock(lambda _: (1, 80)) + console.getheightwidth = MagicMock(return_value=(1, 80)) def same_reader(_): return reader diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 07eaccd1124cd6..f622104fe08c6a 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -89,7 +89,7 @@ def test_resize_wider(self): console.height = 20 console.width = 80 - console.getheightwidth = MagicMock(lambda _: (20, 80)) + console.getheightwidth = MagicMock(return_value=(20, 80)) def same_reader(_): return reader @@ -117,7 +117,7 @@ def test_resize_narrower(self): console.height = 20 console.width = 4 - console.getheightwidth = MagicMock(lambda _: (20, 4)) + console.getheightwidth = MagicMock(return_value=(20, 4)) def same_reader(_): return reader @@ -251,7 +251,7 @@ def test_resize_bigger_on_multiline_function(self): reader, console = self.handle_events_short(events) console.height = 2 - console.getheightwidth = MagicMock(lambda _: (2, 80)) + console.getheightwidth = MagicMock(return_value=(2, 80)) def same_reader(_): return reader @@ -289,7 +289,7 @@ def test_resize_smaller_on_multiline_function(self): reader, console = self.handle_events_height_3(events) console.height = 1 - console.getheightwidth = MagicMock(lambda _: (1, 80)) + console.getheightwidth = MagicMock(return_value=(1, 80)) def same_reader(_): return reader diff --git a/Misc/NEWS.d/next/Library/2025-04-09-12-00-07.gh-issue-132267.0mItcB.rst b/Misc/NEWS.d/next/Library/2025-04-09-12-00-07.gh-issue-132267.0mItcB.rst new file mode 100644 index 00000000000000..e3a22025552e90 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-09-12-00-07.gh-issue-132267.0mItcB.rst @@ -0,0 +1 @@ +Fix Console.posxy and Console.screen desynchronization caused by resize.