From 04c247cc8e578552525ca89f793fa66d3c940564 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 30 Mar 2026 18:27:53 -0700 Subject: [PATCH] fix: looks like we resolved the issues with pasting. 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. --- internal/action/paste.go | 114 ++++++++++---- internal/editor/integration_paste_test.go | 183 ++++++++++++++++++++-- 2 files changed, 255 insertions(+), 42 deletions(-) diff --git a/internal/action/paste.go b/internal/action/paste.go index 64bc4b5..12e15b7 100644 --- a/internal/action/paste.go +++ b/internal/action/paste.go @@ -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)) - newLine := curLine[:insertAt] + cnt + curLine[insertAt:] - buf.SetLine(y, newLine) - win.SetCursorCol(x + len(cnt)) + 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)) - newLine := curLine[:insertAt] + cnt + curLine[insertAt:] - buf.SetLine(y, newLine) - win.SetCursorCol(x + len(cnt)) + 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{ diff --git a/internal/editor/integration_paste_test.go b/internal/editor/integration_paste_test.go index 34be61a..186eac8 100644 --- a/internal/editor/integration_paste_test.go +++ b/internal/editor/integration_paste_test.go @@ -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()) } }) }