Gim/internal/editor/integration_motion_word_test.go
Hayden Hargreaves e362c9f118
All checks were successful
Run Test Suite / test (push) Successful in 56s
feat: gap buffer is implemented, tested
Not sure if this is perfect, but it seems to be working
2026-04-02 12:39:30 -07:00

1688 lines
52 KiB
Go

package editor
import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/core"
)
// --- 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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 18 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 18", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'w' at end of file preserves position", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "w")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 11 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 11", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 1 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 1", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'w' from middle of word skips only remaining chars", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "w")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
}
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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 16 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 16", m.ActiveWindow().Cursor.Col)
}
})
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, core.Position{Col: 4, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'e' at end of file preserves position", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 1 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 1", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
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, core.Position{Col: 2, Line: 0})
sendKeys(tm, "e")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
}
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, core.Position{Col: 3, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'b' at word start moves to end previous", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'bbb' moves to back three word", func(t *testing.T) {
lines := []string{"hello world hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 23, Line: 0})
sendKeys(tm, "b", "b", "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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, core.Position{Col: 0, Line: 1})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'b' stops at punctuation", func(t *testing.T) {
lines := []string{"hello.world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 6, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'b' stops at end of punctuation sequence", func(t *testing.T) {
lines := []string{"..hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'b' stops at end of punctuation sequence before word", func(t *testing.T) {
lines := []string{"hello..world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 7, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'b' stops at end of punctuation sequence on newline", func(t *testing.T) {
lines := []string{"hello.", "world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 5 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 5", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("m.ActiveWindow().Cursor.Line = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'b' skips underscore", func(t *testing.T) {
lines := []string{"hello_world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
sendKeys(tm, "b")
m := getFinalModel(t, tm)
if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("m.ActiveWindow().Cursor.Col = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
}
// =============================================================================
// 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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 14 {
t.Errorf("CursorX() = %d, want 14", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' from middle of WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// From middle of "hello", should move to start of "world"
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' from last char of WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// From 'o' in "hello", should move to 'w' in "world"
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' from space before WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// From space, should move to 'w' in "world"
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
// --- 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.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 17 {
t.Errorf("CursorX() = %d, want 17", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 5 {
t.Errorf("CursorX() = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' from punctuation to next WORD", func(t *testing.T) {
lines := []string{"hello. world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// From '.' which is part of "hello.", should move to "world"
if m.ActiveWindow().Cursor.Col != 7 {
t.Errorf("CursorX() = %d, want 7", m.ActiveWindow().Cursor.Col)
}
})
// --- 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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 4 {
t.Errorf("CursorY() = %d, want 4", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'W' from middle line to next", func(t *testing.T) {
lines := []string{"first", "second third", "fourth"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
// --- End of File ---
t.Run("test 'W' at end of file stays put", func(t *testing.T) {
lines := []string{"hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
t.Run("test 'W' on last WORD of file", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
// --- 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.ActiveWindow().Cursor.Col != 9 {
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 9 {
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 3 {
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' from leading whitespace", func(t *testing.T) {
lines := []string{" hello"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// From middle of leading whitespace, move to "hello"
if m.ActiveWindow().Cursor.Col != 3 {
t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col)
}
})
// --- 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.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 14 {
t.Errorf("CursorX() = %d, want 14", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
// --- 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.ActiveWindow().Cursor.Col == m2.ActiveWindow().Cursor.Col {
t.Errorf("W and w should behave differently on punctuation")
}
if m1.ActiveWindow().Cursor.Col != 12 {
t.Errorf("W: CursorX() = %d, want 12", m1.ActiveWindow().Cursor.Col)
}
if m2.ActiveWindow().Cursor.Col != 5 {
t.Errorf("w: CursorX() = %d, want 5", m2.ActiveWindow().Cursor.Col)
}
})
// --- 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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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, core.Position{Col: 6, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// URL is one WORD, should move to "today"
if m.ActiveWindow().Cursor.Col != 35 {
t.Errorf("CursorX() = %d, want 35", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' with email-like text", func(t *testing.T) {
lines := []string{"contact user@example.com now"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 8, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// Email is one WORD, should move to "now"
if m.ActiveWindow().Cursor.Col != 25 {
t.Errorf("CursorX() = %d, want 25", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'W' with file path", func(t *testing.T) {
lines := []string{"edit /home/user/file.txt now"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "W")
m := getFinalModel(t, tm)
// Path is one WORD, should move to "now"
if m.ActiveWindow().Cursor.Col != 25 {
t.Errorf("CursorX() = %d, want 25", m.ActiveWindow().Cursor.Col)
}
})
}
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.ActiveBuffer().Lines[0].String() != "next" {
t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0].String())
}
})
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.ActiveBuffer().Lines[0].String() != "next" {
t.Errorf("dW: Line(0) = %q, want 'next'", m1.ActiveBuffer().Lines[0].String())
}
if m2.ActiveBuffer().Lines[0].String() != ".world next" {
t.Errorf("dw: Line(0) = %q, want '.world next'", m2.ActiveBuffer().Lines[0].String())
}
})
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.ActiveBuffer().Lines[0].String() != "three four" {
t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0].String())
}
})
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() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
if m.ActiveWindow().Anchor.Col != 0 {
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
}
// Should extend to start of "next"
if m.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveBuffer().Lines[0].String() != "ext" {
t.Errorf("Line(0) = %q, want 'ext'", m.ActiveBuffer().Lines[0].String())
}
})
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.ActiveWindow().Anchor.Col != 0 {
t.Errorf("AnchorX() = %d, want 0", m.ActiveWindow().Anchor.Col)
}
// Should extend to start of "three"
if m.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// W should still move cursor, but line mode selects whole lines
if m.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
}
// =============================================================================
// 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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 15 {
t.Errorf("CursorX() = %d, want 15", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'E' from middle of WORD", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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, core.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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'E' from space moves to next WORD end", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "E")
m := getFinalModel(t, tm)
// From space, E goes to end of "world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("'e': CursorX() = %d, want 4", m1.ActiveWindow().Cursor.Col)
}
// 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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("'E': CursorX() = %d, want 10", m2.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 8 {
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 2 {
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
}
})
t.Run("test 'E' from punctuation within WORD", func(t *testing.T) {
lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0})
sendKeys(tm, "E")
m := getFinalModel(t, tm)
// From '.', E goes to 'd' (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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, core.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.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Line != 2 {
t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
if m.ActiveWindow().Cursor.Line != 0 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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.ActiveWindow().Cursor.Col != 13 {
t.Errorf("CursorX() = %d, want 13", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 7 {
t.Errorf("CursorX() = %d, want 7", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 12 {
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 6 {
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
}
if m.ActiveWindow().Cursor.Col != 4 {
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
}
})
// -------------------------------------------------------------------------
// 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.ActiveWindow().Cursor.Col != 23 {
t.Errorf("CursorX() = %d, want 23", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 15 {
t.Errorf("CursorX() = %d, want 15", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 18 {
t.Errorf("CursorX() = %d, want 18", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 11 {
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 22 {
t.Errorf("CursorX() = %d, want 22", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 5 {
t.Errorf("CursorX() = %d, want 5", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveWindow().Cursor.Col != 19 {
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
}
})
}
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.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String())
}
})
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.ActiveBuffer().Lines[0].String() != ".world next" {
t.Errorf("'de': Line(0) = %q, want '.world next'", m1.ActiveBuffer().Lines[0].String())
}
// Now test 'dE'
tm2 := newTestModelWithLines(t, lines)
sendKeys(tm2, "d", "E")
m2 := getFinalModel(t, tm2)
// 'dE' should delete "hello.world" leaving " next"
if m2.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("'dE': Line(0) = %q, want ' next'", m2.ActiveBuffer().Lines[0].String())
}
})
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.ActiveBuffer().Lines[0].String() != " three" {
t.Errorf("Line(0) = %q, want ' three'", m.ActiveBuffer().Lines[0].String())
}
})
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() != core.VisualMode {
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
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.ActiveBuffer().Lines[0].String() != " next" {
t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String())
}
})
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.ActiveWindow().Cursor.Col != 14 {
t.Errorf("CursorX() = %d, want 14", m.ActiveWindow().Cursor.Col)
}
})
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() != core.VisualLineMode {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
}
// Cursor at end of "hello.world" (index 10)
if m.ActiveWindow().Cursor.Col != 10 {
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
}
})
}