diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index cf8b1a6c1e4ec4d..cd52d96de1112fa 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -265,6 +265,12 @@ def do(self) -> None: r.error("start of buffer") return + # Skip prompt-only rows, they hold no buffer positions + row = r.layout.rows[new_y] + while new_y > 0 and not row.char_widths and not row.buffer_advance: + new_y -= 1 + row = r.layout.rows[new_y] + if ( x > ( diff --git a/Lib/_pyrepl/layout.py b/Lib/_pyrepl/layout.py index cff0821cb95f72b..74a326d4b6ffda3 100644 --- a/Lib/_pyrepl/layout.py +++ b/Lib/_pyrepl/layout.py @@ -215,7 +215,7 @@ def layout_content_lines( index_to_wrap_before += 1 column += char_width - if index_to_wrap_before == 0 and start < total: + if index_to_wrap_before == 0 and not current_prompt_width: index_to_wrap_before = 1 # force progress at_line_end = (start + index_to_wrap_before) >= total diff --git a/Lib/test/test_pyrepl/test_layout.py b/Lib/test/test_pyrepl/test_layout.py index 6825659b772d212..e6ead695d553dba 100644 --- a/Lib/test/test_pyrepl/test_layout.py +++ b/Lib/test/test_pyrepl/test_layout.py @@ -229,17 +229,30 @@ def test_line_wraps(self): def test_wrapping_forces_progress(self): # When a single character is wider than available space, force 1 char - prompt = _prompt("P", 1) - body = (ContentFragment("W", 1),) + prompt = _prompt("", 0) + body = (ContentFragment("W", 2),) source = _source("W", has_newline=False) line = ContentLine(source, prompt, body) - # width=2 means prompt(1) + char(1) = 2, which fits (< width would be - # false for width=2 since 1+1 >= 2), so it wraps but forces progress + # width=2 means char(2) >= width, so it wraps but forces progress result = layout_content_lines((line,), 2, 0) self.assertEqual(len(result.wrapped_rows), 1) self.assertEqual(result.wrapped_rows[0].buffer_advance, 1) + def test_prompt_wider_than_terminal(self): + line = _content_line("Hello", prompt=_prompt("a" * 65, 65)) + result = layout_content_lines((line,), 20, 0) + + self.assertEqual(len(result.wrapped_rows), 2) + first, second = result.wrapped_rows + self.assertEqual(first.prompt_width, 65) + self.assertEqual(first.fragments, ()) + self.assertEqual(first.suffix, "\\") + self.assertEqual(first.buffer_advance, 0) + self.assertEqual(second.prompt_width, 0) + self.assertEqual(second.buffer_advance, 6) + self.assertEqual(result.layout_map.pos_to_xy(0), (0, 1)) + def test_layout_map_matches_wrapped_rows(self): line = _content_line("abc") result = layout_content_lines((line,), 80, 0) diff --git a/Lib/test/test_pyrepl/test_reader.py b/Lib/test/test_pyrepl/test_reader.py index 0b32ead357c2c0d..57bb56dfead9495 100644 --- a/Lib/test/test_pyrepl/test_reader.py +++ b/Lib/test/test_pyrepl/test_reader.py @@ -236,6 +236,22 @@ def test_up_arrow_after_ctrl_r(self): reader, _ = handle_all_events(events) self.assertIn(reader.screen, ([], [""])) + def test_up_arrow_with_prompt_wider_than_terminal(self): + events = itertools.chain( + code_to_events("x\nabc"), + [Event(evt="key", data="up", raw=bytearray(b"\x1bOA"))], + ) + reader, _ = handle_all_events( + events, + prepare_console=functools.partial(prepare_console, width=20), + prepare_reader=functools.partial( + prepare_reader, get_prompt=lambda lineno, cursor_on_line: "a" * 30 + ), + ) + # the prompt-only row above "abc" is skipped, landing on "x" + self.assertEqual(reader.pos, 1) + self.assertEqual(reader.cxy, (1, 1)) + def test_newline_within_block_trailing_whitespace(self): # fmt: off code = ( diff --git a/Misc/NEWS.d/next/Library/2026-10-18-03-47-18.gh-issue-159658.7vC6hv.rst b/Misc/NEWS.d/next/Library/2026-10-18-03-47-18.gh-issue-159658.7vC6hv.rst new file mode 100644 index 000000000000000..3b3952d483700ee --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-18-03-47-18.gh-issue-159658.7vC6hv.rst @@ -0,0 +1,2 @@ +Fix the :term:`REPL` mangling typed input when :data:`sys.ps1` is wider than +the terminal.