Skip to content

gh-132267: fix unsynchronized cursor position and buffer mismatch after resize #132313

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/_pyrepl/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...

Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 57 additions & 1 deletion Lib/_pyrepl/unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down
57 changes: 57 additions & 0 deletions Lib/_pyrepl/windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_pyrepl/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_pyrepl/test_unix_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Lib/test/test_pyrepl/test_windows_console.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix Console.posxy and Console.screen desynchronization caused by resize.
Loading