]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119034, REPL: Change page up/down keys to search in history (#123607)
authorVictor Stinner <vstinner@python.org>
Fri, 6 Sep 2024 11:15:00 +0000 (13:15 +0200)
committerGitHub <noreply@github.com>
Fri, 6 Sep 2024 11:15:00 +0000 (13:15 +0200)
Change <page up> and <page down> keys of the Python REPL to history
search forward/backward.

Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Lib/_pyrepl/historical_reader.py
Lib/_pyrepl/readline.py
Lib/_pyrepl/simple_interact.py
Lib/test/test_pyrepl/test_pyrepl.py
Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst [new file with mode: 0644]

index 7f4d0672d02094f950da1c5c20963edfd5cdc84f..f6e14bdffc335217d80930cc713a95e098aa16b2 100644 (file)
@@ -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"\<page down>", "last-history"),
-            (r"\<page up>", "first-history"),
+            (r"\<page down>", "history-search-forward"),
+            (r"\<page up>", "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
index a6ef138e8b4ec82bb3da23939b34ebae7b6b806d..4929dd317102406658ed28e80297cd5d7a828bc9 100644 (file)
@@ -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)
index 91aef5e01eb86712595f5e408256d8d0cd2ddaa9..3c79cf61d0405193eee1dfa8198a58578373c90d 100644 (file)
@@ -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")
index d9d83c4c07ed79ac4199babeb65e81f9ee46388a..84030e05d2a94c9d6a9d031f24e87fbf64f506f5 100644 (file)
@@ -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 <page up> 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 <page up> 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 (file)
index 0000000..f528691
--- /dev/null
@@ -0,0 +1,2 @@
+Change ``<page up>`` and ``<page down>`` keys of the Python REPL to history
+search forward/backward. Patch by Victor Stinner.