Skip to content

Commit fa45e48

Browse files
committed
Separate _pyrepl overlays from base rendering
Move completion and message UI onto overlays layered over the base rendered screen. The completion menu stops pretending it is the base reality.
1 parent 654385c commit fa45e48

3 files changed

Lines changed: 65 additions & 22 deletions

File tree

Lib/_pyrepl/completing_reader.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
import re
2626
from . import commands, console, reader
27-
from .render import RenderLine, RenderedScreen
27+
from .render import RenderLine, ScreenOverlay
2828
from .reader import Reader
2929

3030

@@ -283,15 +283,15 @@ def after_command(self, cmd: Command) -> None:
283283
if not isinstance(cmd, (complete, self_insert)):
284284
self.cmpltn_reset()
285285

286-
def calc_screen(self) -> RenderedScreen:
287-
rendered_screen = super().calc_screen()
288-
if self.cmpltn_menu_visible:
289-
rendered_screen = rendered_screen.with_overlay(
286+
def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]:
287+
if not self.cmpltn_menu_visible:
288+
return ()
289+
return (
290+
ScreenOverlay(
290291
self.lxy[1] + 1,
291-
(RenderLine.from_rendered_text(line) for line in self.cmpltn_menu),
292-
)
293-
self.rendered_screen = rendered_screen
294-
return rendered_screen
292+
tuple(RenderLine.from_rendered_text(line) for line in self.cmpltn_menu),
293+
),
294+
)
295295

296296
def finish(self) -> None:
297297
super().finish()

Lib/_pyrepl/reader.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
process_prompt as build_prompt_content,
3737
)
3838
from .layout import LayoutMap, LayoutResult, LayoutRow, WrappedRow, layout_content_lines
39-
from .render import RenderCell, RenderLine, RenderedScreen, StyleRef
39+
from .render import RenderCell, RenderLine, RenderedScreen, ScreenOverlay, StyleRef
4040
from .utils import ANSI_ESCAPE_SEQUENCE, wlen, gen_colors
4141
from .trace import trace
4242

@@ -380,7 +380,7 @@ def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
380380
return default_keymap
381381

382382
def calc_screen(self) -> RenderedScreen:
383-
"""Translate changes in self.buffer into a structured rendered screen."""
383+
"""Translate the editable buffer into a base rendered screen."""
384384
num_common_lines = 0
385385
offset = 0
386386
if self.last_refresh_cache.valid(self):
@@ -427,12 +427,7 @@ def calc_screen(self) -> RenderedScreen:
427427
layout_rows,
428428
last_refresh_line_end_offsets,
429429
)
430-
431-
render_lines = base_render_lines.copy()
432-
render_lines.extend(self._render_message_lines())
433-
434-
self.rendered_screen = RenderedScreen(tuple(render_lines), self.cxy)
435-
return self.rendered_screen
430+
return RenderedScreen(tuple(base_render_lines), self.cxy)
436431

437432
def _buffer_refresh_from_pos(self) -> int | None:
438433
buffer_from_pos = self.invalidation.buffer_rebuild_from_pos
@@ -536,9 +531,9 @@ def _render_wrapped_rows(
536531
for row in wrapped_rows
537532
]
538533

539-
def _render_message_lines(self) -> list[RenderLine]:
534+
def _render_message_lines(self) -> tuple[RenderLine, ...]:
540535
if not self.msg:
541-
return []
536+
return ()
542537
width = self.console.width
543538
render_lines: list[RenderLine] = []
544539
for message_line in self.msg.split("\n"):
@@ -551,7 +546,19 @@ def _render_message_lines(self) -> list[RenderLine]:
551546
render_lines.append(
552547
RenderLine.from_rendered_text(message_line[offset : offset + width])
553548
)
554-
return render_lines
549+
return tuple(render_lines)
550+
551+
def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]:
552+
return ()
553+
554+
def compose_rendered_screen(self, base_screen: RenderedScreen) -> RenderedScreen:
555+
overlays = list(self.get_screen_overlays())
556+
message_lines = self._render_message_lines()
557+
if message_lines:
558+
overlays.append(ScreenOverlay(len(base_screen.lines), message_lines))
559+
if not overlays:
560+
return base_screen
561+
return RenderedScreen(base_screen.lines, base_screen.cursor, tuple(overlays))
555562

556563
def _render_line(
557564
self,
@@ -763,7 +770,8 @@ def prepare(self) -> None:
763770
self.rendered_screen = RenderedScreen.empty()
764771
self.invalidate_full()
765772
self.last_command = None
766-
self.calc_screen()
773+
base_screen = self.calc_screen()
774+
self.rendered_screen = self.compose_rendered_screen(base_screen)
767775
except BaseException:
768776
self.restore()
769777
raise
@@ -821,7 +829,9 @@ def update_screen(self) -> None:
821829
def refresh(self) -> None:
822830
"""Recalculate and refresh the screen."""
823831
# this call sets up self.cxy, so call it first.
824-
rendered_screen = self.calc_screen()
832+
base_screen = self.calc_screen()
833+
rendered_screen = self.compose_rendered_screen(base_screen)
834+
self.rendered_screen = rendered_screen
825835
trace(
826836
"reader.refresh cursor={cursor} lines={lines} "
827837
"dims=({width},{height}) invalidation={invalidation}",

Lib/test/test_pyrepl/test_reader.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,41 @@ def test_message_refresh_keeps_base_render_cache(self):
324324
reader.update_screen()
325325

326326
self.assertEqual([line.text for line in reader.last_refresh_cache.render_lines], ["ab"])
327+
self.assertEqual([line.text for line in reader.rendered_screen.lines], ["ab"])
328+
self.assertEqual(len(reader.rendered_screen.overlays), 1)
327329
self.assertEqual(reader.screen, ["ab", "! boom "])
328330

331+
def test_completion_overlay_keeps_base_render_cache(self):
332+
namespace = {"itertools": itertools}
333+
code = "itertools."
334+
events = itertools.chain(
335+
code_to_events(code),
336+
[
337+
Event(evt="key", data="\t", raw=bytearray(b"\t")),
338+
Event(evt="key", data="\t", raw=bytearray(b"\t")),
339+
],
340+
)
341+
342+
completing_reader = functools.partial(
343+
prepare_reader,
344+
readline_completer=rlcompleter.Completer(namespace).complete,
345+
)
346+
reader, _ = handle_all_events(events, prepare_reader=completing_reader)
347+
348+
self.assertEqual([line.text for line in reader.last_refresh_cache.render_lines], [code])
349+
self.assertEqual([line.text for line in reader.rendered_screen.lines], [code])
350+
self.assertEqual(len(reader.rendered_screen.overlays), 1)
351+
self.assertEqual(reader.screen[0], code)
352+
self.assertEqual(reader.screen[1].rstrip(), "itertools.accumulate(")
353+
354+
reader.cmpltn_reset()
355+
reader.update_screen()
356+
357+
self.assertEqual([line.text for line in reader.last_refresh_cache.render_lines], [code])
358+
self.assertEqual([line.text for line in reader.rendered_screen.lines], [code])
359+
self.assertEqual(reader.rendered_screen.overlays, ())
360+
self.assertEqual(reader.screen, [code])
361+
329362
def test_completions_updated_on_key_press(self):
330363
namespace = {"itertools": itertools}
331364
code = "itertools."

0 commit comments

Comments
 (0)