Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
41e8b78
Add _pyrepl render primitives
pablogsal Mar 23, 2026
8ed00a8
Switch _pyrepl consoles to rendered screens
pablogsal Mar 23, 2026
65c8481
Extract _pyrepl content and layout helpers
pablogsal Mar 23, 2026
bc71f4d
Preserve style metadata in _pyrepl cells
pablogsal Mar 23, 2026
654385c
Refactor _pyrepl refresh invalidation state
pablogsal Mar 23, 2026
fa45e48
Separate _pyrepl overlays from base rendering
pablogsal Mar 23, 2026
ae551ea
Tighten _pyrepl prompt caching and follow-ups
pablogsal Mar 23, 2026
3883037
Update content.py
pablogsal Mar 31, 2026
270df8b
Fix _pyrepl incremental refresh and render edge cases
pablogsal Apr 4, 2026
a1026c0
fixup! Fix _pyrepl incremental refresh and render edge cases
pablogsal Apr 5, 2026
1e79b00
Merge branch 'main' into pyrepl-new
pablogsal Apr 5, 2026
164c692
Apply suggestions from code review
ambv Apr 7, 2026
9e3ebb3
Restore deleted Lib/test/test_pyrepl/test_render.py
ambv Apr 7, 2026
999c94e
Achieve 100% statement and branch coverage in test_render
ambv Apr 7, 2026
4f68c04
Add a test suite for _pyrepl.layout
ambv Apr 7, 2026
42a0304
Fix lint error
johnslavik Apr 7, 2026
b0c8ac3
Fix lint error
johnslavik Apr 7, 2026
0177c2d
Add more docstrings/comments
johnslavik Apr 7, 2026
58ae9cb
Add visual docs (wip)
johnslavik Apr 7, 2026
8bd3cd2
Add visual docs for `PromptContent`
johnslavik Apr 7, 2026
08df3d6
Fix spacing
johnslavik Apr 7, 2026
df102d6
Better visualization of cells
johnslavik Apr 7, 2026
c13ea9b
Repair `ScreenOverlay` docstring
johnslavik Apr 7, 2026
e9637f4
Trim some docstrings that don't add value
johnslavik Apr 7, 2026
2c750c3
Merge pull request #123 from johnslavik/pablo-pyrepl-docstrings
pablogsal Apr 8, 2026
111a2f8
fixup! Merge pull request #123 from johnslavik/pablo-pyrepl-docstrings
pablogsal Apr 8, 2026
2d360ab
Fix history mode exiting on Windows
johnslavik Apr 8, 2026
2a4f5a1
Lint (fix trailing whitespace)
johnslavik Apr 8, 2026
bb602ec
Merge pull request #124 from johnslavik/pablo-pyrepl-windows
pablogsal Apr 8, 2026
82a5f68
Merge branch 'main' into pyrepl-new
johnslavik Apr 8, 2026
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
50 changes: 33 additions & 17 deletions Lib/_pyrepl/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from __future__ import annotations
import os
import time
from typing import TYPE_CHECKING

# Categories of actions:
# killing
Expand All @@ -32,10 +33,11 @@
# finishing
# [completion]

from .render import RenderedScreen
from .trace import trace

# types
if False:
if TYPE_CHECKING:
from .historical_reader import HistoricalReader


Expand Down Expand Up @@ -74,7 +76,7 @@ def kill_range(self, start: int, end: int) -> None:
else:
r.kill_ring.append(text)
r.pos = start
r.dirty = True
r.invalidate_buffer(start)


class YankCommand(Command):
Expand Down Expand Up @@ -125,24 +127,27 @@ def do(self) -> None:
r.arg = 10 * r.arg - d
else:
r.arg = 10 * r.arg + d
r.dirty = True
r.invalidate_prompt()


class clear_screen(Command):
def do(self) -> None:
r = self.reader
trace("command.clear_screen")
r.console.clear()
r.dirty = True
r.invalidate_full()


class refresh(Command):
def do(self) -> None:
self.reader.dirty = True
trace("command.refresh")
self.reader.invalidate_full()


class repaint(Command):
def do(self) -> None:
self.reader.dirty = True
trace("command.repaint")
self.reader.invalidate_full()
self.reader.console.repaint()


Expand Down Expand Up @@ -208,9 +213,10 @@ def do(self) -> None:
repl = len(r.kill_ring[-1])
r.kill_ring.insert(0, r.kill_ring.pop())
t = r.kill_ring[-1]
start = r.pos - repl
b[r.pos - repl : r.pos] = t
r.pos = r.pos - repl + len(t)
r.dirty = True
r.invalidate_buffer(start)


class interrupt(FinishCommand):
Expand Down Expand Up @@ -242,8 +248,9 @@ def do(self) -> None:
r.console.prepare()
r.pos = p
# r.posxy = 0, 0 # XXX this is invalid
r.dirty = True
r.console.screen = []
r.invalidate_full()
trace("command.suspend sync_rendered_screen")
r.console.sync_rendered_screen(RenderedScreen.empty(), r.console.posxy)


