feat: finally got the tests passing.
All checks were successful
Run Test Suite / test (push) Successful in 19s
Run Test Suite / test (pull_request) Successful in 16s

Most of the tests were just written poorly, the code was right. Though
the yank related questions were actually broken.
This commit is contained in:
Hayden Hargreaves 2026-04-04 11:08:33 -07:00
parent 7ba94eaeea
commit 8b7a479ecb
4 changed files with 114 additions and 85 deletions

View File

@ -24,6 +24,8 @@
glibc_multi glibc_multi
]; ];
name = "Gim";
# Define the shell that will be executed. # Define the shell that will be executed.
# Here, we explicitly use zsh. # Here, we explicitly use zsh.
# Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs` # Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs`

View File

@ -1707,9 +1707,8 @@ func TestMoveBackwardWORD(t *testing.T) {
sendKeys(tm, "B", "B") sendKeys(tm, "B", "B")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should move to start of "one" (index 0) if m.ActiveWindow().Cursor.Col != 4 {
if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
} }
}) })
@ -1719,9 +1718,8 @@ func TestMoveBackwardWORD(t *testing.T) {
sendKeys(tm, "2", "B") sendKeys(tm, "2", "B")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should move to start of "one" (index 0) if m.ActiveWindow().Cursor.Col != 4 {
if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col)
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
} }
}) })
@ -1767,9 +1765,8 @@ func TestMoveBackwardWORD(t *testing.T) {
sendKeys(tm, "B") sendKeys(tm, "B")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should move to start of "hello" on previous line if m.ActiveWindow().Cursor.Line != 1 {
if m.ActiveWindow().Cursor.Line != 0 { t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
} }
if m.ActiveWindow().Cursor.Col != 0 { if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -1795,8 +1792,8 @@ func TestMoveBackwardWORD(t *testing.T) {
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should skip spaces and move to "hello" // Should skip spaces and move to "hello"
if m.ActiveWindow().Cursor.Col != 0 { if m.ActiveWindow().Cursor.Col != 9 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col)
} }
}) })
@ -1806,9 +1803,8 @@ func TestMoveBackwardWORD(t *testing.T) {
sendKeys(tm, "B") sendKeys(tm, "B")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should skip empty line and move to "hello" if m.ActiveWindow().Cursor.Line != 2 {
if m.ActiveWindow().Cursor.Line != 0 { t.Errorf("CursorY() = %d, want 2", m.ActiveWindow().Cursor.Line)
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line)
} }
if m.ActiveWindow().Cursor.Col != 0 { if m.ActiveWindow().Cursor.Col != 0 {
t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col)
@ -1847,7 +1843,6 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) {
sendKeys(tm, "d", "B") sendKeys(tm, "d", "B")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete " next" leaving "hello.worldt"
if m.ActiveBuffer().Lines[0].String() != "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.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
} }
@ -1861,7 +1856,6 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) {
sendKeys(tm1, "d", "b") sendKeys(tm1, "d", "b")
m1 := getFinalModel(t, tm1) m1 := getFinalModel(t, tm1)
// 'db' should delete "next" leaving "hello.world "
if m1.ActiveBuffer().Lines[0].String() != "hello.world t" { if m1.ActiveBuffer().Lines[0].String() != "hello.world t" {
t.Errorf("'db': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String()) t.Errorf("'db': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String())
} }
@ -1871,7 +1865,6 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) {
sendKeys(tm2, "d", "B") sendKeys(tm2, "d", "B")
m2 := getFinalModel(t, tm2) m2 := getFinalModel(t, tm2)
// 'dB' should delete " next" leaving "hello.worldt"
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" { 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.Errorf("'dB': Line(0) = %q, want 'hello.world t'", m2.ActiveBuffer().Lines[0].String())
} }
@ -1889,6 +1882,7 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) {
} }
}) })
// 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) { t.Run("test 'yB' yanks WORD including punctuation", func(t *testing.T) {
lines := []string{"hello.world next"} lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0}) tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
@ -1943,9 +1937,8 @@ func TestMoveBackwardWORDVisualMode(t *testing.T) {
sendKeys(tm, "v", "B", "d") sendKeys(tm, "v", "B", "d")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete " next" leaving "hello.worldt" if m.ActiveBuffer().Lines[0].String() != "hello.world " {
if m.ActiveBuffer().Lines[0].String() != "hello.worldt" { t.Errorf("Line(0) = %q, want 'hello.world '", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'hello.worldt'", m.ActiveBuffer().Lines[0].String())
} }
}) })
@ -1971,8 +1964,8 @@ func TestMoveBackwardWORDVisualMode(t *testing.T) {
t.Errorf("Mode() = %v, want VisualLineMode", m.Mode()) t.Errorf("Mode() = %v, want VisualLineMode", m.Mode())
} }
// Should select both lines // Should select both lines
if m.ActiveWindow().Cursor.Line != 0 { if m.ActiveWindow().Cursor.Line != 1 {
t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line) t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line)
} }
}) })
} }
@ -2114,9 +2107,8 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) {
sendKeys(tm, "d", "g", "e") sendKeys(tm, "d", "g", "e")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete from 't' back to end of "world" (inclusive) if m.ActiveBuffer().Lines[0].String() != "hello worl" {
if m.ActiveBuffer().Lines[0].String() != "hello world t" { t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'hello world t'", m.ActiveBuffer().Lines[0].String())
} }
}) })
@ -2126,12 +2118,12 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) {
sendKeys(tm, "d", "2", "g", "e") sendKeys(tm, "d", "2", "g", "e")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete backward to end of "one" if m.ActiveBuffer().Lines[0].String() != "on" {
if m.ActiveBuffer().Lines[0].String() != "one e" { t.Errorf("Line(0) = %q, want 'on'", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'one e'", 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) { t.Run("test 'yge' yanks backward to word end", func(t *testing.T) {
lines := []string{"hello world next"} lines := []string{"hello world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0}) tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
@ -2158,8 +2150,8 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) {
if m.Mode() != core.InsertMode { if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode()) t.Errorf("Mode() = %v, want InsertMode", m.Mode())
} }
if m.ActiveBuffer().Lines[0].String() != "hello world t" { if m.ActiveBuffer().Lines[0].String() != "hello worl" {
t.Errorf("Line(0) = %q, want 'hello world t'", m.ActiveBuffer().Lines[0].String()) t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
} }
}) })
} }
@ -2186,9 +2178,8 @@ func TestMoveBackwardWordEndVisualMode(t *testing.T) {
sendKeys(tm, "v", "g", "e", "d") sendKeys(tm, "v", "g", "e", "d")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete selection leaving "hello world t" if m.ActiveBuffer().Lines[0].String() != "hello worl" {
if m.ActiveBuffer().Lines[0].String() != "hello world t" { t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'hello world t'", m.ActiveBuffer().Lines[0].String())
} }
}) })
@ -2381,9 +2372,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
sendKeys(tm, "d", "g", "E") sendKeys(tm, "d", "g", "E")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete from 't' back to end of "hello.world" (inclusive) if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
if m.ActiveBuffer().Lines[0].String() != "hello.world t" { t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
} }
}) })
@ -2396,8 +2386,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
m1 := getFinalModel(t, tm1) m1 := getFinalModel(t, tm1)
// 'dge' should delete to end of "world" // 'dge' should delete to end of "world"
if m1.ActiveBuffer().Lines[0].String() != "hello.world t" { if m1.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("'dge': Line(0) = %q, want 'hello.world t'", m1.ActiveBuffer().Lines[0].String()) t.Errorf("'dge': Line(0) = %q, want 'hello.worl'", m1.ActiveBuffer().Lines[0].String())
} }
// Now test 'dgE' // Now test 'dgE'
@ -2405,9 +2395,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
sendKeys(tm2, "d", "g", "E") sendKeys(tm2, "d", "g", "E")
m2 := getFinalModel(t, tm2) m2 := getFinalModel(t, tm2)
// 'dgE' should delete to end of "hello.world" (treats as one WORD) if m2.ActiveBuffer().Lines[0].String() != "hello.worl" {
if m2.ActiveBuffer().Lines[0].String() != "hello.world t" { t.Errorf("'dgE': Line(0) = %q, want 'hello.worl'", m2.ActiveBuffer().Lines[0].String())
t.Errorf("'dgE': Line(0) = %q, want 'hello.world t'", m2.ActiveBuffer().Lines[0].String())
} }
}) })
@ -2417,12 +2406,12 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
sendKeys(tm, "d", "2", "g", "E") sendKeys(tm, "d", "2", "g", "E")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete backward to end of "one.a" if m.ActiveBuffer().Lines[0].String() != "one." {
if m.ActiveBuffer().Lines[0].String() != "one.a e" { t.Errorf("Line(0) = %q, want 'one.'", m.ActiveBuffer().Lines[0].String())
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 'ygE' yanks backward to WORD end", func(t *testing.T) { t.Run("test 'ygE' yanks backward to WORD end", func(t *testing.T) {
lines := []string{"hello.world next"} lines := []string{"hello.world next"}
tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0}) tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0})
@ -2449,7 +2438,7 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) {
if m.Mode() != core.InsertMode { if m.Mode() != core.InsertMode {
t.Errorf("Mode() = %v, want InsertMode", m.Mode()) t.Errorf("Mode() = %v, want InsertMode", m.Mode())
} }
if m.ActiveBuffer().Lines[0].String() != "hello.world t" { if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String()) t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
} }
}) })
@ -2477,9 +2466,8 @@ func TestMoveBackwardWORDEndVisualMode(t *testing.T) {
sendKeys(tm, "v", "g", "E", "d") sendKeys(tm, "v", "g", "E", "d")
m := getFinalModel(t, tm) m := getFinalModel(t, tm)
// Should delete selection leaving "hello.world t" if m.ActiveBuffer().Lines[0].String() != "hello.worl" {
if m.ActiveBuffer().Lines[0].String() != "hello.world t" { t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String())
t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String())
} }
}) })

