diff --git a/internal/action/paste.go b/internal/action/paste.go index 12e15b7..46808a8 100644 --- a/internal/action/paste.go +++ b/internal/action/paste.go @@ -240,9 +240,11 @@ func (a PasteBefore) WithCount(n int) Action { return PasteBefore{Count: n} } -// VisualPaste implements Action (p in visual mode) - replaces selection with register content +// VisualPaste implements Action (p/p in visual mode) - replaces selection with +// register content when Replace flag is set type VisualPaste struct { - Count int + Count int + Replace bool } // VisualPaste.Execute: Replaces visual selection with register content (p in visual mode). @@ -266,11 +268,11 @@ func (a VisualPaste) Execute(m Model) tea.Cmd { switch mode { case core.VisualMode: - visualCharPaste(m, reg, start, end) + visualCharPaste(m, reg, start, end, a.Replace) case core.VisualBlockMode: - visualBlockPaste(m, reg, start, end) + visualBlockPaste(m, reg, start, end, a.Replace) case core.VisualLineMode: - visualLinePaste(m, reg, start, end) + visualLinePaste(m, reg, start, end, a.Replace) } // Exit visual mode @@ -295,7 +297,7 @@ func normalizeSelection(m Model) (core.Position, core.Position) { } // visualCharPaste: Handles paste operation in visual (character) mode. -func visualCharPaste(m Model, reg core.Register, start, end core.Position) { +func visualCharPaste(m Model, reg core.Register, start, end core.Position, replace bool) { win := m.ActiveWindow() buf := m.ActiveBuffer() @@ -344,11 +346,13 @@ func visualCharPaste(m Model, reg core.Register, start, end core.Position) { } // Update register with deleted text - m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText}) + if replace { + m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText}) + } } // visualBlockPaste: Handles paste operation in visual block mode. -func visualBlockPaste(m Model, reg core.Register, start, end core.Position) { +func visualBlockPaste(m Model, reg core.Register, start, end core.Position, replace bool) { win := m.ActiveWindow() buf := m.ActiveBuffer() @@ -400,11 +404,13 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position) { win.SetCursorCol(startCol) // Update register with deleted block text (joined) - m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")}) + if replace { + m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")}) + } } // visualLinePaste: Handles paste operation in visual line mode. -func visualLinePaste(m Model, reg core.Register, start, end core.Position) { +func visualLinePaste(m Model, reg core.Register, start, end core.Position, replace bool) { win := m.ActiveWindow() buf := m.ActiveBuffer() @@ -451,7 +457,9 @@ func visualLinePaste(m Model, reg core.Register, start, end core.Position) { win.SetCursorCol(0) // Update register with deleted lines - m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines) + if replace { + m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines) + } } // extractCharSelection: Extracts text from a character selection range. @@ -531,5 +539,5 @@ var _ Repeatable = VisualPaste{} // VisualPaste.WithCount: Returns a new VisualPaste with the given count. func (a VisualPaste) WithCount(n int) Action { - return VisualPaste{Count: n} + return VisualPaste{Count: n, Replace: a.Replace} } diff --git a/internal/editor/integration_paste_test.go b/internal/editor/integration_paste_test.go index 186eac8..d15f8dd 100644 --- a/internal/editor/integration_paste_test.go +++ b/internal/editor/integration_paste_test.go @@ -1165,9 +1165,9 @@ func TestYankThenPasteCharwise(t *testing.T) { WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) - sendKeys(tm, "y", "w") // yank "hello " - sendKeys(tm, "$") // go to end - sendKeys(tm, "p") // paste + sendKeys(tm, "y", "w") // yank "hello " + sendKeys(tm, "$") // go to end + sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "hello worldhello " { @@ -1180,9 +1180,9 @@ func TestYankThenPasteCharwise(t *testing.T) { WithLines([]string{"hello world"}), WithCursorPos(core.Position{Line: 0, Col: 0}), ) - sendKeys(tm, "y", "e") // yank "hello" - sendKeys(tm, "$") // go to end - sendKeys(tm, "p") // paste + sendKeys(tm, "y", "e") // yank "hello" + sendKeys(tm, "$") // go to end + sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "hello worldhello" { @@ -1196,8 +1196,8 @@ func TestYankThenPasteCharwise(t *testing.T) { WithCursorPos(core.Position{Line: 0, Col: 0}), ) sendKeys(tm, "v", "l", "l", "y") // select and yank "hel" - sendKeys(tm, "$") // go to end - sendKeys(tm, "p") // paste + sendKeys(tm, "$") // go to end + sendKeys(tm, "p") // paste m := getFinalModel(t, tm) if m.ActiveBuffer().Lines[0] != "hello worldhel" { @@ -1552,6 +1552,46 @@ func TestVisualPasteRegisterBehavior(t *testing.T) { t.Errorf("register type = %v, want LinewiseRegister", reg.Type) } }) + + t.Run("vP does not put deleted text into register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(core.Position{Line: 0, Col: 0}), + WithRegister('"', core.CharwiseRegister, []string{"NEW"}), + ) + sendKeys(tm, "v", "l", "l", "l", "l", "P") // select "hello", paste "NEW" + + m := getFinalModel(t, tm) + // After visual paste, the deleted "hello" should be in the register + reg, ok := m.GetRegister('"') + if !ok { + t.Fatal("unnamed register not found") + } + if len(reg.Content) != 1 || reg.Content[0] != "NEW" { + t.Errorf("register content = %q, want 'NEW'", reg.Content) + } + }) + + t.Run("VP does not put deleted line into register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two"}), + WithCursorPos(core.Position{Line: 0, Col: 0}), + WithRegister('"', core.LinewiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "V", "P") + + m := getFinalModel(t, tm) + reg, ok := m.GetRegister('"') + if !ok { + t.Fatal("unnamed register not found") + } + if len(reg.Content) != 1 || reg.Content[0] != "REPLACED" { + t.Errorf("register content = %q, want 'REPLACED'", reg.Content) + } + if reg.Type != core.LinewiseRegister { + t.Errorf("register type = %v, want LinewiseRegister", reg.Type) + } + }) } func TestVisualPasteEdgeCases(t *testing.T) { diff --git a/internal/input/keymap.go b/internal/input/keymap.go index 688aab0..8f0db71 100644 --- a/internal/input/keymap.go +++ b/internal/input/keymap.go @@ -134,7 +134,8 @@ func NewVisualKeymap() *Keymap { "c": operator.ChangeOperator{}, }, actions: map[string]action.Action{ - "p": action.VisualPaste{Count: 1}, + "p": action.VisualPaste{Count: 1, Replace: true}, + "P": action.VisualPaste{Count: 1, Replace: false}, ".": action.Repeat{Count: 1}, // ":": action.EnterComandMode{}, // Different OP },