Skip to content

Commit 58ae9cb

Browse files
committed
Add visual docs (wip)
1 parent 0177c2d commit 58ae9cb

File tree

5 files changed

+141
-11
lines changed

5 files changed

+141
-11
lines changed

Lib/_pyrepl/content.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@
77

88
@dataclass(frozen=True, slots=True)
99
class ContentFragment:
10+
"""A single display character with its visual width and style.
11+
12+
The body of ``>>> def greet`` becomes one fragment per character::
13+
14+
d e f g r e e t
15+
╰──┴──╯ ╰──┴──┴──┴──╯
16+
keyword (unstyled)
17+
18+
e.g. ``ContentFragment("d", 1, StyleRef(tag="keyword"))``.
19+
"""
20+
1021
text: str
1122
width: int
1223
style: StyleRef = StyleRef()
@@ -21,7 +32,21 @@ class PromptContent:
2132

2233
@dataclass(frozen=True, slots=True)
2334
class SourceLine:
24-
"""One logical line from the editor buffer, before styling."""
35+
"""One logical line from the editor buffer, before styling.
36+
37+
Given this two-line input in the REPL::
38+
39+
>>> def greet(name):
40+
... return name
41+
▲ cursor
42+
43+
The buffer ``"def greet(name):\\n return name"`` yields::
44+
45+
SourceLine(lineno=0, text="def greet(name):",
46+
start_offset=0, has_newline=True)
47+
SourceLine(lineno=1, text=" return name",
48+
start_offset=17, cursor_index=14)
49+
"""
2550

2651
lineno: int
2752
text: str
@@ -36,6 +61,15 @@ def cursor_on_line(self) -> bool:
3661

3762
@dataclass(frozen=True, slots=True)
3863
class ContentLine:
64+
"""A logical line paired with its prompt and styled body.
65+
66+
For ``>>> def greet(name):``::
67+
68+
>>> def greet(name):
69+
╰─╯ ╰──────────────╯
70+
prompt body: one ContentFragment per character
71+
"""
72+
3973
source: SourceLine
4074
prompt: PromptContent
4175
body: tuple[ContentFragment, ...]

Lib/_pyrepl/layout.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,16 @@
1111

1212
@dataclass(frozen=True, slots=True)
1313
class LayoutRow:
14-
"""Metadata for one physical screen row."""
14+
"""Metadata for one physical screen row.
15+
16+
For the row ``>>> def greet(name):``::
17+
18+
>>> def greet(name):
19+
╰─╯ ╰──────────────╯
20+
4 char_widths=(1,1,1,…) ← 16 entries
21+
buffer_advance=17 ← includes the newline
22+
"""
23+
1524
prompt_width: int
1625
char_widths: tuple[int, ...]
1726
suffix_width: int = 0
@@ -33,7 +42,13 @@ def screeninfo(self) -> ScreenInfoRow:
3342
class LayoutMap:
3443
"""Mapping between buffer positions and screen coordinates.
3544
36-
Single source of truth for cursor placement.
45+
Single source of truth for cursor placement. Given::
46+
47+
>>> def greet(name): ← row 0, buffer_advance=17
48+
... return name ← row 1, buffer_advance=15
49+
▲cursor
50+
51+
``pos_to_xy(31)`` → ``(18, 1)``: prompt width 4 + 14 body chars.
3752
"""
3853
rows: tuple[LayoutRow, ...]
3954

@@ -102,7 +117,14 @@ def xy_to_pos(self, x: int, y: int) -> int:
102117

103118
@dataclass(frozen=True, slots=True)
104119
class WrappedRow:
105-
"""One physical screen row after wrapping, with all metadata needed for rendering."""
120+
"""One physical screen row after wrapping, ready for rendering.
121+
122+
When a line overflows the terminal width, it splits into
123+
multiple rows with a ``\\`` continuation marker::
124+
125+
>>> x = "a very long li\\ ← suffix="\\", suffix_width=1
126+
ne that wraps" ← prompt_text="" (continuation)
127+
"""
106128
prompt_text: str = ""
107129
prompt_width: int = 0
108130
fragments: tuple[ContentFragment, ...] = ()
@@ -125,8 +147,13 @@ def layout_content_lines(
125147
start_offset: int,
126148
) -> LayoutResult:
127149
"""Wrap content lines to fit *width* columns.
128-
129-
Line boundaries are marked with ``\\``.
150+
151+
A short line passes through as one ``WrappedRow``; a long line is
152+
split at the column boundary with ``\\`` markers::
153+
154+
>>> short = 1 ← one WrappedRow
155+
>>> x = "a long stri\\ ← two WrappedRows, first has suffix="\\"
156+
ng"
130157
"""
131158
if width <= 0:
132159
return LayoutResult((), LayoutMap(()), ())

Lib/_pyrepl/reader.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,13 @@ def make_default_commands() -> dict[CommandName, CommandClass]:
157157

