From 7e72daad2e27757d11a770e0f6f9464e58d4000a Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Tue, 7 May 2024 13:54:56 +0100 Subject: [PATCH] gh-111201: Allow bracketed paste to work (GH-118700) --- Lib/_pyrepl/commands.py | 10 ++++++++++ Lib/_pyrepl/reader.py | 2 ++ Lib/_pyrepl/unix_console.py | 9 +++++++++ Lib/test/test_pyrepl.py | 40 +++++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 60ceb30d2cd77dc..bb6bebace30ec87 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -462,3 +462,13 @@ class paste_mode(Command): def do(self) -> None: self.reader.paste_mode = not self.reader.paste_mode self.reader.dirty = True + + +class enable_bracketed_paste(Command): + def do(self) -> None: + self.reader.paste_mode = True + +class disable_bracketed_paste(Command): + def do(self) -> None: + self.reader.paste_mode = False + self.reader.insert("\n") diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 071dfe54aba8fb3..e36f65c176e81f2 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -127,6 +127,8 @@ def make_default_commands() -> dict[CommandName, type[Command]]: (r"\M-9", "digit-arg"), # (r'\M-\n', 'insert-nl'), ("\\\\", "self-insert"), + (r"\x1b[200~", "enable_bracketed_paste"), + (r"\x1b[201~", "disable_bracketed_paste"), ] + [(c, "self-insert") for c in map(chr, range(32, 127)) if c != "\\"] + [(c, "self-insert") for c in map(chr, range(128, 256)) if c.isalpha()] diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index c22b1d5b5bc2904..605318c82ae2eae 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -336,10 +336,13 @@ def prepare(self): except ValueError: pass + self.__enable_bracketed_paste() + def restore(self): """ Restore the console to the default state """ + self.__disable_bracketed_paste() self.__maybe_write_code(self._rmkx) self.flushoutput() tcsetattr(self.input_fd, termios.TCSADRAIN, self.__svtermstate) @@ -525,6 +528,12 @@ def clear(self): self.__posxy = 0, 0 self.screen = [] + def __enable_bracketed_paste(self) -> None: + os.write(self.output_fd, b"\x1b[?2004h") + + def __disable_bracketed_paste(self) -> None: + os.write(self.output_fd, b"\x1b[?2004l") + def __setup_movement(self): """ Set up the movement functions based on the terminal capabilities. diff --git a/Lib/test/test_pyrepl.py b/Lib/test/test_pyrepl.py index 3df76e02b231dc2..b7ae91b919527a5 100644 --- a/Lib/test/test_pyrepl.py +++ b/Lib/test/test_pyrepl.py @@ -817,6 +817,46 @@ def test_paste_not_in_paste_mode(self): output = multiline_input(reader) self.assertEqual(output, output_code) + def test_bracketed_paste(self): + """Test that bracketed paste using \x1b[200~ and \x1b[201~ works.""" + # fmt: off + input_code = ( + 'def a():\n' + ' for x in range(10):\n' + '\n' + ' if x%2:\n' + ' print(x)\n' + '\n' + ' else:\n' + ' pass\n' + ) + # fmt: on + + output_code = ( + 'def a():\n' + ' for x in range(10):\n' + '\n' + ' if x%2:\n' + ' print(x)\n' + '\n' + ' else:\n' + ' pass\n' + '\n' + ) + + paste_start = "\x1b[200~" + paste_end = "\x1b[201~" + + events = itertools.chain( + code_to_events(paste_start), + code_to_events(input_code), + code_to_events(paste_end), + code_to_events("\n"), + ) + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + class TestReader(TestCase): def assert_screen_equals(self, reader, expected):