1755 lines
39 KiB
Go
1755 lines
39 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|