Skip to content

Commit 09968dd

Browse files
authored
gh-148105: _pyrepl: switch console refresh to structured rendered screens (#146584)
1 parent 442f83a commit 09968dd

23 files changed

+2982
-586
lines changed

Lib/_pyrepl/commands.py

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from __future__ import annotations
2323
import os
2424
import time
25+
from typing import TYPE_CHECKING
2526

2627
# Categories of actions:
2728
# killing
@@ -32,10 +33,11 @@
3233
# finishing
3334
# [completion]
3435

36+
from .render import RenderedScreen
3537
from .trace import trace
3638

3739
# types
38-
if False:
40+
if TYPE_CHECKING:
3941
from .historical_reader import HistoricalReader
4042

4143

@@ -74,7 +76,7 @@ def kill_range(self, start: int, end: int) -> None:
7476
else:
7577
r.kill_ring.append(text)
7678
r.pos = start
77-
r.dirty = True
79+
r.invalidate_buffer(start)
7880

7981

8082
class YankCommand(Command):
@@ -125,24 +127,27 @@ def do(self) -> None:
125127
r.arg = 10 * r.arg - d
126128
else:
127129
r.arg = 10 * r.arg + d
128-
r.dirty = True
130+
r.invalidate_prompt()
129131

130132

131133
class clear_screen(Command):
132134
def do(self) -> None:
133135
r = self.reader
136+
trace("command.clear_screen")
134137
r.console.clear()
135-
r.dirty = True
138+
r.invalidate_full()
136139

137140

138141
class refresh(Command):
139142
def do(self) -> None:
140-
self.reader.dirty = True
143+
trace("command.refresh")
144+
self.reader.invalidate_full()
141145

142146

143147
class repaint(Command):
144148
def do(self) -> None:
145-
self.reader.dirty = True
149+
trace("command.repaint")
150+
self.reader.invalidate_full()
146151
self.reader.console.repaint()
147152

148153

@@ -208,9 +213,10 @@ def do(self) -> None:
208213
repl = len(r.kill_ring[-1])
209214
r.kill_ring.insert(0, r.kill_ring.pop())
210215
t = r.kill_ring[-1]
216+
start = r.pos - repl
211217
b[r.pos - repl : r.pos] = t
212218
r.pos = r.pos - repl + len(t)
213-
r.dirty = True
219+
r.invalidate_buffer(start)
214220

215221

216222
class interrupt(FinishCommand):
@@ -242,8 +248,9 @@ def do(self) -> None:
242248
r.console.prepare()
243249
r.pos = p
244250
# r.posxy = 0, 0 # XXX this is invalid
245-
r.dirty = True
246-
r.console.screen = []
251+
r.invalidate_full()
252+
trace("command.suspend sync_rendered_screen")
253+
r.console.sync_rendered_screen(RenderedScreen.empty(), r.console.posxy)
247254

248255

249256
class up(MotionCommand):
@@ -369,14 +376,15 @@ class self_insert(EditCommand):
369376
def do(self) -> None:
370377
r = self.reader
371378
text = self.event * r.get_arg()
379+
start = r.pos
372380
r.insert(text)
373381
if r.paste_mode:
374382
data = ""
375383
ev = r.console.getpending()
376384
data += ev.data
377385
if data:
378386
r.insert(data)
379-
r.last_refresh_cache.invalidated = True
387+
r.invalidate_buffer(start)
380388

381389

382390
class insert_nl(EditCommand):
@@ -400,20 +408,23 @@ def do(self) -> None:
400408
del b[s]
401409
b.insert(t, c)
402410
r.pos = t
403-
r.dirty = True
411+
r.invalidate_buffer(s)
404412

405413

406414
class backspace(EditCommand):
407415
def do(self) -> None:
408416
r = self.reader
409417
b = r.buffer
418+
changed_from: int | None = None
410419
for i in range(r.get_arg()):
411420
if r.pos > 0:
412421
r.pos -= 1
413422
del b[r.pos]
414-
r.dirty = True
423+
changed_from = r.pos if changed_from is None else min(changed_from, r.pos)
415424
else:
416425
self.reader.error("can't backspace at start")
426+
if changed_from is not None:
427+
r.invalidate_buffer(changed_from)
417428

418429

419430
class delete(EditCommand):
@@ -431,12 +442,15 @@ def do(self) -> None:
431442
r.console.finish()
432443
raise EOFError
433444

445+
changed_from: int | None = None
434446
for i in range(r.get_arg()):
435447
if r.pos != len(b):
436448
del b[r.pos]
437-
r.dirty = True
449+
changed_from = r.pos if changed_from is None else min(changed_from, r.pos)
438450
else:
439451
self.reader.error("end of buffer")
452+
if changed_from is not None:
453+
r.invalidate_buffer(changed_from)
440454

441455

442456
class accept(FinishCommand):
@@ -450,6 +464,7 @@ def do(self) -> None:
450464

451465
with self.reader.suspend():
452466
self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment]
467+
self.reader.invalidate_prompt()
453468

454469

455470
class invalid_key(Command):
@@ -470,22 +485,24 @@ def do(self) -> None:
470485
from .pager import get_pager
471486
from site import gethistoryfile
472487

488+
# After the pager exits, the screen state is unknown (Unix may
489+
# restore via alternate screen, Windows shows pager output).
490+
# Clear and force a full redraw at the end for consistency.
491+
self.reader.console.clear()
492+
473493
history = os.linesep.join(self.reader.history[:])
474494
self.reader.console.restore()
475495
pager = get_pager()
476496
pager(history, gethistoryfile())
477497
self.reader.console.prepare()
478498

