mirror of https://github.com/python/cpython
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:
parent
d683f49a7b
commit
8311b11800
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
Change ``<page up>`` and ``<page down>`` keys of the Python REPL to history
|
||||
search forward/backward. Patch by Victor Stinner.
|
Loading…
Reference in New Issue