package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/core" ) // ============================================================================= // cc (Change Line) Tests // ============================================================================= func TestChangeLine(t *testing.T) { t.Run("cc changes first line and enters insert mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } // First line should be empty (ready for insert) if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "world" { t.Errorf("Line(1) = %q, want 'world'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("cc changes middle line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "line one" { t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line three" { t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("cc changes last line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hello" { t.Errorf("Line(0) = %q, want 'hello'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } }) t.Run("cc puts deleted line in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world", "second line"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 || reg.Content[0] != "hello world" { t.Errorf("register content = %q, want 'hello world'", reg.Content) } if reg.Type != core.LinewiseRegister { t.Errorf("register type = %v, want LinewiseRegister", reg.Type) } }) t.Run("cc on single line file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"only line"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cc cursor at column 0", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(core.Position{Line: 0, Col: 3}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) // Cursor should be at column 0 on the empty line if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) } if m.ActiveWindow().Cursor.Line != 0 { t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line) } }) } func TestChangeLineWithCount(t *testing.T) { t.Run("2cc changes two lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three", "line four"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "2", "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have 3 lines: empty + line three + line four if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line three" { t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("3cc changes three lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one", "two", "three", "four", "five"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "3", "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have 3 lines: one + empty + five if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "one" { t.Errorf("Line(0) = %q, want 'one'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "five" { t.Errorf("Line(2) = %q, want 'five'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("5cc with only 3 lines remaining changes all remaining", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one", "two", "three"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "5", "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("2cc puts both lines in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"first", "second", "third"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "2", "c", "c") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 2 { t.Errorf("register content length = %d, want 2", len(reg.Content)) } }) } // ============================================================================= // c with Horizontal Motions Tests // ============================================================================= func TestChangeWithHorizontalMotion(t *testing.T) { t.Run("cl changes single character", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "l") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "ello world" { t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c2l changes two characters", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "2", "l") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "llo world" { t.Errorf("Line(0) = %q, want 'llo world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("ch changes character to the left", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 5}), ) sendKeys(tm, "c", "h") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hell world" { t.Errorf("Line(0) = %q, want 'hell world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c$ changes to end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "c", "$") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hello " { t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c0 changes to start of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "c", "0") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "world" { t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c^ changes to first non-blank", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{" hello world"}), WithCursorPos(core.Position{Line: 0, Col: 8}), ) sendKeys(tm, "c", "^") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // ^ is exclusive motion, so position 8 (space) is not included // Delete positions 3-7 ("hello"), leaving " " + " world" = " world" if m.ActiveBuffer().Lines[0].String() != " world" { t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String()) } }) } // ============================================================================= // c with Word Motions Tests // ============================================================================= func TestChangeWithWordMotion(t *testing.T) { t.Run("cw changes word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "w") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "world" { t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cw from middle of word changes to next word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 2}), ) sendKeys(tm, "c", "w") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "heworld" { t.Errorf("Line(0) = %q, want 'heworld'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("ce changes to end of word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "e") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != " world" { t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cb changes backward word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "c", "b") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "world" { t.Errorf("Line(0) = %q, want 'world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c2w changes two words", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one two three four"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "2", "w") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "three four" { t.Errorf("Line(0) = %q, want 'three four'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cW changes WORD", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello.world next"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "W") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "next" { t.Errorf("Line(0) = %q, want 'next'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cE changes to end of WORD", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello.world next"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "E") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != " next" { t.Errorf("Line(0) = %q, want ' next'", m.ActiveBuffer().Lines[0].String()) } }) } // ============================================================================= // c with Vertical Motions Tests // ============================================================================= func TestChangeWithVerticalMotion(t *testing.T) { t.Run("cj changes current and next line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "j") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have empty line + line three if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line three" { t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("ck changes current and previous line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "c", "k") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have empty line + line three if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line three" { t.Errorf("Line(1) = %q, want 'line three'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("c2j changes three lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one", "two", "three", "four", "five"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "2", "j") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have empty + four + five if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "four" { t.Errorf("Line(1) = %q, want 'four'", m.ActiveBuffer().Lines[1].String()) } }) } // ============================================================================= // c with Jump Motions Tests // ============================================================================= func TestChangeWithJumpMotion(t *testing.T) { t.Run("cG changes from cursor to end of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "G") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // All lines should be replaced with one empty line if m.ActiveBuffer().LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cgg changes from cursor to start of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) sendKeys(tm, "c", "g", "g") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // All lines should be replaced with one empty line if m.ActiveBuffer().LineCount() != 1 { t.Errorf("LineCount() = %d, want 1", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cG from middle changes to end", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three", "line four"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "c", "G") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have line one + empty if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "line one" { t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } }) } // ============================================================================= // C (Change to end of line) Tests // ============================================================================= func TestChangeToEndOfLine(t *testing.T) { t.Run("C changes from cursor to end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "C") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hello " { t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("C at start of line clears entire line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "C") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("C at end of line enters insert mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 4}), ) sendKeys(tm, "C") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should delete last char if m.ActiveBuffer().Lines[0].String() != "hell" { t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("C puts deleted text in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "C") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 || reg.Content[0] != "world" { t.Errorf("register content = %q, want 'world'", reg.Content) } }) } // ============================================================================= // s (Substitute character) Tests // ============================================================================= func TestSubstituteCharacter(t *testing.T) { t.Run("s deletes character and enters insert mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "s") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "ello" { t.Errorf("Line(0) = %q, want 'ello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("2s deletes two characters", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "2", "s") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "llo" { t.Errorf("Line(0) = %q, want 'llo'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("s at end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 4}), ) sendKeys(tm, "s") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hell" { t.Errorf("Line(0) = %q, want 'hell'", m.ActiveBuffer().Lines[0].String()) } }) } // ============================================================================= // S (Substitute line) Tests // ============================================================================= func TestSubstituteLine(t *testing.T) { t.Run("S clears line and enters insert mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 5}), ) sendKeys(tm, "S") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("S preserves other lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "S") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "line one" { t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line three" { t.Errorf("Line(2) = %q, want 'line three'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("2S substitutes two lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one", "two", "three", "four"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "2", "S") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have empty + three + four if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "three" { t.Errorf("Line(1) = %q, want 'three'", m.ActiveBuffer().Lines[1].String()) } }) } // ============================================================================= // Visual Mode Change Tests // ============================================================================= func TestVisualModeChange(t *testing.T) { t.Run("vc changes single character", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "ello world" { t.Errorf("Line(0) = %q, want 'ello world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("v with motion then c changes selection", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "e", "c") // select "hello" m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != " world" { t.Errorf("Line(0) = %q, want ' world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("v$c changes to end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "v", "$", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "hello " { t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("visual selection spanning lines then c", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 0, Col: 5}), ) sendKeys(tm, "v", "j", "l", "l", "l", "l", "c") // select across lines m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should merge lines with selection removed if m.ActiveBuffer().LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount()) } }) t.Run("visual change puts deleted text in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "l", "l", "l", "l", "c") // select "hello" m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 || reg.Content[0] != "hello" { t.Errorf("register content = %q, want 'hello'", reg.Content) } }) } func TestVisualLineModeChange(t *testing.T) { t.Run("Vc changes line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "V", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } }) t.Run("Vjc changes multiple lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three", "line four"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "V", "j", "c") // select lines two and three m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // Should have: line one, empty, line four if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "line one" { t.Errorf("Line(0) = %q, want 'line one'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "" { t.Errorf("Line(1) = %q, want ''", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line four" { t.Errorf("Line(2) = %q, want 'line four'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("visual line change puts lines in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line one", "line two", "line three"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "V", "j", "c") // select first two lines m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.LinewiseRegister { t.Errorf("register type = %v, want LinewiseRegister", reg.Type) } if len(reg.Content) != 2 { t.Errorf("register content length = %d, want 2", len(reg.Content)) } }) } // ============================================================================= // Edge Cases // ============================================================================= func TestChangeEdgeCases(t *testing.T) { t.Run("cc on empty line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"", "world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "c") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cw on last word of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), ) sendKeys(tm, "c", "w") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } // cw on last word should change to end of line if m.ActiveBuffer().Lines[0].String() != "hello " { t.Errorf("Line(0) = %q, want 'hello '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("c$ on empty line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{""}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "$") m := getFinalModel(t, tm) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } if m.ActiveBuffer().Lines[0].String() != "" { t.Errorf("Line(0) = %q, want ''", m.ActiveBuffer().Lines[0].String()) } }) t.Run("cj at last line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "c", "j") m := getFinalModel(t, tm) // cj at last line should just change current line (can't go down) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } }) t.Run("ck at first line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "c", "k") m := getFinalModel(t, tm) // ck at first line should just change current line (can't go up) if m.Mode() != core.InsertMode { t.Errorf("Mode() = %v, want InsertMode", m.Mode()) } }) }