Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions Lib/_pyrepl/historical_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Lib/_pyrepl/readline.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Lib/_pyrepl/simple_interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_pyrepl/test_pyrepl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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