View File

@ -451,7 +451,7 @@ func prevWORDStart(buf *core.Buffer, x, y int) (int, int) {
} }
} }
// Skip to the start of the current WORD (all non-whitespace is one class) // Skip to the start of the WORD (all non-whitespace is one class)
for x-1 >= 0 && line[x-1] != ' ' && line[x-1] != '\t' { for x-1 >= 0 && line[x-1] != ' ' && line[x-1] != '\t' {
x-- x--
} }
@ -463,6 +463,7 @@ func prevWORDStart(buf *core.Buffer, x, y int) (int, int) {
// respecting word character classes. // respecting word character classes.
func prevWordEnd(buf *core.Buffer, x, y int) (int, int) { func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y) line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end // Back one to avoid being stuck on the current end
x-- x--
@ -473,8 +474,44 @@ func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
y-- y--
line = buf.Line(y) line = buf.Line(y)
x = len(line) - 1 x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current word class if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a word
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
if isWordChar(line[x]) {
// Skip word characters
for x >= 0 && isWordChar(line[x]) {
x--
if x < 0 { if x < 0 {
return 0, y // landed on an empty line if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
} else {
// Skip punctuation
for x >= 0 && isWordPunctuation(line[x]) {
x--
if x < 0 {
if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
}
} }
} }
@ -497,7 +534,7 @@ func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
} }
} }
// We're now at the end of a word - that's the answer! // We're now at the end of the previous word - that's the answer!
return x, y return x, y
} }
@ -505,6 +542,7 @@ func prevWordEnd(buf *core.Buffer, x, y int) (int, int) {
// treating all non-whitespace as a single class. // treating all non-whitespace as a single class.
func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) { func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
line := buf.Line(y) line := buf.Line(y)
origY := y
// Back one to avoid being stuck on the current end // Back one to avoid being stuck on the current end
x-- x--
@ -515,8 +553,25 @@ func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
y-- y--
line = buf.Line(y) line = buf.Line(y)
x = len(line) - 1 x = len(line) - 1
// Don't return early for empty line - we'll handle it in whitespace skip
}
// Skip backward through current WORD if we're on one
// BUT: if we crossed lines in the "back one" step, we're already at the end of a WORD
if y == origY && x >= 0 && line[x] != ' ' && line[x] != '\t' {
for x >= 0 && line[x] != ' ' && line[x] != '\t' {
x--
if x < 0 { if x < 0 {
return 0, y // landed on an empty line if y == 0 {
return 0, 0
}
y--
line = buf.Line(y)
x = len(line) - 1
if x < 0 {
return 0, y
}
}
} }
} }
@ -539,7 +594,7 @@ func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) {
} }
} }
// We're now at the end of a WORD - that's the answer! // We're now at the end of the previous WORD - that's the answer!
return x, y return x, y
} }
@ -599,8 +654,6 @@ func (a MoveBackwardWordEnd) WithCount(n int) action.Action {
return MoveBackwardWordEnd{Count: n} return MoveBackwardWordEnd{Count: n}
} }
// BUG: gE and ge are broken
// MoveBackwardWORDEnd implements Motion (gE) - charwise // MoveBackwardWORDEnd implements Motion (gE) - charwise
type MoveBackwardWORDEnd struct { type MoveBackwardWORDEnd struct {
Count int Count int

View File

@ -30,8 +30,14 @@ func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype co
}) })
} }
win.SetCursorCol(start.Col) // Normalize so cursor is set to the earlier position (important for backward motions)
win.SetCursorLine(start.Line) cursorPos := start
if end.Line < start.Line || (end.Line == start.Line && end.Col < start.Col) {
cursorPos = end
}
win.SetCursorCol(cursorPos.Col)
win.SetCursorLine(cursorPos.Line)
return nil return nil
} }
@ -66,16 +72,6 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
switch { switch {
case mtype.IsCharwise(): case mtype.IsCharwise():
// This shouldn't happen
// if start.Line != end.Line {
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start line and end line must match for charwise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
line := buf.Line(start.Line) line := buf.Line(start.Line)
startX := min(start.Col, end.Col) startX := min(start.Col, end.Col)
@ -92,16 +88,6 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT
m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt}) m.UpdateDefaultRegister(core.CharwiseRegister, []string{cnt})
case mtype == core.Linewise: case mtype == core.Linewise:
// This shouldn't happen
// if start.Col != end.Col {
// m.SetCommandOutput(&core.CommandOutput{
// Lines: []string{"Start column and end column must match for linewise yank operations."},
// Inline: true,
// IsError: true,
// })
// return
// }
// These don't need to be validated, they are validated before being passed into the function // These don't need to be validated, they are validated before being passed into the function
startY := min(start.Line, end.Line) startY := min(start.Line, end.Line)
endY := max(start.Line, end.Line) endY := max(start.Line, end.Line)