Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions Lib/_pyrepl/base_eventqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,13 @@
from .console import Event
from .trace import trace

PASTE_KEYCODES = {
b'\033[200~': 'bracketed paste',
}

class BaseEventQueue:
def __init__(self, encoding: str, keymap_dict: dict[bytes, str]) -> None:
keymap_dict.update(PASTE_KEYCODES)
self.compiled_keymap = keymap.compile_keymap(keymap_dict)
self.keymap = self.compiled_keymap
trace("keymap {k!r}", k=self.keymap)
Expand Down
18 changes: 18 additions & 0 deletions Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
(r"\C-c", "isearch-cancel"),
(r"\C-g", "isearch-cancel"),
(r"\<backspace>", "isearch-backspace"),
(r"\<bracketed paste>", "isearch-bracketed-paste"),
]
)

Expand Down Expand Up @@ -211,6 +212,22 @@ def do(self) -> None:
r.invalidate_prompt()


class isearch_bracketed_paste(commands.Command):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: two blank lines between top-level classes here.

def do(self) -> None:
r = self.reader
b = r.buffer
done = "\x1b[201~"
data = ""
while done not in data:
ev = r.console.getpending()
data += ev.data
paste_content = data.replace(done, "")
r.isearch_term += paste_content
r.invalidate_prompt()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be r.invalidate_prompt(). dirty is not used here, so the prompt can keep showing the previous search term.

@tanloong tanloong Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r.dirty is not read anywhere in _pyrepl, so this is a no-op and the search prompt won't refresh after the paste. We probably want r.invalidate_prompt() here, like isearch_add_character does.

if "".join(b[r.pos:r.pos+len(r.isearch_term)]) != r.isearch_term:
r.isearch_next()


@dataclass
class HistoricalReader(Reader):
"""Adds history support (with incremental history searching) to the
Expand Down Expand Up @@ -245,6 +262,7 @@ def __post_init__(self) -> None:
isearch_backspace,
isearch_forwards,
isearch_backwards,
isearch_bracketed_paste,
operate_and_get_next,
history_search_backward,
history_search_forward,
Expand Down
1 change: 1 addition & 0 deletions Lib/_pyrepl/keymap.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"space": " ",
"tab": "\t",
"up": "up",
"bracketed paste": "bracketed paste",
}


Expand Down
2 changes: 1 addition & 1 deletion Lib/_pyrepl/reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def make_default_commands() -> dict[CommandName, CommandClass]:
(r"\M-9", "digit-arg"),
(r"\M-\n", "accept"),
("\\\\", "self-insert"),
(r"\x1b[200~", "perform-bracketed-paste"),
(r"\<bracketed paste>", "perform-bracketed-paste"),
(r"\x03", "ctrl-c"),
]
+ [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"]
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_pyrepl/test_eventqueue.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ def test_push_with_keymap_in_keymap_and_escape(self, mock_keymap):
self.assertEqual(eq.events[1].evt, "key")
self.assertEqual(eq.events[1].data, "b")

def test_push_bracketed_paste(self):
eq = self.make_eventqueue()
for byte in b"\x1b[200~":
eq.push(byte)
event = eq.get()
self.assertIsNotNone(event)
self.assertEqual(event.evt, "key")
self.assertEqual(event.data, "bracketed paste")
self.assertEqual(event.raw, b"\x1b[200~")

def test_push_special_key(self):
eq = self.make_eventqueue()
eq.keymap = {}
Expand Down
50 changes: 46 additions & 4 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1953,11 +1953,11 @@ def test_bracketed_paste(self):
)
# fmt: on

paste_start = "\x1b[200~"
paste_start = [Event(evt="key", data="bracketed paste", raw=bytearray(b"\x1b[200~"))]
paste_end = "\x1b[201~"

events = itertools.chain(
code_to_events(paste_start),
paste_start,
code_to_events(input_code),
code_to_events(paste_end),
code_to_events("\n"),
Expand All @@ -1969,11 +1969,11 @@ def test_bracketed_paste(self):
def test_bracketed_paste_single_line(self):
input_code = "oneline"

paste_start = "\x1b[200~"
paste_start = [Event(evt="key", data="bracketed paste", raw=bytearray(b"\x1b[200~"))]
paste_end = "\x1b[201~"

events = itertools.chain(
code_to_events(paste_start),
paste_start,
code_to_events(input_code),
code_to_events(paste_end),
code_to_events("\n"),
Expand All @@ -1982,6 +1982,48 @@ def test_bracketed_paste_single_line(self):
output = multiline_input(reader)
self.assertEqual(output, input_code)

def test_bracketed_paste_in_isearch(self):
# The bracketed-paste *start* marker is translated into a synthetic
# ``bracketed paste`` event by the real event queue; the fake console
# used here does not run that translation, so we feed the
# ``bracketed paste`` event directly.
paste_start = [Event(evt="key", data="bracketed paste", raw=bytearray(b"\x1b[200~"))]
paste_end = "\x1b[201~"

events = itertools.chain(
# Add some history
code_to_events("print('hello')\n"),
# Search for 'hello'
[
Event(evt="key", data="\x12", raw=bytearray(b"\x12")),
],
paste_start,
code_to_events("hello"),
code_to_events(paste_end),
[
Event(evt="key", data="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
],
[
Event(evt="key", data="\x12", raw=bytearray(b"\x12")),
],
# Search for 'world', which should not be found
paste_start,
code_to_events("world"),
code_to_events(paste_end),
[
Event(evt="key", data="\n", raw=bytearray(b"\n")),
Event(evt="key", data="\n", raw=bytearray(b"\n")),
],
)

reader = self.prepare_reader(events)
multiline_input(reader)
output = multiline_input(reader)
self.assertEqual(output, "print('hello')")
output = multiline_input(reader)
self.assertEqual(output, "")


@skipUnless(pty, "requires pty")
class TestDumbTerminal(ReplTestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Handle bracketed paste in :mod:`!_pyrepl` isearch mode.
Loading