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")
m := getFinalModel(t, tm)
if m.cursor.y != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
if m.CursorY() != 5 {
t.Errorf("CursorY() = %d, want 5", m.CursorY())
}
})
@ -24,8 +24,8 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G")
m := getFinalModel(t, tm)
if m.cursor.y != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
if m.CursorY() != 5 {
t.Errorf("CursorY() = %d, want 5", m.CursorY())
}
})
@ -34,23 +34,23 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G")
m := getFinalModel(t, tm)
if m.cursor.y != 5 {
t.Errorf("cursor.y = %d, want 5", m.cursor.y)
if m.CursorY() != 5 {
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"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 0})
sendKeys(tm, "G")
m := getFinalModel(t, tm)
if m.cursor.y != 1 {
t.Errorf("cursor.y = %d, want 1", m.cursor.y)
if m.CursorY() != 1 {
t.Errorf("CursorY() = %d, want 1", m.CursorY())
}
want := len(lines[1])
if m.cursor.x != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
if m.CursorX() != want {
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
}
})
@ -60,8 +60,8 @@ func TestMoveToBottom(t *testing.T) {
sendKeys(tm, "G")
m := getFinalModel(t, tm)
if m.cursor.y != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want 0", m.CursorY())
}
})
}
@ -72,8 +72,8 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g")
m := getFinalModel(t, tm)
if m.cursor.y != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want 0", m.CursorY())
}
})
@ -82,8 +82,8 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g")
m := getFinalModel(t, tm)
if m.cursor.y != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want 0", m.CursorY())
}
})
@ -92,28 +92,28 @@ func TestMoveToTop(t *testing.T) {
sendKeys(tm, "g", "g")
m := getFinalModel(t, tm)
if m.cursor.y != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
if m.CursorY() != 0 {
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"}
tm := newTestModelWithLinesAndCursorPos(t, lines, action.Position{Col: 10, Line: 1})
sendKeys(tm, "g", "g")
m := getFinalModel(t, tm)
if m.cursor.y != 0 {
t.Errorf("cursor.y = %d, want 0", m.cursor.y)
if m.CursorY() != 0 {
t.Errorf("CursorY() = %d, want 0", m.CursorY())
}
want := len(lines[0])
if m.cursor.x != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
if m.CursorX() != want {
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
}
})
}
// --- 0 and $ Tests ---
// --- 0, $ and _ Tests ---
func TestMoveToLineStart(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")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
@ -132,8 +132,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
@ -142,8 +142,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
@ -153,8 +153,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
@ -163,8 +163,8 @@ func TestMoveToLineStart(t *testing.T) {
sendKeys(tm, "0")
m := getFinalModel(t, tm)
if m.cursor.y != 2 {
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
if m.CursorY() != 2 {
t.Errorf("CursorY() = %d, want 2", m.CursorY())
}
})
}
@ -177,8 +177,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm)
want := len(lines[0])
if m.cursor.x != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
if m.CursorX() != want {
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
}
})
@ -189,8 +189,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm)
want := len(lines[0])
if m.cursor.x != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
if m.CursorX() != want {
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
}
})
@ -201,8 +201,8 @@ func TestMoveToLineEnd(t *testing.T) {
m := getFinalModel(t, tm)
want := len(lines[0])
if m.cursor.x != want {
t.Errorf("cursor.x = %d, want %d", m.cursor.x, want)
if m.CursorX() != want {
t.Errorf("CursorX() = %d, want %d", m.CursorX(), want)
}
})
@ -212,8 +212,8 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$")
m := getFinalModel(t, tm)
if m.cursor.x != 0 {
t.Errorf("cursor.x = %d, want 0", m.cursor.x)
if m.CursorX() != 0 {
t.Errorf("CursorX() = %d, want 0", m.CursorX())
}
})
@ -222,8 +222,87 @@ func TestMoveToLineEnd(t *testing.T) {
sendKeys(tm, "$")
m := getFinalModel(t, tm)
if m.cursor.y != 2 {
t.Errorf("cursor.y = %d, want 2", m.cursor.y)
if m.CursorY() != 2 {
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{},
"0": motion.MoveToLineStart{},
"$": motion.MoveToLineEnd{},
"_": motion.MoveToLineContentStart{},
"w": motion.MoveForwardWord{Count: 1},
"e": motion.MoveForwardWordEnd{Count: 1},
"b": motion.MoveBackwardWord{Count: 1},

View File

@ -1,8 +1,8 @@
package motion
import (
tea "github.com/charmbracelet/bubbletea"
"git.gophernest.net/azpect/TextEditor/internal/action"
tea "github.com/charmbracelet/bubbletea"
)
// MoveToTop implements Motion (gg)
@ -40,3 +40,26 @@ func (a MoveToLineEnd) Execute(m action.Model) tea.Cmd {
m.ClampCursorX()
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
}