Skip to content

Commit 3ed6987

Browse files
committed
Tighten _pyrepl prompt caching and follow-ups
Finish the refactor with prompt-cell caching, typed aliases, and layout edge-case fixes. Land the remaining render and reader regressions needed for the final shape.
1 parent b121d64 commit 3ed6987

20 files changed

+447
-435
lines changed

Lib/_pyrepl/commands.py

Lines changed: 2 additions & 1 deletion
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
@@ -36,7 +37,7 @@
3637
from .trace import trace
3738

3839
# types
39-
if False:
40+
if TYPE_CHECKING:
4041
from .historical_reader import HistoricalReader
4142

4243

Lib/_pyrepl/completing_reader.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
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
@@ -30,8 +31,8 @@
3031

3132
# types
3233
Command = commands.Command
33-
if False:
34-
from .types import KeySpec, CommandName
34+
if TYPE_CHECKING:
35+
from .types import CommandName, KeySpec, Keymap
3536

3637

3738
def prefix(wordlist: list[str], j: int = 0) -> str:
@@ -189,13 +190,14 @@ def do(self) -> None:
189190
r.insert(p)
190191
if last_is_completer:
191192
r.cmpltn_menu_visible = True
192-
r.cmpltn_message_visible = False
193193
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
194194
r.console, completions, r.cmpltn_menu_end,
195195
r.use_brackets, r.sort_in_column)
196+
if r.msg:
197+
r.msg = ""
198+
r.invalidate_message()
196199
r.invalidate_overlay()
197200
elif not r.cmpltn_menu_visible:
198-
r.cmpltn_message_visible = True
199201
if stem + p in completions:
200202
r.msg = "[ complete but not unique ]"
201203
r.invalidate_message()
@@ -239,7 +241,6 @@ class CompletingReader(Reader):
239241
### Instance variables
240242
cmpltn_menu: list[str] = field(init=False)
241243
cmpltn_menu_visible: bool = field(init=False)
242-
cmpltn_message_visible: bool = field(init=False)
243244
cmpltn_menu_end: int = field(init=False)
244245
cmpltn_menu_choices: list[str] = field(init=False)
245246

@@ -250,7 +251,7 @@ def __post_init__(self) -> None:
250251
self.commands[c.__name__] = c
251252
self.commands[c.__name__.replace('_', '-')] = c
252253

253-
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
254+
def collect_keymap(self) -> Keymap:
254255
return super().collect_keymap() + (
255256
(r'\t', 'complete'),)
256257

@@ -266,6 +267,7 @@ def get_screen_overlays(self) -> tuple[ScreenOverlay, ...]:
266267
ScreenOverlay(
267268
self.lxy[1] + 1,
268269
tuple(RenderLine.from_rendered_text(line) for line in self.cmpltn_menu),
270+
insert=True,
269271
),
270272
)
271273

@@ -278,7 +280,6 @@ def cmpltn_reset(self) -> None:
278280
self.invalidate_overlay()
279281
self.cmpltn_menu = []
280282
self.cmpltn_menu_visible = False
281-
self.cmpltn_message_visible = False
282283
self.cmpltn_menu_end = 0
283284
self.cmpltn_menu_choices = []
284285

Lib/_pyrepl/console.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,15 @@
2929
from dataclasses import dataclass
3030
import re
3131
import sys
32+
from typing import TYPE_CHECKING
3233

3334
from .render import RenderedScreen
3435
from .trace import trace
3536

36-
37-
TYPE_CHECKING = False
38-
3937
if TYPE_CHECKING:
40-
from typing import IO
41-
from typing import Callable
38+
from typing import Callable, IO
39+
40+
from .types import CursorXY
4241

4342

4443
@dataclass
@@ -50,7 +49,7 @@ class Event:
5049