158158
@dataclass(frozen=True, slots=True)
159159
class RefreshInvalidation:
160-
"""Parts of the screen state that have changed and need to be refreshed."""
160+
"""Which parts of the screen need to be recomputed on the next refresh.
161+
162+
Typing a character sets ``buffer_from_pos`` (rebuild from that offset).
163+
Moving the cursor without editing sets ``cursor_only``.
164+
Resizing the terminal sets ``layout`` (reflow all lines).
165+
Opening a completion menu sets ``overlay``.
166+
"""
161167

162168
cursor_only: bool = False
163169
buffer_from_pos: int | None = None
@@ -310,7 +316,12 @@ class Reader:
310316
## cached metadata to speed up screen refreshes
311317
@dataclass
312318
class RefreshCache:
313-
"""Previously computed render/layout data for incremental refresh."""
319+
"""Previously computed render/layout data for incremental refresh.
320+
321+
Stores the output of the last ``calc_screen`` so that the next
322+
refresh can reuse unchanged rows (e.g. only re-render from the
323+
edited line onward instead of the whole screen).
324+
"""
314325

315326
render_lines: list[RenderLine] = field(default_factory=list)
316327
layout_rows: list[LayoutRow] = field(default_factory=list)

Lib/_pyrepl/render.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ def __getitem__(self, key: str, /) -> str: ...
2424

2525
@dataclass(frozen=True, slots=True)
2626
class RenderCell:
27+
"""One terminal cell: a character, its column width, and SGR style.
28+
29+
A screen row like ``>>> def`` is a sequence of cells::
30+
31+
> > > d e f
32+
╰──┴──╯ styled with keyword SGR escape
33+
"""
34+
2735
text: str
2836
width: int
2937
style: StyleRef = field(default_factory=StyleRef)
@@ -95,6 +103,12 @@ def append_plain_text(segment: str) -> None:
95103

96104
@dataclass(frozen=True, slots=True)
97105
class RenderLine:
106+
"""One physical screen row as a tuple of :class:`RenderCell` objects.
107+
108+
``text`` is the pre-rendered terminal string (characters + SGR escapes);
109+
``width`` is the total visible column count.
110+
"""
111+
98112
cells: tuple[RenderCell, ...]
99113
text: str
100114
width: int
@@ -140,10 +154,15 @@ def from_rendered_text(cls, text: str) -> Self:
140154
class ScreenOverlay:
141155
"""An overlay that replaces or inserts lines at a screen position.
142156
143-
If insert is True, lines are spliced in (shifting content down);
144-
if False (default), lines replace existing content at y.
157+
If *insert* is True, lines are spliced in (shifting content down);
158+
if False (default), lines replace existing content at *y*.
159+
160+
For example, a tab-completion menu inserted below the input::
145161
146-
Overlays are used to display tab completion menus and status messages.
162+
>>> os.path.j ← line 0 (base content)
163+
join ← ScreenOverlay(y=1, insert=True)
164+
junction ← (pushes remaining lines down)
165+
... ← line 1 (shifted down by 2)
147166
"""
148167
y: int
149168
lines: tuple[RenderLine, ...]
@@ -152,6 +171,19 @@ class ScreenOverlay:
152171

153172
@dataclass(frozen=True, slots=True)
154173
class RenderedScreen:
174+
"""The complete screen state: content lines, cursor, and overlays.
175+
176+
``lines`` holds the base content; ``composed_lines`` is the final
177+
result after overlays (completion menus, messages) are applied::
178+
179+
lines: composed_lines:
180+
┌──────────────────┐ ┌──────────────────┐
181+
│>>> os.path.j │ │>>> os.path.j │
182+
│... │ ──► │ join │ ← overlay
183+
└──────────────────┘ │... │
184+
└──────────────────┘
185+
"""
186+
155187
lines: tuple[RenderLine, ...]
156188
cursor: CursorXY
157189
overlays: tuple[ScreenOverlay, ...] = ()
@@ -220,6 +252,17 @@ def screen_lines(self) -> tuple[str, ...]:
220252

221253
@dataclass(frozen=True, slots=True)
222254
class LineDiff:
255+
"""The changed region between an old and new version of one screen row.
256+
257+
When the user types ``e`` so the row changes from
258+
``>>> nam`` to ``>>> name``::
259+
260+
>>> n a m old
261+
>>> n a m e new
262+
╰─╯
263+
start_cell=7, new_cells=("m","e"), old_cells=("m",)
264+
"""
265+
223266
start_cell: int
224267
start_x: int
225268
old_cells: tuple[RenderCell, ...]

Lib/_pyrepl/unix_console.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ def poll(self, timeout: float | None = None) -> list[int]:
151151

152152
@dataclass(frozen=True, slots=True)
153153
class UnixRefreshPlan:
154+
"""Instructions for updating the terminal after a screen change.
155+
156+
After the user types ``e`` to complete ``name``::
157+
158+
Before: >>> def greet(nam|):
159+
160+
LineUpdate here: insert_char "e"
161+
162+
After: >>> def greet(name|):
163+
164+
165+
Only the changed cells are sent to the terminal; unchanged rows
166+
are skipped entirely.
167+
"""
168+
154169
grow_lines: int
155170
"""Number of blank lines to append at the bottom to accommodate new content."""
156171
use_tall_mode: bool

0 commit comments

Comments
 (0)