Skip to content

Commit 1406699

Browse files
committed
Implement W and B vi motions
1 parent 237f559 commit 1406699

File tree

3 files changed

+86
-0
lines changed

3 files changed

+86
-0
lines changed

Lib/_pyrepl/reader.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ def make_default_commands() -> dict[CommandName, type[Command]]:
157157
(r"0", "beginning-of-line"),
158158
(r"$", "end-of-line"),
159159
(r"w", "vi-forward-word"),
160+
(r"W", "vi-forward-word-ws"),
160161
(r"b", "vi-backward-word"),
162+
(r"B", "vi-backward-word-ws"),
161163
(r"e", "end-of-word"),
162164
(r"^", "first-non-whitespace-character"),
163165

@@ -599,6 +601,29 @@ def vi_forward_word(self, p: int | None = None) -> int:
599601
# Clamp to valid buffer range
600602
return min(p, len(b) - 1) if b else 0
601603

604+
def vi_forward_word_ws(self, p: int | None = None) -> int:
605+
"""Return the 0-based index of the first character of the next WORD
606+
(vi 'W' semantics).
607+
608+
Treats white space as the only separator."""
609+
if p is None:
610+
p = self.pos
611+
b = self.buffer
612+
613+
if not b or p >= len(b):
614+
return max(0, len(b) - 1) if b else 0
615+
616+
# Skip all non-whitespace (the current WORD)
617+
while p < len(b) and not b[p].isspace():
618+
p += 1
619+
620+
# Skip whitespace to find next WORD
621+
while p < len(b) and b[p].isspace():
622+
p += 1
623+
624+
# Clamp to valid buffer range
625+
return min(p, len(b) - 1) if b else 0
626+
602627
def vi_bow(self, p: int | None = None) -> int:
603628
"""Return the 0-based index of the beginning of the word preceding p
604629
(vi 'b' semantics).
@@ -633,6 +658,33 @@ def vi_bow(self, p: int | None = None) -> int:
633658

634659
return p
635660

661+
def vi_bow_ws(self, p: int | None = None) -> int:
662+
"""Return the 0-based index of the beginning of the WORD preceding p
663+
(vi 'B' semantics).
664+
665+
Treats white space as the only separator."""
666+
if p is None:
667+
p = self.pos
668+
b = self.buffer
669+
670+
if not b or p <= 0:
671+
return 0
672+
673+
p -= 1
674+
675+
# Skip whitespace going backward
676+
while p >= 0 and b[p].isspace():
677+
p -= 1
678+
679+
if p < 0:
680+
return 0
681+
682+
# Now skip the WORD we landed in
683+
while p > 0 and not b[p - 1].isspace():
684+
p -= 1
685+
686+
return p
687+
636688
def bol(self, p: int | None = None) -> int:
637689
"""Return the 0-based index of the line break preceding p most
638690
immediately.

Lib/_pyrepl/vi_commands.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def do(self) -> None:
2727
for _ in range(r.get_arg()):
2828
r.pos = r.vi_forward_word()
2929

30+
class vi_forward_word_ws(MotionCommand):
31+
def do(self) -> None:
32+
r = self.reader
33+
for _ in range(r.get_arg()):
34+
r.pos = r.vi_forward_word_ws()
3035

3136
class vi_backward_word(MotionCommand):
3237
def do(self) -> None:
@@ -35,6 +40,14 @@ def do(self) -> None:
3540
r.pos = r.vi_bow()
3641

3742

43+
class vi_backward_word_ws(MotionCommand):
44+
def do(self) -> None:
45+
r = self.reader
46+
for _ in range(r.get_arg()):
47+
r.pos = r.vi_bow_ws()
48+
49+
50+
3851
# ============================================================================
3952
# Mode Switching Commands
4053
# ============================================================================

Lib/test/test_pyrepl/test_reader.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,27 @@ def test_vi_word_boundaries(self):
10211021
("get_value(x)", "$b", 10, "b from end lands on x"),
10221022
("get_value(x)", "$bb", 9, "second b lands on ("),
10231023
("get_value(x)", "$bbb", 0, "third b lands on get_value"),
1024+
1025+
# W (WORD motion by whitespace-delimited words)
1026+
("foo.bar baz", "0W", 8, "W skips punctuation to baz"),
1027+
("one,two three", "0W", 8, "W skips comma to three"),
1028+
("hello world", "0W", 8, "W handles multiple spaces"),
1029+
("get_value(x)", "0W", 11, "W clamps to end (no whitespace)"),
1030+
1031+
# Backward W (B)
1032+
("foo.bar baz", "$B", 8, "B from end lands on baz"),
1033+
("foo.bar baz", "$BB", 0, "second B lands on foo.bar"),
1034+
("one,two three", "$B", 8, "B from end lands on three"),
1035+
("one,two three", "$BB", 0, "second B lands on one,two"),
1036+
("hello world", "$B", 8, "B from end lands on world"),
1037+
("hello world", "$BB", 0, "second B lands on hello"),
1038+
1039+
# Edge cases
1040+
(" spaces", "0w", 3, "w from BOL skips leading spaces"),
1041+
("trailing ", "0w", 10, "w clamps at end after trailing spaces"),
1042+
("a", "0w", 0, "w on single char stays in bounds"),
1043+
("", "0w", 0, "w on empty buffer stays at 0"),
1044+
("a b c", "0www", 4, "multiple w's work correctly"),
10241045
]
10251046

10261047
for text, keys, expected_pos, desc in test_cases:

0 commit comments

Comments
 (0)