-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
gh-141374: Add vi mode to PyREPL #141375
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
base: main
Are you sure you want to change the base?
gh-141374: Add vi mode to PyREPL #141375
Conversation
The first line of multi-line input should display ps1 (primary prompt) not ps2 (continuation prompt). This is important for vi mode where users navigate through existing multi-line code with cursor movements.
In emacs mode _ is not a word boundary but in vi it is.
Vi has three character classes: word chars (alnum + _), punctuation (non-word, non-whitespace), and whitespace. Now w, e, and b treat punctuation sequences as separate words, matching vim behavior.
Use vi_backward_word on b key in vi mode as the original emacs mode implementation respects different word boundaries.
Let's not clutter the Reader with more vi state fields.
d1421e2 to
d3e358d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will only make a high-level review because I'm not a REPL expert but this needs polishing and I don't really like the extra churn for all users who do not care about vi mode (it makes the default REPL implementation tied to conditionals depending on the vi mode)
We also need a What's New entry.
Lib/_pyrepl/commands.py
Outdated
|
|
||
|
|
||
| class KillCommand(Command): | ||
| modifies_buffer = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not fond of having commands implementation depending on VI mode. Insteaad, I would suggest having two sets of commands that are distinct classes instead, e.g.:
class right(Command): ...
class vi_right(right): ...
where you can override whatever you need. Having each command being aware of the VI mode makes the implementation too rigid.
When entering a specific vi mode, the command handler should be replaced by the corresponding vi-compatible handler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I'm moving all of these into separate subclasses.
Lib/_pyrepl/reader.py
Outdated
| MAX_VI_UNDO_STACK_SIZE = 100 | ||
|
|
||
|
|
||
| from .types import ViMode, ViFindState |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Group the imports.
Lib/_pyrepl/reader.py
Outdated
| p += 1 | ||
| return p | ||
|
|
||
| def vi_eow(self, p: int | None = None) -> int: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would appreciate all VI-related implementation to be on a different mixin but I don't know if it's feasible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could make it work with a mixin, but there is a potentially simpler solution here: since the vi_ methods only need the pos and buffer properties of the Reader, I can create separate pure functions for all of these, which take these as arguments. All these functions can be kept in a separate file. This should make testing simpler as well.
def vi_bow(buffer: list[str], pos: int) -> int:
...In vi_commands.py:
r.pos = vi_bow(r.buffer, r.pos)
Lib/_pyrepl/reader.py
Outdated
| return 0 | ||
|
|
||
| p -= 1 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General comment: avoid blank lines and only use them to separate sections inside functions as per PEP-8. The stdlib is usually written compactly.
Lib/_pyrepl/types.py
Outdated
| type CharWidths = list[int] | ||
|
|
||
|
|
||
| class ViMode(str, enum.Enum): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use StrEnum instead. Personally, I wouldn't even use an enum and just use plain constants (0, 1).
Lib/_pyrepl/unix_console.py
Outdated
| or bool(self.pollob.poll(timeout)) | ||
| ) | ||
|
|
||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid unrelated changes.
|
|
||
| while self.event_queue.empty(): | ||
| if self.event_queue.has_pending_escape_sequence(): | ||
| current_time_ms = time.monotonic() * 1000 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this? why can't someone just keep their key pressed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape is the key to switch to normal mode, but it is also the first byte of all terminal escape sequences, so if we were to map esc to the normal mode switch, that would overlap with those sequences and we wouldn't be able to tell them apart. When this happens pyrepl actually disables the overlapping commands and prints a warning about this.
The solution is to wait a bit to see if any other keys have been pressed immediately after the esc to tell these apart. This should not affect how escape sequences work and only introduce a small delay in vi mode when switching modes. I think keeping esc pressed will work fine, unless I misunderstood your concern.
| from .trace import trace | ||
|
|
||
| class BaseEventQueue: | ||
| _ESCAPE_TIMEOUT_MS = 50 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's correct to expect timeouts. People can write slowly. Or not. Hold a key, or not. I also can't figure an example of when this would happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above.
|
|
||
|
|
||
| def _is_vi_mode_enabled() -> bool: | ||
| return os.environ.get("PYREPL_VI_MODE", "").lower() in {"1", "true", "on", "yes"} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need something better. Like PYTHON_REPL_VI_MODE instead. Before we had PYTHON_BASIC_REPL for basic repl.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I agree. PYTHON_REPL_VI_MODE sounds good to me.
| self.reader = ReadlineAlikeReader( | ||
| console=console, | ||
| config=self.config, | ||
| use_vi_mode=_is_vi_mode_enabled() | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, I would rather have two different classes. One reader for vi mode and one without. We can have a base reader class though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this designing the feature, but the Reader is subclassed in multiple places and the ReadlineAlikeReader composes those subclasses, making it impractical to have a separate Reader for vi mode. There could be a separate ReadlineAlikeReader but that seems like the wrong abstraction.
Would you still have this concern with #141375 (comment) moving the vi_ methods out of Reader? Also, all emacs shortcuts are directly handled in Reader so having a separate class for vi seems inconsistent. After the cleanup, relatively little vi-specific logic would remain in Reader: keymap setup, mode switching, undo stack and find state handling.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
Thanks for the review @picnixz. I intended this PR as a starting point to collect feedback on the design & implementation. I'm happy to iterate further based on the feedback. I'll address the comments shortly. |
|
For the implementation I would advise waiting for the code owners first. They may have different opinions (or may decide to reject the feature) |
This adds basic vi mode commands to pyrepl, supporting basic motions. The feature can be enabled by setting the
PYREPL_VI_MODEenvironment variable.