From 8311b11800509c975023e062e2c336f417c5e4c0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 6 Sep 2024 13:15:00 +0200 Subject: [PATCH] gh-119034, REPL: Change page up/down keys to search in history (#123607) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change and keys of the Python REPL to history search forward/backward. Co-authored-by: Ɓukasz Langa --- Lib/_pyrepl/historical_reader.py | 71 ++++++++++++++++++- Lib/_pyrepl/readline.py | 2 +- Lib/_pyrepl/simple_interact.py | 3 +- Lib/test/test_pyrepl/test_pyrepl.py | 39 ++++++++++ ...-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst | 2 + 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py index 7f4d0672d02..f6e14bdffc3 100644 --- a/Lib/_pyrepl/historical_reader.py +++ b/Lib/_pyrepl/historical_reader.py @@ -71,6 +71,18 @@ class previous_history(commands.Command): r.select_item(r.historyi - 1) +class history_search_backward(commands.Command): + def do(self) -> None: + r = self.reader + r.search_next(forwards=False) + + +class history_search_forward(commands.Command): + def do(self) -> None: + r = self.reader + r.search_next(forwards=True) + + class restore_history(commands.Command): def do(self) -> None: r = self.reader @@ -234,6 +246,8 @@ class HistoricalReader(Reader): isearch_forwards, isearch_backwards, operate_and_get_next, + history_search_backward, + history_search_forward, ]: self.commands[c.__name__] = c self.commands[c.__name__.replace("_", "-")] = c @@ -251,8 +265,8 @@ class HistoricalReader(Reader): (r"\C-s", "forward-history-isearch"), (r"\M-r", "restore-history"), (r"\M-.", "yank-arg"), - (r"\", "last-history"), - (r"\", "first-history"), + (r"\", "history-search-forward"), + (r"\", "history-search-backward"), ) def select_item(self, i: int) -> None: @@ -305,6 +319,59 @@ class HistoricalReader(Reader): else: return super().get_prompt(lineno, cursor_on_line) + def search_next(self, *, forwards: bool) -> None: + """Search history for the current line contents up to the cursor. + + Selects the first item found. If nothing is under the cursor, any next + item in history is selected. + """ + pos = self.pos + s = self.get_unicode() + history_index = self.historyi + + # In multiline contexts, we're only interested in the current line. + nl_index = s.rfind('\n', 0, pos) + prefix = s[nl_index + 1:pos] + pos = len(prefix) + + match_prefix = len(prefix) + len_item = 0 + if history_index < len(self.history): + len_item = len(self.get_item(history_index)) + if len_item and pos == len_item: + match_prefix = False + elif not pos: + match_prefix = False + + while 1: + if forwards: + out_of_bounds = history_index >= len(self.history) - 1 + else: + out_of_bounds = history_index == 0 + if out_of_bounds: + if forwards and not match_prefix: + self.pos = 0 + self.buffer = [] + self.dirty = True + else: + self.error("not found") + return + + history_index += 1 if forwards else -1 + s = self.get_item(history_index) + + if not match_prefix: + self.select_item(history_index) + return + + len_acc = 0 + for i, line in enumerate(s.splitlines(keepends=True)): + if line.startswith(prefix): + self.select_item(history_index) + self.pos = pos + len_acc + return + len_acc += len(line) + def isearch_next(self) -> None: st = self.isearch_term p = self.pos diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index a6ef138e8b4..4929dd31710 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -438,7 +438,7 @@ class _ReadlineWrapper: else: line = self._histline(line) if buffer: - line = "".join(buffer).replace("\r", "") + line + line = self._histline("".join(buffer).replace("\r", "") + line) del buffer[:] if line: history.append(line) diff --git a/Lib/_pyrepl/simple_interact.py b/Lib/_pyrepl/simple_interact.py index 91aef5e01eb..3c79cf61d04 100644 --- a/Lib/_pyrepl/simple_interact.py +++ b/Lib/_pyrepl/simple_interact.py @@ -163,7 +163,8 @@ def run_multiline_interactive_console( r.isearch_direction = '' r.console.forgetinput() r.pop_input_trans() - r.dirty = True + r.pos = len(r.get_unicode()) + r.dirty = True r.refresh() r.in_bracketed_paste = False console.write("\nKeyboardInterrupt\n") diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index d9d83c4c07e..84030e05d2a 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -676,6 +676,45 @@ class TestPyReplOutput(TestCase): self.assertEqual(output, "c\x1d") self.assertEqual(clean_screen(reader.screen), "c") + def test_history_search_backward(self): + # Test history search backward with "imp" input + events = itertools.chain( + code_to_events("import os\n"), + code_to_events("imp"), + [ + Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')), + Event(evt="key", data="\n", raw=bytearray(b"\n")), + ], + ) + + # fill the history + reader = self.prepare_reader(events) + multiline_input(reader) + + # search for "imp" in history + output = multiline_input(reader) + self.assertEqual(output, "import os") + self.assertEqual(clean_screen(reader.screen), "import os") + + def test_history_search_backward_empty(self): + # Test history search backward with an empty input + events = itertools.chain( + code_to_events("import os\n"), + [ + Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')), + Event(evt="key", data="\n", raw=bytearray(b"\n")), + ], + ) + + # fill the history + reader = self.prepare_reader(events) + multiline_input(reader) + + # search backward in history + output = multiline_input(reader) + self.assertEqual(output, "import os") + self.assertEqual(clean_screen(reader.screen), "import os") + class TestPyReplCompleter(TestCase): def prepare_reader(self, events, namespace): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst new file mode 100644 index 00000000000..f528691e1b6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst @@ -0,0 +1,2 @@ +Change ```` and ```` keys of the Python REPL to history +search forward/backward. Patch by Victor Stinner.