feat: implemented :<n> command, tested
All checks were successful
Run Test Suite / test (push) Successful in 17s
All checks were successful
Run Test Suite / test (push) Successful in 17s
Also added tons of tests for the actual command mode, since that was all untested...
This commit is contained in:
parent
e3b0f30c75
commit
ea3ebcdc83
62
FEATURES.md
62
FEATURES.md
@ -219,7 +219,7 @@
|
||||
- [x] `:q!` - Force quit
|
||||
- [x] `:e {file}` - Edit file
|
||||
- [x] `:bn` / `:bp` - Next/previous buffer
|
||||
- [ ] `:{range}` - Go to line
|
||||
- [x] `:{range}` - Go to line
|
||||
- [ ] `:%s/old/new/g` - Search and replace
|
||||
- [ ] `:!{cmd}` - Run shell command
|
||||
- [ ] `:help` - Show help
|
||||
@ -408,63 +408,3 @@ Buffers are in-memory representations of files. A buffer exists for each open fi
|
||||
- [ ] Spell check
|
||||
|
||||
---
|
||||
|
||||
### Well Tested - Editor Core
|
||||
|
||||
#### Command Execution (179 tests)
|
||||
- [x] Command parsing and validation
|
||||
- [x] Command lookup and prefix matching
|
||||
- [x] Force flag handling (!)
|
||||
- [x] Write commands (`:w`, `:w {file}`, `:w!`)
|
||||
- [x] Write all commands (`:wa`, `:wall`, `:wa!`)
|
||||
- [x] Quit commands (`:q`, `:q!`, `:qa`, `:qa!`)
|
||||
- [x] Write-quit commands (`:wq`, `:wq!`, `:wqa`, `:wqa!`)
|
||||
- [x] Edit command (`:e {file}`)
|
||||
- [x] Register display (`:register`, `:reg {name}`)
|
||||
- [x] Set commands (`:set number`, `:set tabstop=N`, etc.)
|
||||
- [x] Setting lookup and validation
|
||||
- [x] Buffer-level readonly protection
|
||||
- [x] Scratch buffer write protection
|
||||
- [x] Force write bypassing readonly/scratch checks
|
||||
- [x] Multiple buffer write operations
|
||||
- [x] File write error handling (permissions, paths)
|
||||
- [x] Modified buffer tracking
|
||||
- [x] Unicode filename and content handling
|
||||
- [x] Edge cases (empty args, long filenames, special chars)
|
||||
|
||||
#### Program Initialization (70 tests)
|
||||
- [x] Empty program creation
|
||||
- [x] File program with nonexistent files (new file buffers)
|
||||
- [x] File program with existing files (content loading)
|
||||
- [x] Line ending handling (Unix `\n`, Windows `\r\n`, mixed)
|
||||
- [x] Tab to space conversion based on TabStop
|
||||
- [x] Unicode content preservation (CJK, emoji)
|
||||
- [x] File extension and type detection
|
||||
- [x] Buffer state initialization (flags, metadata)
|
||||
- [x] Large file handling (10,000+ lines)
|
||||
- [x] Long line handling (10,000+ chars)
|
||||
- [x] Empty file handling
|
||||
- [x] Builder pattern method chaining
|
||||
- [x] Program option accumulation
|
||||
- [x] Model state defaults (settings, registers, mode)
|
||||
- [x] Error handling (permissions, invalid paths)
|
||||
- [x] Integration workflows (end-to-end)
|
||||
- [x] Edge cases (empty filenames, relative paths, dot files)
|
||||
|
||||
### Moderately Tested
|
||||
- [x] Basic motions (h, j, k, l)
|
||||
- [x] Word motions (w, e, b)
|
||||
- [x] Jump motions (G, gg, 0, $, _, ^, |)
|
||||
- [x] Scroll actions (ctrl+u, ctrl+d)
|
||||
- [x] Delete operator (d, dd)
|
||||
- [x] Yank operator (y, yy)
|
||||
- [x] Paste actions (p, P)
|
||||
- [x] Change operator (c, cc, C)
|
||||
- [x] Substitute action (s, S)
|
||||
- [x] Insert mode entry (i, a, I, A, o, O)
|
||||
- [x] Insert mode editing (enter, backspace, delete, tab, ctrl+w)
|
||||
- [x] Visual modes (v, V, ctrl+v)
|
||||
- [x] Visual mode with motions
|
||||
- [x] Delete actions (x, D)
|
||||
- [x] Register behavior
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@ -139,6 +141,12 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||
// history = append([]string{cmdLine}, history...)
|
||||
m.SetCommandHistory(history)
|
||||
|
||||
// try to parse entire thing as a number
|
||||
num, err := strconv.Atoi(cmdLine)
|
||||
if err == nil {
|
||||
return cmdGoToLine(m, num)
|
||||
}
|
||||
|
||||
cmd, err := a.Registry.Execute(m, cmdLine)
|
||||
if err != nil {
|
||||
out := core.CommandOutput{
|
||||
@ -152,3 +160,23 @@ func (a CommandExecute) Execute(m Model) tea.Cmd {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// cmdGoToLine: DOES NOT implement command.Command. Instead, is called directly
|
||||
// by CommandExecture.Execute(). Jumps the cursor to the line provided
|
||||
func cmdGoToLine(m Model, line int) tea.Cmd {
|
||||
// number below 0 just goes back that many
|
||||
|
||||
win := m.ActiveWindow()
|
||||
buf := m.ActiveBuffer()
|
||||
|
||||
if line <= 0 {
|
||||
newLine := max(0, win.Cursor.Line+line)
|
||||
win.SetCursorLine(newLine)
|
||||
return nil
|
||||
}
|
||||
|
||||
newLine := min(line-1, buf.LineCount())
|
||||
win.SetCursorLine(newLine)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
725
internal/action/command_test.go
Normal file
725
internal/action/command_test.go
Normal file
@ -0,0 +1,725 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.gophernest.net/azpect/TextEditor/internal/core"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
|
||||
// mockCommandRegistry for testing - returns nil for all commands (used for numeric goto)
|
||||
type mockCommandRegistry struct{}
|
||||
|
||||
func (r *mockCommandRegistry) Execute(m Model, cmdLine string) (tea.Cmd, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TestCommandExecute_GoToLine tests the :<num> command functionality
|
||||
func TestCommandExecute_GoToLine(t *testing.T) {
|
||||
t.Run("goto positive line number within bounds", func(t *testing.T) {
|
||||
// Create buffer with 10 lines
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5", "line6", "line7", "line8", "line9", "line10"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(0)
|
||||
|
||||
// Set command to "5"
|
||||
m.SetCommand("5")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should jump to line 5 (0-indexed: line 4)
|
||||
if win.Cursor.Line != 4 {
|
||||
t.Fatalf("expected cursor at line 4, got %d", win.Cursor.Line)
|
||||
}
|
||||
|
||||
// Should exit command mode
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Fatalf("expected normal mode, got %v", m.Mode())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("goto line 1", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(3) // Start at line 4
|
||||
|
||||
m.SetCommand("1")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should jump to line 1 (0-indexed: line 0)
|
||||
if win.Cursor.Line != 0 {
|
||||
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("goto last line", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(0)
|
||||
|
||||
m.SetCommand("5")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should jump to line 5 (0-indexed: line 4)
|
||||
if win.Cursor.Line != 4 {
|
||||
t.Fatalf("expected cursor at line 4, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("goto line beyond buffer length clamps to last line", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(0)
|
||||
|
||||
m.SetCommand("999")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clamp to last line (0-indexed: line 2)
|
||||
// Note: cmdGoToLine uses min(line-1, buf.LineCount())
|
||||
// LineCount() returns 3, so min(998, 3) = 3
|
||||
// But SetCursorLine calls ClampCursor() which clamps to valid range [0, LineCount-1]
|
||||
lastLine := buf.LineCount() - 1
|
||||
if win.Cursor.Line != lastLine {
|
||||
t.Fatalf("expected cursor at line %d (last line), got %d", lastLine, win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("goto line zero goes to beginning", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(3)
|
||||
|
||||
m.SetCommand("0")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Line 0 or below goes relative: max(0, currentLine + 0) = currentLine
|
||||
// But with the logic: if line <= 0, newLine = max(0, win.Cursor.Line + line)
|
||||
// So for line=0: max(0, 3+0) = 3 (stays at same line)
|
||||
if win.Cursor.Line != 3 {
|
||||
t.Fatalf("expected cursor at line 3, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative number moves relative backwards", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5", "line6", "line7", "line8", "line9", "line10"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(5) // Start at line 6 (0-indexed)
|
||||
|
||||
m.SetCommand("-3")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should move back 3 lines: 5 + (-3) = 2
|
||||
if win.Cursor.Line != 2 {
|
||||
t.Fatalf("expected cursor at line 2, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative number beyond start clamps to line 0", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(2)
|
||||
|
||||
m.SetCommand("-10")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clamp to line 0: max(0, 2 + (-10)) = max(0, -8) = 0
|
||||
if win.Cursor.Line != 0 {
|
||||
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("goto same line", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(2) // At line 3
|
||||
|
||||
m.SetCommand("3")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should stay at line 3 (0-indexed: line 2)
|
||||
if win.Cursor.Line != 2 {
|
||||
t.Fatalf("expected cursor at line 2, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty buffer", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{""}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
|
||||
m.SetCommand("1")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should stay at line 0
|
||||
if win.Cursor.Line != 0 {
|
||||
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single line buffer goto line 1", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"only line"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
|
||||
m.SetCommand("1")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
if win.Cursor.Line != 0 {
|
||||
t.Fatalf("expected cursor at line 0, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single line buffer goto beyond", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"only line"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
|
||||
m.SetCommand("10")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clamp to the available lines
|
||||
if win.Cursor.Line > 0 {
|
||||
t.Fatalf("expected cursor at line 0 or 1, got %d", win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("large line number", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
|
||||
m.SetCommand("1000000")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should clamp to last available line
|
||||
lineCount := buf.LineCount()
|
||||
if win.Cursor.Line > lineCount {
|
||||
t.Fatalf("expected cursor at or before line %d, got %d", lineCount, win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("command with leading/trailing spaces", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
win.SetCursorLine(0)
|
||||
|
||||
// strconv.Atoi should handle leading spaces
|
||||
m.SetCommand(" 3 ")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// strconv.Atoi(" 3 ") will fail, so this won't be treated as a line number
|
||||
// It should stay at the same position and potentially show an error
|
||||
// depending on the CommandRegistry behavior
|
||||
})
|
||||
|
||||
t.Run("mixed alphanumeric command not treated as line number", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
win := m.ActiveWindow()
|
||||
initialLine := 1
|
||||
win.SetCursorLine(initialLine)
|
||||
|
||||
m.SetCommand("3abc")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// "3abc" is not a valid number, so won't trigger goto line
|
||||
// Cursor should stay at original position
|
||||
if win.Cursor.Line != initialLine {
|
||||
t.Fatalf("expected cursor at line %d, got %d", initialLine, win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandExecute_EmptyCommand tests empty command behavior
|
||||
func TestCommandExecute_EmptyCommand(t *testing.T) {
|
||||
t.Run("empty command does nothing", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
win := m.ActiveWindow()
|
||||
initialLine := 0
|
||||
win.SetCursorLine(initialLine)
|
||||
|
||||
m.SetCommand("")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
// Should exit command mode
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Fatalf("expected normal mode, got %v", m.Mode())
|
||||
}
|
||||
|
||||
// Cursor should not move
|
||||
if win.Cursor.Line != initialLine {
|
||||
t.Fatalf("expected cursor at line %d, got %d", initialLine, win.Cursor.Line)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandExecute_CommandHistory tests that commands are added to history
|
||||
func TestCommandExecute_CommandHistory(t *testing.T) {
|
||||
t.Run("numeric command added to history", func(t *testing.T) {
|
||||
buf := core.NewBufferBuilder().
|
||||
WithLines([]string{"line1", "line2", "line3", "line4", "line5"}).
|
||||
Build()
|
||||
m := NewMockModelWithBuffer(&buf)
|
||||
|
||||
m.SetCommand("3")
|
||||
m.SetMode(core.CommandMode)
|
||||
m.SetCommandHistory([]string{})
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 1 {
|
||||
t.Fatalf("expected 1 item in history, got %d", len(history))
|
||||
}
|
||||
if history[0] != "3" {
|
||||
t.Fatalf("expected '3' in history, got '%s'", history[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("commands prepended to existing history", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommandHistory([]string{"oldcommand1", "oldcommand2"})
|
||||
|
||||
m.SetCommand("5")
|
||||
m.SetMode(core.CommandMode)
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
history := m.CommandHistory()
|
||||
if len(history) != 3 {
|
||||
t.Fatalf("expected 3 items in history, got %d", len(history))
|
||||
}
|
||||
if history[0] != "5" {
|
||||
t.Fatalf("expected '5' as first item, got '%s'", history[0])
|
||||
}
|
||||
if history[1] != "oldcommand1" {
|
||||
t.Fatalf("expected 'oldcommand1' as second item, got '%s'", history[1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandExecute_ModeTransition tests command mode exit behavior
|
||||
func TestCommandExecute_ModeTransition(t *testing.T) {
|
||||
t.Run("exits command mode after execution", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetMode(core.CommandMode)
|
||||
m.SetCommand("5")
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Fatalf("expected normal mode, got %v", m.Mode())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resets command cursor to 0", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommandCursor(10)
|
||||
m.SetCommand("5")
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Fatalf("expected command cursor at 0, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resets command history cursor to 0", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommandHistoryCursor(5)
|
||||
m.SetCommand("5")
|
||||
|
||||
action := CommandExecute{Registry: &mockCommandRegistry{}}
|
||||
action.Execute(m)
|
||||
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Fatalf("expected command history cursor at 0, got %d", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestExitCommandMode tests the Esc key behavior
|
||||
func TestExitCommandMode(t *testing.T) {
|
||||
t.Run("exit command mode clears state", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetMode(core.CommandMode)
|
||||
m.SetCommand("test command")
|
||||
m.SetCommandCursor(5)
|
||||
m.SetCommandHistoryCursor(3)
|
||||
|
||||
action := ExitCommandMode{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Mode() != core.NormalMode {
|
||||
t.Fatalf("expected normal mode, got %v", m.Mode())
|
||||
}
|
||||
if m.Command() != "" {
|
||||
t.Fatalf("expected empty command, got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Fatalf("expected command cursor at 0, got %d", m.CommandCursor())
|
||||
}
|
||||
if m.CommandHistoryCursor() != 0 {
|
||||
t.Fatalf("expected command history cursor at 0, got %d", m.CommandHistoryCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestInsertCommandChar tests character insertion in command mode
|
||||
func TestInsertCommandChar(t *testing.T) {
|
||||
t.Run("insert character at empty command", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("")
|
||||
m.SetCommandCursor(0)
|
||||
|
||||
action := InsertCommandChar{Char: "5"}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "5" {
|
||||
t.Fatalf("expected '5', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 1 {
|
||||
t.Fatalf("expected cursor at 1, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("insert character at end", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("12")
|
||||
m.SetCommandCursor(2)
|
||||
|
||||
action := InsertCommandChar{Char: "3"}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "123" {
|
||||
t.Fatalf("expected '123', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 3 {
|
||||
t.Fatalf("expected cursor at 3, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("insert character in middle", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("13")
|
||||
m.SetCommandCursor(1)
|
||||
|
||||
action := InsertCommandChar{Char: "2"}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "123" {
|
||||
t.Fatalf("expected '123', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 2 {
|
||||
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandBackspace tests backspace in command mode
|
||||
func TestCommandBackspace(t *testing.T) {
|
||||
t.Run("backspace at beginning does nothing", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(0)
|
||||
|
||||
action := CommandBackspace{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "123" {
|
||||
t.Fatalf("expected '123', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backspace in middle", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(2)
|
||||
|
||||
action := CommandBackspace{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "13" {
|
||||
t.Fatalf("expected '13', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 1 {
|
||||
t.Fatalf("expected cursor at 1, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("backspace at end", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(3)
|
||||
|
||||
action := CommandBackspace{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "12" {
|
||||
t.Fatalf("expected '12', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 2 {
|
||||
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandDelete tests delete key in command mode
|
||||
func TestCommandDelete(t *testing.T) {
|
||||
t.Run("delete at cursor position 0 deletes character after cursor", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(0)
|
||||
|
||||
action := CommandDelete{}
|
||||
action.Execute(m)
|
||||
|
||||
// At position 0, delete removes char at position 1 (the next char)
|
||||
// Result: "1" + "3" = "13"
|
||||
if m.Command() != "13" {
|
||||
t.Fatalf("expected '13', got '%s'", m.Command())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete at cursor position 1", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(1)
|
||||
|
||||
action := CommandDelete{}
|
||||
action.Execute(m)
|
||||
|
||||
// At position 1, delete removes char at position 2 (the next char)
|
||||
// Result: "12" + "" = "12"
|
||||
if m.Command() != "12" {
|
||||
t.Fatalf("expected '12', got '%s'", m.Command())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete at last character", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(2)
|
||||
|
||||
action := CommandDelete{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "12" {
|
||||
t.Fatalf("expected '12', got '%s'", m.Command())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete at end acts as backspace", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("123")
|
||||
m.SetCommandCursor(3)
|
||||
|
||||
action := CommandDelete{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "12" {
|
||||
t.Fatalf("expected '12', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 2 {
|
||||
t.Fatalf("expected cursor at 2, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommandDeletePreviousWord tests Ctrl+W behavior in command mode
|
||||
func TestCommandDeletePreviousWord(t *testing.T) {
|
||||
t.Run("delete word from middle of text", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("hello world")
|
||||
m.SetCommandCursor(11) // At end
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "hello " {
|
||||
t.Fatalf("expected 'hello ', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 6 {
|
||||
t.Fatalf("expected cursor at 6, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete word with leading spaces", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("hello world")
|
||||
m.SetCommandCursor(13) // At end
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "hello " {
|
||||
t.Fatalf("expected 'hello ', got '%s'", m.Command())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete at beginning does nothing", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("hello")
|
||||
m.SetCommandCursor(0)
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "hello" {
|
||||
t.Fatalf("expected 'hello', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete punctuation", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("hello...")
|
||||
m.SetCommandCursor(8) // After the dots
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "hello" {
|
||||
t.Fatalf("expected 'hello', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 5 {
|
||||
t.Fatalf("expected cursor at 5, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete word in middle of command", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("one two three")
|
||||
m.SetCommandCursor(7) // After "two"
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "one three" {
|
||||
t.Fatalf("expected 'one three', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 4 {
|
||||
t.Fatalf("expected cursor at 4, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete single character word", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("a b c")
|
||||
m.SetCommandCursor(3) // After "b"
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
if m.Command() != "a c" {
|
||||
t.Fatalf("expected 'a c', got '%s'", m.Command())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete from whitespace position", func(t *testing.T) {
|
||||
m := NewMockModel()
|
||||
m.SetCommand("hello world")
|
||||
m.SetCommandCursor(6) // At the space after "hello"
|
||||
|
||||
action := CommandDeletePreviousWord{}
|
||||
action.Execute(m)
|
||||
|
||||
// Should skip the space and delete "hello" and the space
|
||||
if m.Command() != "world" {
|
||||
t.Fatalf("expected 'world', got '%s'", m.Command())
|
||||
}
|
||||
if m.CommandCursor() != 0 {
|
||||
t.Fatalf("expected cursor at 0, got %d", m.CommandCursor())
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user