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)
|
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):
|
class restore_history(commands.Command):
|
||||||
def do(self) -> None:
|
def do(self) -> None:
|
||||||
r = self.reader
|
r = self.reader
|
||||||
@ -234,6 +246,8 @@ class HistoricalReader(Reader):
|
|||||||
isearch_forwards,
|
isearch_forwards,
|
||||||
isearch_backwards,
|
isearch_backwards,
|
||||||
operate_and_get_next,
|
operate_and_get_next,
|
||||||
|
history_search_backward,
|
||||||
|
history_search_forward,
|
||||||
]:
|
]:
|
||||||
self.commands[c.__name__] = c
|
self.commands[c.__name__] = c
|
||||||
self.commands[c.__name__.replace("_", "-")] = c
|
self.commands[c.__name__.replace("_", "-")] = c
|
||||||
@ -251,8 +265,8 @@ class HistoricalReader(Reader):
|
|||||||
(r"\C-s", "forward-history-isearch"),
|
(r"\C-s", "forward-history-isearch"),
|
||||||
(r"\M-r", "restore-history"),
|
(r"\M-r", "restore-history"),
|
||||||
(r"\M-.", "yank-arg"),
|
(r"\M-.", "yank-arg"),
|
||||||
(r"\<page down>", "last-history"),
|
(r"\<page down>", "history-search-forward"),
|
||||||
(r"\<page up>", "first-history"),
|
(r"\<page up>", "history-search-backward"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def select_item(self, i: int) -> None:
|
def select_item(self, i: int) -> None:
|
||||||
@ -305,6 +319,59 @@ class HistoricalReader(Reader):
|
|||||||
else:
|
else:
|
||||||
return super().get_prompt(lineno, cursor_on_line)
|
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:
|
def isearch_next(self) -> None:
|
||||||
st = self.isearch_term
|
st = self.isearch_term
|
||||||
p = self.pos
|
p = self.pos
|
||||||
|
@ -438,7 +438,7 @@ class _ReadlineWrapper:
|
|||||||
else:
|
else:
|
||||||
line = self._histline(line)
|
line = self._histline(line)
|
||||||
if buffer:
|
if buffer:
|
||||||
line = "".join(buffer).replace("\r", "") + line
|
line = self._histline("".join(buffer).replace("\r", "") + line)
|
||||||
del buffer[:]
|
del buffer[:]
|
||||||
if line:
|
if line:
|
||||||
history.append(line)
|
history.append(line)
|
||||||
|
@ -163,7 +163,8 @@ def run_multiline_interactive_console(
|
|||||||
r.isearch_direction = ''
|
r.isearch_direction = ''
|
||||||
r.console.forgetinput()
|
r.console.forgetinput()
|
||||||
r.pop_input_trans()
|
r.pop_input_trans()
|
||||||
r.dirty = True
|
r.pos = len(r.get_unicode())
|
||||||
|
r.dirty = True
|
||||||
r.refresh()
|
r.refresh()
|
||||||
r.in_bracketed_paste = False
|
r.in_bracketed_paste = False
|
||||||
console.write("\nKeyboardInterrupt\n")
|
console.write("\nKeyboardInterrupt\n")
|
||||||
|
@ -676,6 +676,45 @@ class TestPyReplOutput(TestCase):
|
|||||||
self.assertEqual(output, "c\x1d")
|
self.assertEqual(output, "c\x1d")
|
||||||
self.assertEqual(clean_screen(reader.screen), "c")
|
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):
|
class TestPyReplCompleter(TestCase):
|
||||||
def prepare_reader(self, events, namespace):
|
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…
x
Reference in New Issue
Block a user