@@ -679,6 +679,303 @@ def test_translator_stack_preserves_mode(self):
679679 reader , _ = self ._run_vi (events_normal_path )
680680 self .assertTrue (reader .editor_mode .is_normal ())
681681
682+ def test_insert_bol_and_append_eol (self ):
683+ events = itertools .chain (
684+ code_to_events ("hello" ),
685+ [
686+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC to normal
687+ Event (evt = "key" , data = "I" , raw = bytearray (b"I" )), # Insert at BOL
688+ Event (evt = "key" , data = "[" , raw = bytearray (b"[" )),
689+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # Back to normal
690+ Event (evt = "key" , data = "A" , raw = bytearray (b"A" )), # Append at EOL
691+ Event (evt = "key" , data = "]" , raw = bytearray (b"]" )),
692+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
693+ ],
694+ )
695+ reader , _ = self ._run_vi (events )
696+ self .assertEqual (reader .get_unicode (), "[hello]" )
697+ self .assertTrue (reader .editor_mode .is_normal ())
698+
699+ def test_insert_mode_from_normal (self ):
700+ events = itertools .chain (
701+ code_to_events ("hello" ),
702+ [
703+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC to normal
704+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Go to beginning
705+ Event (evt = "key" , data = "l" , raw = bytearray (b"l" )), # Move right
706+ Event (evt = "key" , data = "l" , raw = bytearray (b"l" )), # Move right again
707+ Event (evt = "key" , data = "i" , raw = bytearray (b"i" )), # Insert mode
708+ Event (evt = "key" , data = "X" , raw = bytearray (b"X" )),
709+ ],
710+ )
711+ reader , _ = self ._run_vi (events )
712+ self .assertEqual (reader .get_unicode (), "heXllo" )
713+ self .assertTrue (reader .editor_mode .is_insert ())
714+
715+ def test_hjkl_motions (self ):
716+ events = itertools .chain (
717+ code_to_events ("hello" ),
718+ [
719+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC to normal
720+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Go to start of line
721+ Event (evt = "key" , data = "l" , raw = bytearray (b"l" )), # Right (h->e)
722+ Event (evt = "key" , data = "l" , raw = bytearray (b"l" )), # Right (e->l)
723+ Event (evt = "key" , data = "h" , raw = bytearray (b"h" )), # Left (l->e)
724+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 'e'
725+ ],
726+ )
727+ reader , _ = self ._run_vi (events )
728+ self .assertEqual (reader .get_unicode (), "hllo" )
729+ self .assertTrue (reader .editor_mode .is_normal ())
730+
731+ def test_dollar_end_of_line (self ):
732+ events = itertools .chain (
733+ code_to_events ("hello" ),
734+ [
735+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
736+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning
737+ Event (evt = "key" , data = "$" , raw = bytearray (b"$" )), # End (on last char)
738+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 'o'
739+ ],
740+ )
741+ reader , _ = self ._run_vi (events )
742+ self .assertEqual (reader .get_unicode (), "hell" )
743+
744+ def test_word_motions (self ):
745+ events = itertools .chain (
746+ code_to_events ("one two" ),
747+ [
748+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
749+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning
750+ Event (evt = "key" , data = "w" , raw = bytearray (b"w" )), # Forward word
751+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete first char of 'two'
752+ ],
753+ )
754+ reader , _ = self ._run_vi (events )
755+ self .assertIn ("one" , reader .get_unicode ())
756+ self .assertNotEqual (reader .get_unicode (), "one two" ) # Something was deleted
757+
758+ def test_repeat_counts (self ):
759+ events = itertools .chain (
760+ code_to_events ("abcdefghij" ),
761+ [
762+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
763+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning
764+ Event (evt = "key" , data = "3" , raw = bytearray (b"3" )), # Count 3
765+ Event (evt = "key" , data = "l" , raw = bytearray (b"l" )), # Move right 3 times
766+ Event (evt = "key" , data = "2" , raw = bytearray (b"2" )), # Count 2
767+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 2 chars (d, e)
768+ ],
769+ )
770+ reader , _ = self ._run_vi (events )
771+ self .assertEqual (reader .get_unicode (), "abcfghij" )
772+ self .assertTrue (reader .editor_mode .is_normal ())
773+
774+ def test_multiline_navigation (self ):
775+ # Test j/k navigation across multiple lines
776+ code = "first\n second\n third"
777+ events = itertools .chain (
778+ code_to_events (code ),
779+ [
780+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
781+ Event (evt = "key" , data = "k" , raw = bytearray (b"k" )), # Up to "second"
782+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning of line
783+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 's'
784+ Event (evt = "key" , data = "j" , raw = bytearray (b"j" )), # Down to "third"
785+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning
786+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 't'
787+ ],
788+ )
789+ reader , _ = self ._run_vi (events )
790+ self .assertEqual (reader .get_unicode (), "first\n econd\n hird" )
791+
792+ def test_arrow_keys_in_normal_mode (self ):
793+ events = itertools .chain (
794+ code_to_events ("test" ),
795+ [
796+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
797+ Event (evt = "key" , data = "left" , raw = bytearray (b"\x1b [D" )), # Left arrow
798+ Event (evt = "key" , data = "left" , raw = bytearray (b"\x1b [D" )), # Left arrow
799+ Event (evt = "key" , data = "x" , raw = bytearray (b"x" )), # Delete 'e'
800+ ],
801+ )
802+ reader , _ = self ._run_vi (events )
803+ self .assertEqual (reader .get_unicode (), "tst" )
804+
805+ def test_escape_in_normal_mode_is_noop (self ):
806+ events = itertools .chain (
807+ code_to_events ("hello" ),
808+ [
809+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC to normal
810+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC again (no-op)
811+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC again (no-op)
812+ ],
813+ )
814+ reader , _ = self ._run_vi (events )
815+ self .assertTrue (reader .editor_mode .is_normal ())
816+ self .assertEqual (reader .get_unicode (), "hello" )
817+
818+ def test_backspace_in_normal_mode (self ):
819+ events = itertools .chain (
820+ code_to_events ("abcd" ),
821+ [
822+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
823+ Event (evt = "key" , data = "\x7f " , raw = bytearray (b"\x7f " )), # Backspace
824+ Event (evt = "key" , data = "\x7f " , raw = bytearray (b"\x7f " )), # Backspace again
825+ ],
826+ )
827+ reader , _ = self ._run_vi (events )
828+ self .assertTrue (reader .editor_mode .is_normal ())
829+ self .assertIsNotNone (reader .get_unicode ())
830+
831+ def test_end_of_word_motion (self ):
832+ events = itertools .chain (
833+ code_to_events ("hello world test" ),
834+ [
835+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
836+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Beginning
837+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # End of "hello"
838+ ],
839+ )
840+ reader , _ = self ._run_vi (events )
841+ # Should be on 'o' of "hello" (last char of word)
842+ self .assertEqual (reader .pos , 4 )
843+ self .assertEqual (reader .buffer [reader .pos ], 'o' )
844+
845+ # Test multiple 'e' commands
846+ events2 = itertools .chain (
847+ code_to_events ("one two three" ),
848+ [
849+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
850+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )),
851+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # End of "one"
852+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # End of "two"
853+ ],
854+ )
855+ reader2 , _ = self ._run_vi (events2 )
856+ # Should be on 'o' of "two"
857+ self .assertEqual (reader2 .buffer [reader2 .pos ], 'o' )
858+
859+ def test_backward_word_motion (self ):
860+ # Test from end of buffer
861+ events = itertools .chain (
862+ code_to_events ("one two" ),
863+ [
864+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC at end
865+ Event (evt = "key" , data = "b" , raw = bytearray (b"b" )), # Back to start of "two"
866+ ],
867+ )
868+ reader , _ = self ._run_vi (events )
869+ self .assertEqual (reader .pos , 4 ) # At 't' of "two"
870+ self .assertEqual (reader .buffer [reader .pos ], 't' )
871+
872+ # Test multiple backwards
873+ events2 = itertools .chain (
874+ code_to_events ("one two three" ),
875+ [
876+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
877+ Event (evt = "key" , data = "b" , raw = bytearray (b"b" )), # Back to "three"
878+ Event (evt = "key" , data = "b" , raw = bytearray (b"b" )), # Back to "two"
879+ Event (evt = "key" , data = "b" , raw = bytearray (b"b" )), # Back to "one"
880+ ],
881+ )
882+ reader2 , _ = self ._run_vi (events2 )
883+ # Should be at beginning of "one"
884+ self .assertEqual (reader2 .pos , 0 )
885+ self .assertEqual (reader2 .buffer [reader2 .pos ], 'o' )
886+
887+ def test_first_non_whitespace_character (self ):
888+ events = itertools .chain (
889+ code_to_events (" hello world" ),
890+ [
891+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )), # ESC
892+ Event (evt = "key" , data = "^" , raw = bytearray (b"^" )), # First non-ws
893+ ],
894+ )
895+ reader , _ = self ._run_vi (events )
896+ # Should be at 'h' of "hello", skipping the 3 spaces
897+ self .assertEqual (reader .pos , 3 )
898+ self .assertEqual (reader .buffer [reader .pos ], 'h' )
899+
900+ # Test with tabs and spaces
901+ events2 = itertools .chain (
902+ code_to_events ("\t text" ),
903+ [
904+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
905+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )), # Go to BOL first
906+ Event (evt = "key" , data = "^" , raw = bytearray (b"^" )), # Then to first non-ws
907+ ],
908+ )
909+ reader2 , _ = self ._run_vi (events2 )
910+ self .assertEqual (reader2 .buffer [reader2 .pos ], 't' )
911+
912+ def test_word_motion_edge_cases (self ):
913+ # Test with punctuation - underscore should be a word boundary
914+ events = itertools .chain (
915+ code_to_events ("hello_world" ),
916+ [
917+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
918+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )),
919+ Event (evt = "key" , data = "w" , raw = bytearray (b"w" )), # Forward word
920+ ],
921+ )
922+ reader , _ = self ._run_vi (events )
923+ # 'w' moves to next word, underscore is not alphanumeric so treated as boundary
924+ self .assertIn (reader .pos , [5 , 6 ]) # Could be on '_' or 'w' depending on implementation
925+
926+ # Test 'e' at end of buffer stays in bounds
927+ events2 = itertools .chain (
928+ code_to_events ("end" ),
929+ [
930+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
931+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # Already at end of word
932+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # Should stay in bounds
933+ ],
934+ )
935+ reader2 , _ = self ._run_vi (events2 )
936+ # Should not go past end of buffer
937+ self .assertLessEqual (reader2 .pos , len (reader2 .buffer ) - 1 )
938+
939+ # Test 'b' at beginning doesn't crash
940+ events3 = itertools .chain (
941+ code_to_events ("start" ),
942+ [
943+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
944+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )),
945+ Event (evt = "key" , data = "b" , raw = bytearray (b"b" )), # Should stay at 0
946+ ],
947+ )
948+ reader3 , _ = self ._run_vi (events3 )
949+ self .assertEqual (reader3 .pos , 0 )
950+
951+ def test_repeat_count_with_word_motions (self ):
952+ events = itertools .chain (
953+ code_to_events ("one two three four" ),
954+ [
955+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
956+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )),
957+ Event (evt = "key" , data = "2" , raw = bytearray (b"2" )), # Count 2
958+ Event (evt = "key" , data = "w" , raw = bytearray (b"w" )), # Forward 2 words
959+ ],
960+ )
961+ reader , _ = self ._run_vi (events )
962+ # Should be at start of "three" (2 words forward from "one")
963+ self .assertEqual (reader .buffer [reader .pos ], 't' ) # 't' of "three"
964+
965+ # Test with 'e'
966+ events2 = itertools .chain (
967+ code_to_events ("alpha beta gamma" ),
968+ [
969+ Event (evt = "key" , data = "\x1b " , raw = bytearray (b"\x1b " )),
970+ Event (evt = "key" , data = "0" , raw = bytearray (b"0" )),
971+ Event (evt = "key" , data = "2" , raw = bytearray (b"2" )),
972+ Event (evt = "key" , data = "e" , raw = bytearray (b"e" )), # End of 2nd word
973+ ],
974+ )
975+ reader2 , _ = self ._run_vi (events2 )
976+ # Should be at end of "beta"
977+ self .assertEqual (reader2 .buffer [reader2 .pos ], 'a' ) # Last 'a' of "beta"
978+
682979
683980@force_not_colorized_test_class
684981class TestHistoricalReaderBindings (TestCase ):
0 commit comments