@@ -911,7 +911,7 @@ def test_first_non_whitespace_character(self):
911911 self .assertEqual (reader2 .buffer [reader2 .pos ], 't' )
912912
913913 def test_word_motion_edge_cases (self ):
914- # Test with punctuation - underscore should be a word boundary
914+ # Test with underscore - in vi mode, underscore IS a word character
915915 events = itertools .chain (
916916 code_to_events ("hello_world" ),
917917 [
@@ -921,8 +921,9 @@ def test_word_motion_edge_cases(self):
921921 ],
922922 )
923923 reader , _ = self ._run_vi (events )
924- # 'w' moves to next word, underscore is not alphanumeric so treated as boundary
925- self .assertIn (reader .pos , [5 , 6 ]) # Could be on '_' or 'w' depending on implementation
924+ # In vi mode, underscore is part of word, so 'w' goes past end of "hello_world"
925+ # which clamps to end of buffer (pos 10, on 'd')
926+ self .assertEqual (reader .pos , 10 )
926927
927928 # Test 'e' at end of buffer stays in bounds
928929 events2 = itertools .chain (
@@ -977,6 +978,43 @@ def test_repeat_count_with_word_motions(self):
977978 # Should be at end of "beta"
978979 self .assertEqual (reader2 .buffer [reader2 .pos ], 'a' ) # Last 'a' of "beta"
979980
981+ def test_vi_word_boundaries (self ):
982+ """Test vi word motions match vim behavior for word characters.
983+
984+ In vi, word characters are alphanumeric + underscore.
985+ """
986+ # Test cases: (text, start_key_sequence, expected_pos, description)
987+ test_cases = [
988+ # Underscore is part of word in vi, unlike emacs mode
989+ ("function_name" , "0w" , 12 , "underscore is word char, w clamps to end" ),
990+ ("hello_world test" , "0w" , 12 , "underscore word, then to next word" ),
991+ ("get_value(x)" , "0w" , 10 , "underscore word, skip ( to x" ),
992+
993+ # Basic word motion
994+ ("hello world" , "0w" , 6 , "basic word jump" ),
995+ ("one two" , "0w" , 5 , "double space handled" ),
996+ ("abc def ghi" , "0ww" , 8 , "two w's" ),
997+
998+ # End of word (e) - lands ON last char
999+ ("function_name" , "0e" , 12 , "e lands on last char of underscore word" ),
1000+ ("foo bar" , "0e" , 2 , "e lands on last char of foo" ),
1001+ ("one two three" , "0ee" , 6 , "two e's land on end of two" ),
1002+ ]
1003+
1004+ for text , keys , expected_pos , desc in test_cases :
1005+ with self .subTest (text = text , keys = keys , desc = desc ):
1006+ key_events = []
1007+ for k in keys :
1008+ key_events .append (Event (evt = "key" , data = k , raw = bytearray (k .encode ())))
1009+ events = itertools .chain (
1010+ code_to_events (text ),
1011+ [Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " ))], # ESC
1012+ key_events ,
1013+ )
1014+ reader , _ = self ._run_vi (events )
1015+ self .assertEqual (reader .pos , expected_pos ,
1016+ f"Expected pos { expected_pos } but got { reader .pos } for '{ text } ' with keys '{ keys } '" )
1017+
9801018
9811019@force_not_colorized_test_class
9821020class TestHistoricalReaderBindings (TestCase ):
0 commit comments