fix: p and P differ in V mode, resolved and tested.

P should not replace the register, p should
This commit is contained in:
Hayden Hargreaves 2026-04-01 18:21:15 -07:00
parent 78dc00a5e9
commit 1a98d3a4de
3 changed files with 70 additions and 21 deletions

View File

@ -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
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
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)
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
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}
}

View File

@ -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) {

View File

@ -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
},