Most of the tests were just written poorly, the code was right. Though the yank related questions were actually broken.
2501 lines
79 KiB
Go
2501 lines
79 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)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- B Motion Tests ---
|
|
|
|
func TestMoveBackwardWORD(t *testing.T) {
|
|
t.Run("test 'B' moves backward one WORD", 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)
|
|
// Should move to start of "world" (index 6)
|
|
if m.ActiveWindow().Cursor.Col != 6 {
|
|
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'BB' moves backward two WORDs", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "B", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test '2B' moves backward two WORDs with count", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "2", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' on punctuation-heavy text", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// "hello.world" is one WORD, should move to "next" (index 12)
|
|
if m.ActiveWindow().Cursor.Col != 12 {
|
|
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' vs 'b' on punctuation", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
|
|
// Test 'b'
|
|
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm1, "b")
|
|
m1 := getFinalModel(t, tm1)
|
|
|
|
// 'b' moves to start of "next" (index 12)
|
|
if m1.ActiveWindow().Cursor.Col != 12 {
|
|
t.Errorf("'b': CursorX() = %d, want 12", m1.ActiveWindow().Cursor.Col)
|
|
}
|
|
|
|
// Test 'B'
|
|
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm2, "B")
|
|
m2 := getFinalModel(t, tm2)
|
|
|
|
// 'B' treats "hello.world" as one WORD, moves to index 12
|
|
if m2.ActiveWindow().Cursor.Col != 12 {
|
|
t.Errorf("'B': CursorX() = %d, want 12", m2.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' crosses lines backward", func(t *testing.T) {
|
|
lines := []string{"hello", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 1})
|
|
sendKeys(tm, "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveWindow().Cursor.Line != 1 {
|
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
if m.ActiveWindow().Cursor.Col != 0 {
|
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' at beginning of file", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
|
|
sendKeys(tm, "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should stay at 0,0
|
|
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' with multiple spaces", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
|
|
sendKeys(tm, "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should skip spaces and move to "hello"
|
|
if m.ActiveWindow().Cursor.Col != 9 {
|
|
t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' on empty lines", func(t *testing.T) {
|
|
lines := []string{"hello", "", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 2})
|
|
sendKeys(tm, "B")
|
|
|
|
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 != 0 {
|
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'B' complex code-like text", func(t *testing.T) {
|
|
lines := []string{"foo.bar(baz) next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
|
|
sendKeys(tm, "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// "foo.bar(baz)" is one WORD, should move to "next" (index 13)
|
|
if m.ActiveWindow().Cursor.Col != 13 {
|
|
t.Errorf("CursorX() = %d, want 13", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'BB' complex code-like text", func(t *testing.T) {
|
|
lines := []string{"foo.bar(baz) next.thing"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 22, Line: 0})
|
|
sendKeys(tm, "B", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to start of first WORD "foo.bar(baz)" (index 0)
|
|
if m.ActiveWindow().Cursor.Col != 0 {
|
|
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMoveBackwardWORDWithOperator(t *testing.T) {
|
|
t.Run("test 'dB' deletes backward WORD including punctuation", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "d", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
|
|
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'dB' vs 'db' on dotted text", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
|
|
// First test 'db'
|
|
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm1, "d", "b")
|
|
m1 := getFinalModel(t, tm1)
|
|
|
|
if m1.ActiveBuffer().Lines[0].String() != "hello.world t" {
|
|
t.Errorf("'db': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String())
|
|
}
|
|
|
|
// Now test 'dB'
|
|
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm2, "d", "B")
|
|
m2 := getFinalModel(t, tm2)
|
|
|
|
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" {
|
|
t.Errorf("'dB': Line(0) = %q, want 'hello.world t'", m2.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'd2B' deletes two WORDs backward", func(t *testing.T) {
|
|
lines := []string{"one.a two.b three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
|
|
sendKeys(tm, "d", "2", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should delete "two.b three" leaving "one.a e"
|
|
if m.ActiveBuffer().Lines[0].String() != "one.a e" {
|
|
t.Errorf("Line(0) = %q, want 'one.a e'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
// BUG: This is a failing tests, cursor is not moving at start of yank
|
|
t.Run("test 'yB' yanks WORD including punctuation", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "y", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Text should remain unchanged
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
|
|
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
// Cursor should be at start of yanked region (index 12)
|
|
if m.ActiveWindow().Cursor.Col != 12 {
|
|
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'cB' changes WORD backward", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "c", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should delete " next" and enter insert mode
|
|
if m.Mode() != core.InsertMode {
|
|
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
|
|
}
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.world t" {
|
|
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMoveBackwardWORDVisualMode(t *testing.T) {
|
|
t.Run("test 'vB' selects backward WORD", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != core.VisualMode {
|
|
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
|
|
}
|
|
// Cursor at start of "next" (index 12)
|
|
if m.ActiveWindow().Cursor.Col != 12 {
|
|
t.Errorf("CursorX() = %d, want 12", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'vBd' deletes selected WORD", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "B", "d")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.world " {
|
|
t.Errorf("Line(0) = %q, want 'hello.world '", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'v2B' selects backward two WORDs", func(t *testing.T) {
|
|
lines := []string{"foo.bar baz.qux rest"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
|
|
sendKeys(tm, "v", "2", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Cursor at start of "baz.qux" (index 8)
|
|
if m.ActiveWindow().Cursor.Col != 8 {
|
|
t.Errorf("CursorX() = %d, want 8", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'VB' in visual line mode", func(t *testing.T) {
|
|
lines := []string{"hello.world", "next line"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 1})
|
|
sendKeys(tm, "V", "B")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != core.VisualLineMode {
|
|
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
|
}
|
|
// Should select both lines
|
|
if m.ActiveWindow().Cursor.Line != 1 {
|
|
t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- ge Motion Tests ---
|
|
|
|
func TestMoveBackwardWordEnd(t *testing.T) {
|
|
t.Run("test 'ge' moves backward to previous word end", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gege' moves backward two word ends", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "g", "e", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "one" (index 2)
|
|
if m.ActiveWindow().Cursor.Col != 2 {
|
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test '2ge' moves backward two word ends with count", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "2", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "one" (index 2)
|
|
if m.ActiveWindow().Cursor.Col != 2 {
|
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' on punctuation-heavy text", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move 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 'ge' crosses lines backward", func(t *testing.T) {
|
|
lines := []string{"hello", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "hello" on previous line (index 4)
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' at beginning of file", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should stay at 0,0
|
|
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' with multiple spaces", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should skip spaces and move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' on empty lines", func(t *testing.T) {
|
|
lines := []string{"hello", "", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should skip empty line and move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' respects word classes", func(t *testing.T) {
|
|
lines := []string{"foo-bar baz"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "bar" (index 6)
|
|
if m.ActiveWindow().Cursor.Col != 6 {
|
|
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'ge' on method chain", func(t *testing.T) {
|
|
lines := []string{"obj.method().chain() next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
|
|
sendKeys(tm, "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of ")" (index 19)
|
|
if m.ActiveWindow().Cursor.Col != 19 {
|
|
t.Errorf("CursorX() = %d, want 19", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMoveBackwardWordEndWithOperator(t *testing.T) {
|
|
t.Run("test 'dge' deletes backward to word end", func(t *testing.T) {
|
|
lines := []string{"hello world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "d", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'd2ge' deletes backward two word ends", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "d", "2", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "on" {
|
|
t.Errorf("Line(0) = %q, want 'on'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
// BUG: This is a failing tests, cursor is not moving at start of yank
|
|
t.Run("test 'yge' yanks backward to word end", func(t *testing.T) {
|
|
lines := []string{"hello world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "y", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Text should remain unchanged
|
|
if m.ActiveBuffer().Lines[0].String() != "hello world next" {
|
|
t.Errorf("Line(0) = %q, want 'hello world next'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
// Cursor 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 'cge' changes backward to word end", func(t *testing.T) {
|
|
lines := []string{"hello world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "c", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should delete and enter insert mode
|
|
if m.Mode() != core.InsertMode {
|
|
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
|
|
}
|
|
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMoveBackwardWordEndVisualMode(t *testing.T) {
|
|
t.Run("test 'vge' selects backward to word end", func(t *testing.T) {
|
|
lines := []string{"hello world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != core.VisualMode {
|
|
t.Errorf("Mode() = %v, want VisualMode", m.Mode())
|
|
}
|
|
// Cursor 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 'vged' deletes selection", func(t *testing.T) {
|
|
lines := []string{"hello world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "g", "e", "d")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'v2ge' selects backward two word ends", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "v", "2", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Cursor at end of "one" (index 2)
|
|
if m.ActiveWindow().Cursor.Col != 2 {
|
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'Vge' in visual line mode", func(t *testing.T) {
|
|
lines := []string{"hello", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
|
sendKeys(tm, "V", "g", "e")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != core.VisualLineMode {
|
|
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
|
}
|
|
// Should select both lines
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
}
|
|
|
|
// --- gE Motion Tests ---
|
|
|
|
func TestMoveBackwardWORDEnd(t *testing.T) {
|
|
t.Run("test 'gE' moves backward to previous WORD end", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 10, Line: 0})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gEgE' moves backward two WORD ends", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "g", "E", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "one" (index 2)
|
|
if m.ActiveWindow().Cursor.Col != 2 {
|
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test '2gE' moves backward two WORD ends with count", func(t *testing.T) {
|
|
lines := []string{"one two three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 12, Line: 0})
|
|
sendKeys(tm, "2", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "one" (index 2)
|
|
if m.ActiveWindow().Cursor.Col != 2 {
|
|
t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' on punctuation-heavy text", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// "hello.world" is one WORD ending at index 10
|
|
if m.ActiveWindow().Cursor.Col != 10 {
|
|
t.Errorf("CursorX() = %d, want 10", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' vs 'ge' on punctuation", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
|
|
// Test 'ge'
|
|
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm1, "g", "e")
|
|
m1 := getFinalModel(t, tm1)
|
|
|
|
// 'ge' treats punctuation as separate word
|
|
if m1.ActiveWindow().Cursor.Col != 10 {
|
|
t.Errorf("'ge': CursorX() = %d, want 10", m1.ActiveWindow().Cursor.Col)
|
|
}
|
|
|
|
// Test 'gE'
|
|
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm2, "g", "E")
|
|
m2 := getFinalModel(t, tm2)
|
|
|
|
// 'gE' treats "hello.world" as one WORD
|
|
if m2.ActiveWindow().Cursor.Col != 10 {
|
|
t.Errorf("'gE': CursorX() = %d, want 10", m2.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' crosses lines backward", func(t *testing.T) {
|
|
lines := []string{"hello", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should move to end of "hello" on previous line (index 4)
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' at beginning of file", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLines(t, lines) // Cursor at 0,0
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should stay at 0,0
|
|
if m.ActiveWindow().Cursor.Col != 0 || m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("Cursor = (%d,%d), want (0,0)", m.ActiveWindow().Cursor.Col, m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' with multiple spaces", func(t *testing.T) {
|
|
lines := []string{"hello world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 13, Line: 0})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should skip spaces and move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' on empty lines", func(t *testing.T) {
|
|
lines := []string{"hello", "", "world"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should skip empty line and move to end of "hello" (index 4)
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
if m.ActiveWindow().Cursor.Col != 4 {
|
|
t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' complex code-like text", func(t *testing.T) {
|
|
lines := []string{"foo.bar(baz) next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
|
|
sendKeys(tm, "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// "foo.bar(baz)" is one WORD ending at index 11
|
|
if m.ActiveWindow().Cursor.Col != 11 {
|
|
t.Errorf("CursorX() = %d, want 11", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'gE' on method chain", func(t *testing.T) {
|
|
lines := []string{"obj.method().chain() next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 24, Line: 0})
|
|
sendKeys(tm, "g", "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 TestMoveBackwardWORDEndWithOperator(t *testing.T) {
|
|
t.Run("test 'dgE' deletes backward to WORD end", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "d", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'dgE' vs 'dge' on dotted text", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
|
|
// First test 'dge'
|
|
tm1 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm1, "d", "g", "e")
|
|
m1 := getFinalModel(t, tm1)
|
|
|
|
// 'dge' should delete to end of "world"
|
|
if m1.ActiveBuffer().Lines[0].String() != "hello.worl" {
|
|
t.Errorf("'dge': Line(0) = %q, want 'hello.worl'", m1.ActiveBuffer().Lines[0].String())
|
|
}
|
|
|
|
// Now test 'dgE'
|
|
tm2 := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm2, "d", "g", "E")
|
|
m2 := getFinalModel(t, tm2)
|
|
|
|
if m2.ActiveBuffer().Lines[0].String() != "hello.worl" {
|
|
t.Errorf("'dgE': Line(0) = %q, want 'hello.worl'", m2.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'd2gE' deletes backward two WORD ends", func(t *testing.T) {
|
|
lines := []string{"one.a two.b three"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 16, Line: 0})
|
|
sendKeys(tm, "d", "2", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "one." {
|
|
t.Errorf("Line(0) = %q, want 'one.'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
// BUG: This is a failing tests, cursor is not moving at start of yank
|
|
t.Run("test 'ygE' yanks backward to WORD end", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "y", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Text should remain unchanged
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.world next" {
|
|
t.Errorf("Line(0) = %q, want 'hello.world next'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
// Cursor should be 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 'cgE' changes backward to WORD end", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "c", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Should delete and enter insert mode
|
|
if m.Mode() != core.InsertMode {
|
|
t.Errorf("Mode() = %v, want InsertMode", m.Mode())
|
|
}
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMoveBackwardWORDEndVisualMode(t *testing.T) {
|
|
t.Run("test 'vgE' selects backward to WORD end", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "g", "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 'vgEd' deletes selection", func(t *testing.T) {
|
|
lines := []string{"hello.world next"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
|
|
sendKeys(tm, "v", "g", "E", "d")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
|
|
t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("test 'v2gE' selects backward two WORD ends", func(t *testing.T) {
|
|
lines := []string{"foo.bar baz.qux rest"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 19, Line: 0})
|
|
sendKeys(tm, "v", "2", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
// Cursor at end of "foo.bar" (index 6)
|
|
if m.ActiveWindow().Cursor.Col != 6 {
|
|
t.Errorf("CursorX() = %d, want 6", m.ActiveWindow().Cursor.Col)
|
|
}
|
|
})
|
|
|
|
t.Run("test 'VgE' in visual line mode", func(t *testing.T) {
|
|
lines := []string{"hello.world", "next line"}
|
|
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1})
|
|
sendKeys(tm, "V", "g", "E")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != core.VisualLineMode {
|
|
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
|
|
}
|
|
// Should select both lines
|
|
if m.ActiveWindow().Cursor.Line != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
|
|
}
|
|
})
|
|
}
|