package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/action" ) // --- w, e, and b Tests --- func TestMoveForwardWord(t *testing.T) { t.Run("test 'w' moves forward one word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 6 { t.Errorf("m.CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'www' moves forward three words", func(t *testing.T) { lines := []string{"hello world hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w", "w", "w") m := getFinalModel(t, tm) if m.CursorX() != 18 { t.Errorf("m.CursorX() = %d, want 18", m.CursorX()) } }) t.Run("test 'w' moves to next line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("m.CursorY() = %d, want 1", m.CursorY()) } }) t.Run("test 'w' at end of file preserves position", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("m.CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'w' stops at punctuation", func(t *testing.T) { lines := []string{"hello.word"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'w' skips underscore", func(t *testing.T) { lines := []string{"hello_world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 11 { t.Errorf("m.CursorX() = %d, want 11", m.CursorX()) } }) t.Run("test 'w' moves once past punctuation", func(t *testing.T) { lines := []string{".hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 1 { t.Errorf("m.CursorX() = %d, want 1", m.CursorX()) } }) t.Run("test 'w' from middle of word skips only remaining chars", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "w") m := getFinalModel(t, tm) if m.CursorX() != 6 { t.Errorf("m.CursorX() = %d, want 6", m.CursorX()) } }) } func TestMoveToWordEnd(t *testing.T) { t.Run("test 'e' moves to end of current word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("m.CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'eee' moves to end of three words", func(t *testing.T) { lines := []string{"hello world hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e", "e", "e") m := getFinalModel(t, tm) if m.CursorX() != 16 { t.Errorf("m.CursorX() = %d, want 16", m.CursorX()) } }) t.Run("test 'e' moves to next line when at end of word", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("m.CursorX() = %d, want 4", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("m.CursorY() = %d, want 1", m.CursorY()) } }) t.Run("test 'e' at end of file preserves position", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("m.CursorX() = %d, want 4", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("m.CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'e' stops at end of word before punctuation", func(t *testing.T) { lines := []string{"hello.world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("m.CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'e' stops at end of punctuation sequence", func(t *testing.T) { lines := []string{"..hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 1 { t.Errorf("m.CursorX() = %d, want 1", m.CursorX()) } }) t.Run("test 'e' skips underscore", func(t *testing.T) { lines := []string{"hello_world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 10 { t.Errorf("m.CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'e' moves past leading punctuation to end of word", func(t *testing.T) { lines := []string{".hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'e' from middle of word moves to end of current word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "e") m := getFinalModel(t, tm) if m.CursorX() != 4 { t.Errorf("m.CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'ee' lands at end of each class in multi-char punctuation", func(t *testing.T) { lines := []string{"hello..world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "e", "e") m := getFinalModel(t, tm) if m.CursorX() != 6 { t.Errorf("m.CursorX() = %d, want 6", m.CursorX()) } }) } func TestMoveBackwardWord(t *testing.T) { t.Run("test 'b' moves to start of current word", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 3, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } }) t.Run("test 'b' at word start moves to end previous", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } }) t.Run("test 'bbb' moves to back three word", func(t *testing.T) { lines := []string{"hello world hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 23, Line: 0}) sendKeys(tm, "b", "b", "b") m := getFinalModel(t, tm) if m.CursorX() != 6 { t.Errorf("m.CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'b' moves to prev line when at start of line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("m.CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'b' at start of file preserves position", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } }) t.Run("test 'b' stops at punctuation", func(t *testing.T) { lines := []string{"hello.world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'b' stops at end of punctuation sequence", func(t *testing.T) { lines := []string{"..hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } }) t.Run("test 'b' stops at end of punctuation sequence before word", func(t *testing.T) { lines := []string{"hello..world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'b' stops at end of punctuation sequence on newline", func(t *testing.T) { lines := []string{"hello.", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 1}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 5 { t.Errorf("m.CursorX() = %d, want 5", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("m.CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'b' skips underscore", func(t *testing.T) { lines := []string{"hello_world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0}) sendKeys(tm, "b") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("m.CursorX() = %d, want 0", m.CursorX()) } }) } // ============================================================================= // W (WORD motion) Tests // ============================================================================= // In Vim, W moves forward to the start of the next WORD. // A WORD is a sequence of non-blank characters separated by whitespace. // Unlike 'w', punctuation does NOT create word boundaries for 'W'. // // Key difference from 'w': // "hello,world" with 'w': stops at "hello", ",", "world" (3 words) // "hello,world" with 'W': entire thing is 1 WORD // // ============================================================================= func TestMoveForwardWORD(t *testing.T) { // --- Basic Movement --- t.Run("test 'W' moves forward one WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Should move to 'w' in "world" at index 6 if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'WW' moves forward two WORDs", func(t *testing.T) { lines := []string{"one two three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W", "W") m := getFinalModel(t, tm) // Should move to 't' in "three" at index 8 if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test 'WWW' moves forward three WORDs", func(t *testing.T) { lines := []string{"one two three four"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W", "W", "W") m := getFinalModel(t, tm) // Should move to 'f' in "four" at index 14 if m.CursorX() != 14 { t.Errorf("CursorX() = %d, want 14", m.CursorX()) } }) t.Run("test 'W' from middle of WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From middle of "hello", should move to start of "world" if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'W' from last char of WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From 'o' in "hello", should move to 'w' in "world" if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'W' from space before WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From space, should move to 'w' in "world" if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) // --- Key Difference: Punctuation Handling --- t.Run("test 'W' skips punctuation (does NOT stop at dot)", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Unlike 'w', 'W' treats "hello.world" as one WORD // Should skip to "next" at index 12 if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test 'W' skips comma", func(t *testing.T) { lines := []string{"hello,world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "hello,world" is one WORD, should move to "next" if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test 'W' skips multiple punctuation", func(t *testing.T) { lines := []string{"hello...world!!! next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "hello...world!!!" is one WORD if m.CursorX() != 17 { t.Errorf("CursorX() = %d, want 17", m.CursorX()) } }) t.Run("test 'W' skips brackets and parens", func(t *testing.T) { lines := []string{"func(arg) next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "func(arg)" is one WORD if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'W' skips operators", func(t *testing.T) { lines := []string{"a+=b next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "a+=b" is one WORD if m.CursorX() != 5 { t.Errorf("CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'W' skips quotes", func(t *testing.T) { lines := []string{`"hello" next`} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // `"hello"` is one WORD if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test 'W' handles underscore (same as w)", func(t *testing.T) { lines := []string{"hello_world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "hello_world" is one WORD (same behavior as w for underscores) if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test 'W' on pure punctuation sequence", func(t *testing.T) { lines := []string{"... next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "..." is one WORD if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'W' from punctuation to next WORD", func(t *testing.T) { lines := []string{"hello. world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From '.' which is part of "hello.", should move to "world" if m.CursorX() != 7 { t.Errorf("CursorX() = %d, want 7", m.CursorX()) } }) // --- Line Crossing --- t.Run("test 'W' moves to next line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) t.Run("test 'W' skips empty lines", func(t *testing.T) { lines := []string{"hello", "", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } }) t.Run("test 'W' skips multiple empty lines", func(t *testing.T) { lines := []string{"hello", "", "", "", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 4 { t.Errorf("CursorY() = %d, want 4", m.CursorY()) } }) t.Run("test 'W' skips whitespace-only lines", func(t *testing.T) { lines := []string{"hello", " ", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } }) t.Run("test 'W' from middle line to next", func(t *testing.T) { lines := []string{"first", "second third", "fourth"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 7, Line: 1}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From "third" on line 1, should move to "fourth" on line 2 if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } }) // --- End of File --- t.Run("test 'W' at end of file stays put", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // At last char, no more WORDs, should stay (or move to end) if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'W' on last WORD of file", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // On "world", no next WORD, should go to end or stay if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'W' past end of single line file", func(t *testing.T) { lines := []string{"word"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Only one WORD, should move to end or stay if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) // --- Whitespace Handling --- t.Run("test 'W' skips multiple spaces", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 9 { t.Errorf("CursorX() = %d, want 9", m.CursorX()) } }) t.Run("test 'W' skips tabs", func(t *testing.T) { lines := []string{"hello\tworld"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Tab is whitespace, "world" starts after tab if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'W' skips mixed whitespace", func(t *testing.T) { lines := []string{"hello \t world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "hello \t world" - world starts at index 9 // h=0, e=1, l=2, l=3, o=4, space=5, tab=6, space=7, space=8, w=9 if m.CursorX() != 9 { t.Errorf("CursorX() = %d, want 9", m.CursorX()) } }) t.Run("test 'W' with leading whitespace", func(t *testing.T) { lines := []string{" hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // From start (in whitespace), should move to "hello" at index 3 if m.CursorX() != 3 { t.Errorf("CursorX() = %d, want 3", m.CursorX()) } }) t.Run("test 'W' from leading whitespace", func(t *testing.T) { lines := []string{" hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 1, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // From middle of leading whitespace, move to "hello" if m.CursorX() != 3 { t.Errorf("CursorX() = %d, want 3", m.CursorX()) } }) // --- Count Handling --- t.Run("test '2W' moves forward two WORDs", func(t *testing.T) { lines := []string{"one two three four"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "W") m := getFinalModel(t, tm) // Skip "one" and "two", land on "three" if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test '3W' moves forward three WORDs", func(t *testing.T) { lines := []string{"one two three four five"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "W") m := getFinalModel(t, tm) // Skip "one", "two", "three", land on "four" if m.CursorX() != 14 { t.Errorf("CursorX() = %d, want 14", m.CursorX()) } }) t.Run("test '5W' with fewer WORDs available", func(t *testing.T) { lines := []string{"one two three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "5", "W") m := getFinalModel(t, tm) // Only 3 WORDs, should stop at/after last WORD if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test '2W' across lines", func(t *testing.T) { lines := []string{"one", "two", "three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "W") m := getFinalModel(t, tm) // Skip "one" and "two", land on "three" if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } }) t.Run("test '10W' moves as far as possible", func(t *testing.T) { lines := []string{"a b c"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "1", "0", "W") m := getFinalModel(t, tm) // Only 3 WORDs if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) // --- Comparison with 'w' --- t.Run("test 'W' vs 'w' on dotted text", func(t *testing.T) { // This test documents the difference between W and w lines := []string{"hello.world next"} // Test W behavior tm1 := newTestModelWithLines(t, lines) sendKeys(tm1, "W") m1 := getFinalModel(t, tm1) // Test w behavior tm2 := newTestModelWithLines(t, lines) sendKeys(tm2, "w") m2 := getFinalModel(t, tm2) // W should skip to "next", w should stop at "." if m1.CursorX() == m2.CursorX() { t.Errorf("W and w should behave differently on punctuation") } if m1.CursorX() != 12 { t.Errorf("W: CursorX() = %d, want 12", m1.CursorX()) } if m2.CursorX() != 5 { t.Errorf("w: CursorX() = %d, want 5", m2.CursorX()) } }) // --- Edge Cases --- t.Run("test 'W' on empty file", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'W' on single character", func(t *testing.T) { lines := []string{"a"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Single char, no next WORD if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'W' on single punctuation", func(t *testing.T) { lines := []string{"."} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'W' on line with only whitespace after cursor", func(t *testing.T) { lines := []string{"hello ", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // Should skip trailing whitespace and go to next line if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) t.Run("test 'W' complex code-like text", func(t *testing.T) { lines := []string{"func(a,b) { return a+b; }"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W") m := getFinalModel(t, tm) // "func(a,b)" is one WORD, next is "{" if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'WW' on complex code-like text", func(t *testing.T) { lines := []string{"func(a,b) { return a+b; }"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "W", "W") m := getFinalModel(t, tm) // After "func(a,b)" and "{", should be on "return" if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test 'W' with URL-like text", func(t *testing.T) { lines := []string{"visit https://example.com/path?q=1 today"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // URL is one WORD, should move to "today" if m.CursorX() != 35 { t.Errorf("CursorX() = %d, want 35", m.CursorX()) } }) t.Run("test 'W' with email-like text", func(t *testing.T) { lines := []string{"contact user@example.com now"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 8, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // Email is one WORD, should move to "now" if m.CursorX() != 25 { t.Errorf("CursorX() = %d, want 25", m.CursorX()) } }) t.Run("test 'W' with file path", func(t *testing.T) { lines := []string{"edit /home/user/file.txt now"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "W") m := getFinalModel(t, tm) // Path is one WORD, should move to "now" if m.CursorX() != 25 { t.Errorf("CursorX() = %d, want 25", m.CursorX()) } }) } func TestMoveForwardWORDWithOperator(t *testing.T) { t.Run("test 'dW' deletes WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "W") m := getFinalModel(t, tm) // Should delete "hello.world " (including trailing space) if m.Line(0) != "next" { t.Errorf("Line(0) = %q, want 'next'", m.Line(0)) } }) t.Run("test 'dW' vs 'dw' on dotted text", func(t *testing.T) { // dW deletes entire "hello.world " lines1 := []string{"hello.world next"} tm1 := newTestModelWithLines(t, lines1) sendKeys(tm1, "d", "W") m1 := getFinalModel(t, tm1) // dw deletes only "hello" lines2 := []string{"hello.world next"} tm2 := newTestModelWithLines(t, lines2) sendKeys(tm2, "d", "w") m2 := getFinalModel(t, tm2) if m1.Line(0) != "next" { t.Errorf("dW: Line(0) = %q, want 'next'", m1.Line(0)) } if m2.Line(0) != ".world next" { t.Errorf("dw: Line(0) = %q, want '.world next'", m2.Line(0)) } }) t.Run("test 'd2W' deletes two WORDs", func(t *testing.T) { lines := []string{"one two three four"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "2", "W") m := getFinalModel(t, tm) if m.Line(0) != "three four" { t.Errorf("Line(0) = %q, want 'three four'", m.Line(0)) } }) t.Run("test 'yW' yanks WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "y", "W") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // Should yank "hello.world " (with trailing space) if len(reg.Content) != 1 || reg.Content[0] != "hello.world " { t.Errorf("register content = %q, want 'hello.world '", reg.Content) } }) t.Run("test 'y2W' yanks two WORDs", func(t *testing.T) { lines := []string{"one two three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "y", "2", "W") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 || reg.Content[0] != "one two " { t.Errorf("register content = %q, want 'one two '", reg.Content) } }) t.Run("test 'cW' changes WORD (placeholder - c not implemented)", func(t *testing.T) { // Skip if change operator not implemented t.Skip("Change operator not implemented yet") }) } func TestMoveForwardWORDInVisualMode(t *testing.T) { t.Run("test 'vW' selects WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "W") m := getFinalModel(t, tm) if m.Mode() != action.VisualMode { t.Errorf("Mode() = %v, want VisualMode", m.Mode()) } if m.AnchorX() != 0 { t.Errorf("AnchorX() = %d, want 0", m.AnchorX()) } // Should extend to start of "next" if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test 'vWd' deletes selection", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "W", "d") m := getFinalModel(t, tm) // Visual selection from 0 to 12, delete "hello.world " if m.Line(0) != "ext" { t.Errorf("Line(0) = %q, want 'ext'", m.Line(0)) } }) t.Run("test 'v2W' selects two WORDs", func(t *testing.T) { lines := []string{"one two three four"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "2", "W") m := getFinalModel(t, tm) if m.AnchorX() != 0 { t.Errorf("AnchorX() = %d, want 0", m.AnchorX()) } // Should extend to start of "three" if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test 'VW' in visual line mode", func(t *testing.T) { lines := []string{"hello world", "next line"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "V", "W") m := getFinalModel(t, tm) if m.Mode() != action.VisualLineMode { t.Errorf("Mode() = %v, want VisualLineMode", m.Mode()) } // W should still move cursor, but line mode selects whole lines if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) } // ============================================================================= // E (WORD end) Motion Tests // ============================================================================= // E moves forward to the end of the current WORD. // Key difference from 'e': E treats ALL non-whitespace as one WORD. // On "hello.world", 'e' stops at 'o' (end of "hello"), 'E' stops at 'd' (end of "hello.world"). func TestMoveForwardWORDEnd(t *testing.T) { // ------------------------------------------------------------------------- // Basic Movement // ------------------------------------------------------------------------- t.Run("test 'E' moves to end of WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "hello" ends at index 4 if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'EE' moves to end of second WORD", func(t *testing.T) { lines := []string{"hello world test"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) // "world" ends at index 10 if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'EEE' moves to end of third WORD", func(t *testing.T) { lines := []string{"hello world test"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E", "E") m := getFinalModel(t, tm) // "test" ends at index 15 if m.CursorX() != 15 { t.Errorf("CursorX() = %d, want 15", m.CursorX()) } }) t.Run("test 'E' from middle of WORD", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0}) sendKeys(tm, "E") m := getFinalModel(t, tm) // From index 2 in "hello", E goes to index 4 (end of "hello") if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'E' from last char of WORD moves to next WORD end", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "E") m := getFinalModel(t, tm) // From end of "hello", E goes to end of "world" (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'E' from space moves to next WORD end", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "E") m := getFinalModel(t, tm) // From space, E goes to end of "world" (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) // ------------------------------------------------------------------------- // Punctuation Handling (KEY DIFFERENCE from 'e') // ------------------------------------------------------------------------- t.Run("test 'E' goes through punctuation (does NOT stop at dot)", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // E treats "hello.world" as one WORD, ends at 'd' (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'e' vs 'E' on dotted text", func(t *testing.T) { // First test 'e' lines := []string{"hello.world next"} tm1 := newTestModelWithLines(t, lines) sendKeys(tm1, "e") m1 := getFinalModel(t, tm1) // 'e' should stop at end of "hello" (index 4) if m1.CursorX() != 4 { t.Errorf("'e': CursorX() = %d, want 4", m1.CursorX()) } // Now test 'E' tm2 := newTestModelWithLines(t, lines) sendKeys(tm2, "E") m2 := getFinalModel(t, tm2) // 'E' should go to end of "hello.world" (index 10) if m2.CursorX() != 10 { t.Errorf("'E': CursorX() = %d, want 10", m2.CursorX()) } }) t.Run("test 'E' goes through comma", func(t *testing.T) { lines := []string{"foo,bar baz"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "foo,bar" ends at index 6 if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'E' goes through multiple punctuation", func(t *testing.T) { lines := []string{"a...b...c next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "a...b...c" ends at index 8 if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test 'E' goes through brackets and parens", func(t *testing.T) { lines := []string{"func(arg) next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "func(arg)" ends at index 8 if m.CursorX() != 8 { t.Errorf("CursorX() = %d, want 8", m.CursorX()) } }) t.Run("test 'E' goes through operators", func(t *testing.T) { lines := []string{"a+b*c next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "a+b*c" ends at index 4 if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'E' goes through quotes", func(t *testing.T) { lines := []string{`"hello" next`} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // `"hello"` ends at index 6 if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test 'E' on pure punctuation sequence", func(t *testing.T) { lines := []string{"... next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "..." ends at index 2 if m.CursorX() != 2 { t.Errorf("CursorX() = %d, want 2", m.CursorX()) } }) t.Run("test 'E' from punctuation within WORD", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 5, Line: 0}) sendKeys(tm, "E") m := getFinalModel(t, tm) // From '.', E goes to 'd' (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) // ------------------------------------------------------------------------- // Line Crossing // ------------------------------------------------------------------------- t.Run("test 'E' moves to next line when at end", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0}) sendKeys(tm, "E") m := getFinalModel(t, tm) // Should be at end of "world" on line 1, index 4 if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'E' skips empty lines", func(t *testing.T) { lines := []string{"hello", "", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) // First E: end of "hello" (0,4) // Second E: skips empty line, end of "world" (2,4) if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) t.Run("test 'E' skips whitespace-only lines", func(t *testing.T) { lines := []string{"hello", " ", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) // ------------------------------------------------------------------------- // End of File Behavior // ------------------------------------------------------------------------- t.Run("test 'E' at end of file stays put", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) // Go to end of file sendKeys(tm, "E", "E", "E") m := getFinalModel(t, tm) // Should stay at end of "hello" (index 4) if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("test 'E' on last WORD of file", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E", "E") m := getFinalModel(t, tm) // Should be at end of "world" (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'E' on single character file", func(t *testing.T) { lines := []string{"a"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // Already at end, should stay at 0 if m.CursorX() != 0 { t.Errorf("CursorX() = %d, want 0", m.CursorX()) } }) // ------------------------------------------------------------------------- // Whitespace Handling // ------------------------------------------------------------------------- t.Run("test 'E' skips multiple spaces", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) // "world" ends at index 13 if m.CursorX() != 13 { t.Errorf("CursorX() = %d, want 13", m.CursorX()) } }) t.Run("test 'E' skips tabs", func(t *testing.T) { lines := []string{"hello\tworld"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) // "world" ends at index 10 if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'E' with leading whitespace", func(t *testing.T) { lines := []string{" hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // From start (in whitespace), E goes to end of "hello" (index 7) if m.CursorX() != 7 { t.Errorf("CursorX() = %d, want 7", m.CursorX()) } }) // ------------------------------------------------------------------------- // Count Handling // ------------------------------------------------------------------------- t.Run("test '2E' moves to end of second WORD", func(t *testing.T) { lines := []string{"one two three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "E") m := getFinalModel(t, tm) // "two" ends at index 6 if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test '3E' moves to end of third WORD", func(t *testing.T) { lines := []string{"one two three four"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "E") m := getFinalModel(t, tm) // "three" ends at index 12 if m.CursorX() != 12 { t.Errorf("CursorX() = %d, want 12", m.CursorX()) } }) t.Run("test '5E' with fewer WORDs available", func(t *testing.T) { lines := []string{"one two"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "5", "E") m := getFinalModel(t, tm) // Only 2 WORDs, should stop at end of "two" (index 6) if m.CursorX() != 6 { t.Errorf("CursorX() = %d, want 6", m.CursorX()) } }) t.Run("test '2E' across lines", func(t *testing.T) { lines := []string{"hello", "world test"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "E") m := getFinalModel(t, tm) // First E: end of "hello" (0,4) // Second E: end of "world" (1,4) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } if m.CursorX() != 4 { t.Errorf("CursorX() = %d, want 4", m.CursorX()) } }) // ------------------------------------------------------------------------- // Complex/Real-World Cases // ------------------------------------------------------------------------- t.Run("test 'E' with URL-like text", func(t *testing.T) { lines := []string{"https://example.com/path next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // Entire URL is one WORD, ends at index 23 if m.CursorX() != 23 { t.Errorf("CursorX() = %d, want 23", m.CursorX()) } }) t.Run("test 'E' with email-like text", func(t *testing.T) { lines := []string{"user@example.com next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // Entire email is one WORD, ends at index 15 if m.CursorX() != 15 { t.Errorf("CursorX() = %d, want 15", m.CursorX()) } }) t.Run("test 'E' with file path", func(t *testing.T) { lines := []string{"/home/user/file.txt next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // Entire path is one WORD, ends at index 18 if m.CursorX() != 18 { t.Errorf("CursorX() = %d, want 18", m.CursorX()) } }) t.Run("test 'E' complex code-like text", func(t *testing.T) { lines := []string{"foo.bar(baz) next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "foo.bar(baz)" ends at index 11 if m.CursorX() != 11 { t.Errorf("CursorX() = %d, want 11", m.CursorX()) } }) t.Run("test 'EE' complex code-like text", func(t *testing.T) { lines := []string{"foo.bar(baz) next.thing"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E", "E") m := getFinalModel(t, tm) // Second WORD "next.thing" ends at index 22 if m.CursorX() != 22 { t.Errorf("CursorX() = %d, want 22", m.CursorX()) } }) t.Run("test 'E' on array access", func(t *testing.T) { lines := []string{"arr[0] next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // "arr[0]" ends at index 5 if m.CursorX() != 5 { t.Errorf("CursorX() = %d, want 5", m.CursorX()) } }) t.Run("test 'E' on method chain", func(t *testing.T) { lines := []string{"obj.method().chain() next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "E") m := getFinalModel(t, tm) // Entire chain is one WORD, ends at index 19 if m.CursorX() != 19 { t.Errorf("CursorX() = %d, want 19", m.CursorX()) } }) } func TestMoveForwardWORDEndWithOperator(t *testing.T) { t.Run("test 'dE' deletes to end of WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "E") m := getFinalModel(t, tm) // Should delete "hello.world" leaving " next" if m.Line(0) != " next" { t.Errorf("Line(0) = %q, want ' next'", m.Line(0)) } }) t.Run("test 'dE' vs 'de' on dotted text", func(t *testing.T) { // First test 'de' lines := []string{"hello.world next"} tm1 := newTestModelWithLines(t, lines) sendKeys(tm1, "d", "e") m1 := getFinalModel(t, tm1) // 'de' should delete "hello" leaving ".world next" if m1.Line(0) != ".world next" { t.Errorf("'de': Line(0) = %q, want '.world next'", m1.Line(0)) } // Now test 'dE' tm2 := newTestModelWithLines(t, lines) sendKeys(tm2, "d", "E") m2 := getFinalModel(t, tm2) // 'dE' should delete "hello.world" leaving " next" if m2.Line(0) != " next" { t.Errorf("'dE': Line(0) = %q, want ' next'", m2.Line(0)) } }) t.Run("test 'd2E' deletes two WORDs", func(t *testing.T) { lines := []string{"one.a two.b three"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "d", "2", "E") m := getFinalModel(t, tm) // Should delete "one.a two.b" leaving " three" if m.Line(0) != " three" { t.Errorf("Line(0) = %q, want ' three'", m.Line(0)) } }) t.Run("test 'yE' yanks WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "y", "E") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // E is inclusive, should yank "hello.world" (no trailing space) if len(reg.Content) != 1 || reg.Content[0] != "hello.world" { t.Errorf("register content = %q, want 'hello.world'", reg.Content) } }) t.Run("test 'y2E' yanks two WORDs", func(t *testing.T) { lines := []string{"foo.bar baz.qux rest"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "y", "2", "E") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // E is inclusive, should yank "foo.bar baz.qux" (no trailing space) if len(reg.Content) != 1 || reg.Content[0] != "foo.bar baz.qux" { t.Errorf("register content = %q, want 'foo.bar baz.qux'", reg.Content) } }) } func TestMoveForwardWORDEndInVisualMode(t *testing.T) { t.Run("test 'vE' selects to end of WORD including punctuation", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "E") m := getFinalModel(t, tm) if m.Mode() != action.VisualMode { t.Errorf("Mode() = %v, want VisualMode", m.Mode()) } // Cursor at end of "hello.world" (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) t.Run("test 'vEd' deletes selection", func(t *testing.T) { lines := []string{"hello.world next"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "E", "d") m := getFinalModel(t, tm) // Should delete "hello.world" leaving " next" if m.Line(0) != " next" { t.Errorf("Line(0) = %q, want ' next'", m.Line(0)) } }) t.Run("test 'v2E' selects to end of second WORD", func(t *testing.T) { lines := []string{"foo.bar baz.qux rest"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "v", "2", "E") m := getFinalModel(t, tm) // Cursor at end of "baz.qux" (index 14) if m.CursorX() != 14 { t.Errorf("CursorX() = %d, want 14", m.CursorX()) } }) t.Run("test 'VE' in visual line mode", func(t *testing.T) { lines := []string{"hello.world", "next line"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "V", "E") m := getFinalModel(t, tm) if m.Mode() != action.VisualLineMode { t.Errorf("Mode() = %v, want VisualLineMode", m.Mode()) } // Cursor at end of "hello.world" (index 10) if m.CursorX() != 10 { t.Errorf("CursorX() = %d, want 10", m.CursorX()) } }) }