package editor import ( "testing" "git.gophernest.net/azpect/TextEditor/internal/action" ) func TestPasteLinewiseBasic(t *testing.T) { t.Run("p pastes single line after cursor line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "p") 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) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } if m.Line(2) != "line 2" { t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2)) } }) t.Run("p moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(action.Position{Line: 0, Col: 5}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) t.Run("p from middle of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } if m.Line(2) != "inserted" { t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2)) } if m.CursorY() != 2 { t.Errorf("CursorY() = %d, want 2", m.CursorY()) } }) t.Run("p at end of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.LineCount()) } if m.Line(2) != "inserted" { t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2)) } }) } func TestPasteLinewiseMultipleLines(t *testing.T) { t.Run("p pastes multiple lines in correct order", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second", "third"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 5 { t.Errorf("LineCount() = %d, want 5", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "first" { t.Errorf("Line(1) = %q, want 'first'", m.Line(1)) } if m.Line(2) != "second" { t.Errorf("Line(2) = %q, want 'second'", m.Line(2)) } if m.Line(3) != "third" { t.Errorf("Line(3) = %q, want 'third'", m.Line(3)) } if m.Line(4) != "line 2" { t.Errorf("Line(4) = %q, want 'line 2'", m.Line(4)) } }) t.Run("p with multiple lines moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) } func TestPasteLinewiseWithCount(t *testing.T) { t.Run("2p pastes content twice", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "2", "p") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } // Both "inserted" lines should appear after line 1 if m.Line(1) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } if m.Line(2) != "inserted" { t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2)) } if m.Line(3) != "line 2" { t.Errorf("Line(3) = %q, want 'line 2'", m.Line(3)) } }) t.Run("3p pastes content three times", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"pasted"}), ) sendKeys(tm, "3", "p") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } for i := 1; i <= 3; i++ { if m.Line(i) != "pasted" { t.Errorf("Line(%d) = %q, want 'pasted'", i, m.Line(i)) } } }) t.Run("2p with multiple lines pastes all lines twice in order", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second"}), ) sendKeys(tm, "2", "p") m := getFinalModel(t, tm) // Should be: original, first, second, first, second if m.LineCount() != 5 { t.Errorf("LineCount() = %d, want 5", m.LineCount()) } if m.Line(0) != "original" { t.Errorf("Line(0) = %q, want 'original'", m.Line(0)) } if m.Line(1) != "first" { t.Errorf("Line(1) = %q, want 'first'", m.Line(1)) } if m.Line(2) != "second" { t.Errorf("Line(2) = %q, want 'second'", m.Line(2)) } if m.Line(3) != "first" { t.Errorf("Line(3) = %q, want 'first'", m.Line(3)) } if m.Line(4) != "second" { t.Errorf("Line(4) = %q, want 'second'", m.Line(4)) } }) t.Run("count paste moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "3", "p") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) } // Tests for P (paste before) func TestPasteBeforeLinewiseBasic(t *testing.T) { t.Run("P pastes single line before cursor line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "P") 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) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } if m.Line(2) != "line 2" { t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2)) } }) t.Run("P moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(action.Position{Line: 1, Col: 5}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) t.Run("P at first line pastes at very top", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.LineCount() != 3 { t.Errorf("LineCount() = %d, want 3", m.LineCount()) } if m.Line(0) != "inserted" { t.Errorf("Line(0) = %q, want 'inserted'", m.Line(0)) } if m.Line(1) != "line 1" { t.Errorf("Line(1) = %q, want 'line 1'", m.Line(1)) } if m.CursorY() != 0 { t.Errorf("CursorY() = %d, want 0", m.CursorY()) } }) t.Run("P from middle of file", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2", "line 3"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } if m.Line(1) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } if m.Line(2) != "line 2" { t.Errorf("Line(2) = %q, want 'line 2'", m.Line(2)) } }) } func TestPasteBeforeLinewiseMultipleLines(t *testing.T) { t.Run("P pastes multiple lines in correct order", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second", "third"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.LineCount() != 5 { t.Errorf("LineCount() = %d, want 5", m.LineCount()) } if m.Line(0) != "line 1" { t.Errorf("Line(0) = %q, want 'line 1'", m.Line(0)) } if m.Line(1) != "first" { t.Errorf("Line(1) = %q, want 'first'", m.Line(1)) } if m.Line(2) != "second" { t.Errorf("Line(2) = %q, want 'second'", m.Line(2)) } if m.Line(3) != "third" { t.Errorf("Line(3) = %q, want 'third'", m.Line(3)) } if m.Line(4) != "line 2" { t.Errorf("Line(4) = %q, want 'line 2'", m.Line(4)) } }) t.Run("P with multiple lines moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) } func TestPasteBeforeLinewiseWithCount(t *testing.T) { t.Run("2P pastes content twice", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "2", "P") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } // Both "inserted" lines should appear before line 2 if m.Line(1) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } if m.Line(2) != "inserted" { t.Errorf("Line(2) = %q, want 'inserted'", m.Line(2)) } if m.Line(3) != "line 2" { t.Errorf("Line(3) = %q, want 'line 2'", m.Line(3)) } }) t.Run("3P pastes content three times", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"pasted"}), ) sendKeys(tm, "3", "P") m := getFinalModel(t, tm) if m.LineCount() != 4 { t.Errorf("LineCount() = %d, want 4", m.LineCount()) } for i := 0; i < 3; i++ { if m.Line(i) != "pasted" { t.Errorf("Line(%d) = %q, want 'pasted'", i, m.Line(i)) } } if m.Line(3) != "original" { t.Errorf("Line(3) = %q, want 'original'", m.Line(3)) } }) t.Run("2P with multiple lines pastes all lines twice in order", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"first", "second"}), ) sendKeys(tm, "2", "P") m := getFinalModel(t, tm) // Should be: first, second, first, second, original if m.LineCount() != 5 { t.Errorf("LineCount() = %d, want 5", m.LineCount()) } if m.Line(0) != "first" { t.Errorf("Line(0) = %q, want 'first'", m.Line(0)) } if m.Line(1) != "second" { t.Errorf("Line(1) = %q, want 'second'", m.Line(1)) } if m.Line(2) != "first" { t.Errorf("Line(2) = %q, want 'first'", m.Line(2)) } if m.Line(3) != "second" { t.Errorf("Line(3) = %q, want 'second'", m.Line(3)) } if m.Line(4) != "original" { t.Errorf("Line(4) = %q, want 'original'", m.Line(4)) } }) t.Run("count paste before moves cursor to first pasted line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "3", "P") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) } func TestPasteBeforeLinewiseEdgeCases(t *testing.T) { t.Run("P on single line buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"only line"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "inserted" { t.Errorf("Line(0) = %q, want 'inserted'", m.Line(0)) } if m.Line(1) != "only line" { t.Errorf("Line(1) = %q, want 'only line'", m.Line(1)) } }) t.Run("P with empty register content does nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } }) t.Run("P preserves indentation in pasted lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{" indented", "\ttabbed"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.Line(0) != " indented" { t.Errorf("Line(0) = %q, want ' indented'", m.Line(0)) } if m.Line(1) != "\ttabbed" { t.Errorf("Line(1) = %q, want '\\ttabbed'", m.Line(1)) } }) t.Run("P with large count", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"x"}), ) sendKeys(tm, "1", "0", "P") // 10P m := getFinalModel(t, tm) if m.LineCount() != 11 { t.Errorf("LineCount() = %d, want 11", m.LineCount()) } // Original should be at the end if m.Line(10) != "original" { t.Errorf("Line(10) = %q, want 'original'", m.Line(10)) } }) } func TestPasteLinewiseEdgeCases(t *testing.T) { t.Run("p on single line buffer", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"only line"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"inserted"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } if m.Line(0) != "only line" { t.Errorf("Line(0) = %q, want 'only line'", m.Line(0)) } if m.Line(1) != "inserted" { t.Errorf("Line(1) = %q, want 'inserted'", m.Line(1)) } }) t.Run("p with empty register content does nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } }) t.Run("p preserves indentation in pasted lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{" indented", "\ttabbed"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(1) != " indented" { t.Errorf("Line(1) = %q, want ' indented'", m.Line(1)) } if m.Line(2) != "\ttabbed" { t.Errorf("Line(2) = %q, want '\\ttabbed'", m.Line(2)) } }) t.Run("p with large count", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"original"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.LinewiseRegister, []string{"x"}), ) sendKeys(tm, "1", "0", "p") // 10p m := getFinalModel(t, tm) if m.LineCount() != 11 { t.Errorf("LineCount() = %d, want 11", m.LineCount()) } }) } // ============================================================================= // Charwise Paste (p) Tests // ============================================================================= func TestPasteCharwiseBasic(t *testing.T) { t.Run("p pastes text after cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o' WithRegister('"', action.CharwiseRegister, []string{"XYZ"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) // Should insert after 'o': "helloXYZ world" if m.Line(0) != "helloXYZ world" { t.Errorf("Line(0) = %q, want 'helloXYZ world'", m.Line(0)) } }) t.Run("p at start of line pastes after first char", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "hXello" { t.Errorf("Line(0) = %q, want 'hXello'", m.Line(0)) } }) t.Run("p at end of line pastes at end", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o' WithRegister('"', action.CharwiseRegister, []string{"!"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "hello!" { t.Errorf("Line(0) = %q, want 'hello!'", m.Line(0)) } }) t.Run("p on empty line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{""}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"text"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "text" { t.Errorf("Line(0) = %q, want 'text'", m.Line(0)) } }) t.Run("p does not add new lines", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello", "world"}), WithCursorPos(action.Position{Line: 0, Col: 2}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.LineCount() != 2 { t.Errorf("LineCount() = %d, want 2", m.LineCount()) } }) } func TestPasteCharwiseWithCount(t *testing.T) { t.Run("2p pastes content twice", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "2", "p") m := getFinalModel(t, tm) if m.Line(0) != "hXXello" { t.Errorf("Line(0) = %q, want 'hXXello'", m.Line(0)) } }) t.Run("3p pastes word three times", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"start end"}), WithCursorPos(action.Position{Line: 0, Col: 4}), // on 't' WithRegister('"', action.CharwiseRegister, []string{"-"}), ) sendKeys(tm, "3", "p") m := getFinalModel(t, tm) if m.Line(0) != "start--- end" { t.Errorf("Line(0) = %q, want 'start--- end'", m.Line(0)) } }) t.Run("count paste with multi-char content", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"ab"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"XY"}), ) sendKeys(tm, "2", "p") m := getFinalModel(t, tm) if m.Line(0) != "aXYXYb" { t.Errorf("Line(0) = %q, want 'aXYXYb'", m.Line(0)) } }) } func TestPasteCharwiseCursorPosition(t *testing.T) { t.Run("p moves cursor to end of pasted text", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"XYZ"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) // Cursor should be at position after pasted text if m.CursorX() != 3 { t.Errorf("CursorX() = %d, want 3", m.CursorX()) } }) t.Run("p cursor stays on same line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"line 1", "line 2"}), WithCursorPos(action.Position{Line: 1, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.CursorY() != 1 { t.Errorf("CursorY() = %d, want 1", m.CursorY()) } }) } // ============================================================================= // Charwise Paste Before (P) Tests // ============================================================================= func TestPasteBeforeCharwiseBasic(t *testing.T) { t.Run("P pastes text before cursor", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(action.Position{Line: 0, Col: 5}), // on space WithRegister('"', action.CharwiseRegister, []string{"XYZ"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) // Should insert before space: "helloXYZ world" if m.Line(0) != "helloXYZ world" { t.Errorf("Line(0) = %q, want 'helloXYZ world'", m.Line(0)) } }) t.Run("P at start of line pastes at beginning", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.Line(0) != "Xhello" { t.Errorf("Line(0) = %q, want 'Xhello'", m.Line(0)) } }) t.Run("P at end of line pastes before last char", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 4}), // on 'o' WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.Line(0) != "hellXo" { t.Errorf("Line(0) = %q, want 'hellXo'", m.Line(0)) } }) t.Run("P on empty line", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{""}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"text"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.Line(0) != "text" { t.Errorf("Line(0) = %q, want 'text'", m.Line(0)) } }) } func TestPasteBeforeCharwiseWithCount(t *testing.T) { t.Run("2P pastes content twice", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 2}), // on first 'l' WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "2", "P") m := getFinalModel(t, tm) if m.Line(0) != "heXXllo" { t.Errorf("Line(0) = %q, want 'heXXllo'", m.Line(0)) } }) t.Run("3P pastes word three times", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"ab"}), WithCursorPos(action.Position{Line: 0, Col: 1}), // on 'b' WithRegister('"', action.CharwiseRegister, []string{"-"}), ) sendKeys(tm, "3", "P") m := getFinalModel(t, tm) if m.Line(0) != "a---b" { t.Errorf("Line(0) = %q, want 'a---b'", m.Line(0)) } }) } // ============================================================================= // Multi-line Charwise Paste Tests (from visual mode yank) // ============================================================================= func TestPasteCharwiseMultiLine(t *testing.T) { t.Run("p with multi-line charwise content errors gracefully", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"line1", "line2"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) // Current implementation errors - line should be unchanged if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want 'hello' (unchanged due to error)", m.Line(0)) } }) t.Run("P with multi-line charwise content errors gracefully", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"line1", "line2"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) // Current implementation errors - line should be unchanged if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want 'hello' (unchanged due to error)", m.Line(0)) } }) } // ============================================================================= // Blockwise Paste Tests // ============================================================================= func TestPasteBlockwiseBasic(t *testing.T) { t.Run("p with blockwise content errors gracefully", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"aaaa", "bbbb", "cccc"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.BlockwiseRegister, []string{"XX", "YY", "ZZ"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) // Current implementation errors - lines should be unchanged if m.Line(0) != "aaaa" { t.Errorf("Line(0) = %q, want 'aaaa' (unchanged due to error)", m.Line(0)) } }) t.Run("P with blockwise content errors gracefully", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"aaaa", "bbbb", "cccc"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.BlockwiseRegister, []string{"XX", "YY", "ZZ"}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) // Current implementation errors - lines should be unchanged if m.Line(0) != "aaaa" { t.Errorf("Line(0) = %q, want 'aaaa' (unchanged due to error)", m.Line(0)) } }) } // ============================================================================= // Charwise Paste Edge Cases // ============================================================================= func TestPasteCharwiseEdgeCases(t *testing.T) { t.Run("p with empty charwise register does nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want 'hello'", m.Line(0)) } }) t.Run("p with empty string in register", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{""}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want 'hello'", m.Line(0)) } }) t.Run("p preserves special characters", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"ab"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"\t"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "a\tb" { t.Errorf("Line(0) = %q, want 'a\\tb'", m.Line(0)) } }) t.Run("p with spaces", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"ab"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{" "}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != "a b" { t.Errorf("Line(0) = %q, want 'a b'", m.Line(0)) } }) t.Run("p on line with only whitespace", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{" "}), WithCursorPos(action.Position{Line: 0, Col: 2}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "p") m := getFinalModel(t, tm) if m.Line(0) != " X " { t.Errorf("Line(0) = %q, want ' X '", m.Line(0)) } }) t.Run("P with empty charwise register does nothing", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{}), ) sendKeys(tm, "P") m := getFinalModel(t, tm) if m.Line(0) != "hello" { t.Errorf("Line(0) = %q, want 'hello'", m.Line(0)) } }) t.Run("large count paste", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"ab"}), WithCursorPos(action.Position{Line: 0, Col: 0}), WithRegister('"', action.CharwiseRegister, []string{"X"}), ) sendKeys(tm, "1", "0", "p") // 10p m := getFinalModel(t, tm) if m.Line(0) != "aXXXXXXXXXXb" { t.Errorf("Line(0) = %q, want 'aXXXXXXXXXXb'", m.Line(0)) } }) } // ============================================================================= // Integration: Yank then Paste // ============================================================================= func TestYankThenPasteCharwise(t *testing.T) { t.Run("yw then p pastes yanked word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(action.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "w") // yank "hello " sendKeys(tm, "$") // go to end sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.Line(0) != "hello worldhello " { t.Errorf("Line(0) = %q, want 'hello worldhello '", m.Line(0)) } }) t.Run("ye then p pastes yanked word", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(action.Position{Line: 0, Col: 0}), ) sendKeys(tm, "y", "e") // yank "hello" sendKeys(tm, "$") // go to end sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.Line(0) != "hello worldhello" { t.Errorf("Line(0) = %q, want 'hello worldhello'", m.Line(0)) } }) t.Run("visual select then y then p", func(t *testing.T) { tm := newTestModel(t, WithLines([]string{"hello world"}), WithCursorPos(action.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "l", "l", "y") // select and yank "hel" sendKeys(tm, "$") // go to end sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.Line(0) != "hello worldhel" { t.Errorf("Line(0) = %q, want 'hello worldhel'", m.Line(0)) } }) }