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 : 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()) } }) }