package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/core" ) // ================================================== // Replace Char (r) Tests // ================================================== func TestReplaceChar(t *testing.T) { t.Run("test 'rx' replaces character under cursor", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xello" { t.Errorf("lines[0] = %q, want 'xello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' in middle of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hexlo" { t.Errorf("lines[0] = %q, want 'hexlo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' at end of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 4, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hellx" { t.Errorf("lines[0] = %q, want 'hellx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' cursor position remains at replaced char", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col) } }) t.Run("test 'r' stays in normal mode", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.Mode() != core.NormalMode { t.Errorf("Mode() = %v, want NormalMode", m.Mode()) } }) t.Run("test 'r' with space replaces with space", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", " ") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != " ello" { t.Errorf("lines[0] = %q, want ' ello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' with digit replaces with digit", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "5") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "5ello" { t.Errorf("lines[0] = %q, want '5ello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' with special char replaces correctly", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "@") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "@ello" { t.Errorf("lines[0] = %q, want '@ello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test multiple 'r' operations in sequence", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "l", "r", "y") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xyllo" { t.Errorf("lines[0] = %q, want 'xyllo'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharWithCount(t *testing.T) { t.Run("test '3rx' replaces three characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxxlo" { t.Errorf("lines[0] = %q, want 'xxxlo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '3rx' cursor position after replace", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "r", "x") m := getFinalModel(t, tm) // Cursor should be at last replaced character if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col) } }) t.Run("test '5rx' replaces all five characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "5", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxxxx" { t.Errorf("lines[0] = %q, want 'xxxxx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '10rx' with overflow stops at line end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "1", "0", "r", "x") m := getFinalModel(t, tm) // Should only replace 5 chars (all available) if m.ActiveBuffer().Lines[0].String() != "xxxxx" { t.Errorf("lines[0] = %q, want 'xxxxx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '2rx' from middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "2", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hxxlo" { t.Errorf("lines[0] = %q, want 'hxxlo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '2rx' cursor position from middle", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "2", "r", "x") m := getFinalModel(t, tm) if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col) } }) t.Run("test '4rx' from near end stops at line end", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 3, Line: 0}) sendKeys(tm, "4", "r", "x") m := getFinalModel(t, tm) // Should only replace 2 chars (3 and 4) if m.ActiveBuffer().Lines[0].String() != "helxx" { t.Errorf("lines[0] = %q, want 'helxx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test large count '100rx' doesn't crash", func(t *testing.T) { lines := []string{"short"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "1", "0", "0", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxxxx" { t.Errorf("lines[0] = %q, want 'xxxxx'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharEdgeCases(t *testing.T) { t.Run("test 'r' on empty line does nothing", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("test 'r' on single character line", func(t *testing.T) { lines := []string{"a"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "x" { t.Errorf("Line(0) = %q, want 'x'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' at last char replaces it", func(t *testing.T) { lines := []string{"ab"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "ax" { t.Errorf("Line(0) = %q, want 'ax'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' with whitespace", func(t *testing.T) { lines := []string{"a b c"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "axb c" { t.Errorf("Line(0) = %q, want 'axb c'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' preserves other lines", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[1].String() != "world" { t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test 'r' on line with tabs", func(t *testing.T) { lines := []string{"a\tb"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "axb" { t.Errorf("Line(0) = %q, want 'axb'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '5rx' with only 3 chars available", func(t *testing.T) { lines := []string{"abc"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "5", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxx" { t.Errorf("Line(0) = %q, want 'xxx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' in middle preserves surrounding chars", func(t *testing.T) { lines := []string{"abcde"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abxde" { t.Errorf("Line(0) = %q, want 'abxde'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' on whitespace-only line", func(t *testing.T) { lines := []string{" "} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != " x " { t.Errorf("Line(0) = %q, want ' x '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'r' on different lines independently", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "j", "r", "y") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xello" { t.Errorf("Line(0) = %q, want 'xello'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "yorld" { t.Errorf("Line(1) = %q, want 'yorld'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test 'r' does not affect line count", func(t *testing.T) { lines := []string{"line 1", "line 2", "line 3"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } }) t.Run("test 'r' with newline character is treated as single char", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) // Note: In vim, 'r' with enter doesn't work the same way // This test documents the current behavior sendKeys(tm, "r", "\n") m := getFinalModel(t, tm) // Replacing with "\n" string (not actual newline) if m.ActiveBuffer().Lines[0].String() != "\nello" { t.Errorf("Line(0) = %q, want '\\nello'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharWithMotion(t *testing.T) { t.Run("test 'lrx' moves then replaces", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "l", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hxllo" { t.Errorf("lines[0] = %q, want 'hxllo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test 'wrx' moves to next word start then replaces", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "w", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello xorld" { t.Errorf("lines[0] = %q, want 'hello xorld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '$rx' moves to end then replaces", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "$", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hellx" { t.Errorf("lines[0] = %q, want 'hellx'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '2lrx' moves 2 right then replaces", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "l", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hexlo" { t.Errorf("lines[0] = %q, want 'hexlo'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharUndo(t *testing.T) { t.Run("test 'rx' can be undone with 'u'", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "u") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '3rx' can be undone with 'u'", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "r", "x", "u") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test multiple 'r' operations can be undone separately", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "l", "r", "y", "u") m := getFinalModel(t, tm) // Only the last 'ry' should be undone if m.ActiveBuffer().Lines[0].String() != "xello" { t.Errorf("lines[0] = %q, want 'xello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test undo then redo 'rx'", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "u", "ctrl+r") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xello" { t.Errorf("lines[0] = %q, want 'xello'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharRepeat(t *testing.T) { t.Run("test 'rx' can be repeated with '.'", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "x", "l", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxllo" { t.Errorf("lines[0] = %q, want 'xxllo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '3rx' can be repeated with '.'", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "3", "r", "x", "w", ".") m := getFinalModel(t, tm) // First 3rx at position 0, second at position 6 if m.ActiveBuffer().Lines[0].String() != "xxxlo xxxld" { t.Errorf("lines[0] = %q, want 'xxxlo xxxld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '.' repeats last replace char", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "r", "a", "l", ".", "l", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "aaalo" { t.Errorf("lines[0] = %q, want 'aaalo'", m.ActiveBuffer().Lines[0].String()) } }) // NOTE: This is the same as Vim's handling, but that is okay, I like this more, feels more honest t.Run("test '.' with count multiplies", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "r", "x", "w", "3", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "xxllo xxxxx" { t.Errorf("lines[0] = %q, want 'xxllo xxxxx'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceCharMultiLine(t *testing.T) { t.Run("test 'r' on second line", func(t *testing.T) { lines := []string{"first", "second"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "first" { t.Errorf("Line(0) = %q, want 'first'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "xecond" { t.Errorf("Line(1) = %q, want 'xecond'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test 'r' does not cross line boundaries", func(t *testing.T) { lines := []string{"ab", "cd"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "5", "r", "x") m := getFinalModel(t, tm) // Should only replace 'b', not cross to next line if m.ActiveBuffer().Lines[0].String() != "ax" { t.Errorf("Line(0) = %q, want 'ax'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "cd" { t.Errorf("Line(1) = %q, want 'cd'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test 'r' on last line", func(t *testing.T) { lines := []string{"first", "second", "third"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 2}) sendKeys(tm, "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[2].String() != "xhird" { t.Errorf("Line(2) = %q, want 'xhird'", m.ActiveBuffer().Lines[2].String()) } }) } func TestReplaceCharCombinations(t *testing.T) { t.Run("test 'frx' finds then replaces", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "f", "w", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello xorld" { t.Errorf("lines[0] = %q, want 'hello xorld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '2frx' finds second occurrence then replaces", func(t *testing.T) { lines := []string{"hello hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "2", "f", "l", "r", "x") m := getFinalModel(t, tm) // Find second 'l', which is at position 3 if m.ActiveBuffer().Lines[0].String() != "helxo hello" { t.Errorf("lines[0] = %q, want 'helxo hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test '^rx' moves to first non-blank then replaces", func(t *testing.T) { lines := []string{" hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "^", "r", "x") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != " xello" { t.Errorf("lines[0] = %q, want ' xello'", m.ActiveBuffer().Lines[0].String()) } }) } // ================================================== // Replace Mode (R) Tests // ================================================== func TestReplaceModeEntry(t *testing.T) { t.Run("test 'R' enters replace mode", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") m := getFinalModel(t, tm) if m.Mode() != core.ReplaceMode { t.Errorf("Mode() = %v, want ReplaceMode", m.Mode()) } }) t.Run("test 'esc' exits replace mode to normal", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "x", "esc") m := getFinalModel(t, tm) if m.Mode() != core.NormalMode { t.Errorf("Mode() = %v, want NormalMode", m.Mode()) } }) } func TestReplaceModeBasic(t *testing.T) { t.Run("test single character overwrites", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "Xello" { t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test multiple characters overwrite", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abclo" { t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test typing past end of line appends", func(t *testing.T) { lines := []string{"hi"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "R") sendKeyString(tm, "there") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hithere" { t.Errorf("lines[0] = %q, want 'hithere'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode starting at end of line", func(t *testing.T) { lines := []string{"hi"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "R") sendKeyString(tm, "!!") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hi!!" { t.Errorf("lines[0] = %q, want 'hi!!'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode from middle of line", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "R") sendKeyString(tm, "XX") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "heXXo" { t.Errorf("lines[0] = %q, want 'heXXo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test cursor position after exiting replace mode", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc") m := getFinalModel(t, tm) // Cursor should be at last replaced character if m.ActiveWindow().Cursor.Col != 2 { t.Errorf("CursorX() = %d, want 2", m.ActiveWindow().Cursor.Col) } }) } func TestReplaceModeBackspace(t *testing.T) { t.Run("test backspace deletes character", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "backspace", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "ablo" { t.Errorf("lines[0] = %q, want 'ablo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test backspace at start does nothing", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "backspace", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test multiple backspaces", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "backspace", "backspace", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "alo" { t.Errorf("lines[0] = %q, want 'alo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test backspace then type more", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "backspace") sendKeyString(tm, "XY") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abXY" { t.Errorf("lines[0] = %q, want 'abXY'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceModeNavigation(t *testing.T) { t.Run("test right arrow moves cursor", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "right", "right", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "heXlo" { t.Errorf("lines[0] = %q, want 'heXlo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test left arrow moves cursor", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 2, Line: 0}) sendKeys(tm, "R", "left", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hXllo" { t.Errorf("lines[0] = %q, want 'hXllo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test up/down arrows navigate between lines", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "X", "down", "Y", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "Xello" { t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "wYrld" { t.Errorf("lines[1] = %q, want 'wYrld'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test up arrow from first line stays on first line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "up", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "Xello" { t.Errorf("lines[0] = %q, want 'Xello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test down arrow from last line stays on last line", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1}) sendKeys(tm, "R", "down", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[1].String() != "Xorld" { t.Errorf("lines[1] = %q, want 'Xorld'", m.ActiveBuffer().Lines[1].String()) } }) } func TestReplaceModeSpecialKeys(t *testing.T) { t.Run("test enter splits line", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 5, Line: 0}) sendKeys(tm, "R", "enter", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "world" { t.Errorf("lines[1] = %q, want 'world'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test tab inserts tab character", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "tab", "esc") m := getFinalModel(t, tm) // Tab is expanded to spaces based on tabstop setting (default 2) if m.ActiveBuffer().Lines[0].String() != " ello" { t.Errorf("lines[0] = %q, want ' ello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test ctrl+w deletes previous word", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "foo bar") sendKeys(tm, "ctrl+w", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "foo orld" { t.Errorf("lines[0] = %q, want 'foo orld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test ctrl+w at beginning does nothing", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "ctrl+w", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test delete key removes character ahead", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "delete", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "ello" { t.Errorf("lines[0] = %q, want 'ello'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceModeUndo(t *testing.T) { t.Run("test replace mode changes can be undone", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc", "u") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode changes can be redone", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc", "u", "ctrl+r") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abclo" { t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test multiple undo operations", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc") sendKeys(tm, "l", "R") sendKeyString(tm, "XY") sendKeys(tm, "esc", "u") m := getFinalModel(t, tm) // Only the second replace should be undone if m.ActiveBuffer().Lines[0].String() != "abclo" { t.Errorf("lines[0] = %q, want 'abclo'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceModeEdgeCases(t *testing.T) { t.Run("test replace mode on empty line", func(t *testing.T) { lines := []string{""} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "test") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "test" { t.Errorf("lines[0] = %q, want 'test'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode on different lines", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1}) sendKeys(tm, "R", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("lines[0] = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "Xorld" { t.Errorf("lines[1] = %q, want 'Xorld'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test overwriting tab character", func(t *testing.T) { lines := []string{"a\tb"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 1, Line: 0}) sendKeys(tm, "R", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "aXb" { t.Errorf("lines[0] = %q, want 'aXb'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode with whitespace characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "a b") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "a blo" { t.Errorf("lines[0] = %q, want 'a blo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode on single character line", func(t *testing.T) { lines := []string{"a"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "X", "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "X" { t.Errorf("lines[0] = %q, want 'X'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test replace mode preserves other lines", func(t *testing.T) { lines := []string{"first", "second", "third"} tm := newTestModelWithLinesAndCursorPos(t, lines, core.Position{Col: 0, Line: 1}) sendKeys(tm, "R") sendKeyString(tm, "XXX") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "first" { t.Errorf("lines[0] = %q, want 'first'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "XXXond" { t.Errorf("lines[1] = %q, want 'XXXond'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "third" { t.Errorf("lines[2] = %q, want 'third'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("test replace mode with special characters", func(t *testing.T) { lines := []string{"hello"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "@#$") sendKeys(tm, "esc") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "@#$lo" { t.Errorf("lines[0] = %q, want '@#$lo'", m.ActiveBuffer().Lines[0].String()) } }) } func TestReplaceModeRepeat(t *testing.T) { t.Run("test replace mode can be repeated with '.'", func(t *testing.T) { lines := []string{"hello", "world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "XX") sendKeys(tm, "esc", "j", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "XXllo" { t.Errorf("lines[0] = %q, want 'XXllo'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "wXXld" { t.Errorf("lines[1] = %q, want 'wXXld'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("test dot repeats last replace operation", func(t *testing.T) { lines := []string{"hello world"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R") sendKeyString(tm, "abc") sendKeys(tm, "esc", "w", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abclo abcld" { t.Errorf("lines[0] = %q, want 'abclo abcld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("test dot after navigation", func(t *testing.T) { lines := []string{"aaa", "bbb", "ccc"} tm := newTestModelWithLines(t, lines) sendKeys(tm, "R", "X", "esc", "j", "j", ".") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "Xaa" { t.Errorf("lines[0] = %q, want 'Xaa'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[2].String() != "Xcc" { t.Errorf("lines[2] = %q, want 'Xcc'", m.ActiveBuffer().Lines[2].String()) } }) }