feat: implemented '_' action, and tested.

This commit is contained in:
Hayden Hargreaves 2026-02-10 22:20:03 -07:00
parent 84a7983a21
commit 2cadb09350
3 changed files with 149 additions and 46 deletions

View File

@ -14,8 +14,8 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G") sendKeys(tm, "G")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 5 { if m.CursorY() != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y) t.Errorf("CursorY() = %d, want 5", m.CursorY())
} }
}) })
@ -24,8 +24,8 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G") sendKeys(tm, "G")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 5 { if m.CursorY() != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y) t.Errorf("CursorY() = %d, want 5", m.CursorY())
} }
}) })
@ -34,23 +34,23 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G") sendKeys(tm, "G")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 5 { if m.CursorY() != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y) t.Errorf("CursorY() = %d, want 5", m.CursorY())
} }
}) })
t.Run("test 'G' clamps cursor.x", func(t *testing.T) { t.Run("test 'G' clamps CursorX()", func(t *testing.T) {
lines := []string{"long line here", "short"} lines := []string{"long line here", "short"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0}) tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "G") sendKeys(tm, "G")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 1 { if m.CursorY() != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y) t.Errorf("CursorY() = %d, want 1", m.CursorY())
} }
want := len(lines[1]) want := len(lines[1])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
} }
}) })
@ -60,8 +60,8 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G") sendKeys(tm, "G")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("CursorY() = %d, want 0", m.CursorY())
} }
}) })
} }
@ -72,8 +72,8 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g") sendKeys(tm, "g", "g")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("CursorY() = %d, want 0", m.CursorY())
} }
}) })
@ -82,8 +82,8 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g") sendKeys(tm, "g", "g")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("CursorY() = %d, want 0", m.CursorY())
} }
}) })
@ -92,28 +92,28 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g") sendKeys(tm, "g", "g")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("CursorY() = %d, want 0", m.CursorY())
} }
}) })
t.Run("test 'gg' clamps cursor.x", func(t *testing.T) { t.Run("test 'gg' clamps CursorX()", func(t *testing.T) {
lines := []string{"short", "long line here"} lines := []string{"short", "long line here"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1}) tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
sendKeys(tm, "g", "g") sendKeys(tm, "g", "g")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 0 { if m.CursorY() != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y) t.Errorf("CursorY() = %d, want 0", m.CursorY())
} }
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
} }
}) })
} }
// --- 0 and $ Tests --- // --- 0, $ and _ Tests ---
func TestMoveToLineStart(t *testing.T) { func TestMoveToLineStart(t *testing.T) {
t.Run("test '0' from middle of line", func(t *testing.T) { t.Run("test '0' from middle of line", func(t *testing.T) {
@ -121,8 +121,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0") sendKeys(tm, "0")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
@ -132,8 +132,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0") sendKeys(tm, "0")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
@ -142,8 +142,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0") sendKeys(tm, "0")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
@ -153,8 +153,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0") sendKeys(tm, "0")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
@ -163,8 +163,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0") sendKeys(tm, "0")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 2 { if m.CursorY() != 2 {
t.Errorf("cursor.y = %d, want 2", m.cursor.y) t.Errorf("CursorY() = %d, want 2", m.CursorY())
} }
}) })
} }
@ -177,8 +177,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
} }
}) })
@ -189,8 +189,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
} }
}) })
@ -201,8 +201,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
want := len(lines[0]) want := len(lines[0])
if m.cursor.x != want { if m.CursorX() != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want) t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
} }
}) })
@ -212,8 +212,8 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$") sendKeys(tm, "$")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.x != 0 { if m.CursorX() != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x) t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
@ -222,8 +222,87 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$") sendKeys(tm, "$")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
if m.cursor.y != 2 { if m.CursorY() != 2 {
t.Errorf("cursor.y = %d, want 2", m.cursor.y) t.Errorf("CursorY() = %d, want 2", m.CursorY())
}
})
}
func TestMoveToLineContentStart(t *testing.T) {
t.Run("test '_' from middle of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 6, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
t.Run("test '_' from middle of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX())
}
})
t.Run("test '_' from start of line with leading whitespace", func(t *testing.T) {
lines := []string{" hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX())
}
})
t.Run("test '_' from start of line with no leading whitespace", func(t *testing.T) {
lines := []string{"hello world"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
t.Run("test '_' from middle of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 2, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX())
}
})
t.Run("test '_' from end of line with only whitespace", func(t *testing.T) {
lines := []string{" "}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 4, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 4 {
t.Errorf("CursorX() = %d, want 4", m.CursorX())
}
})
t.Run("test '_' on empty line", func(t *testing.T) {
lines := []string{""}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 0, Line: 0})
sendKeys(tm, "_")
m := getFinalModel(t, tm)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
} }
}) })
} }

View File

@ -22,6 +22,7 @@ func NewNormalKeymap() *Keymap {
"gg": motion.MoveToTop{}, "gg": motion.MoveToTop{},
"0": motion.MoveToLineStart{}, "0": motion.MoveToLineStart{},
"$": motion.MoveToLineEnd{}, "$": motion.MoveToLineEnd{},
"_": motion.MoveToLineContentStart{},
"w": motion.MoveForwardWord{Count: 1}, "w": motion.MoveForwardWord{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1}, "e": motion.MoveForwardWordEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1}, "b": motion.MoveBackwardWord{Count: 1},

View File

@ -1,8 +1,8 @@
package motion package motion
import ( import (
tea "github.com/charmbracelet/bubbletea"
"git.gophernest.net/azpect/TextEditor/internal/action" "git.gophernest.net/azpect/TextEditor/internal/action"
tea "github.com/charmbracelet/bubbletea"
) )
// MoveToTop implements Motion (gg) // MoveToTop implements Motion (gg)
@ -40,3 +40,26 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
m.ClampCursorX() m.ClampCursorX()
return nil return nil
} }
// MoveToLineContentStart implements Motion (_)
type MoveToLineContentStart struct{}
func (a MoveToLineContentStart) Execute(m action.Model) tea.Cmd {
line := m.Line(m.CursorY())
x := 0
for x < len(line) {
ch := line[x]
if ch != ' ' && ch != '\t' {
break
}
x++
}
// If we are on the last char, we overflew, back once
if x == len(line) && x > 0 {
x--
}
m.SetCursorX(x)
return nil
}