1041 lines
29 KiB
Go
1041 lines
29 KiB
Go
package editor
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
)
|
|
|
|
// =============================================================================
|
|
// 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(action.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 != action.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(action.Position{Line: 1, Col: 0}),
|
|
)
|
|
sendKeys(tm, "y", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.LineCount() != 3 {
|
|
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
|
}
|
|
if m.Line(0) != "line 1" {
|
|
t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0))
|
|
}
|
|
if m.Line(1) != "line 2" {
|
|
t.Errorf("Line(1) = %q, want 'line 2'", m.Line(1))
|
|
}
|
|
if m.Line(2) != "line 3" {
|
|
t.Errorf("Line(2) = %q, want 'line 3'", m.Line(2))
|
|
}
|
|
})
|
|
|
|
t.Run("yy does not move cursor", func(t *testing.T) {
|
|
tm := newTestModel(t,
|
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
|
WithCursorPos(action.Position{Line: 1, Col: 3}),
|
|
)
|
|
sendKeys(tm, "y", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.CursorY() != 1 {
|
|
t.Errorf("CursorY() = %d, want 1", m.CursorY())
|
|
}
|
|
if m.CursorX() != 3 {
|
|
t.Errorf("CursorX() = %d, want 3", m.CursorX())
|
|
}
|
|
})
|
|
|
|
t.Run("yy from middle of file", func(t *testing.T) {
|
|
tm := newTestModel(t,
|
|
WithLines([]string{"first", "second", "third", "fourth"}),
|
|
WithCursorPos(action.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(action.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(action.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(action.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(action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "3", "y", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.LineCount() != 3 {
|
|
t.Errorf("LineCount() = %d, want 3", m.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(action.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(action.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(action.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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(action.Position{Line: 0, Col: 3}),
|
|
)
|
|
sendKeys(tm, "y", "j")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.CursorY() != 0 {
|
|
t.Errorf("CursorY() = %d, want 0", m.CursorY())
|
|
}
|
|
})
|
|
|
|
t.Run("yG does not modify buffer", func(t *testing.T) {
|
|
tm := newTestModel(t,
|
|
WithLines([]string{"line 1", "line 2", "line 3"}),
|
|
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "y", "G")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.LineCount() != 3 {
|
|
t.Errorf("LineCount() = %d, want 3", m.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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 != action.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(action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "y", "w")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.CursorX() != 0 {
|
|
t.Errorf("CursorX() = %d, want 0", m.CursorX())
|
|
}
|
|
})
|
|
|
|
t.Run("yw does not modify buffer", func(t *testing.T) {
|
|
tm := newTestModel(t,
|
|
WithLines([]string{"hello world"}),
|
|
WithCursorPos(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "y", "w")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Line(0) != "hello world" {
|
|
t.Errorf("Line(0) = %q, want 'hello world'", m.Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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(action.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 != action.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(action.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 != action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "v", "l", "l", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "v", "l", "l", "l", "l", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Line(0) != "hello world" {
|
|
t.Errorf("Line(0) = %q, want 'hello world'", m.Line(0))
|
|
}
|
|
})
|
|
}
|
|
|
|
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(action.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 != action.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(action.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 != action.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(action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "V", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != action.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(action.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 != action.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(action.Position{Line: 0, Col: 0}),
|
|
)
|
|
sendKeys(tm, "ctrl+v", "j", "l", "y")
|
|
|
|
m := getFinalModel(t, tm)
|
|
if m.Mode() != action.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(action.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(action.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(action.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(action.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.LineCount() != 3 {
|
|
t.Errorf("LineCount() = %d, want 3", m.LineCount())
|
|
}
|
|
if m.Line(1) != "to copy" {
|
|
t.Errorf("Line(1) = %q, want 'to copy'", m.Line(1))
|
|
}
|
|
})
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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(action.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(action.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(action.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(action.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(action.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(action.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])
|
|
}
|
|
})
|
|
}
|