class up(MotionCommand):
Expand Down Expand Up @@ -369,14 +376,15 @@ class self_insert(EditCommand):
def do(self) -> None:
r = self.reader
text = self.event * r.get_arg()
start = r.pos
r.insert(text)
if r.paste_mode:
data = ""
ev = r.console.getpending()
data += ev.data
if data:
r.insert(data)
r.last_refresh_cache.invalidated = True
r.invalidate_buffer(start)


class insert_nl(EditCommand):
Expand All @@ -400,20 +408,23 @@ def do(self) -> None:
del b[s]
b.insert(t, c)
r.pos = t
r.dirty = True
r.invalidate_buffer(s)


class backspace(EditCommand):
def do(self) -> None:
r = self.reader
b = r.buffer
changed_from: int | None = None
for i in range(r.get_arg()):
if r.pos > 0:
r.pos -= 1
del b[r.pos]
r.dirty = True
changed_from = r.pos if changed_from is None else min(changed_from, r.pos)
else:
self.reader.error("can't backspace at start")
if changed_from is not None:
r.invalidate_buffer(changed_from)


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

changed_from: int | None = None
for i in range(r.get_arg()):
if r.pos != len(b):
del b[r.pos]
r.dirty = True
changed_from = r.pos if changed_from is None else min(changed_from, r.pos)
else:
self.reader.error("end of buffer")
if changed_from is not None:
r.invalidate_buffer(changed_from)


class accept(FinishCommand):
Expand Down Expand Up @@ -478,14 +492,17 @@ def do(self) -> None:

# We need to copy over the state so that it's consistent between
# console and reader, and console does not overwrite/append stuff
self.reader.console.screen = self.reader.screen.copy()
self.reader.console.posxy = self.reader.cxy
trace("command.show_history sync_rendered_screen")
self.reader.console.sync_rendered_screen(
self.reader.rendered_screen,
self.reader.cxy,
)


class paste_mode(Command):
def do(self) -> None:
self.reader.paste_mode = not self.reader.paste_mode
self.reader.dirty = True
self.reader.invalidate_prompt()


class perform_bracketed_paste(Command):
Expand All @@ -502,4 +519,3 @@ def do(self) -> None:
s=time.time() - start,
)
self.reader.insert(data.replace(done, ""))
self.reader.last_refresh_cache.invalidated = True
54 changes: 31 additions & 23 deletions Lib/_pyrepl/completing_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,18 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

import re
from . import commands, console, reader
from .render import RenderLine, ScreenOverlay
from .reader import Reader


# types
Command = commands.Command
TYPE_CHECKING = False
if TYPE_CHECKING:
from .types import KeySpec, CommandName, CompletionAction
from .types import CommandName, CompletionAction, Keymap, KeySpec


def prefix(wordlist: list[str], j: int = 0) -> str:
Expand Down Expand Up @@ -175,6 +176,8 @@ def do(self) -> None:
r.cmpltn_action = None # consumed
if msg:
r.msg = msg
r.cmpltn_message_visible = True
r.invalidate_message()
else: # other input since last tab: cancel action
r.cmpltn_action = None

Expand All @@ -192,7 +195,8 @@ def do(self) -> None:
completion = stripcolor(completions[0])
if completions_unchangable and len(completion) == len(stem):
r.msg = "[ sole completion ]"
r.dirty = True
r.cmpltn_message_visible = True
r.invalidate_message()
r.insert(completion[len(stem):])
else:
clean_completions = [stripcolor(word) for word in completions]
Expand All @@ -201,19 +205,23 @@ def do(self) -> None:
r.insert(p)
if last_is_completer:
r.cmpltn_menu_visible = True
r.cmpltn_message_visible = False
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
r.console, completions, r.cmpltn_menu_end,
r.use_brackets, r.sort_in_column)
r.dirty = True
if r.msg:
r.msg = ""
r.cmpltn_message_visible = False
r.invalidate_message()
r.invalidate_overlay()
elif not r.cmpltn_menu_visible:
r.cmpltn_message_visible = True
if stem + p in clean_completions:
r.msg = "[ complete but not unique ]"
r.dirty = True
r.cmpltn_message_visible = True
r.invalidate_message()
else:
r.msg = "[ not unique ]"
r.dirty = True
r.cmpltn_message_visible = True
r.invalidate_message()

if r.cmpltn_action:
if r.msg and r.cmpltn_message_visible:
Expand All @@ -223,7 +231,7 @@ def do(self) -> None:
else:
r.msg = r.cmpltn_action[0]
r.cmpltn_message_visible = True
r.dirty = True
r.invalidate_message()


class self_insert(commands.self_insert):
Expand All @@ -243,6 +251,7 @@ def do(self) -> None:
r.cmpltn_menu, r.cmpltn_menu_end = build_menu(
r.console, completions, 0,
r.use_brackets, r.sort_in_column)
r.invalidate_overlay()
else:
r.cmpltn_reset()

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

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

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

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

def finish(self) -> None:
super().finish()
self.cmpltn_reset()

def cmpltn_reset(self) -> None:
if getattr(self, "cmpltn_menu_visible", False):
self.invalidate_overlay()
self.cmpltn_menu = []
self.cmpltn_menu_visible = False
self.cmpltn_message_visible = False
Expand Down
Loading
Loading