Skip to content
Open
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
6 changes: 6 additions & 0 deletions Lib/_pyrepl/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
> (
Expand Down
2 changes: 1 addition & 1 deletion Lib/_pyrepl/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 17 additions & 4 deletions Lib/test/test_pyrepl/test_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_pyrepl/test_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix the :term:`REPL` mangling typed input when :data:`sys.ps1` is wider than
the terminal.
Loading