Skip to content

Conversation

@lkollar
Copy link
Contributor

@lkollar lkollar commented Nov 10, 2025

This adds basic vi mode commands to pyrepl, supporting basic motions. The feature can be enabled by setting the PYREPL_VI_MODE environment variable.

@vstinner vstinner changed the title gh-141374: Add vi mode gh-141374: Add vi mode to PyREPL Nov 13, 2025
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.
Copy link
Member

@picnixz picnixz left a 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.



class KillCommand(Command):
modifies_buffer = True
Copy link
Member

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.

Copy link
Contributor Author

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.

MAX_VI_UNDO_STACK_SIZE = 100


from .types import ViMode, ViFindState
Copy link
Member

Choose a reason for hiding this comment

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

Group the imports.

p += 1
return p

def vi_eow(self, p: int | None = None) -> int:
Copy link
Member

@picnixz picnixz Nov 22, 2025

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.

Copy link
Contributor Author

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)

return 0

p -= 1

Copy link
Member

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.

type CharWidths = list[int]


class ViMode(str, enum.Enum):
Copy link
Member

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).

or bool(self.pollob.poll(timeout))
)


Copy link
Member

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
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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"}
Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines +368 to +372
self.reader = ReadlineAlikeReader(
console=console,
config=self.config,
use_vi_mode=_is_vi_mode_enabled()
)
Copy link
Member

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.

Copy link
Contributor Author

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.

@bedevere-app
Copy link

bedevere-app bot commented Nov 22, 2025

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@lkollar
Copy link
Contributor Author

lkollar commented Nov 22, 2025

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)

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.

@picnixz
Copy link
Member

picnixz commented Nov 22, 2025

For the implementation I would advise waiting for the code owners first. They may have different opinions (or may decide to reject the feature)

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