https://github.com/python/cpython/commit/c787a5161ca147cb8f9867c3a61b7ab4edf6c2a7
commit: c787a5161ca147cb8f9867c3a61b7ab4edf6c2a7
branch: 3.13
author: Miss Islington (bot) <31488909+miss-isling...@users.noreply.github.com>
committer: ambv <luk...@langa.pl>
date: 2024-09-06T14:04:11+02:00
summary:

[3.13] gh-119034, REPL: Change page up/down keys to search in history 
(GH-123607) (GH-123773)

Change <page up> and <page down> keys of the Python REPL to history
search forward/backward.

(cherry picked from commit 8311b11800509c975023e062e2c336f417c5e4c0)

Co-authored-by: Victor Stinner <vstin...@python.org>
Co-authored-by: Ɓukasz Langa <luk...@langa.pl>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2024-09-02-17-32-15.gh-issue-119034.HYh5Vj.rst
M Lib/_pyrepl/historical_reader.py
M Lib/_pyrepl/readline.py
M Lib/_pyrepl/simple_interact.py
M Lib/test/test_pyrepl/test_pyrepl.py

diff --git a/Lib/_pyrepl/historical_reader.py b/Lib/_pyrepl/historical_reader.py
index 7f4d0672d02094..f6e14bdffc3352 100644
--- a/Lib/_pyrepl/historical_reader.py
+++ b/Lib/_pyrepl/historical_reader.py
@@ -71,6 +71,18 @@ def do(self) -> None:
         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 @@ def __post_init__(self) -> None:
             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 @@ def collect_keymap(self) -> tuple[tuple[KeySpec, 
CommandName], ...]:
             (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 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> 
str:
         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 a6ef138e8b4ec8..4929dd31710240 100644
--- a/Lib/_pyrepl/readline.py
+++ b/Lib/_pyrepl/readline.py
@@ -438,7 +438,7 @@ def read_history_file(self, filename: str = 
gethistoryfile()) -> None:
                 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 91aef5e01eb867..3c79cf61d04051 100644
--- a/Lib/_pyrepl/simple_interact.py
+++ b/Lib/_pyrepl/simple_interact.py
@@ -163,7 +163,8 @@ def maybe_run_command(statement: str) -> bool:
                 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 d9d83c4c07ed79..84030e05d2a94c 100644
--- a/Lib/test/test_pyrepl/test_pyrepl.py
+++ b/Lib/test/test_pyrepl/test_pyrepl.py
@@ -676,6 +676,45 @@ def test_control_character(self):
         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
index 00000000000000..f528691e1b6f9f
--- /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 ``<page up>`` and ``<page down>`` keys of the Python REPL to history
+search forward/backward. Patch by Victor Stinner.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to