fix: cleaned up the testing mocking. New single module
All checks were successful
Run Test Suite / test (push) Successful in 42s

This commit is contained in:
Hayden Hargreaves 2026-03-19 17:31:53 -07:00
parent 3c98dca777
commit a01369f407
5 changed files with 1190 additions and 1400 deletions

View File

@ -7,29 +7,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
// mockModelForHistory is a minimal mock for testing command history
type mockModelForHistory struct {
mockModel
commandHistory []string
commandHistoryCursor int
}
func (m *mockModelForHistory) CommandHistory() []string {
return m.commandHistory
}
func (m *mockModelForHistory) SetCommandHistory(history []string) {
m.commandHistory = history
}
func (m *mockModelForHistory) CommandHistoryCursor() int {
return m.commandHistoryCursor
}
func (m *mockModelForHistory) SetCommandHistoryCursor(cur int) {
m.commandHistoryCursor = cur
}
// mockRegistry for testing command execution without actual command handlers
type mockRegistry struct{}
@ -41,14 +18,11 @@ func (r *mockRegistry) Execute(m Model, cmdLine string) (tea.Cmd, error) {
// TestCommandExecuteUpdatesHistory tests that executing commands adds them to history
func TestCommandExecuteUpdatesHistory(t *testing.T) {
t.Run("first command added to empty history", func(t *testing.T) {
m := &mockModelForHistory{
mockModel: mockModel{
mode: core.CommandMode,
command: "w test.txt",
},
commandHistory: []string{},
commandHistoryCursor: 0,
}
m := NewMockModel()
m.ModeVal = core.CommandMode
m.CommandVal = "w test.txt"
m.CommandHistoryList = []string{}
m.CommandHistoryCur = 0
registry := &mockRegistry{}
action := CommandExecute{Registry: registry}
@ -65,14 +39,11 @@ func TestCommandExecuteUpdatesHistory(t *testing.T) {
})
t.Run("multiple commands prepended to history", func(t *testing.T) {
m := &mockModelForHistory{
mockModel: mockModel{
mode: core.CommandMode,
command: "w file1.txt",
},
commandHistory: []string{},
commandHistoryCursor: 0,
}
m := NewMockModel()
m.ModeVal = core.CommandMode
m.CommandVal = "w file1.txt"
m.CommandHistoryList = []string{}
m.CommandHistoryCur = 0
registry := &mockRegistry{}
action := CommandExecute{Registry: registry}
@ -81,11 +52,11 @@ func TestCommandExecuteUpdatesHistory(t *testing.T) {
action.Execute(m)
// Execute second command
m.command = "q"
m.CommandVal = "q"
action.Execute(m)
// Execute third command
m.command = "set number"
m.CommandVal = "set number"
action.Execute(m)
history := m.CommandHistory()
@ -103,14 +74,11 @@ func TestCommandExecuteUpdatesHistory(t *testing.T) {
})
t.Run("empty command not added to history", func(t *testing.T) {
m := &mockModelForHistory{
mockModel: mockModel{
mode: core.CommandMode,
command: "",
},
commandHistory: []string{"previous command"},
commandHistoryCursor: 0,
}
m := NewMockModel()
m.ModeVal = core.CommandMode
m.CommandVal = ""
m.CommandHistoryList = []string{"previous command"}
m.CommandHistoryCur = 0
registry := &mockRegistry{}
action := CommandExecute{Registry: registry}
@ -127,14 +95,11 @@ func TestCommandExecuteUpdatesHistory(t *testing.T) {
})
t.Run("history cursor resets on execute", func(t *testing.T) {
m := &mockModelForHistory{
mockModel: mockModel{
mode: core.CommandMode,
command: "w",
},
commandHistory: []string{},
commandHistoryCursor: 5, // Set to non-zero
}
m := NewMockModel()
m.ModeVal = core.CommandMode
m.CommandVal = "w"
m.CommandHistoryList = []string{}
m.CommandHistoryCur = 5 // Set to non-zero
registry := &mockRegistry{}
action := CommandExecute{Registry: registry}
@ -146,14 +111,11 @@ func TestCommandExecuteUpdatesHistory(t *testing.T) {
})
t.Run("duplicate commands are added to history", func(t *testing.T) {
m := &mockModelForHistory{
mockModel: mockModel{
mode: core.CommandMode,
command: "w",
},
commandHistory: []string{"w"},
commandHistoryCursor: 0,
}
m := NewMockModel()
m.ModeVal = core.CommandMode
m.CommandVal = "w"
m.CommandHistoryList = []string{"w"}
m.CommandHistoryCur = 0
registry := &mockRegistry{}
action := CommandExecute{Registry: registry}

View File

@ -4,130 +4,8 @@ import (
"testing"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
)
// ==================================================
// 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
commandOutput *core.CommandOutput
commandHistory []string
commandHistoryCursor int
lastFind core.LastFindCommand
styles style.Styles
}
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() {}
func (m *mockModel) SetLastFind(char string, forward, inclusive bool) {
m.lastFind = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive}
}
func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind }
// 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) CommandOutput() *core.CommandOutput { return m.commandOutput }
func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out }
func (m *mockModel) CommandHistory() []string { return m.commandHistory }
func (m *mockModel) SetCommandHistory(history []string) { m.commandHistory = history }
func (m *mockModel) CommandHistoryCursor() int { return m.commandHistoryCursor }
func (m *mockModel) SetCommandHistoryCursor(cur int) { m.commandHistoryCursor = cur }
// 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 }
func (m *mockModel) Styles() style.Styles { return m.styles }
func (m *mockModel) SetStyles(s style.Styles) { m.styles = 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
// ==================================================
@ -143,7 +21,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0). // At 'h'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -170,7 +48,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 4). // At first 'o'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -197,7 +75,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "x",
@ -224,7 +102,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "d",
@ -251,7 +129,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 5). // At space after 'hello'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -278,7 +156,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -305,7 +183,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -332,7 +210,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: " ",
@ -359,7 +237,7 @@ func TestFindCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "b",
@ -392,7 +270,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 10). // At 'd'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -419,7 +297,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 7). // At 'o' in 'world'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -446,7 +324,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "x",
@ -473,7 +351,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "h",
@ -500,7 +378,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 6). // At 'w'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -527,7 +405,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -554,7 +432,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -581,7 +459,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: " ",
@ -608,7 +486,7 @@ func TestFindCharBackward(t *testing.T) {
WithCursorPos(0, 9). // At last 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -641,7 +519,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0). // At 'h'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -668,7 +546,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "x",
@ -695,7 +573,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0). // At 'a'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "b",
@ -723,7 +601,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "d",
@ -750,7 +628,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -777,7 +655,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: " ",
@ -804,7 +682,7 @@ func TestTillCharForward(t *testing.T) {
WithCursorPos(0, 5). // At space
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -837,7 +715,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 10). // At 'd'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -864,7 +742,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "x",
@ -891,7 +769,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 1). // At 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -919,7 +797,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "h",
@ -946,7 +824,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -973,7 +851,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: " ",
@ -1000,7 +878,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 6). // At 'w'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "o",
@ -1027,7 +905,7 @@ func TestTillCharBackward(t *testing.T) {
WithCursorPos(0, 9). // At last 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -1060,7 +938,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0). // At 'h'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1091,7 +969,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1122,7 +1000,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1149,7 +1027,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "w",
@ -1176,7 +1054,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -1204,7 +1082,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 4). // At first 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -1241,7 +1119,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10). // At 'd'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1272,7 +1150,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1304,7 +1182,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1331,7 +1209,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10). // At last 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "b",
@ -1359,7 +1237,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 6). // At middle 'b'
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -1396,7 +1274,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1423,7 +1301,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1450,7 +1328,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1477,7 +1355,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
WithCursorPos(0, 0).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "a",
@ -1511,7 +1389,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1538,7 +1416,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1565,7 +1443,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "l",
@ -1592,7 +1470,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
WithCursorPos(0, 10).
Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
action := FindChar{
Char: "b",
@ -1731,7 +1609,7 @@ func TestRepeatFind_Semicolon_After_f(t *testing.T) {
t.Run("basic: lands on next inclusive match", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
FindChar{Char: "o", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1745,7 +1623,7 @@ func TestRepeatFind_Semicolon_After_f(t *testing.T) {
t.Run("no further match: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
FindChar{Char: "o", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1759,7 +1637,7 @@ func TestRepeatFind_Semicolon_After_f(t *testing.T) {
t.Run("cursor at end of line: no move", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 10).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
FindChar{Char: "o", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1775,7 +1653,7 @@ func TestRepeatFind_Semicolon_After_f(t *testing.T) {
t.Run("count=2 skips first match lands on second", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"aXbXcX"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", true, true)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 2, Repeated: true}.Execute(m)
@ -1789,7 +1667,7 @@ func TestRepeatFind_Semicolon_After_f(t *testing.T) {
t.Run("does not overwrite lastFind when Repeated", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
FindChar{Char: "o", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1810,7 +1688,7 @@ func TestRepeatFind_Semicolon_After_F(t *testing.T) {
t.Run("basic: lands on previous inclusive match", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, true)
FindChar{Char: "o", Forward: false, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1824,7 +1702,7 @@ func TestRepeatFind_Semicolon_After_F(t *testing.T) {
t.Run("no earlier match: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, true)
FindChar{Char: "o", Forward: false, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1838,7 +1716,7 @@ func TestRepeatFind_Semicolon_After_F(t *testing.T) {
t.Run("cursor at start of line: no 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)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, true)
FindChar{Char: "o", Forward: false, Inclusive: true, Count: 1, Repeated: true}.Execute(m)
@ -1853,7 +1731,7 @@ func TestRepeatFind_Semicolon_After_F(t *testing.T) {
t.Run("count=2 backward skips one lands on second", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"XaXbX"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", false, true)
FindChar{Char: "X", Forward: false, Inclusive: true, Count: 2, Repeated: true}.Execute(m)
@ -1876,7 +1754,7 @@ func TestRepeatFind_Semicolon_After_t(t *testing.T) {
t.Run("basic: skips adjacent target, lands before next", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 3).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
FindChar{Char: "o", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -1891,7 +1769,7 @@ func TestRepeatFind_Semicolon_After_t(t *testing.T) {
t.Run("no further match after second repeat: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 6).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
FindChar{Char: "o", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -1907,7 +1785,7 @@ func TestRepeatFind_Semicolon_After_t(t *testing.T) {
t.Run("col+2 out of bounds: no move", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"Xo"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
FindChar{Char: "o", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -1924,7 +1802,7 @@ func TestRepeatFind_Semicolon_After_t(t *testing.T) {
t.Run("three chained repeats advance correctly", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"aXbXcXd"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", true, false)
// First repeat
@ -1952,7 +1830,7 @@ func TestRepeatFind_Semicolon_After_t(t *testing.T) {
t.Run("count=2 repeated exclusive forward", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"aXbXcX"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", true, false)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 2, Repeated: true}.Execute(m)
@ -1974,7 +1852,7 @@ func TestRepeatFind_Semicolon_After_T(t *testing.T) {
t.Run("basic: skips adjacent target, lands after previous", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, false)
FindChar{Char: "o", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -1988,7 +1866,7 @@ func TestRepeatFind_Semicolon_After_T(t *testing.T) {
t.Run("no earlier match after second repeat: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 5).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, false)
FindChar{Char: "o", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -2003,7 +1881,7 @@ func TestRepeatFind_Semicolon_After_T(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"oX"}).Build()
// Cursor at col 1 (as if `TX` landed at x+1=1 where x=0).
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -2021,7 +1899,7 @@ func TestRepeatFind_Semicolon_After_T(t *testing.T) {
t.Run("three chained repeats advance correctly backward", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"dXcXbXa"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 6).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -2047,7 +1925,7 @@ func TestRepeatFind_Semicolon_After_T(t *testing.T) {
t.Run("count=2 repeated exclusive backward", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"dXcXbXa"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 6).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 2, Repeated: true}.Execute(m)
@ -2090,7 +1968,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
// Simulate with two sequential ; presses
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", true, true)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
@ -2098,7 +1976,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
// Simulate with a single 2; (Count=2)
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", true, true)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 2, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2113,7 +1991,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", true, true)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
@ -2121,7 +1999,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
threeSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 1).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", true, true)
FindChar{Char: "X", Forward: true, Inclusive: true, Count: 3, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2141,14 +2019,14 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", false, true)
FindChar{Char: "X", Forward: false, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: false, Inclusive: true, Count: 1, Repeated: true}.Execute(m1)
twoSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", false, true)
FindChar{Char: "X", Forward: false, Inclusive: true, Count: 2, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2169,14 +2047,14 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", true, false)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
twoSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", true, false)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 2, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2191,7 +2069,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", true, false)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
@ -2199,7 +2077,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
threeSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 0).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", true, false)
FindChar{Char: "X", Forward: true, Inclusive: false, Count: 3, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2219,14 +2097,14 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
twoSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 2, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2241,7 +2119,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{line}).Build()
win1 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m1 := newMockModelWithWindow(&win1)
m1 := NewMockModelWithWindow(&win1)
m1.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m1)
@ -2249,7 +2127,7 @@ func TestRepeatFind_CountedRepeats(t *testing.T) {
threeSemicolons := m1.ActiveWindow().Cursor.Col
win2 := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m2 := newMockModelWithWindow(&win2)
m2 := NewMockModelWithWindow(&win2)
m2.SetLastFind("X", false, false)
FindChar{Char: "X", Forward: false, Inclusive: false, Count: 3, Repeated: true}.Execute(m2)
counted := m2.ActiveWindow().Cursor.Col
@ -2276,7 +2154,7 @@ func TestRepeatFind_Comma_After_f(t *testing.T) {
t.Run("no previous match after fo: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
// Simulate: lastFind was set by `fo`
m.SetLastFind("o", true, true)
@ -2293,7 +2171,7 @@ func TestRepeatFind_Comma_After_f(t *testing.T) {
t.Run("after ;, comma returns to previous match", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
// , reversed → backward inclusive from col 7, start at col 6: finds 'o' at 4
@ -2315,7 +2193,7 @@ func TestRepeatFind_Comma_After_F(t *testing.T) {
t.Run("no further match forward: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, true)
// , reversed → forward inclusive
@ -2330,7 +2208,7 @@ func TestRepeatFind_Comma_After_F(t *testing.T) {
t.Run("after ;, comma returns forward to next match", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, true)
// , reversed → forward inclusive from col 4, start at col 5: finds 'o' at 7
@ -2353,7 +2231,7 @@ func TestRepeatFind_Comma_After_t(t *testing.T) {
t.Run("no previous exclusive match: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 3).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
// , reversed → backward exclusive, repeated
@ -2369,7 +2247,7 @@ func TestRepeatFind_Comma_After_t(t *testing.T) {
t.Run("after ;, comma goes backward exclusive", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 6).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
FindChar{Char: "o", Forward: false, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -2391,7 +2269,7 @@ func TestRepeatFind_Comma_After_T(t *testing.T) {
t.Run("no further exclusive match forward: cursor stays", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, false)
// , reversed → forward exclusive, repeated
@ -2407,7 +2285,7 @@ func TestRepeatFind_Comma_After_T(t *testing.T) {
t.Run("after ;, comma goes forward exclusive", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 5).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, false)
FindChar{Char: "o", Forward: true, Inclusive: false, Count: 1, Repeated: true}.Execute(m)
@ -2423,10 +2301,10 @@ func TestRepeatFind_Comma_After_T(t *testing.T) {
// --------------------------------------------------
func TestRepeatFind_Resolve(t *testing.T) {
makeMock := func(char string, forward, inclusive bool) *mockModel {
makeMock := func(char string, forward, inclusive bool) *MockModel {
buf := core.NewBufferBuilder().WithLines([]string{"x"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind(char, forward, inclusive)
return m
}
@ -2548,7 +2426,7 @@ func TestRepeatFind_Execute(t *testing.T) {
t.Run("Execute via ; after f moves cursor correctly", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 4).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
// ; = RepeatFind{Reverse: false}
@ -2562,7 +2440,7 @@ func TestRepeatFind_Execute(t *testing.T) {
t.Run("Execute via , after f reverses and moves cursor correctly", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 7).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, true)
// , = RepeatFind{Reverse: true}
@ -2576,7 +2454,7 @@ func TestRepeatFind_Execute(t *testing.T) {
t.Run("Execute via ; after t skips adjacent and moves cursor", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 3).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", true, false)
RepeatFind{Count: 1, Reverse: false}.Execute(m)
@ -2589,7 +2467,7 @@ func TestRepeatFind_Execute(t *testing.T) {
t.Run("Execute via ; after T skips adjacent backward and moves cursor", func(t *testing.T) {
buf := core.NewBufferBuilder().WithLines([]string{"hello world"}).Build()
win := core.NewWindowBuilder().WithBuffer(&buf).WithCursorPos(0, 8).Build()
m := newMockModelWithWindow(&win)
m := NewMockModelWithWindow(&win)
m.SetLastFind("o", false, false)
RepeatFind{Count: 1, Reverse: false}.Execute(m)

133
internal/action/mock.go Normal file
View File

@ -0,0 +1,133 @@
package action
import (
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
)
// MockModel is a shared test implementation of the Model interface.
// Used by test files across multiple packages to avoid duplication.
// All fields are exported to allow direct manipulation in tests.
type MockModel struct {
WindowsList []*core.Window
ActiveWindowVal *core.Window
BuffersList []*core.Buffer
SettingsVal core.EditorSettings
ModeVal core.Mode
RegistersMap map[rune]core.Register
InsertKeysList []string
CommandVal string
CommandCursorVal int
CommandOutputVal *core.CommandOutput
CommandHistoryList []string
CommandHistoryCur int
LastFindVal core.LastFindCommand
StylesVal style.Styles
}
// NewMockModel creates a mock with an empty buffer and 24x80 window.
func NewMockModel() *MockModel {
buf := core.NewBufferBuilder().
WithLines([]string{""}).
Build()
win := core.NewWindowBuilder().
WithBuffer(&buf).
WithHeight(24).
WithWidth(80).
Build()
return &MockModel{
WindowsList: []*core.Window{&win},
ActiveWindowVal: &win,
BuffersList: []*core.Buffer{&buf},
SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(),
}
}
// NewMockModelWithBuffer creates a mock with a custom buffer.
func NewMockModelWithBuffer(buf *core.Buffer) *MockModel {
win := core.NewWindowBuilder().
WithBuffer(buf).
WithHeight(24).
WithWidth(80).
Build()
return &MockModel{
WindowsList: []*core.Window{&win},
ActiveWindowVal: &win,
BuffersList: []*core.Buffer{buf},
SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(),
}
}
// NewMockModelWithWindow creates a mock with a custom window.
func NewMockModelWithWindow(win *core.Window) *MockModel {
return &MockModel{
WindowsList: []*core.Window{win},
ActiveWindowVal: win,
BuffersList: []*core.Buffer{win.Buffer},
SettingsVal: core.NewDefaultSettings(),
ModeVal: core.NormalMode,
RegistersMap: core.DefaultRegisters(),
}
}
// ==================================================
// Model Interface Implementation
// ==================================================
// Core Data Access
func (m *MockModel) Windows() []*core.Window { return m.WindowsList }
func (m *MockModel) ActiveWindow() *core.Window { return m.ActiveWindowVal }
func (m *MockModel) Buffers() []*core.Buffer { return m.BuffersList }
func (m *MockModel) SetBuffers(bufs []*core.Buffer) { m.BuffersList = bufs }
func (m *MockModel) ActiveBuffer() *core.Buffer { return m.ActiveWindowVal.Buffer }
// Insert Mode State
func (m *MockModel) InsertKeys() []string { return m.InsertKeysList }
func (m *MockModel) SetInsertKeys(keys []string) { m.InsertKeysList = keys }
func (m *MockModel) SetInsertRecording(count int, a Action) {}
func (m *MockModel) ExitInsertMode() {}
func (m *MockModel) SetLastFind(char string, forward, inclusive bool) {
m.LastFindVal = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive}
}
func (m *MockModel) GetLastFind() *core.LastFindCommand { return &m.LastFindVal }
// Command Mode State
func (m *MockModel) Command() string { return m.CommandVal }
func (m *MockModel) SetCommand(cmd string) { m.CommandVal = cmd }
func (m *MockModel) CommandCursor() int { return m.CommandCursorVal }
func (m *MockModel) SetCommandCursor(cur int) { m.CommandCursorVal = cur }
func (m *MockModel) CommandOutput() *core.CommandOutput { return m.CommandOutputVal }
func (m *MockModel) SetCommandOutput(out *core.CommandOutput) { m.CommandOutputVal = out }
func (m *MockModel) CommandHistory() []string { return m.CommandHistoryList }
func (m *MockModel) SetCommandHistory(history []string) { m.CommandHistoryList = history }
func (m *MockModel) CommandHistoryCursor() int { return m.CommandHistoryCur }
func (m *MockModel) SetCommandHistoryCursor(cur int) { m.CommandHistoryCur = cur }
// Editor-wide State
func (m *MockModel) Mode() core.Mode { return m.ModeVal }
func (m *MockModel) SetMode(mode core.Mode) { m.ModeVal = mode }
func (m *MockModel) Settings() core.EditorSettings { return m.SettingsVal }
func (m *MockModel) SetSettings(s core.EditorSettings) { m.SettingsVal = s }
func (m *MockModel) Styles() style.Styles { return m.StylesVal }
func (m *MockModel) SetStyles(s style.Styles) { m.StylesVal = s }
// Registers
func (m *MockModel) Registers() map[rune]core.Register { return m.RegistersMap }
func (m *MockModel) GetRegister(name rune) (core.Register, bool) {
reg, ok := m.RegistersMap[name]
return reg, ok
}
func (m *MockModel) SetRegister(name rune, t core.RegisterType, cnt []string) error {
m.RegistersMap[name] = core.Register{Type: t, Content: cnt}
return nil
}
func (m *MockModel) UpdateDefaultRegister(t core.RegisterType, cnt []string) {
m.RegistersMap['"'] = core.Register{Type: t, Content: cnt}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,92 +5,19 @@ import (
"git.gophernest.net/azpect/TextEditor/internal/action"
"git.gophernest.net/azpect/TextEditor/internal/core"
"git.gophernest.net/azpect/TextEditor/internal/style"
)
// ==================================================
// 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
commandOutput *core.CommandOutput
commandHistory []string
commandHistoryCursor int
lastFind core.LastFindCommand
styles style.Styles
}
// 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.Action) {}
func (m *mockModel) ExitInsertMode() {}
func (m *mockModel) SetLastFind(char string, forward, inclusive bool) {
m.lastFind = core.LastFindCommand{Char: char, Forward: forward, Inclusive: inclusive}
}
func (m *mockModel) GetLastFind() *core.LastFindCommand { return &m.lastFind }
// 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) CommandOutput() *core.CommandOutput { return m.commandOutput }
func (m *mockModel) SetCommandOutput(out *core.CommandOutput) { m.commandOutput = out }
func (m *mockModel) CommandHistory() []string { return m.commandHistory }
func (m *mockModel) SetCommandHistory(history []string) { m.commandHistory = history }
func (m *mockModel) CommandHistoryCursor() int { return m.commandHistoryCursor }
func (m *mockModel) SetCommandHistoryCursor(cur int) { m.commandHistoryCursor = cur }
// 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 }
func (m *mockModel) Styles() style.Styles { return m.styles }
func (m *mockModel) SetStyles(s style.Styles) { m.styles = 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}
}
// ==================================================
// MoveCommandHistoryUp Tests
// ==================================================
func TestMoveCommandHistoryUp(t *testing.T) {
t.Run("navigate up from start", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 0,
}
action := MoveCommandHistoryUp{}
@ -113,11 +40,11 @@ func TestMoveCommandHistoryUp(t *testing.T) {
})
t.Run("navigate up multiple times", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 0,
}
action := MoveCommandHistoryUp{}
@ -146,11 +73,11 @@ func TestMoveCommandHistoryUp(t *testing.T) {
})
t.Run("cannot navigate past end of history", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 3, // Already at end
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 3, // Already at end
}
action := MoveCommandHistoryUp{}
@ -168,11 +95,11 @@ func TestMoveCommandHistoryUp(t *testing.T) {
})
t.Run("empty history does nothing", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "current",
commandHistory: []string{},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "current",
CommandHistoryList: []string{},
CommandHistoryCur: 0,
}
action := MoveCommandHistoryUp{}
@ -189,12 +116,12 @@ func TestMoveCommandHistoryUp(t *testing.T) {
})
t.Run("command cursor moves to end", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandCursor: 0,
commandHistory: []string{"long command here"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandCursorVal: 0,
CommandHistoryList: []string{"long command here"},
CommandHistoryCur: 0,
}
action := MoveCommandHistoryUp{}
@ -213,11 +140,11 @@ func TestMoveCommandHistoryUp(t *testing.T) {
func TestMoveCommandHistoryDown(t *testing.T) {
t.Run("navigate down from middle of history", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "cmd2",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 2, // At cmd2
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "cmd2",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 2, // At cmd2
}
action := MoveCommandHistoryDown{}
@ -235,11 +162,11 @@ func TestMoveCommandHistoryDown(t *testing.T) {
})
t.Run("navigate down to empty command", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "cmd3",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 1, // At first command
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "cmd3",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 1, // At first command
}
action := MoveCommandHistoryDown{}
@ -260,11 +187,11 @@ func TestMoveCommandHistoryDown(t *testing.T) {
})
t.Run("navigate down from cursor 0 does nothing", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0, // Already at bottom
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 0, // Already at bottom
}
action := MoveCommandHistoryDown{}
@ -281,12 +208,12 @@ func TestMoveCommandHistoryDown(t *testing.T) {
})
t.Run("command cursor moves to end", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandCursor: 0,
commandHistory: []string{"cmd3", "long command here"},
commandHistoryCursor: 2,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandCursorVal: 0,
CommandHistoryList: []string{"cmd3", "long command here"},
CommandHistoryCur: 2,
}
action := MoveCommandHistoryDown{}
@ -305,11 +232,11 @@ func TestMoveCommandHistoryDown(t *testing.T) {
func TestCommandHistoryIntegration(t *testing.T) {
t.Run("up down up sequence", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd3", "cmd2", "cmd1"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
CommandHistoryCur: 0,
}
upAction := MoveCommandHistoryUp{}
@ -334,11 +261,11 @@ func TestCommandHistoryIntegration(t *testing.T) {
})
t.Run("navigate up past end does not crash", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd1", "cmd2"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd1", "cmd2"},
CommandHistoryCur: 0,
}
upAction := MoveCommandHistoryUp{}
@ -356,11 +283,11 @@ func TestCommandHistoryIntegration(t *testing.T) {
})
t.Run("navigate down past bottom does not crash", func(t *testing.T) {
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: []string{"cmd1"},
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: []string{"cmd1"},
CommandHistoryCur: 0,
}
downAction := MoveCommandHistoryDown{}
@ -392,11 +319,11 @@ func TestCommandHistoryWithLongHistory(t *testing.T) {
history[i] = string(rune('A' + i))
}
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: history,
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: history,
CommandHistoryCur: 0,
}
upAction := MoveCommandHistoryUp{}
@ -421,11 +348,11 @@ func TestCommandHistoryWithLongHistory(t *testing.T) {
history[i] = string(rune('0' + (i % 10)))
}
m := &mockModel{
mode: core.CommandMode,
command: "",
commandHistory: history,
commandHistoryCursor: 0,
m := &action.MockModel{
ModeVal: core.CommandMode,
CommandVal: "",
CommandHistoryList: history,
CommandHistoryCur: 0,
}
upAction := MoveCommandHistoryUp{}