479-
# We need to copy over the state so that it's consistent between
480-
# console and reader, and console does not overwrite/append stuff
481-
self.reader.console.screen = self.reader.screen.copy()
482-
self.reader.console.posxy = self.reader.cxy
499+
self.reader.invalidate_full()
483500

484501

485502
class paste_mode(Command):
486503
def do(self) -> None:
487504
self.reader.paste_mode = not self.reader.paste_mode
488-
self.reader.dirty = True
505+
self.reader.invalidate_prompt()
489506

490507

491508
class perform_bracketed_paste(Command):
@@ -502,4 +519,3 @@ def do(self) -> None:
502519
s=time.time() - start,
503520
)
504521
self.reader.insert(data.replace(done, ""))
505-
self.reader.last_refresh_cache.invalidated = True

Lib/_pyrepl/completing_reader.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@
2121
from __future__ import annotations
2222

2323
from dataclasses import dataclass, field
24+
from typing import TYPE_CHECKING
2425

2526
import re
2627
from . import commands, console, reader
28+
from .render import RenderLine, ScreenOverlay
2729
from .reader import Reader
2830

2931

3032
# types
3133
Command = commands.Command
32-
TYPE_CHECKING = False
3334
if TYPE_CHECKING:
34-
from .types import KeySpec, CommandName, CompletionAction
35+
from .types import CommandName, CompletionAction, Keymap, KeySpec
3536

3637

3738
def prefix(wordlist: list[str], j: int = 0) -> str:
@@ -175,6 +176,8 @@ def do(self) -> None:
175176
r.cmpltn_action = None # consumed
176177
if msg:
177178
r.msg = msg
179+
r.cmpltn_message_visible = True
180+
r.invalidate_message()
178181
else: # other input since last tab: cancel action
179182
r.cmpltn_action = None
180183

@@ -192,7 +195,8 @@ def do(self) -> None:
192195
completion = stripcolor(completions[0])
193196
if completions_unchangable and len(completion) == len(stem):
194197
r.msg = "[ sole completion ]"
195-
r.dirty = True
198+
r.cmpltn_message_visible = True
199+
r.invalidate_message()
196200
r.insert(completion[len(stem):])
197201
else:
198202
clean_completions = [stripcolor(word) for word in completions]
@@ -201,19 +205,23 @@ def do(self) -> None:
201205
r.insert(p)
202206
if last_is_completer:
203207
r.cmpltn_menu_visible = True
204-
r.cmpltn_message_visible = False
205208
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
206209
r.console, completions, r.cmpltn_menu_end,
207210
r.use_brackets, r.sort_in_column)
208-
r.dirty = True
211+
if r.msg:
212+
r.msg = ""
213+
r.cmpltn_message_visible = False
214+
r.invalidate_message()
215+
r.invalidate_overlay()
209216
elif not r.cmpltn_menu_visible:
210-
r.cmpltn_message_visible = True
211217
if stem + p in clean_completions:
212218
r.msg = "[ complete but not unique ]"
213-
r.dirty = True
219+
r.cmpltn_message_visible = True
220+
r.invalidate_message()
214221
else:
215222
r.msg = "[ not unique ]"
216-
r.dirty = True
223+
r.cmpltn_message_visible = True
224+
r.invalidate_message()
217225

218226
if r.cmpltn_action:
219227
if r.msg and r.cmpltn_message_visible:
@@ -223,7 +231,7 @@ def do(self) -> None:
223231
else:
224232
r.msg = r.cmpltn_action[0]
225233
r.cmpltn_message_visible = True
226-
r.dirty = True
234+
r.invalidate_message()
227235

228236

229237
class self_insert(commands.self_insert):
@@ -243,6 +251,7 @@ def do(self) -> None:
243251
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
244252
r.console, completions, 0,
245253
r.use_brackets, r.sort_in_column)
254+
r.invalidate_overlay()
246255
else:
247256
r.cmpltn_reset()
248257

@@ -272,7 +281,7 @@ def __post_init__(self) -> None:
272281
self.commands[c.__name__] = c
273282
self.commands[c.__name__.replace('_', '-')] = c
274283

275-
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
284+
def collect_keymap(self) -> Keymap:
276285
return super().collect_keymap() + (
277286
(r'\t', 'complete'),)
278287

@@ -281,25 +290,24 @@ def after_command(self, cmd: Command) -> None:
281290
if not isinstance(cmd, (complete, self_insert)):
282291
self.cmpltn_reset()
283292

284-
def calc_screen(self) -> list[str]:
285-
screen = super().calc_screen()
286-
if self.cmpltn_menu_visible:
287-
# We display the completions menu below the current prompt
288-
ly = self.lxy[1] + 1
289-
screen[ly:ly] = self.cmpltn_menu
290-
# If we're not in the middle of multiline edit, don't append to screeninfo
291-
# since that screws up the position calculation in pos2xy function.
292-
# This is a hack to prevent the cursor jumping
293-
# into the completions menu when pressing left or down arrow.
294-
if self.pos != len(self.buffer):
295-
self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu)
296-
return screen
293+
def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]:
294+
if not self.cmpltn_menu_visible:
295+
return ()
296+
return (
297+
ScreenOverlay(
298+
self.lxy[1] + 1,
299+
tuple(RenderLine.from_rendered_text(line) for line in self.cmpltn_menu),
300+
insert=True,
301+
),
302+
)
297303

298304
def finish(self) -> None:
299305
super().finish()
300306
self.cmpltn_reset()
301307

302308
def cmpltn_reset(self) -> None:
309+
if getattr(self, "cmpltn_menu_visible", False):
310+
self.invalidate_overlay()
303311
self.cmpltn_menu = []
304312
self.cmpltn_menu_visible = False
305313
self.cmpltn_message_visible = False

0 commit comments

Comments
 (0)