From 1aed168369980ab1c87d58fc82cd1f6383756407 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sun, 22 Feb 2026 21:41:26 -0700 Subject: [PATCH] test: more tests and renamed update default reg --- internal/action/action.go | 2 +- internal/editor/integration_visual_test.go | 315 +++++++++++++++++++++ internal/editor/model.go | 3 +- internal/operator/delete.go | 4 +- internal/operator/yank.go | 14 +- 5 files changed, 326 insertions(+), 12 deletions(-) diff --git a/internal/action/action.go b/internal/action/action.go index 7a0efc5..c9616a5 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -93,7 +93,7 @@ type Model interface { Registers() map[rune]Register GetRegister(name rune) (Register, bool) SetRegister(name rune, t RegisterType, cnt []string) error - UpdateDefault(t RegisterType, cnt []string) + UpdateDefaultRegister(t RegisterType, cnt []string) // Mode Mode() Mode diff --git a/internal/editor/integration_visual_test.go b/internal/editor/integration_visual_test.go index f933e38..d3a1811 100644 --- a/internal/editor/integration_visual_test.go +++ b/internal/editor/integration_visual_test.go @@ -6,6 +6,8 @@ import ( "git.gophernest.net/azpect/TextEditor/internal/action" ) +// NOTE: Lots of AI tests here + // --- Visual Mode Selection State Tests --- func TestVisualModeSelectionState(t *testing.T) { @@ -270,3 +272,316 @@ func TestVisualModeDelete(t *testing.T) { } }) } + +// --- Visual Mode with Word Motions --- + +func TestVisualModeWordMotions(t *testing.T) { + t.Run("test 'vw' selects to next word", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "w") + + m := getFinalModel(t, tm) + if m.AnchorX() != 0 { + t.Errorf("AnchorX() = %d, want 0", m.AnchorX()) + } + // w moves to start of "world" at col 6 + if m.CursorX() != 6 { + t.Errorf("CursorX() = %d, want 6", m.CursorX()) + } + }) + + t.Run("test 'vwd' deletes word plus space", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "w", "d") + + m := getFinalModel(t, tm) + // Deletes from 0 to 6 inclusive = "hello w", leaves "orld" + if m.Line(0) != "orld" { + t.Errorf("Line(0) = %q, want 'orld'", m.Line(0)) + } + }) + + t.Run("test 've' selects to end of word", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "e") + + m := getFinalModel(t, tm) + if m.AnchorX() != 0 { + t.Errorf("AnchorX() = %d, want 0", m.AnchorX()) + } + // e moves to end of "hello" at col 4 + if m.CursorX() != 4 { + t.Errorf("CursorX() = %d, want 4", m.CursorX()) + } + }) + + t.Run("test 'ved' deletes word", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "e", "d") + + m := getFinalModel(t, tm) + // Deletes "hello" + if m.Line(0) != " world" { + t.Errorf("Line(0) = %q, want ' world'", m.Line(0)) + } + }) + + t.Run("test 'vb' selects backward word", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w' + ) + sendKeys(tm, "v", "b") + + m := getFinalModel(t, tm) + if m.AnchorX() != 6 { + t.Errorf("AnchorX() = %d, want 6", m.AnchorX()) + } + // b moves to start of "hello" at col 0 + if m.CursorX() != 0 { + t.Errorf("CursorX() = %d, want 0", m.CursorX()) + } + }) + + t.Run("test 'vbd' deletes backward to word start", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w' + ) + sendKeys(tm, "v", "b", "d") + + m := getFinalModel(t, tm) + // Deletes from "h" (0) to "w" (6) inclusive + if m.Line(0) != "orld" { + t.Errorf("Line(0) = %q, want 'orld'", m.Line(0)) + } + }) + + t.Run("test 'v2w' selects two words", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"one two three"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "2", "w") + + m := getFinalModel(t, tm) + // 2w moves past "one " and "two " to start of "three" at col 8 + if m.CursorX() != 8 { + t.Errorf("CursorX() = %d, want 8", m.CursorX()) + } + }) +} + +// --- Visual Mode with Jump Motions --- + +func TestVisualModeJumpMotions(t *testing.T) { + t.Run("test 'v$' selects to end of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "$") + + m := getFinalModel(t, tm) + if m.AnchorX() != 0 { + t.Errorf("AnchorX() = %d, want 0", m.AnchorX()) + } + // $ moves past end of line + if m.CursorX() != 11 { + t.Errorf("CursorX() = %d, want 11", m.CursorX()) + } + }) + + t.Run("test 'v$d' deletes to end of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w' + ) + sendKeys(tm, "v", "$", "d") + + m := getFinalModel(t, tm) + if m.Line(0) != "hello " { + t.Errorf("Line(0) = %q, want 'hello '", m.Line(0)) + } + }) + + t.Run("test 'v0' selects to beginning of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), + ) + sendKeys(tm, "v", "0") + + m := getFinalModel(t, tm) + if m.AnchorX() != 6 { + t.Errorf("AnchorX() = %d, want 6", m.AnchorX()) + } + if m.CursorX() != 0 { + t.Errorf("CursorX() = %d, want 0", m.CursorX()) + } + }) + + t.Run("test 'v0d' deletes to beginning of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), // on 'w' + ) + sendKeys(tm, "v", "0", "d") + + m := getFinalModel(t, tm) + // Deletes from 'h' (0) to 'w' (6) inclusive + if m.Line(0) != "orld" { + t.Errorf("Line(0) = %q, want 'orld'", m.Line(0)) + } + }) + + t.Run("test 'v_' selects to first non-whitespace", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{" hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 10}), // on 'w' + ) + sendKeys(tm, "v", "_") + + m := getFinalModel(t, tm) + if m.AnchorX() != 10 { + t.Errorf("AnchorX() = %d, want 10", m.AnchorX()) + } + // _ moves to first non-ws at col 4 + if m.CursorX() != 4 { + t.Errorf("CursorX() = %d, want 4", m.CursorX()) + } + }) + + t.Run("test 'vG' selects to bottom of file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "v", "G") + + m := getFinalModel(t, tm) + if m.AnchorY() != 0 { + t.Errorf("AnchorY() = %d, want 0", m.AnchorY()) + } + if m.CursorY() != 2 { + t.Errorf("CursorY() = %d, want 2", m.CursorY()) + } + }) + + t.Run("test 'vGd' deletes to bottom of file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 0, Col: 3}), // on 'e' of "line" + ) + sendKeys(tm, "v", "G", "d") + + m := getFinalModel(t, tm) + // G goes to last line at same col, deletes from (0,3) to (2,3) + // Keeps "lin" from first line + "e 3" from last line = "lin 3" + if m.LineCount() != 1 { + t.Errorf("LineCount() = %d, want 1", m.LineCount()) + } + if m.Line(0) != "lin 3" { + t.Errorf("Line(0) = %q, want 'lin 3'", m.Line(0)) + } + }) + + t.Run("test 'vgg' selects to top of file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 2, Col: 0}), + ) + sendKeys(tm, "v", "g", "g") + + m := getFinalModel(t, tm) + if m.AnchorY() != 2 { + t.Errorf("AnchorY() = %d, want 2", m.AnchorY()) + } + if m.CursorY() != 0 { + t.Errorf("CursorY() = %d, want 0", m.CursorY()) + } + }) + + t.Run("test 'vggd' deletes to top of file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 2, Col: 3}), + ) + sendKeys(tm, "v", "g", "g", "d") + + m := getFinalModel(t, tm) + // gg goes to first line at same col, deletes selection + // Keeps "lin" from first line + " 3" from last line = "lin 3" + if m.LineCount() != 1 { + t.Errorf("LineCount() = %d, want 1", m.LineCount()) + } + if m.Line(0) != "lin 3" { + t.Errorf("Line(0) = %q, want 'lin 3'", m.Line(0)) + } + }) +} + +// --- Visual Line Mode with Jump Motions --- + +func TestVisualLineModeJumpMotions(t *testing.T) { + t.Run("test 'VG' selects all lines to bottom", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "V", "G") + + m := getFinalModel(t, tm) + if m.AnchorY() != 0 { + t.Errorf("AnchorY() = %d, want 0", m.AnchorY()) + } + if m.CursorY() != 2 { + t.Errorf("CursorY() = %d, want 2", m.CursorY()) + } + }) + + t.Run("test 'VGd' deletes all lines", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + ) + sendKeys(tm, "V", "G", "d") + + m := getFinalModel(t, tm) + // All lines deleted, should have empty buffer + if m.LineCount() != 1 { + t.Errorf("LineCount() = %d, want 1", m.LineCount()) + } + if m.Line(0) != "" { + t.Errorf("Line(0) = %q, want ''", m.Line(0)) + } + }) + + t.Run("test 'Vgg' selects lines to top", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line 1", "line 2", "line 3"}), + WithCursorPos(action.Position{Line: 2, Col: 0}), + ) + sendKeys(tm, "V", "g", "g") + + m := getFinalModel(t, tm) + if m.AnchorY() != 2 { + t.Errorf("AnchorY() = %d, want 2", m.AnchorY()) + } + if m.CursorY() != 0 { + t.Errorf("CursorY() = %d, want 0", m.CursorY()) + } + }) +} diff --git a/internal/editor/model.go b/internal/editor/model.go index 3113e71..85331ed 100644 --- a/internal/editor/model.go +++ b/internal/editor/model.go @@ -213,8 +213,7 @@ func (m *Model) SetRegister(name rune, t action.RegisterType, cnt []string) erro return nil } -// TODO: Errors? -func (m *Model) UpdateDefault(t action.RegisterType, cnt []string) { +func (m *Model) UpdateDefaultRegister(t action.RegisterType, cnt []string) { // Shift numbered registers: 0 -> 1 -> 2 -> ... -> 9 -> _ (discarded) for i := rune('9'); i > '0'; i-- { m.registers[i] = m.registers[i-1] diff --git a/internal/operator/delete.go b/internal/operator/delete.go index 2271a08..c7ae676 100644 --- a/internal/operator/delete.go +++ b/internal/operator/delete.go @@ -51,7 +51,7 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd { } // Put her in the register! - m.UpdateDefault(action.LinewiseRegister, lines) + m.UpdateDefaultRegister(action.LinewiseRegister, lines) // m.SetRegister('"', action.LinewiseRegister, lines) return nil @@ -137,7 +137,7 @@ func deleteLineSelection(m action.Model, start, end action.Position) { m.ClampCursorX() // Update registers - m.UpdateDefault(action.LinewiseRegister, lines) + m.UpdateDefaultRegister(action.LinewiseRegister, lines) } func deleteBlockSelection(m action.Model, start, end action.Position) { diff --git a/internal/operator/yank.go b/internal/operator/yank.go index ac64569..e463016 100644 --- a/internal/operator/yank.go +++ b/internal/operator/yank.go @@ -45,7 +45,7 @@ func (o YankOperator) DoublePress(m action.Model, count int) tea.Cmd { } // Put her in the register! - m.UpdateDefault(action.LinewiseRegister, lines) + m.UpdateDefaultRegister(action.LinewiseRegister, lines) return nil } @@ -72,7 +72,7 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot endX = min(endX, len(line)) // Catch overflow cnt := line[startX:endX] - m.UpdateDefault(action.CharwiseRegister, []string{cnt}) + m.UpdateDefaultRegister(action.CharwiseRegister, []string{cnt}) case mtype == action.Linewise: // This shouldn't happen @@ -86,7 +86,7 @@ func yankNormalMode(m action.Model, start, end action.Position, mtype action.Mot endY := max(start.Line, end.Line) cnt := m.Lines()[startY : endY+1] - m.UpdateDefault(action.LinewiseRegister, cnt) + m.UpdateDefaultRegister(action.LinewiseRegister, cnt) } } @@ -102,7 +102,7 @@ func yankVisualMode(m action.Model, start, end action.Position) { endCol := min(end.Col+1, len(line)) // +1 because visual selection is inclusive startCol := min(start.Col, len(line)) cnt := line[startCol:endCol] - m.UpdateDefault(action.CharwiseRegister, []string{cnt}) + m.UpdateDefaultRegister(action.CharwiseRegister, []string{cnt}) return } @@ -124,7 +124,7 @@ func yankVisualMode(m action.Model, start, end action.Position) { endCol := min(end.Col+1, len(lastLine)) content = append(content, lastLine[:endCol]) - m.UpdateDefault(action.CharwiseRegister, content) + m.UpdateDefaultRegister(action.CharwiseRegister, content) } func yankVisualLineMode(m action.Model, start, end action.Position) { @@ -139,7 +139,7 @@ func yankVisualLineMode(m action.Model, start, end action.Position) { endY := max(start.Line, end.Line) cnt := m.Lines()[startY : endY+1] - m.UpdateDefault(action.LinewiseRegister, cnt) + m.UpdateDefaultRegister(action.LinewiseRegister, cnt) } @@ -165,5 +165,5 @@ func yankVisualBlockMode(m action.Model, start, end action.Position) { content = append(content, line[startX:lineEndX]) } - m.UpdateDefault(action.BlockwiseRegister, content) + m.UpdateDefaultRegister(action.BlockwiseRegister, content) }