Skip to content

gh-145375: Fix bracketed paste in PyREPL isearch mode#145398

Open
tanloong wants to merge 13 commits into
python:mainfrom
tanloong:isearch-paste
Open

gh-145375: Fix bracketed paste in PyREPL isearch mode#145398
tanloong wants to merge 13 commits into
python:mainfrom
tanloong:isearch-paste

Conversation

@tanloong

@tanloong tanloong commented Mar 1, 2026

Copy link
Copy Markdown
Contributor

This excludes \x1b from isearch-end keymap and add new isearch_bracketed_paste function to handle bracketed paste in isearch mode.

data += ev.data
paste_content = data.replace(done, "")
r.isearch_term += paste_content
r.invalidate_prompt()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be r.invalidate_prompt(). dirty is not used here, so the prompt can keep showing the previous search term.

@tanloong tanloong Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data += ev.data
paste_content = data.replace(done, "")
r.isearch_term += paste_content
r.invalidate_prompt()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

r.dirty is not read anywhere in _pyrepl, so this is a no-op and the search prompt won't refresh after the paste. We probably want r.invalidate_prompt() here, like isearch_add_character does.

Comment thread Lib/_pyrepl/historical_reader.py Outdated

isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
[("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"]
[("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) not in ("\\", "\x1b")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I am missing something, this breaks plain Esc: it now waits for the rest of a bracketed-paste sequence and does not end isearch. Can we keep the old Esc path?

@tanloong tanloong Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! You're right that removing \x1b from isearch-end makes it look like bare Esc would stop working.

However, I just noticed that even without this change, a single ESC doesn't immediately end isearch, because EventQueue treats \x1b as a prefix for all escape sequences, it does not send the Esc event but instead waits for the next byte. That's a pre‑existing behavior (press Esc twice to exit isearch).

This should be another issue: Because a single Esc byte can be the start of a multi-byte escape sequence, it's ambiguous whether it's a standalone key press. I wonder if we should stop using Esc to quit isearch. Or, we could add a timeout mechanism (as other TUI apps like ncurses, Vim, Tmux) to tell them apart?

@tanloong tanloong Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a cleaner way that avoids touching isearch-end.

Instead of binding \x1b[200~ in the keymap, we can move detection to the event-queue layer, translating \x1b[200~ into a named event. If \x1b[200~ is consumed at the event-queue level, the isearch keymap never sees the leading \x1b during a paste. So \x1b stays bound to isearch-end, Esc is not impacted, although the Esc delay is already pre-existing, which is a separate issue.

If you think this named-event approach is OK: This change goes a bit beyond the original scope of this PR. Since \x1b[200~ is now translated at the event-queue level, the normal-mode keymap in reader.py never sees the raw sequence and needs to be updated to also bind the named event. If you'd prefer, I'm happy to split this into two separate PRs:

  1. isearch fix only (removing \x1b from isearch-end, bind raw \x1b[200~ to isearch-bracketed-paste)
  2. named-event refactor (restore old Esc path to isearch-end, let both normal and isearch keymap bind the new event).

Comment thread Lib/_pyrepl/historical_reader.py Outdated

isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
[("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"]
[("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) not in ("\\", "\x1b")]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing \x1b from isearch-end means a bare ESC no longer ends the search immediately, it now waits for the next key. Is that intentional? Pressing Escape to leave isearch won't take effect until another key arrives.

r.pop_input_trans()
r.invalidate_prompt()

class isearch_bracketed_paste(commands.Command):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: two blank lines between top-level classes here.

tanloong added 6 commits July 17, 2026 00:55
The event queue now turns the terminal's \x1b[200~ bracketed-paste
start sequence into a synthetic "bracketed paste" named event, so
readers can bind \<bracketed paste> instead of matching the raw
escape sequence directly.
Bind \<bracketed paste> to isearch-bracketed-paste and keep \x1b bound
to isearch-end. Because the paste start marker is now consumed by the
event queue, a lone Esc still ends the search immediately (no
disambiguation delay), while a paste is dispatched to the command.
Update the normal (non-isearch) keymap to use \<bracketed paste> so it
stays consistent with the event queue translation.
- test_eventqueue: assert \x1b[200~ is translated to "bracketed paste".
- test_pyrepl: feed the "bracketed paste" event and cover isearch mode.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants