diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py index c4b95fa2e81ee6..7a17c31f126abe 100644 --- a/Lib/_pyrepl/historical_reader.py +++ b/Lib/_pyrepl/historical_reader.py @@ -31,7 +31,7 @@ isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple( - [("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"] + [("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) not in ("\\", "\x1b")] + [(c, "isearch-add-character") for c in map(chr, range(32, 127)) if c != "\\"] + [ ("\\%03o" % c, "isearch-add-character") @@ -45,6 +45,7 @@ (r"\C-c", "isearch-cancel"), (r"\C-g", "isearch-cancel"), (r"\", "isearch-backspace"), + (r"\x1b[200~", "isearch-bracketed-paste"), ] ) @@ -209,6 +210,21 @@ def do(self) -> None: r.pop_input_trans() r.dirty = True +class isearch_bracketed_paste(commands.Command): + 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.dirty = True + if "".join(b[r.pos:r.pos+len(r.isearch_term)]) != r.isearch_term: + r.isearch_next() + @dataclass class HistoricalReader(Reader): @@ -245,6 +261,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, diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 35a1733787e7a2..6839147b944a3f 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -1509,6 +1509,44 @@ def test_bracketed_paste_single_line(self): output = multiline_input(reader) self.assertEqual(output, input_code) + def test_bracketed_paste_in_isearch(self): + paste_start = "\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")), + ], + code_to_events(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 + code_to_events(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): diff --git a/Misc/NEWS.d/next/Library/2026-03-02-02-51-27.gh-issue-145375.j9r8TS.rst b/Misc/NEWS.d/next/Library/2026-03-02-02-51-27.gh-issue-145375.j9r8TS.rst new file mode 100644 index 00000000000000..4f9eda901db34f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-02-02-51-27.gh-issue-145375.j9r8TS.rst @@ -0,0 +1 @@ +Handle bracketed paste in :mod:`!_pyrepl` isearch mode.