fix: looks like we resolved the issues with pasting.
All checks were successful
Run Test Suite / test (push) Successful in 46s

We means me and Claude (heavy on the Claude). Originally, if we copied a
many line segment into a charwise register, the paste op would error,
this is not right, it should paste, just differently.
This commit is contained in:
Hayden Hargreaves 2026-03-30 18:27:53 -07:00
parent ffad4f86f6
commit 04c247cc8e
2 changed files with 255 additions and 42 deletions

View File

@ -57,29 +57,56 @@ func (a Paste) Execute(m Model) tea.Cmd {
{
lines := reg.Content
// Shouldn't happen, just a check
if len(lines) != 1 {
out := core.CommandOutput{
Lines: []string{"Charwise register should only have a single line of content."},
Inline: true,
IsError: true,
}
m.SetCommandOutput(&out)
if len(lines) == 0 {
break
}
x := win.Cursor.Col
y := win.Cursor.Line
cnt := strings.Repeat(lines[0], max(1, a.Count))
curLine := buf.Lines[y]
// Catch edge cases, end of line, start of blank line
insertAt := min(x+1, len(curLine))
if len(lines) == 1 {
// Single-line charwise paste
cnt := strings.Repeat(lines[0], max(1, a.Count))
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
buf.SetLine(y, newLine)
win.SetCursorCol(x + len(cnt))
} else {
// Multi-line charwise paste (e.g., from vi{ yank)
suffix := curLine[insertAt:] // Save the part after cursor
// For count > 1, we paste the content multiple times
// Each paste continues from where the previous one ended
var content strings.Builder
for i := 0; i < a.Count; i++ {
for j, line := range lines {
if j > 0 {
content.WriteString("\n")
}
content.WriteString(line)
}
}
// Split the pasted content into lines
pastedLines := strings.Split(content.String(), "\n")
// First line: append to current line
buf.SetLine(y, curLine[:insertAt]+pastedLines[0])
// Middle lines: insert as new lines
for i := 1; i < len(pastedLines); i++ {
buf.InsertLine(y+i, pastedLines[i])
}
// Last line: append the suffix
lastLineIdx := y + len(pastedLines) - 1
buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix)
// Set cursor to end of last pasted content (before suffix)
win.SetCursorLine(lastLineIdx)
win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1)
}
}
default:
out := core.CommandOutput{
@ -142,29 +169,56 @@ func (a PasteBefore) Execute(m Model) tea.Cmd {
{
lines := reg.Content
// Shouldn't happen, just a check
if len(lines) != 1 {
out := core.CommandOutput{
Lines: []string{"Charwise register should only have a single line of content."},
Inline: true,
IsError: true,
}
m.SetCommandOutput(&out)
if len(lines) == 0 {
break
}
x := win.Cursor.Col
y := win.Cursor.Line
cnt := strings.Repeat(lines[0], max(1, a.Count))
curLine := buf.Lines[y]
// Catch edge cases, end of line, start of blank line
insertAt := min(x, len(curLine))
if len(lines) == 1 {
// Single-line charwise paste before cursor
cnt := strings.Repeat(lines[0], max(1, a.Count))
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
buf.SetLine(y, newLine)
win.SetCursorCol(x + len(cnt))
} else {
// Multi-line charwise paste before cursor
suffix := curLine[insertAt:] // Save the part after cursor
// For count > 1, we paste the content multiple times
// Each paste continues from where the previous one ended
var content strings.Builder
for i := 0; i < a.Count; i++ {
for j, line := range lines {
if j > 0 {
content.WriteString("\n")
}
content.WriteString(line)
}
}
// Split the pasted content into lines
pastedLines := strings.Split(content.String(), "\n")
// First line: insert at cursor position
buf.SetLine(y, curLine[:insertAt]+pastedLines[0])
// Middle lines: insert as new lines
for i := 1; i < len(pastedLines); i++ {
buf.InsertLine(y+i, pastedLines[i])
}
// Last line: append the suffix
lastLineIdx := y + len(pastedLines) - 1
buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix)
// Set cursor to end of last pasted content (before suffix)
win.SetCursorLine(lastLineIdx)
win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1)
}
}
default:
out := core.CommandOutput{

View File

@ -821,37 +821,196 @@ func TestPasteBeforeCharwiseWithCount(t *testing.T) {
}
// =============================================================================
// Multi-line Charwise Paste Tests (from visual mode yank)
// Multi-line Charwise Paste Tests (from visual mode yank like vi{)
// =============================================================================
func TestPasteCharwiseMultiLine(t *testing.T) {
t.Run("p with multi-line charwise content errors gracefully", func(t *testing.T) {
t.Run("p with 2-line charwise content inserts correctly", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"line1", "line2"}),
WithCursorPos(core.Position{Line: 0, Col: 1}), // on 'e'
WithRegister('"', core.CharwiseRegister, []string{"AAA", "BBB"}),
)
sendKeys(tm, "p")
m := getFinalModel(t, tm)
// Current implementation errors - line should be unchanged
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello' (unchanged due to error)", m.ActiveBuffer().Lines[0])
// Should paste after 'e': "heAAA\nBBBllo"
if m.ActiveBuffer().LineCount() != 2 {
t.Errorf("LineCount() = %d, want 2", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0] != "heAAA" {
t.Errorf("Line(0) = %q, want 'heAAA'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "BBBllo" {
t.Errorf("Line(1) = %q, want 'BBBllo'", m.ActiveBuffer().Lines[1])
}
})
t.Run("P with multi-line charwise content errors gracefully", func(t *testing.T) {
t.Run("p with 3-line charwise content inserts correctly", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"test"}),
WithCursorPos(core.Position{Line: 0, Col: 1}), // on 'e'
WithRegister('"', core.CharwiseRegister, []string{"AAA", "BBB", "CCC"}),
)
sendKeys(tm, "p")
m := getFinalModel(t, tm)
// Should paste: "teAAA\nBBB\nCCCst"
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0] != "teAAA" {
t.Errorf("Line(0) = %q, want 'teAAA'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "BBB" {
t.Errorf("Line(1) = %q, want 'BBB'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2] != "CCCst" {
t.Errorf("Line(2) = %q, want 'CCCst'", m.ActiveBuffer().Lines[2])
}
})
t.Run("p with multi-line at start of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(core.Position{Line: 0, Col: 0}), // on 'h'
WithRegister('"', core.CharwiseRegister, []string{"X", "Y"}),
)
sendKeys(tm, "p")
m := getFinalModel(t, tm)
// After 'h': "hX\nYello"
if m.ActiveBuffer().Lines[0] != "hX" {
t.Errorf("Line(0) = %q, want 'hX'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "Yello" {
t.Errorf("Line(1) = %q, want 'Yello'", m.ActiveBuffer().Lines[1])
}
})
t.Run("p with multi-line at end of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(core.Position{Line: 0, Col: 4}), // on 'o'
WithRegister('"', core.CharwiseRegister, []string{"X", "Y"}),
)
sendKeys(tm, "p")
m := getFinalModel(t, tm)
// After 'o': "helloX\nY"
if m.ActiveBuffer().Lines[0] != "helloX" {
t.Errorf("Line(0) = %q, want 'helloX'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "Y" {
t.Errorf("Line(1) = %q, want 'Y'", m.ActiveBuffer().Lines[1])
}
})
t.Run("p with multi-line on empty line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{""}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"line1", "line2"}),
WithRegister('"', core.CharwiseRegister, []string{"AAA", "BBB"}),
)
sendKeys(tm, "p")
m := getFinalModel(t, tm)
if m.ActiveBuffer().Lines[0] != "AAA" {
t.Errorf("Line(0) = %q, want 'AAA'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "BBB" {
t.Errorf("Line(1) = %q, want 'BBB'", m.ActiveBuffer().Lines[1])
}
})
t.Run("P with multi-line charwise content before cursor", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(core.Position{Line: 0, Col: 2}), // on first 'l'
WithRegister('"', core.CharwiseRegister, []string{"X", "Y"}),
)
sendKeys(tm, "P")
m := getFinalModel(t, tm)
// Current implementation errors - line should be unchanged
if m.ActiveBuffer().Lines[0] != "hello" {
t.Errorf("Line(0) = %q, want 'hello' (unchanged due to error)", m.ActiveBuffer().Lines[0])
// Before 'l': "heX\nYllo"
if m.ActiveBuffer().Lines[0] != "heX" {
t.Errorf("Line(0) = %q, want 'heX'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "Yllo" {
t.Errorf("Line(1) = %q, want 'Yllo'", m.ActiveBuffer().Lines[1])
}
})
t.Run("P with multi-line at start of line", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"hello"}),
WithCursorPos(core.Position{Line: 0, Col: 0}),
WithRegister('"', core.CharwiseRegister, []string{"X", "Y"}),
)
sendKeys(tm, "P")
m := getFinalModel(t, tm)
// Before 'h': "X\nYhello"
if m.ActiveBuffer().Lines[0] != "X" {
t.Errorf("Line(0) = %q, want 'X'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "Yhello" {
t.Errorf("Line(1) = %q, want 'Yhello'", m.ActiveBuffer().Lines[1])
}
})
t.Run("p with multi-line and count", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{"test"}),
WithCursorPos(core.Position{Line: 0, Col: 1}),
WithRegister('"', core.CharwiseRegister, []string{"A", "B"}),
)
sendKeys(tm, "2", "p")
m := getFinalModel(t, tm)
// 2p should paste twice: "teA\nBA\nBst"
if m.ActiveBuffer().LineCount() != 3 {
t.Errorf("LineCount() = %d, want 3", m.ActiveBuffer().LineCount())
}
if m.ActiveBuffer().Lines[0] != "teA" {
t.Errorf("Line(0) = %q, want 'teA'", m.ActiveBuffer().Lines[0])
}
if m.ActiveBuffer().Lines[1] != "BA" {
t.Errorf("Line(1) = %q, want 'BA'", m.ActiveBuffer().Lines[1])
}
if m.ActiveBuffer().Lines[2] != "Bst" {
t.Errorf("Line(2) = %q, want 'Bst'", m.ActiveBuffer().Lines[2])
}
})
t.Run("real world: vi{ then y then p", func(t *testing.T) {
tm := newTestModel(t,
WithLines([]string{
"function() {",
" body",
"}",
"test",
}),
WithCursorPos(core.Position{Line: 1, Col: 0}),
)
// Yank the content inside braces
sendKeys(tm, "v", "i", "{", "y")
// Move to test line and paste
sendKeys(tm, "j", "j", "$", "p")
m := getFinalModel(t, tm)
// The yanked content should be multi-line charwise
reg, ok := m.GetRegister('"')
if !ok {
t.Fatal("register not found")
}
if reg.Type != core.CharwiseRegister {
t.Errorf("register type = %v, want CharwiseRegister", reg.Type)
}
// Should paste after 't' in "test"
// Depending on what vi{ yanks, this verifies multi-line paste works
if m.ActiveBuffer().LineCount() < 4 {
t.Errorf("LineCount() = %d, want at least 4", m.ActiveBuffer().LineCount())
}
})
}