Skip to content

Commit 37b78e4

Browse files
committed
Implement vi mode and basic motions in pyrepl
1 parent 6d45cd8 commit 37b78e4

File tree

6 files changed

+431
-14
lines changed

6 files changed

+431
-14
lines changed

Lib/_pyrepl/commands.py

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,20 +325,41 @@ def do(self) -> None:
325325
b = r.buffer
326326
for _ in range(r.get_arg()):
327327
p = r.pos + 1
328-
if p <= len(b):
329-
r.pos = p
328+
# In vi normal mode, don't move past the last character
329+
if r.editor_mode.is_normal():
330+
eol_pos = r.eol()
331+
max_pos = max(r.bol(), eol_pos - 1) if eol_pos > r.bol() else r.bol()
332+
if p <= max_pos:
333+
r.pos = p
334+
else:
335+
self.reader.error("end of line")
330336
else:
331-
self.reader.error("end of buffer")
337+
if p <= len(b):
338+
r.pos = p
339+
else:
340+
self.reader.error("end of buffer")
332341

333342

334343
class beginning_of_line(MotionCommand):
335344
def do(self) -> None:
336345
self.reader.pos = self.reader.bol()
337346

338347

348+
class first_non_whitespace_character(MotionCommand):
349+
def do(self) -> None:
350+
self.reader.pos = self.reader.first_non_whitespace()
351+
352+
339353
class end_of_line(MotionCommand):
340354
def do(self) -> None:
341-
self.reader.pos = self.reader.eol()
355+
r = self.reader
356+
eol_pos = r.eol()
357+
if r.editor_mode.is_normal():
358+
bol_pos = r.bol()
359+
# Don't go past the last character (but stay at bol if line is empty)
360+
r.pos = max(bol_pos, eol_pos - 1) if eol_pos > bol_pos else bol_pos
361+
else:
362+
r.pos = eol_pos
342363

343364

344365
class home(MotionCommand):
@@ -365,6 +386,20 @@ def do(self) -> None:
365386
r.pos = r.bow()
366387

367388

389+
class end_of_word(MotionCommand):
390+
def do(self) -> None:
391+
r = self.reader
392+
for _ in range(r.get_arg()):
393+
r.pos = r.vi_eow()
394+
395+
396+
class vi_forward_word(MotionCommand):
397+
def do(self) -> None:
398+
r = self.reader
399+
for _ in range(r.get_arg()):
400+
r.pos = r.vi_forward_word()
401+
402+
368403
class self_insert(EditCommand):
369404
def do(self) -> None:
370405
r = self.reader
@@ -503,3 +538,55 @@ def do(self) -> None:
503538
)
504539
self.reader.insert(data.replace(done, ""))
505540
self.reader.last_refresh_cache.invalidated = True
541+
542+
543+
class vi_normal_mode(Command):
544+
def do(self) -> None:
545+
self.reader.enter_normal_mode()
546+
547+
548+
class vi_insert_mode(Command):
549+
def do(self) -> None:
550+
self.reader.enter_insert_mode()
551+
552+
553+
class vi_append_mode(Command):
554+
def do(self) -> None:
555+
if self.reader.pos < len(self.reader.buffer):
556+
self.reader.pos += 1
557+
self.reader.enter_insert_mode()
558+
559+
560+
class vi_append_eol(Command):
561+
def do(self) -> None:
562+
while self.reader.pos < len(self.reader.buffer):
563+
if self.reader.buffer[self.reader.pos] == '\n':
564+
break
565+
self.reader.pos += 1
566+
self.reader.enter_insert_mode()
567+
568+
569+
class vi_insert_bol(Command):
570+
def do(self) -> None:
571+
self.reader.pos = self.reader.first_non_whitespace()
572+
self.reader.enter_insert_mode()
573+
574+
575+
class vi_open_below(Command):
576+
def do(self) -> None:
577+
while self.reader.pos < len(self.reader.buffer):
578+
if self.reader.buffer[self.reader.pos] == '\n':
579+
break
580+
self.reader.pos += 1
581+
582+
self.reader.insert('\n')
583+
self.reader.enter_insert_mode()
584+
585+
class vi_open_above(Command):
586+
def do(self) -> None:
587+
while self.reader.pos > 0 and self.reader.buffer[self.reader.pos - 1] != '\n':
588+
self.reader.pos -= 1
589+
590+
self.reader.insert('\n')
591+
self.reader.pos -= 1
592+
self.reader.enter_insert_mode()

Lib/_pyrepl/historical_reader.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -257,19 +257,27 @@ def __post_init__(self) -> None:
257257
)
258258

259259
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
260-
return super().collect_keymap() + (
260+
bindings: list[tuple[KeySpec, CommandName]] = [
261261
(r"\C-n", "next-history"),
262262
(r"\C-p", "previous-history"),
263263
(r"\C-o", "operate-and-get-next"),
264264
(r"\C-r", "reverse-history-isearch"),
265265
(r"\C-s", "forward-history-isearch"),
266-
(r"\M-r", "restore-history"),
267-
(r"\M-.", "yank-arg"),
268266
(r"\<page down>", "history-search-forward"),
269-
(r"\x1b[6~", "history-search-forward"),
270267
(r"\<page up>", "history-search-backward"),
271-
(r"\x1b[5~", "history-search-backward"),
272-
)
268+
]
269+
270+
if not self.use_vi_mode:
271+
bindings.extend(
272+
[
273+
(r"\M-r", "restore-history"),
274+
(r"\M-.", "yank-arg"),
275+
(r"\x1b[6~", "history-search-forward"),
276+
(r"\x1b[5~", "history-search-backward"),
277+
]
278+
)
279+
280+
return super().collect_keymap() + tuple(bindings)
273281

274282
def select_item(self, i: int) -> None:
275283
self.transient_history[self.historyi] = self.get_unicode()

0 commit comments

Comments
 (0)