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} 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 { type VisualPaste struct {
Count int Count int
Replace bool
} }
// VisualPaste.Execute: Replaces visual selection with register content (p in visual mode). // 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 { switch mode {
case core.VisualMode: case core.VisualMode:
visualCharPaste(m, reg, start, end) visualCharPaste(m, reg, start, end, a.Replace)
case core.VisualBlockMode: case core.VisualBlockMode:
visualBlockPaste(m, reg, start, end) visualBlockPaste(m, reg, start, end, a.Replace)
case core.VisualLineMode: case core.VisualLineMode:
visualLinePaste(m, reg, start, end) visualLinePaste(m, reg, start, end, a.Replace)
} }
// Exit visual mode // Exit visual mode
@ -295,7 +297,7 @@ func normalizeSelection(m Model) (core.Position, core.Position) {
} }
// visualCharPaste: Handles paste operation in visual (character) mode. // 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() win := m.ActiveWindow()
buf := m.ActiveBuffer() buf := m.ActiveBuffer()
@ -344,11 +346,13 @@ func visualCharPaste(m Model, reg core.Register, start, end core.Position) {
} }
// Update register with deleted text // Update register with deleted text
if replace {
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText}) m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
} }
}
// visualBlockPaste: Handles paste operation in visual block mode. // 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() win := m.ActiveWindow()
buf := m.ActiveBuffer() buf := m.ActiveBuffer()
@ -400,11 +404,13 @@ func visualBlockPaste(m Model, reg core.Register, start, end core.Position) {
win.SetCursorCol(startCol) win.SetCursorCol(startCol)
// Update register with deleted block text (joined) // Update register with deleted block text (joined)
if replace {
m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")}) m.UpdateDefaultRegister(core.CharwiseRegister, []string{strings.Join(deletedLines, "\n")})
} }
}
// visualLinePaste: Handles paste operation in visual line mode. // 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() win := m.ActiveWindow()
buf := m.ActiveBuffer() buf := m.ActiveBuffer()
@ -451,8 +457,10 @@ func visualLinePaste(m Model, reg core.Register, start, end core.Position) {
win.SetCursorCol(0) win.SetCursorCol(0)
// Update register with deleted lines // Update register with deleted lines
if replace {
m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines) m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines)
} }
}
// extractCharSelection: Extracts text from a character selection range. // extractCharSelection: Extracts text from a character selection range.
func extractCharSelection(m Model, start, end core.Position) string { func extractCharSelection(m Model, start, end core.Position) string {
@ -531,5 +539,5 @@ var _ Repeatable = VisualPaste{}
// VisualPaste.WithCount: Returns a new VisualPaste with the given count. // VisualPaste.WithCount: Returns a new VisualPaste with the given count.
func (a VisualPaste) WithCount(n int) Action { 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.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) { func TestVisualPasteEdgeCases(t *testing.T) {

View File

@ -134,7 +134,8 @@ func NewVisualKeymap() *Keymap {
"c": operator.ChangeOperator{}, "c": operator.ChangeOperator{},
}, },
actions: map[string]action.Action{ 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.Repeat{Count: 1},
// ":": action.EnterComandMode{}, // Different OP // ":": action.EnterComandMode{}, // Different OP
}, },