Skip to content

Commit 3240160

Browse files
vstinnerambv
authored andcommitted
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 <[email protected]>
1 parent 0494859 commit 3240160

File tree

5 files changed

+113
-4
lines changed

5 files changed

+113
-4
lines changed

Lib/_pyrepl/historical_reader.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ def do(self) -> None:
7171
r.select_item(r.historyi - 1)
7272

7373

74+
class history_search_backward(commands.Command):
75+
def do(self) -> None:
76+
r = self.reader
77+
r.search_next(forwards=False)
78+
79+
80+
class history_search_forward(commands.Command):
81+
def do(self) -> None:
82+
r = self.reader
83+
r.search_next(forwards=True)
84+
85+
7486
class restore_history(commands.Command):
7587
def do(self) -> None:
7688
r = self.reader
@@ -234,6 +246,8 @@ def __post_init__(self) -> None:
234246
isearch_forwards,
235247
isearch_backwards,
236248
operate_and_get_next,
249+
history_search_backward,
250+
history_search_forward,
237251
]:
238252
self.commands[c.__name__] = c
239253
self.commands[c.__name__.replace("_", "-")] = c
@@ -251,8 +265,8 @@ def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
251265
(r"\C-s", "forward-history-isearch"),
252266
(r"\M-r", "restore-history"),
253267
(r"\M-.", "yank-arg"),
254-
(r"\<page down>", "last-history"),
255-
(r"\<page up>", "first-history"),
268+
(r"\<page down>", "history-search-forward"),
269+
(r"\<page up>", "history-search-backward"),
256270
)
257271

258272
def select_item(self, i: int) -> None:
@@ -305,6 +319,59 @@ def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
305319
else:
306320
return super().get_prompt(lineno, cursor_on_line)
307321

322+
def search_next(self, *, forwards: bool) -> None:
323+
"""Search history for the current line contents up to the cursor.
324+
325+
Selects the first item found. If nothing is under the cursor, any next
326+
item in history is selected.
327+
"""
328+
pos = self.pos
329+
s = self.get_unicode()
330+
history_index = self.historyi
331+
332+
# In multiline contexts, we're only interested in the current line.
333+
nl_index = s.rfind('\n', 0, pos)
334+
prefix = s[nl_index + 1:pos]
335+
pos = len(prefix)
336+
337+
match_prefix = len(prefix)
338+
len_item = 0
339+
if history_index < len(self.history):
340+
len_item = len(self.get_item(history_index))
341+
if len_item and pos == len_item:
342+
match_prefix = False
343+
elif not pos:
344+
match_prefix = False
345+
346+
while 1:
347+
if forwards:
348+
out_of_bounds = history_index >= len(self.history) - 1
349+
else:
350+
out_of_bounds = history_index == 0
351+
if out_of_bounds:
352+
if forwards and not match_prefix:
353+
self.pos = 0
354+
self.buffer = []
355+
self.dirty = True
356+
else:
357+
self.error("not found")
358+
return
359+
360+
history_index += 1 if forwards else -1
361+
s = self.get_item(history_index)
362+
363+
if not match_prefix:
364+
self.select_item(history_index)
365+
return
366+
367+
len_acc = 0
368+
for i, line in enumerate(s.splitlines(keepends=True)):
369+
if line.startswith(prefix):
370+
self.select_item(history_index)
371+
self.pos = pos + len_acc
372+
return
373+
len_acc += len(line)
374+
308375
def isearch_next(self) -> None:
309376
st = self.isearch_term
310377
p = self.pos

Lib/_pyrepl/readline.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ def read_history_file(self, filename: str = gethistoryfile()) -> None:
438438
else:
439439
line = self._histline(line)
440440
if buffer:
441-
line = "".join(buffer).replace("\r", "") + line
441+
line = self._histline("".join(buffer).replace("\r", "") + line)
442442
del buffer[:]
443443
if line:
444444
history.append(line)

Lib/_pyrepl/simple_interact.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ def maybe_run_command(statement: str) -> bool:
163163
r.isearch_direction = ''
164164
r.console.forgetinput()
165165
r.pop_input_trans()
166-
r.dirty = True
166+
r.pos = len(r.get_unicode())
167+
r.dirty = True
167168
r.refresh()
168169
r.in_bracketed_paste = False
169170
console.write("\nKeyboardInterrupt\n")

Lib/test/test_pyrepl/test_pyrepl.py

+39
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,45 @@ def test_control_character(self):
676676
self.assertEqual(output, "c\x1d")
677677
self.assertEqual(clean_screen(reader.screen), "c")
678678

679+
def test_history_search_backward(self):
680+
# Test <page up> history search backward with "imp" input
681+
events = itertools.chain(
682+
code_to_events("import os\n"),
683+
code_to_events("imp"),
684+
[
685+
Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')),
686+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
687+
],
688+
)
689+
690+
# fill the history
691+
reader = self.prepare_reader(events)
692+
multiline_input(reader)
693+
694+
# search for "imp" in history
695+
output = multiline_input(reader)
696+
self.assertEqual(output, "import os")
697+
self.assertEqual(clean_screen(reader.screen), "import os")
698+
699+
def test_history_search_backward_empty(self):
700+
# Test <page up> history search backward with an empty input
701+
events = itertools.chain(
702+
code_to_events("import os\n"),
703+
[
704+
Event(evt='key', data='page up', raw=bytearray(b'\x1b[5~')),
705+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
706+
],
707+
)
708+
709+
# fill the history
710+
reader = self.prepare_reader(events)
711+
multiline_input(reader)
712+
713+
# search backward in history
714+
output = multiline_input(reader)
715+
self.assertEqual(output, "import os")
716+
self.assertEqual(clean_screen(reader.screen), "import os")
717+
679718

680719
class TestPyReplCompleter(TestCase):
681720
def prepare_reader(self, events, namespace):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Change ``<page up>`` and ``<page down>`` keys of the Python REPL to history
2+
search forward/backward. Patch by Victor Stinner.

0 commit comments

Comments
 (0)