Compare commits
2 Commits
5ff473d0d9
...
a01369f407
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a01369f407 | ||
|
|
3c98dca777 |
@ -11,6 +11,7 @@ type ExitCommandMode struct{}
|
||||
// ExitCommandMode.Execute: Exits command mode and returns to normal mode (Esc key).
|
||||
func (a ExitCommandMode) Execute(m Model) tea.Cmd {
|
||||
m.SetCommandCursor(0)
|
||||
m.SetCommandHistoryCursor(0)
|
||||
m.SetCommand("")
|
||||
m.SetCommandOutput(&core.CommandOutput{})
|
||||
m.SetMode(core.NormalMode)
|
||||
@ -127,12 +128,17 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||
|
||||
// Clear command state and return to normal mode
|
||||
m.SetCommandCursor(0)
|
||||
m.SetCommandHistoryCursor(0)
|
||||
m.SetMode(core.NormalMode)
|
||||
|
||||
if a.Registry == nil || cmdLine == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
history := append([]string{cmdLine}, m.CommandHistory()...)
|
||||
// history = append([]string{cmdLine}, history...)
|
||||
m.SetCommandHistory(history)
|
||||
|
||||
cmd, err := a.Registry.Execute(m, cmdLine)
|
||||
if err != nil {
|
||||
out := core.CommandOutput{
|
||||
|
||||
133
internal/action/command_history_test.go
Normal file
133
internal/action/command_history_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// mockRegistry for testing command execution without actual command handlers
|
||||
type mockRegistry struct{}
|
||||
|
||||
func (r *mockRegistry) Execute(m Model, cmdLine string) (tea.Cmd, error) {
|
||||
// Mock implementation - just returns nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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 := NewMockModel()
|
||||
m.ModeVal = core.CommandMode
|
||||
m.CommandVal = "w test.txt"
|
||||
m.CommandHistoryList = []string{}
|
||||
m.CommandHistoryCur = 0
|
||||
|
||||
registry := &mockRegistry{}
|
||||
action := CommandExecute{Registry: registry}
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 1 {
|
||||
t.Errorf("History length = %d, want 1", len(history))
|
||||
}
|
||||
|
||||
if history[0] != "w test.txt" {
|
||||
t.Errorf("History[0] = %q, want %q", history[0], "w test.txt")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple commands prepended to history", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.ModeVal = core.CommandMode
|
||||
m.CommandVal = "w file1.txt"
|
||||
m.CommandHistoryList = []string{}
|
||||
m.CommandHistoryCur = 0
|
||||
|
||||
registry := &mockRegistry{}
|
||||
action := CommandExecute{Registry: registry}
|
||||
|
||||
// Execute first command
|
||||
action.Execute(m)
|
||||
|
||||
// Execute second command
|
||||
m.CommandVal = "q"
|
||||
action.Execute(m)
|
||||
|
||||
// Execute third command
|
||||
m.CommandVal = "set number"
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 3 {
|
||||
t.Errorf("History length = %d, want 3", len(history))
|
||||
}
|
||||
|
||||
// Most recent should be first (prepended)
|
||||
want := []string{"set number", "q", "w file1.txt"}
|
||||
for i, cmd := range want {
|
||||
if history[i] != cmd {
|
||||
t.Errorf("History[%d] = %q, want %q", i, history[i], cmd)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty command not added to history", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.ModeVal = core.CommandMode
|
||||
m.CommandVal = ""
|
||||
m.CommandHistoryList = []string{"previous command"}
|
||||
m.CommandHistoryCur = 0
|
||||
|
||||
registry := &mockRegistry{}
|
||||
action := CommandExecute{Registry: registry}
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 1 {
|
||||
t.Errorf("History length = %d, want 1 (empty command should not be added)", len(history))
|
||||
}
|
||||
|
||||
if history[0] != "previous command" {
|
||||
t.Errorf("History[0] = %q, want %q", history[0], "previous command")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("history cursor resets on execute", func(t *testing.T) {
|
||||
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}
|
||||
action.Execute(m)
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0 after execute", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("duplicate commands are added to history", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.ModeVal = core.CommandMode
|
||||
m.CommandVal = "w"
|
||||
m.CommandHistoryList = []string{"w"}
|
||||
m.CommandHistoryCur = 0
|
||||
|
||||
registry := &mockRegistry{}
|
||||
action := CommandExecute{Registry: registry}
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 2 {
|
||||
t.Errorf("History length = %d, want 2 (duplicates should be added)", len(history))
|
||||
}
|
||||
|
||||
if history[0] != "w" || history[1] != "w" {
|
||||
t.Errorf("History = %v, want ['w', 'w']", history)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -4,124 +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
|
||||
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 }
|
||||
|
||||
// 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
|
||||
// ==================================================
|
||||
@ -137,7 +21,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0). // At 'h'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -164,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",
|
||||
@ -191,7 +75,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "x",
|
||||
@ -218,7 +102,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "d",
|
||||
@ -245,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",
|
||||
@ -272,7 +156,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -299,7 +183,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -326,7 +210,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: " ",
|
||||
@ -353,7 +237,7 @@ func TestFindCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "b",
|
||||
@ -386,7 +270,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10). // At 'd'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -413,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",
|
||||
@ -440,7 +324,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "x",
|
||||
@ -467,7 +351,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "h",
|
||||
@ -494,7 +378,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 6). // At 'w'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -521,7 +405,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -548,7 +432,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -575,7 +459,7 @@ func TestFindCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: " ",
|
||||
@ -602,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",
|
||||
@ -635,7 +519,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0). // At 'h'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -662,7 +546,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "x",
|
||||
@ -689,7 +573,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0). // At 'a'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "b",
|
||||
@ -717,7 +601,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "d",
|
||||
@ -744,7 +628,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -771,7 +655,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: " ",
|
||||
@ -798,7 +682,7 @@ func TestTillCharForward(t *testing.T) {
|
||||
WithCursorPos(0, 5). // At space
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -831,7 +715,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10). // At 'd'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -858,7 +742,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "x",
|
||||
@ -885,7 +769,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 1). // At 'b'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -913,7 +797,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "h",
|
||||
@ -940,7 +824,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -967,7 +851,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: " ",
|
||||
@ -994,7 +878,7 @@ func TestTillCharBackward(t *testing.T) {
|
||||
WithCursorPos(0, 6). // At 'w'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "o",
|
||||
@ -1021,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",
|
||||
@ -1054,7 +938,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0). // At 'h'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1085,7 +969,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1116,7 +1000,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1143,7 +1027,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "w",
|
||||
@ -1170,7 +1054,7 @@ func TestFindCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -1198,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",
|
||||
@ -1235,7 +1119,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10). // At 'd'
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1266,7 +1150,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1298,7 +1182,7 @@ func TestFindCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1325,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",
|
||||
@ -1353,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",
|
||||
@ -1390,7 +1274,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1417,7 +1301,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1444,7 +1328,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1471,7 +1355,7 @@ func TestTillCharForwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 0).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "a",
|
||||
@ -1505,7 +1389,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1532,7 +1416,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1559,7 +1443,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "l",
|
||||
@ -1586,7 +1470,7 @@ func TestTillCharBackwardWithCount(t *testing.T) {
|
||||
WithCursorPos(0, 10).
|
||||
Build()
|
||||
|
||||
m := newMockModelWithWindow(&win)
|
||||
m := NewMockModelWithWindow(&win)
|
||||
|
||||
action := FindChar{
|
||||
Char: "b",
|
||||
@ -1725,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)
|
||||
@ -1739,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)
|
||||
@ -1753,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)
|
||||
@ -1769,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)
|
||||
@ -1783,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)
|
||||
@ -1804,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)
|
||||
@ -1818,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)
|
||||
@ -1832,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)
|
||||
@ -1847,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)
|
||||
@ -1870,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)
|
||||
@ -1885,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)
|
||||
@ -1901,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)
|
||||
@ -1918,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
|
||||
@ -1946,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)
|
||||
@ -1968,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)
|
||||
@ -1982,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)
|
||||
@ -1997,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)
|
||||
@ -2015,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)
|
||||
@ -2041,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)
|
||||
@ -2084,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)
|
||||
@ -2092,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
|
||||
@ -2107,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)
|
||||
@ -2115,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
|
||||
@ -2135,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
|
||||
@ -2163,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
|
||||
@ -2185,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)
|
||||
@ -2193,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
|
||||
@ -2213,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
|
||||
@ -2235,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)
|
||||
@ -2243,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
|
||||
@ -2270,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)
|
||||
|
||||
@ -2287,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
|
||||
@ -2309,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
|
||||
@ -2324,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
|
||||
@ -2347,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
|
||||
@ -2363,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)
|
||||
@ -2385,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
|
||||
@ -2401,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)
|
||||
@ -2417,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
|
||||
}
|
||||
@ -2542,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}
|
||||
@ -2556,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}
|
||||
@ -2570,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)
|
||||
@ -2583,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)
|
||||
|
||||
@ -41,6 +41,10 @@ type Model interface {
|
||||
CommandOutput() *core.CommandOutput
|
||||
// DO NOT FORGET TO CALL SetMode()
|
||||
SetCommandOutput(out *core.CommandOutput)
|
||||
CommandHistory() []string
|
||||
SetCommandHistory(history []string)
|
||||
CommandHistoryCursor() int
|
||||
SetCommandHistoryCursor(cur int)
|
||||
|
||||
// ==================================================
|
||||
// Editor-wide State
|
||||
|
||||
133
internal/action/mock.go
Normal file
133
internal/action/mock.go
Normal 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}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -309,6 +310,27 @@ func cmdRegisters(m action.Model, args []string, force bool) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// History Commands
|
||||
// --------------------------------------------------
|
||||
func cmdHistory(m action.Model, args []string, force bool) tea.Cmd {
|
||||
_, _ = args, force
|
||||
|
||||
history := m.CommandHistory()
|
||||
reversed := slices.Clone(history)
|
||||
slices.Reverse(reversed)
|
||||
|
||||
m.SetMode(core.CommandOutputMode)
|
||||
m.SetCommandOutput(&core.CommandOutput{
|
||||
Title: ":history",
|
||||
Lines: reversed,
|
||||
Inline: false,
|
||||
IsError: false,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Buffer Commands
|
||||
// --------------------------------------------------
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -162,6 +162,13 @@ func (r *Registry) registerDefaults() {
|
||||
Handler: cmdRegisters,
|
||||
})
|
||||
|
||||
// History commands
|
||||
r.Register(Command{
|
||||
Name: "history",
|
||||
ShortForm: "his",
|
||||
Handler: cmdHistory,
|
||||
})
|
||||
|
||||
// Buffer commands
|
||||
r.Register(Command{
|
||||
Name: "buffers",
|
||||
@ -224,6 +231,7 @@ func (r *Registry) registerDefaults() {
|
||||
ShortForm: "colo",
|
||||
Handler: cmdColorscheme,
|
||||
})
|
||||
|
||||
r.Register(Command{
|
||||
Name: "colorschemes",
|
||||
ShortForm: "colorschemes",
|
||||
|
||||
@ -40,6 +40,8 @@ type Model struct {
|
||||
command string
|
||||
commandCursor int
|
||||
commandOutput *core.CommandOutput
|
||||
commandHistory []string
|
||||
commandHistoryCursor int
|
||||
|
||||
// Global settings
|
||||
settings core.EditorSettings
|
||||
@ -279,6 +281,22 @@ func (m *Model) SetCommandOutput(out *core.CommandOutput) {
|
||||
m.commandOutput = out
|
||||
}
|
||||
|
||||
func (m *Model) CommandHistory() []string {
|
||||
return m.commandHistory
|
||||
}
|
||||
|
||||
func (m *Model) SetCommandHistory(history []string) {
|
||||
m.commandHistory = history
|
||||
}
|
||||
|
||||
func (m *Model) CommandHistoryCursor() int {
|
||||
return m.commandHistoryCursor
|
||||
}
|
||||
|
||||
func (m *Model) SetCommandHistoryCursor(cur int) {
|
||||
m.commandHistoryCursor = cur
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Editor-wide State
|
||||
// ==================================================
|
||||
|
||||
@ -60,6 +60,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// TODO: Any vim action should exit also
|
||||
// Simple override for command output mode for now
|
||||
if m.Mode() == core.CommandOutputMode {
|
||||
// TODO: Implement g/G/d/u
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
m.SetMode(core.NormalMode)
|
||||
|
||||
@ -142,6 +142,8 @@ func NewCommandKeymap() *Keymap {
|
||||
motions: map[string]action.Motion{
|
||||
"left": motion.MoveCommandLeft{},
|
||||
"right": motion.MoveCommandRight{},
|
||||
"up": motion.MoveCommandHistoryUp{},
|
||||
"down": motion.MoveCommandHistoryDown{},
|
||||
},
|
||||
operators: map[string]action.Operator{}, // this will likely be empty
|
||||
actions: map[string]action.Action{
|
||||
|
||||
@ -31,3 +31,54 @@ func (a MoveCommandRight) Execute(m action.Model) tea.Cmd {
|
||||
|
||||
// MoveCommandRight.Type: Returns CharwiseExclusive for command line motion.
|
||||
func (a MoveCommandRight) Type() core.MotionType { return core.CharwiseExclusive }
|
||||
|
||||
// ==================================================
|
||||
// Command History Motions
|
||||
// ==================================================
|
||||
|
||||
// MoveCommandHistoryUp implements Motion - moves cursor right in command line.
|
||||
type MoveCommandHistoryUp struct{}
|
||||
|
||||
// MoveCommandHistoryUp.Execute: Moves the command history cursor up (if possible).
|
||||
func (a MoveCommandHistoryUp) Execute(m action.Model) tea.Cmd {
|
||||
cur := m.CommandHistoryCursor() // always +1 (bascially indexed at 1)
|
||||
history := m.CommandHistory()
|
||||
|
||||
if cur < len(history) {
|
||||
cmd := history[cur]
|
||||
m.SetCommand(cmd)
|
||||
m.SetCommandCursor(len(cmd))
|
||||
|
||||
// Only go up if we can
|
||||
m.SetCommandHistoryCursor(cur + 1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCommandHistoryUp.Type: Returns Linewise for command line motion.
|
||||
func (a MoveCommandHistoryUp) Type() core.MotionType { return core.Linewise }
|
||||
|
||||
// MoveCommandHistoryDown implements Motion - moves cursor right in command line.
|
||||
type MoveCommandHistoryDown struct{}
|
||||
|
||||
// MoveCommandHistoryDown.Execute: Moves the command history cursor down (if possible).
|
||||
func (a MoveCommandHistoryDown) Execute(m action.Model) tea.Cmd {
|
||||
cur := m.CommandHistoryCursor() // always +1 (bascially indexed at 1)
|
||||
history := m.CommandHistory()
|
||||
|
||||
if cur > 1 {
|
||||
cmd := history[cur-2]
|
||||
m.SetCommand(cmd)
|
||||
m.SetCommandCursor(len(cmd))
|
||||
} else {
|
||||
m.SetCommand("") // BUG: We should probably keep the original in state
|
||||
m.SetCommandCursor(0)
|
||||
}
|
||||
|
||||
m.SetCommandHistoryCursor(max(0, cur-1))
|
||||
return nil
|
||||
}
|
||||
|
||||
// MoveCommandHistoryDown.Type: Returns Linewise for command line motion.
|
||||
func (a MoveCommandHistoryDown) Type() core.MotionType { return core.Linewise }
|
||||
|
||||
376
internal/motion/command_test.go
Normal file
376
internal/motion/command_test.go
Normal file
@ -0,0 +1,376 @@
|
||||
package motion
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
)
|
||||
|
||||
// ==================================================
|
||||
// MoveCommandHistoryUp Tests
|
||||
// ==================================================
|
||||
|
||||
func TestMoveCommandHistoryUp(t *testing.T) {
|
||||
t.Run("navigate up from start", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryUp{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should load first command in history
|
||||
if m.Command() != "cmd3" {
|
||||
t.Errorf("Command = %q, want %q", m.Command(), "cmd3")
|
||||
}
|
||||
|
||||
// Cursor should move to end of command
|
||||
if m.CommandCursor() != len("cmd3") {
|
||||
t.Errorf("CommandCursor = %d, want %d", m.CommandCursor(), len("cmd3"))
|
||||
}
|
||||
|
||||
// History cursor should advance
|
||||
if m.CommandHistoryCursor() != 1 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 1", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate up multiple times", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryUp{}
|
||||
|
||||
// First up
|
||||
action.Execute(m)
|
||||
if m.Command() != "cmd3" {
|
||||
t.Errorf("After 1st up: Command = %q, want 'cmd3'", m.Command())
|
||||
}
|
||||
|
||||
// Second up
|
||||
action.Execute(m)
|
||||
if m.Command() != "cmd2" {
|
||||
t.Errorf("After 2nd up: Command = %q, want 'cmd2'", m.Command())
|
||||
}
|
||||
|
||||
// Third up
|
||||
action.Execute(m)
|
||||
if m.Command() != "cmd1" {
|
||||
t.Errorf("After 3rd up: Command = %q, want 'cmd1'", m.Command())
|
||||
}
|
||||
|
||||
if m.CommandHistoryCursor() != 3 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 3", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cannot navigate past end of history", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 3, // Already at end
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryUp{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should not change command (still showing cmd1)
|
||||
if m.Command() != "" {
|
||||
t.Errorf("Command = %q, want empty (should not go past end)", m.Command())
|
||||
}
|
||||
|
||||
// Cursor should not advance
|
||||
if m.CommandHistoryCursor() != 3 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 3 (should not advance)", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty history does nothing", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "current",
|
||||
CommandHistoryList: []string{},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryUp{}
|
||||
action.Execute(m)
|
||||
|
||||
// Command should not change
|
||||
if m.Command() != "current" {
|
||||
t.Errorf("Command = %q, want 'current' (no history to navigate)", m.Command())
|
||||
}
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("command cursor moves to end", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandCursorVal: 0,
|
||||
CommandHistoryList: []string{"long command here"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryUp{}
|
||||
action.Execute(m)
|
||||
|
||||
expectedLen := len("long command here")
|
||||
if m.CommandCursor() != expectedLen {
|
||||
t.Errorf("CommandCursor = %d, want %d (end of command)", m.CommandCursor(), expectedLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// MoveCommandHistoryDown Tests
|
||||
// ==================================================
|
||||
|
||||
func TestMoveCommandHistoryDown(t *testing.T) {
|
||||
t.Run("navigate down from middle of history", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "cmd2",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 2, // At cmd2
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryDown{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should load cmd3 (more recent)
|
||||
if m.Command() != "cmd3" {
|
||||
t.Errorf("Command = %q, want 'cmd3'", m.Command())
|
||||
}
|
||||
|
||||
// Cursor should be at 1
|
||||
if m.CommandHistoryCursor() != 1 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 1", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate down to empty command", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "cmd3",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 1, // At first command
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryDown{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clear command (back to current input)
|
||||
if m.Command() != "" {
|
||||
t.Errorf("Command = %q, want empty (back to current)", m.Command())
|
||||
}
|
||||
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Errorf("CommandCursor = %d, want 0", m.CommandCursor())
|
||||
}
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate down from cursor 0 does nothing", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 0, // Already at bottom
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryDown{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clear command
|
||||
if m.Command() != "" {
|
||||
t.Errorf("Command = %q, want empty", m.Command())
|
||||
}
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("command cursor moves to end", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandCursorVal: 0,
|
||||
CommandHistoryList: []string{"cmd3", "long command here"},
|
||||
CommandHistoryCur: 2,
|
||||
}
|
||||
|
||||
action := MoveCommandHistoryDown{}
|
||||
action.Execute(m)
|
||||
|
||||
expectedLen := len("cmd3")
|
||||
if m.CommandCursor() != expectedLen {
|
||||
t.Errorf("CommandCursor = %d, want %d (end of command)", m.CommandCursor(), expectedLen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Integration Tests
|
||||
// ==================================================
|
||||
|
||||
func TestCommandHistoryIntegration(t *testing.T) {
|
||||
t.Run("up down up sequence", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd3", "cmd2", "cmd1"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
upAction := MoveCommandHistoryUp{}
|
||||
downAction := MoveCommandHistoryDown{}
|
||||
|
||||
// Up twice
|
||||
upAction.Execute(m) // cursor=1, cmd=cmd3
|
||||
upAction.Execute(m) // cursor=2, cmd=cmd2
|
||||
|
||||
// Down once
|
||||
downAction.Execute(m) // cursor=1, cmd=cmd3
|
||||
|
||||
// Up again
|
||||
upAction.Execute(m) // cursor=2, cmd=cmd2
|
||||
|
||||
if m.Command() != "cmd2" {
|
||||
t.Errorf("Command = %q, want 'cmd2'", m.Command())
|
||||
}
|
||||
if m.CommandHistoryCursor() != 2 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 2", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate up past end does not crash", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd1", "cmd2"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
upAction := MoveCommandHistoryUp{}
|
||||
|
||||
// Navigate to end
|
||||
upAction.Execute(m) // cursor = 1, cmd = cmd1
|
||||
upAction.Execute(m) // cursor = 2, cmd = cmd2
|
||||
|
||||
// Try to go past end
|
||||
upAction.Execute(m) // Should do nothing
|
||||
|
||||
if m.CommandHistoryCursor() != 2 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 2 (should stay at end)", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate down past bottom does not crash", func(t *testing.T) {
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: []string{"cmd1"},
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
downAction := MoveCommandHistoryDown{}
|
||||
|
||||
// Try to go down from bottom
|
||||
downAction.Execute(m)
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0 (should stay at bottom)", m.CommandHistoryCursor())
|
||||
}
|
||||
|
||||
// Try again
|
||||
downAction.Execute(m)
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 0 (should stay at bottom)", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ==================================================
|
||||
// Long History Tests
|
||||
// ==================================================
|
||||
|
||||
func TestCommandHistoryWithLongHistory(t *testing.T) {
|
||||
t.Run("navigate through 20 commands", func(t *testing.T) {
|
||||
history := make([]string, 20)
|
||||
for i := 0; i < 20; i++ {
|
||||
history[i] = string(rune('A' + i))
|
||||
}
|
||||
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: history,
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
upAction := MoveCommandHistoryUp{}
|
||||
|
||||
// Navigate to 10th command
|
||||
for i := 0; i < 10; i++ {
|
||||
upAction.Execute(m)
|
||||
}
|
||||
|
||||
want := string(rune('A' + 9))
|
||||
if m.Command() != want {
|
||||
t.Errorf("Command after 10 ups = %q, want %q", m.Command(), want)
|
||||
}
|
||||
if m.CommandHistoryCursor() != 10 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 10", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("navigate to very end of long history", func(t *testing.T) {
|
||||
history := make([]string, 50)
|
||||
for i := 0; i < 50; i++ {
|
||||
history[i] = string(rune('0' + (i % 10)))
|
||||
}
|
||||
|
||||
m := &action.MockModel{
|
||||
ModeVal: core.CommandMode,
|
||||
CommandVal: "",
|
||||
CommandHistoryList: history,
|
||||
CommandHistoryCur: 0,
|
||||
}
|
||||
|
||||
upAction := MoveCommandHistoryUp{}
|
||||
|
||||
// Navigate all the way to the end
|
||||
for i := 0; i < 100; i++ { // Try to go past end
|
||||
upAction.Execute(m)
|
||||
}
|
||||
|
||||
// Should be at position 50 (end of 50-item array)
|
||||
if m.CommandHistoryCursor() != 50 {
|
||||
t.Errorf("CommandHistoryCursor = %d, want 50 (at end)", m.CommandHistoryCursor())
|
||||
}
|
||||
|
||||
// Should be showing last command
|
||||
want := string(rune('0' + 49%10))
|
||||
if m.Command() != want {
|
||||
t.Errorf("Command = %q, want %q (last in history)", m.Command(), want)
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user