gh-119034, REPL: Change page up/down keys to search in history (#123607)

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

Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
Victor Stinner 2024-09-06 13:15:00 +02:00 committed by GitHub
parent d683f49a7b
commit 8311b11800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 113 additions and 4 deletions

View 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

View 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)

View 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")

View 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):

View File

@ -0,0 +1,2 @@
Change ``<page up>`` and ``<page down>`` keys of the Python REPL to history
search forward/backward. Patch by Victor Stinner.