From 696a4ecc69e4e41d40225d80f0d7540bc2b98dbb Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 19 Oct 2025 23:35:34 +0800 Subject: [PATCH 01/10] Fix REPL cursor position on Windows when module completion suggestion line hits console width --- Lib/_pyrepl/windows_console.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c56dcd6d7dd434..67990d2ef9b708 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -283,9 +283,11 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - # If we wrapped we want to start at the next line - self._move_relative(0, y + 1) - self.posxy = 0, y + 1 + # Wrapping with self._move_relative(0, y+1) can't move cursor down + # here. Windows keeps the cursor at the end of the line. It only + # wraps when the next character is written. + # https://github.com/microsoft/terminal/issues/349 + self.posxy = wlen(newline) - 1, y else: self.posxy = wlen(newline), y From fe459b87718b573443e16cf4f57a315681617186 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 19 Oct 2025 23:44:57 +0800 Subject: [PATCH 02/10] blurb --- .../next/Windows/2025-10-19-23-44-46.gh-issue-140131.AABF2k.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Windows/2025-10-19-23-44-46.gh-issue-140131.AABF2k.rst diff --git a/Misc/NEWS.d/next/Windows/2025-10-19-23-44-46.gh-issue-140131.AABF2k.rst b/Misc/NEWS.d/next/Windows/2025-10-19-23-44-46.gh-issue-140131.AABF2k.rst new file mode 100644 index 00000000000000..3c2d30d8d9813d --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-10-19-23-44-46.gh-issue-140131.AABF2k.rst @@ -0,0 +1,2 @@ +Fix REPL cursor position on Windows when module completion suggestion line +hits console width. From ea2157134692c8782cd8082aceb0495229c461e5 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:13:16 +0800 Subject: [PATCH 03/10] Ensure storing the actual cursor column --- Lib/_pyrepl/windows_console.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 67990d2ef9b708..25c25c25969417 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -287,7 +287,7 @@ def __write_changed_line( # here. Windows keeps the cursor at the end of the line. It only # wraps when the next character is written. # https://github.com/microsoft/terminal/issues/349 - self.posxy = wlen(newline) - 1, y + self.posxy = self.screen_xy[0], y else: self.posxy = wlen(newline), y From 5c0593b1ba8d1904a7e63c49b69806d13c1d4ba3 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:15:48 +0800 Subject: [PATCH 04/10] Add test --- Lib/test/test_pyrepl/test_windows_console.py | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index f9607e02c604ff..11950ba7ec9ced 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -6,6 +6,7 @@ import itertools +import rlcompleter from functools import partial from test.support import force_not_colorized_test_class from typing import Iterable @@ -16,7 +17,10 @@ from .support import prepare_reader as default_prepare_reader try: + from _pyrepl._module_completer import ModuleCompleter from _pyrepl.console import Event, Console + from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig + from _pyrepl.utils import wlen from _pyrepl.windows_console import ( WindowsConsole, MOVE_LEFT, @@ -63,6 +67,45 @@ def handle_events( prepare_reader = prepare_reader or default_prepare_reader return handle_all_events(events, prepare_console, prepare_reader) + def test_cursor_position_console_width_completion_suggestion(self): + height, width = 25, 80 + config = ReadlineConfig() + namespace = {"interpreter": None, "process": None, "thread": None} + config.module_completer = ModuleCompleter(namespace) + code = "from concurrent.futures import \t\tt\n" + con = WindowsConsole(encoding="utf-8") + con.getheightwidth = MagicMock(return_value=(height, width)) + con.height = height + con.width = width + + def assert_posxy_changed_in_parallel_with_screenxy(method): + def wrapper(y, oldline, newline, px_coord): + newline = newline.ljust(width) + posxy_before = con.posxy + screen_xy_before = con.screen_xy + result = method(y, oldline, newline, px_coord) + posxy_after = con.posxy + screen_xy_after = con.screen_xy + posxy_delta = ( + posxy_after[0] - posxy_before[0], + posxy_after[1] - posxy_before[1],) + screen_xy_delta = ( + screen_xy_after[0] - screen_xy_before[0], + screen_xy_after[1] - screen_xy_before[1],) + self.assertEqual(posxy_delta, screen_xy_delta) + return result + return wrapper + + original_method = con._WindowsConsole__write_changed_line + con._WindowsConsole__write_changed_line = ( + assert_posxy_changed_in_parallel_with_screenxy(original_method) + ) + + for c in code: + con.event_queue.push(bytes(c.encode("utf-8"))) + reader = ReadlineAlikeReader(console=con, config=config) + reader.readline() + def handle_events_narrow(self, events): return self.handle_events(events, width=5) From 742096db38f5e6632d1363e9a62b91eb9b06f678 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:16:53 +0800 Subject: [PATCH 05/10] Remove irrelevant self.dch1 comment and unused px_pos variable --- Lib/_pyrepl/windows_console.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 25c25c25969417..16718b05f24bf0 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -249,22 +249,10 @@ def input_hook(self): def __write_changed_line( self, y: int, oldline: str, newline: str, px_coord: int ) -> None: - # this is frustrating; there's no reason to test (say) - # self.dch1 inside the loop -- but alternative ways of - # structuring this function are equally painful (I'm trying to - # avoid writing code generators these days...) minlen = min(wlen(oldline), wlen(newline)) x_pos = 0 x_coord = 0 - px_pos = 0 - j = 0 - for c in oldline: - if j >= px_coord: - break - j += wlen(c) - px_pos += 1 - # reuse the oldline as much as possible, but stop as soon as we # encounter an ESCAPE, because it might be the start of an escape # sequence From 3c3d0aa7eb51f424090df195798ee90d85eb15d6 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:20:35 +0800 Subject: [PATCH 06/10] Revert unrelated change "Remove irrelevant self.dch1 comment and unused px_pos variable" This reverts commit 742096db38f5e6632d1363e9a62b91eb9b06f678. --- Lib/_pyrepl/windows_console.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 16718b05f24bf0..25c25c25969417 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -249,10 +249,22 @@ def input_hook(self): def __write_changed_line( self, y: int, oldline: str, newline: str, px_coord: int ) -> None: + # this is frustrating; there's no reason to test (say) + # self.dch1 inside the loop -- but alternative ways of + # structuring this function are equally painful (I'm trying to + # avoid writing code generators these days...) minlen = min(wlen(oldline), wlen(newline)) x_pos = 0 x_coord = 0 + px_pos = 0 + j = 0 + for c in oldline: + if j >= px_coord: + break + j += wlen(c) + px_pos += 1 + # reuse the oldline as much as possible, but stop as soon as we # encounter an ESCAPE, because it might be the start of an escape # sequence From 45031764a60330fb47abf4413cc90842a01375da Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:24:31 +0800 Subject: [PATCH 07/10] Remove unused import --- Lib/test/test_pyrepl/test_windows_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 11950ba7ec9ced..a397afec09e151 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -20,7 +20,6 @@ from _pyrepl._module_completer import ModuleCompleter from _pyrepl.console import Event, Console from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig - from _pyrepl.utils import wlen from _pyrepl.windows_console import ( WindowsConsole, MOVE_LEFT, From 1dc243b00c377e94adf86a6426a1d26a32896f59 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 22:26:35 +0800 Subject: [PATCH 08/10] Remove another unused import --- Lib/test/test_pyrepl/test_windows_console.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index a397afec09e151..02cd8c5e3cfe98 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -6,7 +6,6 @@ import itertools -import rlcompleter from functools import partial from test.support import force_not_colorized_test_class from typing import Iterable From 53b13146419051c293eddb3bbc3dc7eebe8930f7 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 23:43:46 +0800 Subject: [PATCH 09/10] Current fix breaks other tests, revert all changes --- Lib/_pyrepl/windows_console.py | 8 ++-- Lib/test/test_pyrepl/test_windows_console.py | 41 -------------------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index 25c25c25969417..c56dcd6d7dd434 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -283,11 +283,9 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: - # Wrapping with self._move_relative(0, y+1) can't move cursor down - # here. Windows keeps the cursor at the end of the line. It only - # wraps when the next character is written. - # https://github.com/microsoft/terminal/issues/349 - self.posxy = self.screen_xy[0], y + # If we wrapped we want to start at the next line + self._move_relative(0, y + 1) + self.posxy = 0, y + 1 else: self.posxy = wlen(newline), y diff --git a/Lib/test/test_pyrepl/test_windows_console.py b/Lib/test/test_pyrepl/test_windows_console.py index 02cd8c5e3cfe98..f9607e02c604ff 100644 --- a/Lib/test/test_pyrepl/test_windows_console.py +++ b/Lib/test/test_pyrepl/test_windows_console.py @@ -16,9 +16,7 @@ from .support import prepare_reader as default_prepare_reader try: - from _pyrepl._module_completer import ModuleCompleter from _pyrepl.console import Event, Console - from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig from _pyrepl.windows_console import ( WindowsConsole, MOVE_LEFT, @@ -65,45 +63,6 @@ def handle_events( prepare_reader = prepare_reader or default_prepare_reader return handle_all_events(events, prepare_console, prepare_reader) - def test_cursor_position_console_width_completion_suggestion(self): - height, width = 25, 80 - config = ReadlineConfig() - namespace = {"interpreter": None, "process": None, "thread": None} - config.module_completer = ModuleCompleter(namespace) - code = "from concurrent.futures import \t\tt\n" - con = WindowsConsole(encoding="utf-8") - con.getheightwidth = MagicMock(return_value=(height, width)) - con.height = height - con.width = width - - def assert_posxy_changed_in_parallel_with_screenxy(method): - def wrapper(y, oldline, newline, px_coord): - newline = newline.ljust(width) - posxy_before = con.posxy - screen_xy_before = con.screen_xy - result = method(y, oldline, newline, px_coord) - posxy_after = con.posxy - screen_xy_after = con.screen_xy - posxy_delta = ( - posxy_after[0] - posxy_before[0], - posxy_after[1] - posxy_before[1],) - screen_xy_delta = ( - screen_xy_after[0] - screen_xy_before[0], - screen_xy_after[1] - screen_xy_before[1],) - self.assertEqual(posxy_delta, screen_xy_delta) - return result - return wrapper - - original_method = con._WindowsConsole__write_changed_line - con._WindowsConsole__write_changed_line = ( - assert_posxy_changed_in_parallel_with_screenxy(original_method) - ) - - for c in code: - con.event_queue.push(bytes(c.encode("utf-8"))) - reader = ReadlineAlikeReader(console=con, config=config) - reader.readline() - def handle_events_narrow(self, events): return self.handle_events(events, width=5) From f776e93f733d7ea3963b3a04223f9accaf856e82 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 20 Oct 2025 23:54:53 +0800 Subject: [PATCH 10/10] Write newline to let wrapping take effect --- Lib/_pyrepl/windows_console.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py index c56dcd6d7dd434..d75b9f69599115 100644 --- a/Lib/_pyrepl/windows_console.py +++ b/Lib/_pyrepl/windows_console.py @@ -284,6 +284,7 @@ def __write_changed_line( self.__write(newline[x_pos:]) if wlen(newline) == self.width: # If we wrapped we want to start at the next line + self.__write("\r\n") self._move_relative(0, y + 1) self.posxy = 0, y + 1 else: