package editor import ( "strings" "testing" "git.gophernest.net/azpect/TextEditor/internal/core" ) // ============================================================================= // yy (Yank Line / DoublePress) Tests // ============================================================================= func TestYankLineBasic(t *testing.T) { t.Run("yy yanks current line to register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") 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) != 1 { t.Errorf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } }) t.Run("yy does not modify buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line 2" { t.Errorf("Line(1) = %q, want 'line 2'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line 3" { t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("yy does not move cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 3}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) if m.ActiveWindow().Cursor.Line != 1 { t.Errorf("CursorY() = %d, want 1", m.ActiveWindow().Cursor.Line) } if m.ActiveWindow().Cursor.Col != 3 { t.Errorf("CursorX() = %d, want 3", m.ActiveWindow().Cursor.Col) } }) t.Run("yy from middle of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"first", "second", "third", "fourth"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if reg.Content[0] != "third" { t.Errorf("register content[0] = %q, want 'third'", reg.Content[0]) } }) t.Run("yy at last line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "last line"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if reg.Content[0] != "last line" { t.Errorf("register content[0] = %q, want 'last line'", reg.Content[0]) } }) } func TestYankLineWithCount(t *testing.T) { t.Run("2yy yanks two lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3", "line 4"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "2", "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if len(reg.Content) != 2 { t.Errorf("register content length = %d, want 2", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } if reg.Content[1] != "line 2" { t.Errorf("register content[1] = %q, want 'line 2'", reg.Content[1]) } }) t.Run("3yy yanks three lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "3", "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if len(reg.Content) != 3 { t.Errorf("register content length = %d, want 3", len(reg.Content)) } if reg.Content[0] != "b" { t.Errorf("register content[0] = %q, want 'b'", reg.Content[0]) } if reg.Content[1] != "c" { t.Errorf("register content[1] = %q, want 'c'", reg.Content[1]) } if reg.Content[2] != "d" { t.Errorf("register content[2] = %q, want 'd'", reg.Content[2]) } }) t.Run("yy with count overflow clamps to available lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "1", "0", "y", "y") // 10yy but only 2 lines available m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if len(reg.Content) != 2 { t.Errorf("register content length = %d, want 2 (clamped)", len(reg.Content)) } if reg.Content[0] != "line 2" { t.Errorf("register content[0] = %q, want 'line 2'", reg.Content[0]) } if reg.Content[1] != "line 3" { t.Errorf("register content[1] = %q, want 'line 3'", reg.Content[1]) } }) t.Run("yy with count does not modify buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "3", "y", "y") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } }) } func TestYankLineEdgeCases(t *testing.T) { t.Run("yy on empty line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if len(reg.Content) != 1 { t.Errorf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "" { t.Errorf("register content[0] = %q, want ''", reg.Content[0]) } }) t.Run("yy on single line buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"only line"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if reg.Content[0] != "only line" { t.Errorf("register content[0] = %q, want 'only line'", reg.Content[0]) } }) t.Run("yy preserves whitespace", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{" indented", "\ttabbed", " spaces "}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "3", "y", "y") m := getFinalModel(t, tm) reg, _ := m.GetRegister('"') if reg.Content[0] != " indented" { t.Errorf("register content[0] = %q, want ' indented'", reg.Content[0]) } if reg.Content[1] != "\ttabbed" { t.Errorf("register content[1] = %q, want '\\ttabbed'", reg.Content[1]) } if reg.Content[2] != " spaces " { t.Errorf("register content[2] = %q, want ' spaces '", reg.Content[2]) } }) } // ============================================================================= // Yank with Linewise Motions (yj, yk, yG, ygg) - TDD Tests // ============================================================================= func TestYankWithLinewiseMotions(t *testing.T) { t.Run("yj yanks current line and line below", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "j") 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.Fatalf("register content length = %d, want 2", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } if reg.Content[1] != "line 2" { t.Errorf("register content[1] = %q, want 'line 2'", reg.Content[1]) } }) t.Run("yk yanks current line and line above", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "k") 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.Fatalf("register content length = %d, want 2", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } if reg.Content[1] != "line 2" { t.Errorf("register content[1] = %q, want 'line 2'", reg.Content[1]) } }) t.Run("yG yanks from cursor to end of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3", "line 4"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "G") 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) != 3 { t.Fatalf("register content length = %d, want 3", len(reg.Content)) } if reg.Content[0] != "line 2" { t.Errorf("register content[0] = %q, want 'line 2'", reg.Content[0]) } if reg.Content[2] != "line 4" { t.Errorf("register content[2] = %q, want 'line 4'", reg.Content[2]) } }) t.Run("ygg yanks from cursor to beginning of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3", "line 4"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) sendKeys(tm, "y", "g", "g") 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) != 3 { t.Fatalf("register content length = %d, want 3", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } if reg.Content[2] != "line 3" { t.Errorf("register content[2] = %q, want 'line 3'", reg.Content[2]) } }) t.Run("y2j yanks current and next two lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"a", "b", "c", "d", "e"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "2", "j") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 3 { t.Fatalf("register content length = %d, want 3", len(reg.Content)) } if reg.Content[0] != "b" { t.Errorf("register content[0] = %q, want 'b'", reg.Content[0]) } if reg.Content[1] != "c" { t.Errorf("register content[1] = %q, want 'c'", reg.Content[1]) } if reg.Content[2] != "d" { t.Errorf("register content[2] = %q, want 'd'", reg.Content[2]) } }) t.Run("yj does not move cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 3}), ) sendKeys(tm, "y", "j") m := getFinalModel(t, tm) if m.ActiveWindow().Cursor.Line != 0 { t.Errorf("CursorY() = %d, want 0", m.ActiveWindow().Cursor.Line) } }) t.Run("yG does not modify buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "G") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } }) } // ============================================================================= // Yank with Charwise Motions (yw, ye, yb, y$, y0) - TDD Tests // ============================================================================= func TestYankWithCharwiseMotions(t *testing.T) { t.Run("yw yanks word under cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "w") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } // yw includes trailing space if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello " { t.Errorf("register content = %q, want 'hello '", reg.Content[0]) } }) t.Run("ye yanks to end of word (exclusive)", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "e") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } // ye is inclusive of last char if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello" { t.Errorf("register content = %q, want 'hello'", reg.Content[0]) } }) t.Run("yb yanks backward word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w' ) sendKeys(tm, "y", "b") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } // yb from 'w' back to start of 'hello' if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello " { t.Errorf("register content = %q, want 'hello '", reg.Content[0]) } }) t.Run("y$ yanks to end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w' ) sendKeys(tm, "y", "$") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "world" { t.Errorf("register content = %q, want 'world'", reg.Content[0]) } }) t.Run("y0 yanks to beginning of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w' ) sendKeys(tm, "y", "0") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello " { t.Errorf("register content = %q, want 'hello '", reg.Content[0]) } }) t.Run("y_ yanks to first non-whitespace", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{" hello world"}), WithCursorPos(core.Position{Line: 0, Col: 10}), // on 'w' ) sendKeys(tm, "y", "_") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } // From 'w' back to 'h' (first non-ws) if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello " { t.Errorf("register content = %q, want 'hello '", reg.Content[0]) } }) t.Run("y2w yanks two words", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"one two three four"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "2", "w") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "one two " { t.Errorf("register content = %q, want 'one two '", reg.Content[0]) } }) t.Run("yw does not move cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "w") m := getFinalModel(t, tm) if m.ActiveWindow().Cursor.Col != 0 { t.Errorf("CursorX() = %d, want 0", m.ActiveWindow().Cursor.Col) } }) t.Run("yw does not modify buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "w") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello world" { t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String()) } }) } // ============================================================================= // Visual Mode Yank Tests - TDD Tests // ============================================================================= func TestYankVisualCharwise(t *testing.T) { t.Run("v selection then y yanks selected text", 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", "y") // select "hello" m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello" { t.Errorf("register content = %q, want 'hello'", reg.Content[0]) } }) t.Run("v selection across lines yanks with newlines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(core.Position{Line: 0, Col: 3}), ) sendKeys(tm, "v", "j", "l", "l", "y") // select "e 1\nlin" m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.CharwiseRegister { t.Errorf("register type = %v, want CharwiseRegister", reg.Type) } // Multi-line charwise yank if len(reg.Content) < 1 { t.Fatal("register content empty, expected multi-line selection") } }) t.Run("visual yank exits visual mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "l", "l", "y") m := getFinalModel(t, tm) if m.Mode() != core.NormalMode { t.Errorf("Mode() = %v, want NormalMode", m.Mode()) } }) t.Run("visual yank does not modify buffer", 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", "y") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello world" { t.Errorf("Line(0) = %q, want 'hello world'", m.ActiveBuffer().Lines[0].String()) } }) } func TestYankVisualLinewise(t *testing.T) { t.Run("V selection then y yanks entire lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "V", "j", "y") // select lines 1 and 2 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.Fatalf("register content length = %d, want 2", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } if reg.Content[1] != "line 2" { t.Errorf("register content[1] = %q, want 'line 2'", reg.Content[1]) } }) t.Run("V on single line then y yanks that line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "V", "y") 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) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "line 1" { t.Errorf("register content[0] = %q, want 'line 1'", reg.Content[0]) } }) t.Run("V selection upward yanks in correct order", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) sendKeys(tm, "V", "k", "y") // select from line 3 upward to line 2 m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 2 { t.Fatalf("register content length = %d, want 2", len(reg.Content)) } // Order should be top-to-bottom regardless of selection direction if reg.Content[0] != "line 2" { t.Errorf("register content[0] = %q, want 'line 2'", reg.Content[0]) } if reg.Content[1] != "line 3" { t.Errorf("register content[1] = %q, want 'line 3'", reg.Content[1]) } }) t.Run("visual line yank exits visual mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "V", "y") m := getFinalModel(t, tm) if m.Mode() != core.NormalMode { t.Errorf("Mode() = %v, want NormalMode", m.Mode()) } }) } func TestYankVisualBlock(t *testing.T) { t.Run("ctrl+v selection then y yanks block", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"abcdef", "ghijkl", "mnopqr"}), WithCursorPos(core.Position{Line: 0, Col: 1}), ) sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "y") // select 3x3 block starting at col 1 m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if reg.Type != core.BlockwiseRegister { t.Errorf("register type = %v, want BlockwiseRegister", reg.Type) } // Block should contain "bcd", "hij", "nop" if len(reg.Content) != 3 { t.Fatalf("register content length = %d, want 3", len(reg.Content)) } if reg.Content[0] != "bcd" { t.Errorf("register content[0] = %q, want 'bcd'", reg.Content[0]) } if reg.Content[1] != "hij" { t.Errorf("register content[1] = %q, want 'hij'", reg.Content[1]) } if reg.Content[2] != "nop" { t.Errorf("register content[2] = %q, want 'nop'", reg.Content[2]) } }) t.Run("visual block yank exits visual mode", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"abcd", "efgh"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "ctrl+v", "j", "l", "y") m := getFinalModel(t, tm) if m.Mode() != core.NormalMode { t.Errorf("Mode() = %v, want NormalMode", m.Mode()) } }) t.Run("visual block yank with uneven line lengths", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"abcdefgh", "ij", "klmnop"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "ctrl+v", "j", "j", "l", "l", "l", "y") // 4-wide block m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // Short line should be padded or truncated based on implementation if len(reg.Content) != 3 { t.Fatalf("register content length = %d, want 3", len(reg.Content)) } }) } // ============================================================================= // Register Behavior Tests // ============================================================================= func TestYankRegisterBehavior(t *testing.T) { t.Run("yy updates register 0 and unnamed register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) // Check unnamed register unnamed, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(unnamed.Content) == 0 { t.Fatal("unnamed register is empty") } if unnamed.Content[0] != "line 1" { t.Errorf("unnamed register = %q, want 'line 1'", unnamed.Content[0]) } // Check register 0 reg0, ok := m.GetRegister('0') if !ok { t.Fatal("register 0 not found") } if len(reg0.Content) == 0 { t.Fatal("register 0 is empty") } if reg0.Content[0] != "line 1" { t.Errorf("register 0 = %q, want 'line 1'", reg0.Content[0]) } }) t.Run("multiple yanks shift numbered registers", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"first", "second", "third"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") // yank "first" sendKeys(tm, "j") sendKeys(tm, "y", "y") // yank "second" m := getFinalModel(t, tm) // Most recent yank should be in 0 and unnamed reg0, ok := m.GetRegister('0') if !ok { t.Fatal("register 0 not found") } if len(reg0.Content) == 0 { t.Fatal("register 0 is empty") } if reg0.Content[0] != "second" { t.Errorf("register 0 = %q, want 'second'", reg0.Content[0]) } // Previous yank should shift to register 1 reg1, ok := m.GetRegister('1') if !ok { t.Fatal("register 1 not found") } if len(reg1.Content) == 0 { t.Fatal("register 1 is empty") } if reg1.Content[0] != "first" { t.Errorf("register 1 = %q, want 'first'", reg1.Content[0]) } }) t.Run("yank then paste uses correct content", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original", "to copy"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "y") // yank "to copy" sendKeys(tm, "k") // move up sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[1].String() != "to copy" { t.Errorf("Line(1) = %q, want 'to copy'", m.ActiveBuffer().Lines[1].String()) } }) } // ============================================================================= // Edge Cases and Special Scenarios // ============================================================================= func TestYankEdgeCases(t *testing.T) { t.Run("yy on whitespace-only line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", " ", "line 3"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) == 0 { t.Fatal("register is empty") } if reg.Content[0] != " " { t.Errorf("register content = %q, want ' '", reg.Content[0]) } }) t.Run("yw at end of line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o' ) sendKeys(tm, "y", "w") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // At end of line, yw should yank just the last character if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "o" { t.Errorf("register content = %q, want 'o'", reg.Content[0]) } }) t.Run("y$ at beginning of line yanks entire line content", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "$") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "hello world" { t.Errorf("register content = %q, want 'hello world'", reg.Content[0]) } }) t.Run("y0 at beginning of line yanks nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "0") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // At col 0, y0 should yank empty string if len(reg.Content) != 1 { t.Fatalf("register content length = %d, want 1", len(reg.Content)) } if reg.Content[0] != "" { t.Errorf("register content = %q, want ''", reg.Content[0]) } }) t.Run("yy on very long line", func(t *testing.T) { longLine := strings.Repeat("a", 1000) tm := newTestModel(t, WithLines([]string{longLine}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) == 0 { t.Fatal("register is empty") } if len(reg.Content[0]) != 1000 { t.Errorf("register content length = %d, want 1000", len(reg.Content[0])) } }) t.Run("yy with special characters", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello\tworld", "foo\nbar"}), // tab and embedded newline WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } if len(reg.Content) == 0 { t.Fatal("register is empty") } if reg.Content[0] != "hello\tworld" { t.Errorf("register content = %q, want 'hello\\tworld'", reg.Content[0]) } }) } // ============================================================================= // Visual Yank → Paste Round-Trip Tests // ============================================================================= func TestVisualYankPasteRoundTrip(t *testing.T) { t.Run("visual charwise yank then paste single line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // Select "hello", yank it, move to end, paste sendKeys(tm, "v", "l", "l", "l", "l", "y", "$", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello worldhello" { t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("visual charwise yank then paste before", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 6}), // on 'w' ) // Select "world", yank it, go to start, paste before sendKeys(tm, "v", "$", "y", "0", "P") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "worldhello world" { t.Errorf("Line(0) = %q, want 'worldhello world'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("visual line yank then paste", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // V yank line 1, go to line 2, paste sendKeys(tm, "V", "y", "j", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[2].String() != "line 1" { t.Errorf("Line(2) = %q, want 'line 1'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("visual line yank multiple lines then paste", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3", "line 4"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // V select lines 1-2, yank, go to end, paste sendKeys(tm, "V", "j", "y", "G", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 6 { t.Errorf("LineCount() = %d, want 6", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[4].String() != "line 1" { t.Errorf("Line(4) = %q, want 'line 1'", m.ActiveBuffer().Lines[4].String()) } if m.ActiveBuffer().Lines[5].String() != "line 2" { t.Errorf("Line(5) = %q, want 'line 2'", m.ActiveBuffer().Lines[5].String()) } }) t.Run("visual line yank then paste before", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 2, Col: 0}), ) // V yank line 3, go to line 1, paste before sendKeys(tm, "V", "y", "g", "g", "P") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "line 3" { t.Errorf("Line(0) = %q, want 'line 3'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line 1" { t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String()) } }) t.Run("yy then p duplicates line below", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original", "other"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "y", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "original" { t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "original" { t.Errorf("Line(1) = %q, want 'original'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "other" { t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("yy then P duplicates line above", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original", "other"}), WithCursorPos(core.Position{Line: 1, Col: 0}), ) sendKeys(tm, "y", "y", "P") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "original" { t.Errorf("Line(0) = %q, want 'original'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "other" { t.Errorf("Line(1) = %q, want 'other'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "other" { t.Errorf("Line(2) = %q, want 'other'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("yw then p pastes word after cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // yw yanks "hello ", move to end of world, paste sendKeys(tm, "y", "w", "$", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello worldhello " { t.Errorf("Line(0) = %q, want 'hello worldhello '", m.ActiveBuffer().Lines[0].String()) } }) t.Run("ye then p pastes word after cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // ye yanks "hello" (inclusive), move to end of line, paste sendKeys(tm, "y", "e", "$", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "hello worldhello" { t.Errorf("Line(0) = %q, want 'hello worldhello'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("visual select partial word yank then paste", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"abcdefgh"}), WithCursorPos(core.Position{Line: 0, Col: 2}), // on 'c' ) // Select "cde", yank, go to end, paste sendKeys(tm, "v", "l", "l", "y", "$", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0].String() != "abcdefghcde" { t.Errorf("Line(0) = %q, want 'abcdefghcde'", m.ActiveBuffer().Lines[0].String()) } }) t.Run("visual yank empty selection does nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(core.Position{Line: 0, Col: 2}), ) // Enter visual mode then immediately yank (single char) sendKeys(tm, "v", "y") m := getFinalModel(t, tm) reg, ok := m.GetRegister('"') if !ok { t.Fatal("unnamed register not found") } // Should have yanked single char 'l' if len(reg.Content) != 1 || reg.Content[0] != "l" { t.Errorf("register content = %q, want 'l'", reg.Content) } }) t.Run("dd then p moves deleted line down", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // dd deletes line 1, p pastes it below cursor (now on line 2) sendKeys(tm, "d", "d", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount()) } if m.ActiveBuffer().Lines[0].String() != "line 2" { t.Errorf("Line(0) = %q, want 'line 2'", m.ActiveBuffer().Lines[0].String()) } if m.ActiveBuffer().Lines[1].String() != "line 1" { t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line 3" { t.Errorf("Line(2) = %q, want 'line 3'", m.ActiveBuffer().Lines[2].String()) } }) t.Run("2yy then 2p pastes twice", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) // 2yy yanks lines 1-2, 2p pastes them twice after current line sendKeys(tm, "2", "y", "y", "2", "p") m := getFinalModel(t, tm) if m.ActiveBuffer().LineCount() != 7 { t.Errorf("LineCount() = %d, want 7", m.ActiveBuffer().LineCount()) } // Original + 2 copies of 2 lines = 3 + 4 = 7 if m.ActiveBuffer().Lines[1].String() != "line 1" { t.Errorf("Line(1) = %q, want 'line 1'", m.ActiveBuffer().Lines[1].String()) } if m.ActiveBuffer().Lines[2].String() != "line 2" { t.Errorf("Line(2) = %q, want 'line 2'", m.ActiveBuffer().Lines[2].String()) } if m.ActiveBuffer().Lines[3].String() != "line 1" { t.Errorf("Line(3) = %q, want 'line 1'", m.ActiveBuffer().Lines[3].String()) } if m.ActiveBuffer().Lines[4].String() != "line 2" { t.Errorf("Line(4) = %q, want 'line 2'", m.ActiveBuffer().Lines[4].String()) } }) }