Skip to content

Commit f863425

Browse files
committed
Implement 'cw' and 'r' commands
1 parent e8afc9e commit f863425

File tree

3 files changed

+93
-8
lines changed

3 files changed

+93
-8
lines changed

Lib/_pyrepl/reader.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ def make_default_commands() -> dict[CommandName, type[Command]]:
186186
(r"d0", "vi-delete-to-bol"),
187187
(r"d$", "vi-delete-to-eol"),
188188

189+
# Change commands
190+
(r"cw", "vi-change-word"),
191+
192+
# Replace commands
193+
(r"r", "vi-replace-char"),
194+
189195
# Special keys still work in normal mode
190196
(r"\<left>", "left"),
191197
(r"\<right>", "right"),

Lib/_pyrepl/vi_commands.py

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -146,21 +146,21 @@ def do(self) -> None:
146146
# Find Commands (f/F/t/T)
147147
# ============================================================================
148148

149-
def _make_find_char_keymap() -> tuple[tuple[str, str], ...]:
150-
"""Create a keymap where all printable ASCII maps to vi-find-execute.
149+
def _make_char_capture_keymap(execute_cmd: str, cancel_cmd: str) -> tuple[tuple[str, str], ...]:
150+
"""Create a keymap where all printable ASCII maps to execute_cmd.
151151
152-
Once vi-find-execute is called, it will pop our input translator."""
152+
Once execute_cmd is called, it will pop our input translator."""
153153
entries = []
154154
for i in range(32, 127):
155155
if i == 92: # backslash needs escaping
156-
entries.append((r"\\", "vi-find-execute"))
156+
entries.append((r"\\", execute_cmd))
157157
else:
158-
entries.append((chr(i), "vi-find-execute"))
159-
entries.append((r"\<escape>", "vi-find-cancel"))
158+
entries.append((chr(i), execute_cmd))
159+
entries.append((r"\<escape>", cancel_cmd))
160160
return tuple(entries)
161161

162-
163-
_find_char_keymap = _make_find_char_keymap()
162+
_find_char_keymap = _make_char_capture_keymap("vi-find-execute", "vi-find-cancel")
163+
_replace_char_keymap = _make_char_capture_keymap("vi-replace-execute", "vi-replace-cancel")
164164

165165

166166
class vi_find_char(Command):
@@ -327,3 +327,51 @@ def do(self) -> None:
327327
new_pos += 1
328328
if new_pos < r.pos:
329329
r.pos = new_pos
330+
331+
332+
# ============================================================================
333+
# Change Commands
334+
# ============================================================================
335+
336+
class vi_change_word(KillCommand):
337+
"""Change from cursor to end of word (cw)."""
338+
def do(self) -> None:
339+
r = self.reader
340+
for _ in range(r.get_arg()):
341+
end = r.vi_eow() + 1 # +1 to include last char
342+
if end > r.pos:
343+
self.kill_range(r.pos, end)
344+
r.enter_insert_mode()
345+
346+
347+
# ============================================================================
348+
# Replace Commands
349+
# ============================================================================
350+
351+
class vi_replace_char(Command):
352+
"""Replace character under cursor with next typed character (r)."""
353+
def do(self) -> None:
354+
r = self.reader
355+
translator = _input.KeymapTranslator(
356+
_replace_char_keymap,
357+
invalid_cls="vi-replace-cancel",
358+
character_cls="vi-replace-execute"
359+
)
360+
r.push_input_trans(translator)
361+
362+
class vi_replace_execute(Command):
363+
"""Execute character replacement with the pressed character."""
364+
def do(self) -> None:
365+
r = self.reader
366+
r.pop_input_trans()
367+
pending_char = self.event[-1]
368+
if not pending_char or r.pos >= len(r.buffer):
369+
return
370+
r.buffer[r.pos] = pending_char
371+
r.dirty = True
372+
373+
class vi_replace_cancel(Command):
374+
"""Cancel pending replace operation."""
375+
def do(self) -> None:
376+
r = self.reader
377+
r.pop_input_trans()

Lib/test/test_pyrepl/test_reader.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,37 @@ def test_find_char_escape_cancels(self):
11421142
reader, _ = self._run_vi(events)
11431143
self.assertEqual(reader.pos, 0)
11441144

1145+
def test_change_word(self):
1146+
events = itertools.chain(
1147+
code_to_events("hello world"),
1148+
[
1149+
Event(evt="key", data="\x1b", raw=bytearray(b"\x1b")), # ESC
1150+
Event(evt="key", data="0", raw=bytearray(b"0")), # BOL
1151+
Event(evt="key", data="c", raw=bytearray(b"c")), # change
1152+
Event(evt="key", data="w", raw=bytearray(b"w")), # word
1153+
],
1154+
code_to_events("hi"), # replacement text
1155+
)
1156+
reader, _ = self._run_vi(events)
1157+
self.assertEqual("".join(reader.buffer), "hi world")
1158+
1159+
def test_replace_char(self):
1160+
events = itertools.chain(
1161+
code_to_events("hello"),
1162+
[
1163+
Event(evt="key", data="\x1b", raw=bytearray(b"\x1b")), # ESC
1164+
Event(evt="key", data="0", raw=bytearray(b"0")), # BOL
1165+
Event(evt="key", data="l", raw=bytearray(b"l")), # move right to 'e'
1166+
Event(evt="key", data="l", raw=bytearray(b"l")), # move right to first 'l'
1167+
Event(evt="key", data="r", raw=bytearray(b"r")), # replace
1168+
Event(evt="key", data="X", raw=bytearray(b"X")), # replacement char
1169+
],
1170+
)
1171+
reader, _ = self._run_vi(events)
1172+
self.assertEqual("".join(reader.buffer), "heXlo")
1173+
self.assertEqual(reader.pos, 2) # cursor stays on replaced char
1174+
self.assertEqual(reader.vi_mode, ViMode.NORMAL) # stays in normal mode
1175+
11451176

11461177
@force_not_colorized_test_class
11471178
class TestHistoricalReaderBindings(TestCase):

0 commit comments

Comments
 (0)