From 8b7a479ecb912cb1e3aee4e1bf1f2d22f6f08494 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sat, 4 Apr 2026 11:08:33 -0700 Subject: [PATCH] feat: finally got the tests passing. Most of the tests were just written poorly, the code was right. Though the yank related questions were actually broken. --- flake.nix | 2 + .../editor/integration_motion_word_test.go | 96 ++++++++----------- internal/motion/word.go | 71 ++++++++++++-- internal/operator/yank.go | 30 ++---- 4 files changed, 114 insertions(+), 85 deletions(-) diff --git a/flake.nix b/flake.nix index f9e1db1..12556b6 100644 --- a/flake.nix +++ b/flake.nix @@ -24,6 +24,8 @@ glibc_multi ]; + name = "Gim"; + # Define the shell that will be executed. # Here, we explicitly use zsh. # Note: pkgs.zsh needs to be included in `packages` or `nativeBuildInputs` diff --git a/internal/editor/integration_motion_word_test.go b/internal/editor/integration_motion_word_test.go index 08f813c..9efbad1 100644 --- a/internal/editor/integration_motion_word_test.go +++ b/internal/editor/integration_motion_word_test.go @@ -1707,9 +1707,8 @@ func TestMoveBackwardWORD(t *testing.T) { 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) + if m.ActiveWindow().Cursor.Col != 4 { + t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col) } }) @@ -1719,9 +1718,8 @@ func TestMoveBackwardWORD(t *testing.T) { 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) + if m.ActiveWindow().Cursor.Col != 4 { + t.Errorf("CursorX() = %d, want 4", m.ActiveWindow().Cursor.Col) } }) @@ -1767,9 +1765,8 @@ func TestMoveBackwardWORD(t *testing.T) { 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.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) @@ -1795,8 +1792,8 @@ func TestMoveBackwardWORD(t *testing.T) { 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) + if m.ActiveWindow().Cursor.Col != 9 { + t.Errorf("CursorX() = %d, want 9", m.ActiveWindow().Cursor.Col) } }) @@ -1806,9 +1803,8 @@ func TestMoveBackwardWORD(t *testing.T) { 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.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) @@ -1847,9 +1843,8 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello.world t" { + 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") 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()) } @@ -1871,9 +1865,8 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) { 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()) + if m2.ActiveBuffer().Lines[0].String() != "hello.world t" { + 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) { lines := []string{"hello.world next"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 15, Line: 0}) @@ -1915,8 +1909,8 @@ func TestMoveBackwardWORDWithOperator(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello.world t" { + t.Errorf("Line(0) = %q, want 'hello.world t'", m.ActiveBuffer().Lines[0].String()) } }) } @@ -1943,9 +1937,8 @@ func TestMoveBackwardWORDVisualMode(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello.world " { + t.Errorf("Line(0) = %q, want 'hello.world '", m.ActiveBuffer().Lines[0].String()) } }) @@ -1971,8 +1964,8 @@ func TestMoveBackwardWORDVisualMode(t *testing.T) { 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) + if m.ActiveWindow().Cursor.Line != 1 { + t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line) } }) } @@ -2114,9 +2107,8 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello worl" { + t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String()) } }) @@ -2126,12 +2118,12 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) { 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()) + 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}) @@ -2158,8 +2150,8 @@ func TestMoveBackwardWordEndWithOperator(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello worl" { + 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") 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()) + if m.ActiveBuffer().Lines[0].String() != "hello worl" { + t.Errorf("Line(0) = %q, want 'hello worl'", m.ActiveBuffer().Lines[0].String()) } }) @@ -2381,9 +2372,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello.worl" { + t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String()) } }) @@ -2396,8 +2386,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) { 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()) + 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' @@ -2405,9 +2395,8 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) { 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()) + if m2.ActiveBuffer().Lines[0].String() != "hello.worl" { + t.Errorf("'dgE': Line(0) = %q, want 'hello.worl'", m2.ActiveBuffer().Lines[0].String()) } }) @@ -2417,12 +2406,12 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) { 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()) + 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}) @@ -2449,7 +2438,7 @@ func TestMoveBackwardWORDEndWithOperator(t *testing.T) { if m.Mode() != core.InsertMode { 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()) } }) @@ -2477,9 +2466,8 @@ func TestMoveBackwardWORDEndVisualMode(t *testing.T) { 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()) + if m.ActiveBuffer().Lines[0].String() != "hello.worl" { + t.Errorf("Line(0) = %q, want 'hello.worl'", m.ActiveBuffer().Lines[0].String()) } }) diff --git a/internal/motion/word.go b/internal/motion/word.go index 8984276..97f152c 100644 --- a/internal/motion/word.go +++ b/internal/motion/word.go @@ -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' { x-- } @@ -463,6 +463,7 @@ func prevWORDStart(buf *core.Buffer, x, y int) (int, int) { // respecting word character classes. func prevWordEnd(buf *core.Buffer, x, y int) (int, int) { line := buf.Line(y) + origY := y // Back one to avoid being stuck on the current end x-- @@ -473,8 +474,44 @@ func prevWordEnd(buf *core.Buffer, x, y int) (int, int) { y-- line = buf.Line(y) x = len(line) - 1 - if x < 0 { - return 0, y // landed on an empty line + // 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 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 } @@ -505,6 +542,7 @@ func prevWordEnd(buf *core.Buffer, x, y int) (int, int) { // treating all non-whitespace as a single class. func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) { line := buf.Line(y) + origY := y // Back one to avoid being stuck on the current end x-- @@ -515,8 +553,25 @@ func prevWORDEnd(buf *core.Buffer, x, y int) (int, int) { y-- line = buf.Line(y) x = len(line) - 1 - if x < 0 { - return 0, y // landed on an empty line + // 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 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 } @@ -599,8 +654,6 @@ 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 diff --git a/internal/operator/yank.go b/internal/operator/yank.go index 748b6d9..cd2135b 100644 --- a/internal/operator/yank.go +++ b/internal/operator/yank.go @@ -30,8 +30,14 @@ func (o YankOperator) Operate(m action.Model, start, end core.Position, mtype co }) } - win.SetCursorCol(start.Col) - win.SetCursorLine(start.Line) + // Normalize so cursor is set to the earlier position (important for backward motions) + 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 } @@ -66,16 +72,6 @@ func yankNormalMode(m action.Model, start, end core.Position, mtype core.MotionT switch { 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) 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}) 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 startY := min(start.Line, end.Line) endY := max(start.Line, end.Line)