Gim/internal/editor/integration_operator_change_test.go
Hayden Hargreaves e362c9f118
All checks were successful
Run Test Suite / test (push) Successful in 56s
feat: gap buffer is implemented, tested
Not sure if this is perfect, but it seems to be working
2026-04-02 12:39:30 -07:00

1015 lines
31 KiB
Go

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