package action import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/core" ) // ================================================== // Mock Model Implementation // ================================================== type mockModel struct { windows []*core.Window activeWindow *core.Window buffers []*core.Buffer settings core.EditorSettings mode core.Mode registers map[rune]core.Register insertKeys []string command string commandCursor int commandError error commandOutput string } func newMockModel() *mockModel { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithHeight(24). WithWidth(80). Build() return &mockModel{ windows: []*core.Window{&win}, activeWindow: &win, buffers: []*core.Buffer{&buf}, settings: core.NewDefaultSettings(), mode: core.NormalMode, registers: core.DefaultRegisters(), } } func newMockModelWithBuffer(buf *core.Buffer) *mockModel { win := core.NewWindowBuilder(). WithBuffer(buf). WithHeight(24). WithWidth(80). Build() return &mockModel{ windows: []*core.Window{&win}, activeWindow: &win, buffers: []*core.Buffer{buf}, settings: core.NewDefaultSettings(), mode: core.NormalMode, registers: core.DefaultRegisters(), } } func newMockModelWithWindow(win *core.Window) *mockModel { return &mockModel{ windows: []*core.Window{win}, activeWindow: win, buffers: []*core.Buffer{win.Buffer}, settings: core.NewDefaultSettings(), mode: core.NormalMode, registers: core.DefaultRegisters(), } } // Core Data Access func (m *mockModel) Windows() []*core.Window { return m.windows } func (m *mockModel) ActiveWindow() *core.Window { return m.activeWindow } func (m *mockModel) Buffers() []*core.Buffer { return m.buffers } func (m *mockModel) SetBuffers(bufs []*core.Buffer) { m.buffers = bufs } func (m *mockModel) ActiveBuffer() *core.Buffer { return m.activeWindow.Buffer } // Insert Mode State func (m *mockModel) InsertKeys() []string { return m.insertKeys } func (m *mockModel) SetInsertKeys(keys []string) { m.insertKeys = keys } func (m *mockModel) SetInsertRecording(count int, action Action) {} func (m *mockModel) ExitInsertMode() {} // Command Mode State func (m *mockModel) Command() string { return m.command } func (m *mockModel) SetCommand(cmd string) { m.command = cmd } func (m *mockModel) CommandCursor() int { return m.commandCursor } func (m *mockModel) SetCommandCursor(cur int) { m.commandCursor = cur } func (m *mockModel) CommandError() error { return m.commandError } func (m *mockModel) SetCommandError(err error) { m.commandError = err } func (m *mockModel) CommandOutput() string { return m.commandOutput } func (m *mockModel) SetCommandOutput(out string) { m.commandOutput = out } // Editor-wide State func (m *mockModel) Mode() core.Mode { return m.mode } func (m *mockModel) SetMode(mode core.Mode) { m.mode = mode } func (m *mockModel) Settings() core.EditorSettings { return m.settings } func (m *mockModel) SetSettings(s core.EditorSettings) { m.settings = s } // Registers func (m *mockModel) Registers() map[rune]core.Register { return m.registers } func (m *mockModel) GetRegister(name rune) (core.Register, bool) { reg, ok := m.registers[name] return reg, ok } func (m *mockModel) SetRegister(name rune, t core.RegisterType, cnt []string) error { m.registers[name] = core.Register{Type: t, Content: cnt} return nil } func (m *mockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) { m.registers['"'] = core.Register{Type: t, Content: cnt} } // ================================================== // f (find forward inclusive) Tests // ================================================== func TestFindCharForward(t *testing.T) { t.Run("finds next character on same line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). // At 'h' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should move to first 'o' at position 4 if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("finds character at current position", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 4). // At first 'o' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Current implementation finds char at cursor position // In real vim, 'f' skips current position if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("character not found does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "x", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Cursor should stay at position 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("finds character near end of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "d", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should move to 'd' at position 10 if m.ActiveWindow().Cursor.Col != 10 { t.Errorf("cursor col = %d, want 10", m.ActiveWindow().Cursor.Col) } }) t.Run("finds second occurrence from middle of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 5). // At space after 'hello' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should find 'o' in 'world' at position 7 if m.ActiveWindow().Cursor.Col != 7 { t.Errorf("cursor col = %d, want 7", m.ActiveWindow().Cursor.Col) } }) t.Run("handles empty line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("handles single character line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"a"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should find 'a' at position 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("finds space character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: " ", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should find space at position 5 if m.ActiveWindow().Cursor.Col != 5 { t.Errorf("cursor col = %d, want 5", m.ActiveWindow().Cursor.Col) } }) t.Run("multiple same characters finds first", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaabbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "b", Forward: true, Inclusive: true, Count: 1, } action.Execute(m) // Should find first 'b' at position 5 if m.ActiveWindow().Cursor.Col != 5 { t.Errorf("cursor col = %d, want 5", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // F (find backward inclusive) Tests // ================================================== func TestFindCharBackward(t *testing.T) { t.Run("finds previous character on same line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). // At 'd' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should move to 'o' in 'world' at position 7 if m.ActiveWindow().Cursor.Col != 7 { t.Errorf("cursor col = %d, want 7", m.ActiveWindow().Cursor.Col) } }) t.Run("finds character at current position", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 7). // At 'o' in 'world' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Current implementation finds char at cursor position if m.ActiveWindow().Cursor.Col != 7 { t.Errorf("cursor col = %d, want 7", m.ActiveWindow().Cursor.Col) } }) t.Run("character not found does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "x", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Cursor should stay at position 10 if m.ActiveWindow().Cursor.Col != 10 { t.Errorf("cursor col = %d, want 10", m.ActiveWindow().Cursor.Col) } }) t.Run("finds character at beginning of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "h", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should move to 'h' at position 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("finds first occurrence from middle of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 6). // At 'w' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should find 'o' in 'hello' at position 4 if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("handles empty line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("handles single character line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"a"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should find 'a' at position 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("finds space character backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: " ", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should find space at position 5 if m.ActiveWindow().Cursor.Col != 5 { t.Errorf("cursor col = %d, want 5", m.ActiveWindow().Cursor.Col) } }) t.Run("multiple same characters finds first going backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaabbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 9). // At last 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: true, Count: 1, } action.Execute(m) // Should find last 'a' at position 4 if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // t (till forward exclusive) Tests // ================================================== func TestTillCharForward(t *testing.T) { t.Run("stops before next character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). // At 'h' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 3 (one before 'o' at position 4) if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("cursor col = %d, want 3", m.ActiveWindow().Cursor.Col) } }) t.Run("character not found does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "x", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Cursor should stay at position 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("handles adjacent character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"ab"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). // At 'a' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "b", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 0 (one before 'b' at position 1) // This will be the same position, which matches vim behavior if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("stops before character near end of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "d", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 9 (one before 'd' at position 10) if m.ActiveWindow().Cursor.Col != 9 { t.Errorf("cursor col = %d, want 9", m.ActiveWindow().Cursor.Col) } }) t.Run("handles empty line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("stops before space character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: " ", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 4 (one before space at position 5) if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("stops before second occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 5). // At space Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: true, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 6 (one before 'o' in 'world' at position 7) if m.ActiveWindow().Cursor.Col != 6 { t.Errorf("cursor col = %d, want 6", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // T (till backward exclusive) Tests // ================================================== func TestTillCharBackward(t *testing.T) { t.Run("stops after previous character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). // At 'd' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 8 (one after 'o' in 'world' at position 7) if m.ActiveWindow().Cursor.Col != 8 { t.Errorf("cursor col = %d, want 8", m.ActiveWindow().Cursor.Col) } }) t.Run("character not found does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "x", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Cursor should stay at position 10 if m.ActiveWindow().Cursor.Col != 10 { t.Errorf("cursor col = %d, want 10", m.ActiveWindow().Cursor.Col) } }) t.Run("handles adjacent character", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"ab"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 1). // At 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 1 (one after 'a' at position 0) // This will be the same position, which matches vim behavior if m.ActiveWindow().Cursor.Col != 1 { t.Errorf("cursor col = %d, want 1", m.ActiveWindow().Cursor.Col) } }) t.Run("stops after character at beginning of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "h", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 1 (one after 'h' at position 0) if m.ActiveWindow().Cursor.Col != 1 { t.Errorf("cursor col = %d, want 1", m.ActiveWindow().Cursor.Col) } }) t.Run("handles empty line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{""}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("stops after space character backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: " ", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 6 (one after space at position 5) if m.ActiveWindow().Cursor.Col != 6 { t.Errorf("cursor col = %d, want 6", m.ActiveWindow().Cursor.Col) } }) t.Run("stops after first occurrence going backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 6). // At 'w' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "o", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 5 (one after 'o' in 'hello' at position 4) if m.ActiveWindow().Cursor.Col != 5 { t.Errorf("cursor col = %d, want 5", m.ActiveWindow().Cursor.Col) } }) t.Run("multiple same characters stops after last going backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaabbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 9). // At last 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: false, Count: 1, } action.Execute(m) // Should move to position 5 (one after last 'a' at position 4) if m.ActiveWindow().Cursor.Col != 5 { t.Errorf("cursor col = %d, want 5", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // Count Tests (f with count) // ================================================== func TestFindCharForwardWithCount(t *testing.T) { t.Run("2f finds second occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). // At 'h' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: true, Count: 2, } action.Execute(m) // Should find 2nd 'l' at position 3 // "hello world" // 01234567890 // ^ (first 'l' at 2) // ^ (second 'l' at 3) if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("cursor col = %d, want 3", m.ActiveWindow().Cursor.Col) } }) t.Run("3f finds third occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: true, Count: 3, } action.Execute(m) // Should find 3rd 'l' at position 9 // "hello world" // 01234567890 // ^^ (first two 'l's at 2, 3) // ^ (third 'l' at 9) if m.ActiveWindow().Cursor.Col != 9 { t.Errorf("cursor col = %d, want 9", m.ActiveWindow().Cursor.Col) } }) t.Run("count exceeds available matches does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: true, Count: 5, // Only 3 'l's in the line } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0 (should not move)", m.ActiveWindow().Cursor.Col) } }) t.Run("2f with only one occurrence does not move", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "w", Forward: true, Inclusive: true, Count: 2, // Only 1 'w' in the line } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0 (should not move)", m.ActiveWindow().Cursor.Col) } }) t.Run("5f finds fifth occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaa bbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: true, Count: 5, } action.Execute(m) // Should find 5th 'a' at position 4 if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("count from middle of line", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaa bbb aaa"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 4). // At first 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: true, Count: 2, } action.Execute(m) // From position 4, should find 2nd 'a' ahead at position 9 // "aaa bbb aaa" // 01234567890 // ^ (starting at 4) // ^^ (first 'a' at 8, second at 9) if m.ActiveWindow().Cursor.Col != 9 { t.Errorf("cursor col = %d, want 9", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // Count Tests (F with count) // ================================================== func TestFindCharBackwardWithCount(t *testing.T) { t.Run("2F finds second occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). // At 'd' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: true, Count: 2, } action.Execute(m) // Going backward from 'd', should find 2nd 'l' at position 3 // "hello world" // 01234567890 // ^ (first 'l' backward at 9) // ^ (second 'l' backward at 3) if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("cursor col = %d, want 3", m.ActiveWindow().Cursor.Col) } }) t.Run("3F finds third occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: true, Count: 3, } action.Execute(m) // Going backward, should find 3rd 'l' at position 2 // "hello world" // 01234567890 // ^ (first 'l' backward at 9) // ^ (second 'l' backward at 3) // ^ (third 'l' backward at 2) if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("cursor col = %d, want 2", m.ActiveWindow().Cursor.Col) } }) t.Run("count exceeds available matches does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: true, Count: 5, // Only 3 'l's in the line } action.Execute(m) // Cursor should stay at 10 if m.ActiveWindow().Cursor.Col != 10 { t.Errorf("cursor col = %d, want 10 (should not move)", m.ActiveWindow().Cursor.Col) } }) t.Run("5F finds fifth occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaa bbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). // At last 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "b", Forward: false, Inclusive: true, Count: 5, } action.Execute(m) // Going backward from position 10, should find 5th 'b' at position 6 // "aaaaa bbbbb" // 01234567890 // ^ (at 10) // ^ (5th 'b' backward is at position 6) if m.ActiveWindow().Cursor.Col != 6 { t.Errorf("cursor col = %d, want 6", m.ActiveWindow().Cursor.Col) } }) t.Run("count from middle of line backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaa bbb aaa"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 6). // At middle 'b' Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: false, Inclusive: true, Count: 2, } action.Execute(m) // From position 6 going backward, should find 2nd 'a' at position 1 // "aaa bbb aaa" // 01234567890 // ^ (starting at 6) // ^^ (first 'a' backward at 2, second at 1) if m.ActiveWindow().Cursor.Col != 1 { t.Errorf("cursor col = %d, want 1", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // Count Tests (t with count) // ================================================== func TestTillCharForwardWithCount(t *testing.T) { t.Run("2t stops before second occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: false, Count: 2, } action.Execute(m) // Should stop before 2nd 'l' at position 2 (one before position 3) if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("cursor col = %d, want 2", m.ActiveWindow().Cursor.Col) } }) t.Run("3t stops before third occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: false, Count: 3, } action.Execute(m) // Should stop before 3rd 'l' at position 8 (one before position 9) if m.ActiveWindow().Cursor.Col != 8 { t.Errorf("cursor col = %d, want 8", m.ActiveWindow().Cursor.Col) } }) t.Run("count exceeds available matches does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: true, Inclusive: false, Count: 5, } action.Execute(m) // Cursor should stay at 0 if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("cursor col = %d, want 0 (should not move)", m.ActiveWindow().Cursor.Col) } }) t.Run("5t stops before fifth occurrence", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaa bbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 0). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "a", Forward: true, Inclusive: false, Count: 5, } action.Execute(m) // Should stop before 5th 'a' at position 3 (one before position 4) if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("cursor col = %d, want 3", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // Count Tests (T with count) // ================================================== func TestTillCharBackwardWithCount(t *testing.T) { t.Run("2T stops after second occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: false, Count: 2, } action.Execute(m) // Going backward, should stop after 2nd 'l' at position 4 (one after position 3) if m.ActiveWindow().Cursor.Col != 4 { t.Errorf("cursor col = %d, want 4", m.ActiveWindow().Cursor.Col) } }) t.Run("3T stops after third occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: false, Count: 3, } action.Execute(m) // Going backward, should stop after 3rd 'l' at position 3 (one after position 2) if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("cursor col = %d, want 3", m.ActiveWindow().Cursor.Col) } }) t.Run("count exceeds available matches does not move cursor", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"hello world"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "l", Forward: false, Inclusive: false, Count: 5, } action.Execute(m) // Cursor should stay at 10 if m.ActiveWindow().Cursor.Col != 10 { t.Errorf("cursor col = %d, want 10 (should not move)", m.ActiveWindow().Cursor.Col) } }) t.Run("5T stops after fifth occurrence backward", func(t *testing.T) { buf := core.NewBufferBuilder(). WithLines([]string{"aaaaa bbbbb"}). Build() win := core.NewWindowBuilder(). WithBuffer(&buf). WithCursorPos(0, 10). Build() m := newMockModelWithWindow(&win) action := FindChar{ Char: "b", Forward: false, Inclusive: false, Count: 5, } action.Execute(m) // Going backward, should stop after 5th 'b' at position 7 (one after position 6) if m.ActiveWindow().Cursor.Col != 7 { t.Errorf("cursor col = %d, want 7", m.ActiveWindow().Cursor.Col) } }) } // ================================================== // Motion Type Tests // ================================================== func TestFindCharType(t *testing.T) { t.Run("f returns CharwiseInclusive", func(t *testing.T) { action := FindChar{ Char: "a", Forward: true, Inclusive: true, } if action.Type() != core.CharwiseInclusive { t.Errorf("Type() = %v, want CharwiseInclusive", action.Type()) } }) t.Run("t returns CharwiseExclusive", func(t *testing.T) { action := FindChar{ Char: "a", Forward: true, Inclusive: false, } if action.Type() != core.CharwiseExclusive { t.Errorf("Type() = %v, want CharwiseExclusive", action.Type()) } }) t.Run("F returns CharwiseInclusive", func(t *testing.T) { action := FindChar{ Char: "a", Forward: false, Inclusive: true, } if action.Type() != core.CharwiseInclusive { t.Errorf("Type() = %v, want CharwiseInclusive", action.Type()) } }) t.Run("T returns CharwiseExclusive", func(t *testing.T) { action := FindChar{ Char: "a", Forward: false, Inclusive: false, } if action.Type() != core.CharwiseExclusive { t.Errorf("Type() = %v, want CharwiseExclusive", action.Type()) } }) } // ================================================== // WithChar Tests // ================================================== func TestFindCharWithChar(t *testing.T) { t.Run("WithChar sets the character", func(t *testing.T) { action := FindChar{ Forward: true, Inclusive: true, } newAction := action.WithChar("x") findAction, ok := newAction.(FindChar) if !ok { t.Fatal("WithChar should return FindChar") } if findAction.Char != "x" { t.Errorf("Char = %q, want %q", findAction.Char, "x") } }) t.Run("WithChar preserves other fields", func(t *testing.T) { action := FindChar{ Forward: true, Inclusive: true, Count: 3, } newAction := action.WithChar("y") findAction, ok := newAction.(FindChar) if !ok { t.Fatal("WithChar should return FindChar") } if !findAction.Forward { t.Error("Forward should be preserved") } if !findAction.Inclusive { t.Error("Inclusive should be preserved") } if findAction.Count != 3 { t.Errorf("Count = %d, want 3", findAction.Count) } }) } // ================================================== // WithCount Tests // ================================================== func TestFindCharWithCount(t *testing.T) { t.Run("WithCount sets the count", func(t *testing.T) { action := FindChar{ Char: "a", Forward: true, Inclusive: true, } newAction := action.WithCount(5) findAction, ok := newAction.(FindChar) if !ok { t.Fatal("WithCount should return FindChar") } if findAction.Count != 5 { t.Errorf("Count = %d, want 5", findAction.Count) } }) t.Run("WithCount preserves other fields", func(t *testing.T) { action := FindChar{ Char: "x", Forward: false, Inclusive: false, } newAction := action.WithCount(2) findAction, ok := newAction.(FindChar) if !ok { t.Fatal("WithCount should return FindChar") } if findAction.Char != "x" { t.Errorf("Char = %q, want %q", findAction.Char, "x") } if findAction.Forward { t.Error("Forward should be preserved as false") } if findAction.Inclusive { t.Error("Inclusive should be preserved as false") } }) }