feat: adding word actions, not done, and lots of failing tests #4
@ -1685,3 +1685,828 @@ func TestMoveForwardWORDEndInVisualMode(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- 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)
|
||||
// Should move to start of "one" (index 0)
|
||||
if m.ActiveWindow().Cursor.Col != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", 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)
|
||||
// Should move to start of "one" (index 0)
|
||||
if m.ActiveWindow().Cursor.Col != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", 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)
|
||||
// Should move to start of "hello" on previous line
|
||||
if m.ActiveWindow().Cursor.Line != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", 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 != 0 {
|
||||
t.Errorf("CursorX() = %d, want 0", 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)
|
||||
// Should skip empty line and move to "hello"
|
||||
if m.ActiveWindow().Cursor.Line != 0 {
|
||||
t.Errorf("CursorY() = %d, want 0", 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)
|
||||
// Should delete " next" leaving "hello.worldt"
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello.worldt" {
|
||||
t.Errorf("Line(0) = %q, want 'hello.worldt'", 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)
|
||||
|
||||
// 'db' should delete "next" leaving "hello.world "
|
||||
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)
|
||||
|
||||
// 'dB' should delete " next" leaving "hello.worldt"
|
||||
if m2.ActiveBuffer().Lines[0].String() != "hello.worldt" {
|
||||
t.Errorf("'dB': Line(0) = %q, want 'hello.worldt'", 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())
|
||||
}
|
||||
})
|
||||
|
||||
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.worldt" {
|
||||
t.Errorf("Line(0) = %q, want 'hello.worldt'", 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)
|
||||
// Should delete " next" leaving "hello.worldt"
|
||||
if m.ActiveBuffer().Lines[0].String() != "hello.worldt" {
|
||||
t.Errorf("Line(0) = %q, want 'hello.worldt'", 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 != 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)
|
||||
// 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)
|
||||
// Should delete from 't' back to end of "world" (inclusive)
|
||||
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 '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)
|
||||
// Should delete backward to end of "one"
|
||||
if m.ActiveBuffer().Lines[0].String() != "one e" {
|
||||
t.Errorf("Line(0) = %q, want 'one e'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
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 world t" {
|
||||
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 "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)
|
||||
// Should delete selection leaving "hello world t"
|
||||
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 '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)
|
||||
// Should delete from 't' back to end of "hello.world" (inclusive)
|
||||
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 '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.world t" {
|
||||
t.Errorf("'dge': Line(0) = %q, want 'hello.world t'", 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)
|
||||
|
||||
// 'dgE' should delete to end of "hello.world" (treats as one WORD)
|
||||
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" {
|
||||
t.Errorf("'dgE': Line(0) = %q, want 'hello.world t'", 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)
|
||||
// Should delete backward to end of "one.a"
|
||||
if m.ActiveBuffer().Lines[0].String() != "one.a e" {
|
||||
t.Errorf("Line(0) = %q, want 'one.a e'", m.ActiveBuffer().Lines[0].String())
|
||||
}
|
||||
})
|
||||
|
||||
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.world t" {
|
||||
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)
|
||||
// Should delete selection leaving "hello.world t"
|
||||
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 '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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -38,6 +38,9 @@ func NewNormalKeymap() *Keymap {
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"E": motion.MoveForwardWORDEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
"B": motion.MoveBackwardWORD{Count: 1},
|
||||
"ge": motion.MoveBackwardWordEnd{Count: 1},
|
||||
"gE": motion.MoveBackwardWORDEnd{Count: 1},
|
||||
"ctrl+u": motion.ScrollUpHalfPage{},
|
||||
"ctrl+d": motion.ScrollDownHalfPage{},
|
||||
";": action.RepeatFind{Count: 1, Reverse: false},
|
||||
@ -124,6 +127,9 @@ func NewVisualKeymap() *Keymap {
|
||||
"e": motion.MoveForwardWordEnd{Count: 1},
|
||||
"E": motion.MoveForwardWORDEnd{Count: 1},
|
||||
"b": motion.MoveBackwardWord{Count: 1},
|
||||
"B": motion.MoveBackwardWORD{Count: 1},
|
||||
"ge": motion.MoveBackwardWordEnd{Count: 1},
|
||||
"gE": motion.MoveBackwardWORDEnd{Count: 1},
|
||||
// TODO: O and o. These are fun ones! Should be simple too
|
||||
},
|
||||
operators: map[string]action.Operator{
|
||||
|
||||
@ -412,3 +412,219 @@ func (a MoveBackwardWord) Type() core.MotionType { return core.CharwiseExclusive
|
||||
func (a MoveBackwardWord) WithCount(n int) action.Action {
|
||||
return MoveBackwardWord{Count: n}
|
||||
}
|
||||
|
||||
// prevWORDStart: Finds the start of the previous WORD from position (x,y),
|
||||
// treating all non-whitespace as a single class.
|
||||
func prevWORDStart(buf *core.Buffer, x, y int) (int, int) {
|
||||
line := buf.Line(y)
|
||||
|
||||
// Back one to avoid being stuck on the current start
|
||||
x--
|
||||
if x < 0 {
|
||||
if y == 0 {
|
||||
return 0, 0 // beginning of file, stay put
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if x < 0 {
|
||||
return 0, y // landed on an empty line
|
||||
}
|
||||
}
|
||||
|
||||
// Skip whitespace backward, crossing lines if needed
|
||||
for {
|
||||
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
|
||||
x--
|
||||
}
|
||||
if x >= 0 {
|
||||
break // landed on a non-whitespace char
|
||||
}
|
||||
if y == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if len(line) == 0 {
|
||||
return 0, y // empty line acts as a word boundary
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to the start of the current WORD (all non-whitespace is one class)
|
||||
for x-1 >= 0 && line[x-1] != ' ' && line[x-1] != '\t' {
|
||||
x--
|
||||
}
|
||||
|
||||
return x, y
|
||||
}
|
||||
|
||||
// prevWordEnd: Finds the end of the previous word from position (x,y),
|
||||
// respecting word character classes.
|
||||
func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
|
||||
line := buf.Line(y)
|
||||
|
||||
// Back one to avoid being stuck on the current end
|
||||
x--
|
||||
if x < 0 {
|
||||
if y == 0 {
|
||||
return 0, 0 // beginning of file, stay put
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if x < 0 {
|
||||
return 0, y // landed on an empty line
|
||||
}
|
||||
}
|
||||
|
||||
// Skip whitespace backward, crossing lines if needed
|
||||
for {
|
||||
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
|
||||
x--
|
||||
}
|
||||
if x >= 0 {
|
||||
break // landed on a non-whitespace char, this is our word end!
|
||||
}
|
||||
if y == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if len(line) == 0 {
|
||||
return 0, y // empty line acts as a word boundary
|
||||
}
|
||||
}
|
||||
|
||||
// We're now at the end of a word - that's the answer!
|
||||
return x, y
|
||||
}
|
||||
|
||||
// prevWORDEnd: Finds the end of the previous WORD from position (x,y),
|
||||
// treating all non-whitespace as a single class.
|
||||
func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
|
||||
line := buf.Line(y)
|
||||
|
||||
// Back one to avoid being stuck on the current end
|
||||
x--
|
||||
if x < 0 {
|
||||
if y == 0 {
|
||||
return 0, 0 // beginning of file, stay put
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if x < 0 {
|
||||
return 0, y // landed on an empty line
|
||||
}
|
||||
}
|
||||
|
||||
// Skip whitespace backward, crossing lines if needed
|
||||
for {
|
||||
for x >= 0 && (line[x] == ' ' || line[x] == '\t') {
|
||||
x--
|
||||
}
|
||||
if x >= 0 {
|
||||
break // landed on a non-whitespace char, this is our WORD end!
|
||||
}
|
||||
if y == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
y--
|
||||
line = buf.Line(y)
|
||||
x = len(line) - 1
|
||||
if len(line) == 0 {
|
||||
return 0, y // empty line acts as a word boundary
|
||||
}
|
||||
}
|
||||
|
||||
// We're now at the end of a WORD - that's the answer!
|
||||
return x, y
|
||||
}
|
||||
|
||||
// MoveBackwardWORD implements Motion (B) - charwise
|
||||
type MoveBackwardWORD struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// MoveBackwardWORD.Execute: Moves the cursor backward by Count WORDs (B motion).
|
||||
func (a MoveBackwardWORD) Execute(m action.Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x := win.Cursor.Col
|
||||
y := win.Cursor.Line
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = prevWORDStart(buf, x, y)
|
||||
}
|
||||
win.SetCursorCol(x)
|
||||
win.SetCursorLine(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveBackwardWORD.Type: Returns CharwiseExclusive for backward WORD motion.
|
||||
func (a MoveBackwardWORD) Type() core.MotionType { return core.CharwiseExclusive }
|
||||
|
||||
// MoveBackwardWORD.WithCount: Returns a new MoveBackwardWORD with the given count.
|
||||
func (a MoveBackwardWORD) WithCount(n int) action.Action {
|
||||
return MoveBackwardWORD{Count: n}
|
||||
}
|
||||
|
||||
// MoveBackwardWordEnd implements Motion (ge) - charwise
|
||||
type MoveBackwardWordEnd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// MoveBackwardWordEnd.Execute: Moves the cursor to the end of the previous word (ge motion).
|
||||
func (a MoveBackwardWordEnd) Execute(m action.Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x := win.Cursor.Col
|
||||
y := win.Cursor.Line
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = prevWordEnd(buf, x, y)
|
||||
}
|
||||
win.SetCursorCol(x)
|
||||
win.SetCursorLine(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveBackwardWordEnd.Type: Returns CharwiseInclusive for backward word-end motion.
|
||||
func (a MoveBackwardWordEnd) Type() core.MotionType { return core.CharwiseInclusive }
|
||||
|
||||
// MoveBackwardWordEnd.WithCount: Returns a new MoveBackwardWordEnd with the given count.
|
||||
func (a MoveBackwardWordEnd) WithCount(n int) action.Action {
|
||||
return MoveBackwardWordEnd{Count: n}
|
||||
}
|
||||
|
||||
// BUG: gE and ge are broken
|
||||
|
||||
// MoveBackwardWORDEnd implements Motion (gE) - charwise
|
||||
type MoveBackwardWORDEnd struct {
|
||||
Count int
|
||||
}
|
||||
|
||||
// MoveBackwardWORDEnd.Execute: Moves the cursor to the end of the previous WORD (gE motion).
|
||||
func (a MoveBackwardWORDEnd) Execute(m action.Model) tea.Cmd {
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
x := win.Cursor.Col
|
||||
y := win.Cursor.Line
|
||||
for i := 0; i < a.Count; i++ {
|
||||
x, y = prevWORDEnd(buf, x, y)
|
||||
}
|
||||
win.SetCursorCol(x)
|
||||
win.SetCursorLine(y)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveBackwardWORDEnd.Type: Returns CharwiseInclusive for backward WORD-end motion.
|
||||
func (a MoveBackwardWORDEnd) Type() core.MotionType { return core.CharwiseInclusive }
|
||||
|
||||
// MoveBackwardWORDEnd.WithCount: Returns a new MoveBackwardWORDEnd with the given count.
|
||||
func (a MoveBackwardWORDEnd) WithCount(n int) action.Action {
|
||||
return MoveBackwardWORDEnd{Count: n}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user