5150
@dataclass
5251
class Console(ABC):
53-
posxy: tuple[int, int] = (0, 0)
52+
posxy: CursorXY = (0, 0)
5453
height: int = 25
5554
width: int = 80
5655
_redraw_debug_palette: tuple[str, ...] = (
@@ -94,7 +93,7 @@ def screen(self) -> list[str]:
9493
def sync_rendered_screen(
9594
self,
9695
rendered_screen: RenderedScreen,
97-
posxy: tuple[int, int] | None = None,
96+
posxy: CursorXY | None = None,
9897
) -> None:
9998
if posxy is None:
10099
posxy = rendered_screen.cursor

Lib/_pyrepl/content.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
from dataclasses import dataclass
44

5-
from .render import StyleRef
6-
from .utils import ColorSpan, THEME, iter_display_chars, unbracket, wlen
5+
from .utils import ColorSpan, StyleRef, THEME, iter_display_chars, unbracket, wlen
76

87

98
@dataclass(frozen=True, slots=True)

Lib/_pyrepl/input.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@
3838
from abc import ABC, abstractmethod
3939
import unicodedata
4040
from collections import deque
41+
from typing import TYPE_CHECKING
4142

4243

4344
# types
44-
if False:
45+
if TYPE_CHECKING:
4546
from .types import EventTuple
4647

4748

Lib/_pyrepl/layout.py

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
from dataclasses import dataclass
4+
from typing import Self
45

56
from .content import ContentFragment, ContentLine
7+
from .types import CursorXY, ScreenInfoRow
68

79

810
@dataclass(frozen=True, slots=True)
@@ -17,7 +19,7 @@ def width(self) -> int:
1719
return self.prompt_width + sum(self.char_widths) + self.suffix_width
1820

1921
@property
20-
def screeninfo(self) -> tuple[int, list[int]]:
22+
def screeninfo(self) -> ScreenInfoRow:
2123
widths = list(self.char_widths)
2224
if self.suffix_width:
2325
widths.append(self.suffix_width)
@@ -29,11 +31,11 @@ class LayoutMap:
2931
rows: tuple[LayoutRow, ...]
3032

3133
@classmethod
32-
def empty(cls) -> LayoutMap:
34+
def empty(cls) -> Self:
3335
return cls((LayoutRow(0, ()),))
3436

3537
@property
36-
def screeninfo(self) -> list[tuple[int, list[int]]]:
38+
def screeninfo(self) -> list[ScreenInfoRow]:
3739
return [row.screeninfo for row in self.rows]
3840

3941
def max_column(self, y: int) -> int:
@@ -42,13 +44,18 @@ def max_column(self, y: int) -> int:
4244
def max_row(self) -> int:
4345
return len(self.rows) - 1
4446

45-
def pos_to_xy(self, pos: int) -> tuple[int, int]:
47+
def pos_to_xy(self, pos: int) -> CursorXY:
4648
if not self.rows:
4749
return 0, 0
4850

4951
remaining = pos
5052
for y, row in enumerate(self.rows):
5153
if remaining <= len(row.char_widths):
54+
# Prompt-only leading rows are terminal scenery, not real
55+
# buffer positions. Treating them as real just manufactures
56+
# bugs.
57+
if remaining == 0 and not row.char_widths and row.buffer_advance == 0 and y < len(self.rows) - 1:
58+
continue
5259
x = row.prompt_width
5360
for width in row.char_widths[:remaining]:
5461
x += width
@@ -58,15 +65,26 @@ def pos_to_xy(self, pos: int) -> tuple[int, int]:
5865
return last_row.width - last_row.suffix_width, len(self.rows) - 1
5966

6067
def xy_to_pos(self, x: int, y: int) -> int:
68+
if not self.rows:
69+
return 0
70+
6171
pos = 0
6272
for row in self.rows[:y]:
6373
pos += row.buffer_advance
6474

6575
row = self.rows[y]
6676
cur_x = row.prompt_width
67-
for width in row.char_widths:
77+
char_widths = row.char_widths
78+
i = 0
79+
for i, width in enumerate(char_widths):
6880
if cur_x >= x:
69-
break
81+
# Include trailing zero-width (combining) chars at this position
82+
for trailing_width in char_widths[i:]:
83+
if trailing_width == 0:
84+
pos += 1
85+
else:
86+
break
87+
return pos
7088
if width == 0:
7189
pos += 1
7290
continue
@@ -99,12 +117,16 @@ def layout_content_lines(
99117
width: int,
100118
start_offset: int,
101119
) -> LayoutResult:
120+
if width <= 0:
121+
return LayoutResult((), LayoutMap(()), ())
122+
102123
offset = start_offset
103124
wrapped_rows: list[WrappedRow] = []
104125
layout_rows: list[LayoutRow] = []
105126
line_end_offsets: list[int] = []
106127

107128
for line in lines:
129+
newline_advance = int(line.source.has_newline)
108130
for leading in line.prompt.leading_lines:
109131
line_end_offsets.append(offset)
110132
wrapped_rows.append(
@@ -117,56 +139,62 @@ def layout_content_lines(
117139

118140
prompt_text = line.prompt.text
119141
prompt_width = line.prompt.width
120-
remaining = list(line.body)
121-
remaining_widths = [fragment.width for fragment in remaining]
142+
body = tuple(line.body)
143+
body_widths = tuple(fragment.width for fragment in body)
122144

123-
if not remaining_widths or (sum(remaining_widths) + prompt_width) // width == 0:
124-
offset += len(remaining) + (1 if line.source.has_newline else 0)
145+
if not body_widths or (sum(body_widths) + prompt_width) < width:
146+
offset += len(body) + newline_advance
125147
line_end_offsets.append(offset)
126148
wrapped_rows.append(
127149
WrappedRow(
128150
prompt_text=prompt_text,
129151
prompt_width=prompt_width,
130-
fragments=tuple(remaining),
131-
layout_widths=tuple(remaining_widths),
132-
buffer_advance=len(remaining) + (1 if line.source.has_newline else 0),
152+
fragments=body,
153+
layout_widths=body_widths,
154+
buffer_advance=len(body) + newline_advance,
133155
line_end_offset=offset,
134156
)
135157
)
136158
layout_rows.append(
137159
LayoutRow(
138160
prompt_width,
139-
tuple(remaining_widths),
140-
buffer_advance=len(remaining) + (1 if line.source.has_newline else 0),
161+
body_widths,
162+
buffer_advance=len(body) + newline_advance,
141163
)
142164
)
143165
continue
144166

145167
current_prompt = prompt_text
146168
current_prompt_width = prompt_width
169+
start = 0
170+
total = len(body)
147171
while True:
148172
index_to_wrap_before = 0
149173
column = 0
150-
for char_width in remaining_widths:
174+
for char_width in body_widths[start:]:
151175
if column + char_width + current_prompt_width >= width:
152176
break
153177
index_to_wrap_before += 1
154178
column += char_width
155179

156-
at_line_end = len(remaining) <= index_to_wrap_before
180+
if index_to_wrap_before == 0 and start < total:
181+
index_to_wrap_before = 1 # force progress
182+
183+
at_line_end = (start + index_to_wrap_before) >= total
157184
if at_line_end:
158-
offset += index_to_wrap_before + (1 if line.source.has_newline else 0)
185+
offset += index_to_wrap_before + newline_advance
159186
suffix = ""
160187
suffix_width = 0
161-
buffer_advance = index_to_wrap_before + (1 if line.source.has_newline else 0)
188+
buffer_advance = index_to_wrap_before + newline_advance
162189
else:
163190
offset += index_to_wrap_before
164191
suffix = "\\"
165192
suffix_width = 1
166193
buffer_advance = index_to_wrap_before
167194

168-
row_fragments = tuple(remaining[:index_to_wrap_before])
169-
row_widths = tuple(remaining_widths[:index_to_wrap_before])
195+
end = start + index_to_wrap_before
196+
row_fragments = body[start:end]
197+
row_widths = body_widths[start:end]
170198
line_end_offsets.append(offset)
171199
wrapped_rows.append(
172200
WrappedRow(
@@ -189,8 +217,7 @@ def layout_content_lines(
189217
)
190218
)
191219

192-
remaining = remaining[index_to_wrap_before:]
193-
remaining_widths = remaining_widths[index_to_wrap_before:]
220+
start = end
194221
current_prompt = ""
195222
current_prompt_width = 0
196223
if at_line_end:

0 commit comments

Comments
 (0)