diff --git a/internal/action/paste.go b/internal/action/paste.go index da4076b..67e4b95 100644 --- a/internal/action/paste.go +++ b/internal/action/paste.go @@ -147,3 +147,281 @@ var _ Repeatable = PasteBefore{} func (a PasteBefore) WithCount(n int) Action { return PasteBefore{Count: n} } + +// VisualPaste implements Action (p in visual mode) - replaces selection with register content +type VisualPaste struct { + Count int +} + +func (a VisualPaste) Execute(m Model) tea.Cmd { + // Get register content to paste + reg, found := m.GetRegister('"') + if !found { + m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh.")) + return nil + } + + mode := m.Mode() + + // Get selection bounds (normalize anchor and cursor) + start, end := normalizeSelection(m) + + switch mode { + case VisualMode: + visualCharPaste(m, reg, start, end) + case VisualBlockMode: + visualBlockPaste(m, reg, start, end) + case VisualLineMode: + visualLinePaste(m, reg, start, end) + } + + // Exit visual mode + m.SetMode(NormalMode) + + return nil +} + +// normalizeSelection returns start and end positions with start always before end +func normalizeSelection(m Model) (Position, Position) { + anchorX, anchorY := m.AnchorX(), m.AnchorY() + cursorX, cursorY := m.CursorX(), m.CursorY() + + start := Position{Line: anchorY, Col: anchorX} + end := Position{Line: cursorY, Col: cursorX} + + // Normalize so start is always before end + if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) { + start, end = end, start + } + + return start, end +} + +// visualCharPaste handles paste in visual (character) mode +func visualCharPaste(m Model, reg Register, start, end Position) { + // First, extract the text that will be deleted (to save to register) + deletedText := extractCharSelection(m, start, end) + + // Delete the selection + deleteCharSelectionForPaste(m, start, end) + + // Insert the register content at start position + if len(reg.Content) == 0 { + // Empty register - just delete (already done) + } else if reg.Type == CharwiseRegister { + // Charwise paste: insert text at cursor position + if len(reg.Content) == 1 { + line := m.Line(start.Line) + insertAt := min(start.Col, len(line)) + newLine := line[:insertAt] + reg.Content[0] + line[insertAt:] + m.SetLine(start.Line, newLine) + + // Cursor at end of pasted text + m.SetCursorX(insertAt + len(reg.Content[0]) - 1) + m.SetCursorY(start.Line) + } + } else if reg.Type == LinewiseRegister { + // Linewise paste in visual char mode: replace selection with lines + // Insert each line from register + for i, content := range reg.Content { + if i == 0 { + // First line: insert at start position + line := m.Line(start.Line) + insertAt := min(start.Col, len(line)) + newLine := line[:insertAt] + content + if len(reg.Content) == 1 { + // Single line register - append rest of line + newLine += line[insertAt:] + } + m.SetLine(start.Line, newLine) + } else { + // Subsequent lines: insert new lines + m.InsertLine(start.Line+i, content) + } + } + m.SetCursorY(start.Line) + m.SetCursorX(start.Col) + } + + m.ClampCursorX() + + // Update register with deleted text + m.UpdateDefaultRegister(CharwiseRegister, []string{deletedText}) +} + +// visualBlockPaste handles paste in visual block mode +func visualBlockPaste(m Model, reg Register, start, end Position) { + startCol := min(start.Col, end.Col) + endCol := max(start.Col, end.Col) + + // Extract deleted text (for register) + var deletedLines []string + for y := start.Line; y <= end.Line; y++ { + line := m.Line(y) + if startCol < len(line) { + ec := min(endCol+1, len(line)) + deletedLines = append(deletedLines, line[startCol:ec]) + } else { + deletedLines = append(deletedLines, "") + } + } + + // Delete the block selection + for y := start.Line; y <= end.Line; y++ { + line := m.Line(y) + if startCol >= len(line) { + continue + } + ec := min(endCol+1, len(line)) + m.SetLine(y, line[:startCol]+line[ec:]) + } + + // Insert register content + if len(reg.Content) > 0 { + pasteContent := reg.Content[0] + if reg.Type == LinewiseRegister && len(reg.Content) > 0 { + pasteContent = reg.Content[0] + } + + for y := start.Line; y <= end.Line; y++ { + line := m.Line(y) + insertAt := min(startCol, len(line)) + // Pad with spaces if needed + for len(line) < insertAt { + line += " " + } + newLine := line[:insertAt] + pasteContent + line[insertAt:] + m.SetLine(y, newLine) + } + } + + m.SetCursorY(start.Line) + m.SetCursorX(startCol) + m.ClampCursorX() + + // Update register with deleted block text (joined) + m.UpdateDefaultRegister(CharwiseRegister, []string{strings.Join(deletedLines, "\n")}) +} + +// visualLinePaste handles paste in visual line mode +func visualLinePaste(m Model, reg Register, start, end Position) { + // Extract deleted lines (for register) + var deletedLines []string + for y := start.Line; y <= end.Line; y++ { + deletedLines = append(deletedLines, m.Line(y)) + } + + // Delete the selected lines (from end to start to preserve indices) + for y := end.Line; y >= start.Line; y-- { + m.DeleteLine(y) + } + + // Insert register content + if len(reg.Content) == 0 { + // Empty register - ensure at least one empty line exists + if m.LineCount() == 0 { + m.InsertLine(0, "") + } + } else if reg.Type == LinewiseRegister { + // Linewise register: insert each line + insertPos := start.Line + for _, content := range reg.Content { + m.InsertLine(insertPos, content) + insertPos++ + } + } else { + // Charwise register: insert as a single line + m.InsertLine(start.Line, reg.Content[0]) + } + + // Ensure we have at least one line + if m.LineCount() == 0 { + m.InsertLine(0, "") + } + + // Position cursor at start of pasted content + y := start.Line + if y >= m.LineCount() { + y = m.LineCount() - 1 + } + m.SetCursorY(y) + m.SetCursorX(0) + m.ClampCursorX() + + // Update register with deleted lines + m.UpdateDefaultRegister(LinewiseRegister, deletedLines) +} + +// extractCharSelection extracts text from a character selection +func extractCharSelection(m Model, start, end Position) string { + if start.Line == end.Line { + line := m.Line(start.Line) + endCol := min(end.Col+1, len(line)) + startCol := min(start.Col, len(line)) + if startCol >= endCol { + return "" + } + return line[startCol:endCol] + } + + // Multi-line selection + var result strings.Builder + + // First line: from start.Col to end + firstLine := m.Line(start.Line) + if start.Col < len(firstLine) { + result.WriteString(firstLine[start.Col:]) + } + result.WriteString("\n") + + // Middle lines: entire lines + for y := start.Line + 1; y < end.Line; y++ { + result.WriteString(m.Line(y)) + result.WriteString("\n") + } + + // Last line: from beginning to end.Col + lastLine := m.Line(end.Line) + endCol := min(end.Col+1, len(lastLine)) + result.WriteString(lastLine[:endCol]) + + return result.String() +} + +// deleteCharSelectionForPaste deletes a character selection (similar to operator/delete.go) +func deleteCharSelectionForPaste(m Model, start, end Position) { + if start.Line == end.Line { + line := m.Line(start.Line) + endCol := min(end.Col+1, len(line)) + m.SetLine(start.Line, line[:start.Col]+line[endCol:]) + } else { + startLine := m.Line(start.Line) + endLine := m.Line(end.Line) + + prefix := "" + if start.Col < len(startLine) { + prefix = startLine[:start.Col] + } + + suffix := "" + if end.Col+1 < len(endLine) { + suffix = endLine[end.Col+1:] + } + + // Delete from end back to start to preserve indices + for i := end.Line; i >= start.Line; i-- { + m.DeleteLine(i) + } + m.InsertLine(start.Line, prefix+suffix) + } + + m.SetCursorY(start.Line) + m.SetCursorX(start.Col) +} + +// Ensure VisualPaste implements Repeatable +var _ Repeatable = VisualPaste{} + +func (a VisualPaste) WithCount(n int) Action { + return VisualPaste{Count: n} +} diff --git a/internal/editor/integration_paste_test.go b/internal/editor/integration_paste_test.go index c8c3664..81d48ad 100644 --- a/internal/editor/integration_paste_test.go +++ b/internal/editor/integration_paste_test.go @@ -1046,3 +1046,430 @@ func TestYankThenPasteCharwise(t *testing.T) { } }) } + +// ============================================================================= +// Visual Mode Paste Tests +// ============================================================================= +// In visual mode, 'p' should replace the selected text with register content. +// This is different from normal mode where 'p' inserts after cursor. + +func TestVisualModePasteCharwise(t *testing.T) { + // ------------------------------------------------------------------------- + // Basic Visual Mode Paste with Charwise Register + // ------------------------------------------------------------------------- + + t.Run("vp replaces selection with charwise register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste + + m := getFinalModel(t, tm) + if m.Line(0) != "REPLACED world" { + t.Errorf("Line(0) = %q, want 'REPLACED world'", m.Line(0)) + } + }) + + t.Run("vp replaces single character", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"X"}), + ) + sendKeys(tm, "v", "p") // select "h", paste + + m := getFinalModel(t, tm) + if m.Line(0) != "Xello world" { + t.Errorf("Line(0) = %q, want 'Xello world'", m.Line(0)) + } + }) + + t.Run("vp replaces word in middle of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world goodbye"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), + WithRegister('"', action.CharwiseRegister, []string{"EARTH"}), + ) + sendKeys(tm, "v", "e", "p") // select "world", paste + + m := getFinalModel(t, tm) + if m.Line(0) != "hello EARTH goodbye" { + t.Errorf("Line(0) = %q, want 'hello EARTH goodbye'", m.Line(0)) + } + }) + + t.Run("vp replaces to end of line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), + WithRegister('"', action.CharwiseRegister, []string{"universe"}), + ) + sendKeys(tm, "v", "$", "p") // select "world", paste + + m := getFinalModel(t, tm) + if m.Line(0) != "hello universe" { + t.Errorf("Line(0) = %q, want 'hello universe'", m.Line(0)) + } + }) + + t.Run("vp with empty register deletes selection", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{""}), + ) + sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste empty + + m := getFinalModel(t, tm) + if m.Line(0) != " world" { + t.Errorf("Line(0) = %q, want ' world'", m.Line(0)) + } + }) + + // ------------------------------------------------------------------------- + // Visual Mode Paste Spanning Multiple Lines + // ------------------------------------------------------------------------- + + t.Run("vp replaces selection spanning multiple lines", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two", "line three"}), + WithCursorPos(action.Position{Line: 0, Col: 5}), + WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}), + ) + // v at (0,5), j goes to (1,5), h goes to (1,4) = space after "line" + // Selection: "one\nline " (from 'o' to space) + sendKeys(tm, "v", "j", "h", "p") + + m := getFinalModel(t, tm) + // Should join: "line " + "REPLACED" + "two" + if m.Line(0) != "line REPLACEDtwo" { + t.Errorf("Line(0) = %q, want 'line REPLACEDtwo'", m.Line(0)) + } + if m.LineCount() != 2 { + t.Errorf("LineCount() = %d, want 2", m.LineCount()) + } + }) + + // ------------------------------------------------------------------------- + // Cursor Position After Visual Paste + // ------------------------------------------------------------------------- + + t.Run("vp cursor at start of pasted content", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 6}), + WithRegister('"', action.CharwiseRegister, []string{"EARTH"}), + ) + sendKeys(tm, "v", "e", "p") // select "world", paste "EARTH" + + m := getFinalModel(t, tm) + // Cursor should be at start of pasted content (or end, depending on impl) + // In Vim, cursor goes to last char of pasted text + if m.CursorX() != 10 { + t.Errorf("CursorX() = %d, want 10", m.CursorX()) + } + }) + + t.Run("vp exits visual mode", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"X"}), + ) + sendKeys(tm, "v", "l", "p") + + m := getFinalModel(t, tm) + if m.Mode() != action.NormalMode { + t.Errorf("Mode() = %v, want NormalMode", m.Mode()) + } + }) + + // ------------------------------------------------------------------------- + // Visual Mode Paste with Linewise Register + // ------------------------------------------------------------------------- + + t.Run("vp with linewise register replaces selection", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"NEW LINE"}), + ) + sendKeys(tm, "v", "l", "l", "l", "l", "p") // select "hello", paste linewise content + + m := getFinalModel(t, tm) + // Linewise register content is pasted inline replacing the selection + if m.Line(0) != "NEW LINE world" { + t.Errorf("Line(0) = %q, want 'NEW LINE world'", m.Line(0)) + } + }) +} + +func TestVisualLinePaste(t *testing.T) { + // ------------------------------------------------------------------------- + // Visual Line Mode Paste + // ------------------------------------------------------------------------- + + t.Run("Vp replaces line with charwise register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two", "line three"}), + WithCursorPos(action.Position{Line: 1, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "V", "p") // select line two, paste + + m := getFinalModel(t, tm) + if m.LineCount() != 3 { + t.Errorf("LineCount() = %d, want 3", m.LineCount()) + } + if m.Line(1) != "REPLACED" { + t.Errorf("Line(1) = %q, want 'REPLACED'", m.Line(1)) + } + }) + + t.Run("Vp replaces line with linewise register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two", "line three"}), + WithCursorPos(action.Position{Line: 1, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"NEW LINE"}), + ) + sendKeys(tm, "V", "p") // select line two, paste + + m := getFinalModel(t, tm) + if m.LineCount() != 3 { + t.Errorf("LineCount() = %d, want 3", m.LineCount()) + } + if m.Line(1) != "NEW LINE" { + t.Errorf("Line(1) = %q, want 'NEW LINE'", m.Line(1)) + } + }) + + t.Run("Vp replaces multiple lines with linewise register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two", "line three", "line four"}), + WithCursorPos(action.Position{Line: 1, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "V", "j", "p") // select lines two and three, paste + + m := getFinalModel(t, tm) + if m.LineCount() != 3 { + t.Errorf("LineCount() = %d, want 3", m.LineCount()) + } + if m.Line(0) != "line one" { + t.Errorf("Line(0) = %q, want 'line one'", m.Line(0)) + } + if m.Line(1) != "REPLACED" { + t.Errorf("Line(1) = %q, want 'REPLACED'", m.Line(1)) + } + if m.Line(2) != "line four" { + t.Errorf("Line(2) = %q, want 'line four'", m.Line(2)) + } + }) + + t.Run("Vp replaces multiple lines with multiple linewise register lines", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two", "line three", "line four"}), + WithCursorPos(action.Position{Line: 1, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"NEW A", "NEW B"}), + ) + sendKeys(tm, "V", "j", "p") // select lines two and three, paste two lines + + m := getFinalModel(t, tm) + if m.LineCount() != 4 { + t.Errorf("LineCount() = %d, want 4", m.LineCount()) + } + if m.Line(0) != "line one" { + t.Errorf("Line(0) = %q, want 'line one'", m.Line(0)) + } + if m.Line(1) != "NEW A" { + t.Errorf("Line(1) = %q, want 'NEW A'", m.Line(1)) + } + if m.Line(2) != "NEW B" { + t.Errorf("Line(2) = %q, want 'NEW B'", m.Line(2)) + } + if m.Line(3) != "line four" { + t.Errorf("Line(3) = %q, want 'line four'", m.Line(3)) + } + }) + + t.Run("Vp on first line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"FIRST"}), + ) + sendKeys(tm, "V", "p") + + m := getFinalModel(t, tm) + if m.LineCount() != 2 { + t.Errorf("LineCount() = %d, want 2", m.LineCount()) + } + if m.Line(0) != "FIRST" { + t.Errorf("Line(0) = %q, want 'FIRST'", m.Line(0)) + } + if m.Line(1) != "line two" { + t.Errorf("Line(1) = %q, want 'line two'", m.Line(1)) + } + }) + + t.Run("Vp on last line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two"}), + WithCursorPos(action.Position{Line: 1, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"LAST"}), + ) + sendKeys(tm, "V", "p") + + m := getFinalModel(t, tm) + if m.LineCount() != 2 { + t.Errorf("LineCount() = %d, want 2", m.LineCount()) + } + if m.Line(0) != "line one" { + t.Errorf("Line(0) = %q, want 'line one'", m.Line(0)) + } + if m.Line(1) != "LAST" { + t.Errorf("Line(1) = %q, want 'LAST'", m.Line(1)) + } + }) + + t.Run("Vp exits visual line mode", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "V", "p") + + m := getFinalModel(t, tm) + if m.Mode() != action.NormalMode { + t.Errorf("Mode() = %v, want NormalMode", m.Mode()) + } + }) +} + +func TestVisualPasteRegisterBehavior(t *testing.T) { + // ------------------------------------------------------------------------- + // Register Behavior - deleted text should go to register + // ------------------------------------------------------------------------- + + t.Run("vp puts deleted text into register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.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] != "hello" { + t.Errorf("register content = %q, want 'hello'", reg.Content) + } + }) + + t.Run("Vp puts deleted line into register", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"line one", "line two"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.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] != "line one" { + t.Errorf("register content = %q, want 'line one'", reg.Content) + } + if reg.Type != action.LinewiseRegister { + t.Errorf("register type = %v, want LinewiseRegister", reg.Type) + } + }) +} + +func TestVisualPasteEdgeCases(t *testing.T) { + // ------------------------------------------------------------------------- + // Edge Cases + // ------------------------------------------------------------------------- + + t.Run("vp on empty line", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"", "line two"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"NEW"}), + ) + sendKeys(tm, "v", "p") + + m := getFinalModel(t, tm) + if m.Line(0) != "NEW" { + t.Errorf("Line(0) = %q, want 'NEW'", m.Line(0)) + } + }) + + t.Run("vp selecting entire line content", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello", "world"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "v", "$", "p") // select entire "hello" + + m := getFinalModel(t, tm) + if m.Line(0) != "REPLACED" { + t.Errorf("Line(0) = %q, want 'REPLACED'", m.Line(0)) + } + }) + + t.Run("vp with backwards selection", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"hello world"}), + WithCursorPos(action.Position{Line: 0, Col: 5}), + WithRegister('"', action.CharwiseRegister, []string{"X"}), + ) + sendKeys(tm, "v", "h", "h", "h", "h", "h", "p") // select backwards "hello ", paste + + m := getFinalModel(t, tm) + if m.Line(0) != "Xworld" { + t.Errorf("Line(0) = %q, want 'Xworld'", m.Line(0)) + } + }) + + t.Run("vp in single character file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"a"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.CharwiseRegister, []string{"XYZ"}), + ) + sendKeys(tm, "v", "p") + + m := getFinalModel(t, tm) + if m.Line(0) != "XYZ" { + t.Errorf("Line(0) = %q, want 'XYZ'", m.Line(0)) + } + }) + + t.Run("Vp on single line file", func(t *testing.T) { + tm := newTestModel(t, + WithLines([]string{"only line"}), + WithCursorPos(action.Position{Line: 0, Col: 0}), + WithRegister('"', action.LinewiseRegister, []string{"REPLACED"}), + ) + sendKeys(tm, "V", "p") + + m := getFinalModel(t, tm) + if m.LineCount() != 1 { + t.Errorf("LineCount() = %d, want 1", m.LineCount()) + } + if m.Line(0) != "REPLACED" { + t.Errorf("Line(0) = %q, want 'REPLACED'", m.Line(0)) + } + }) +} diff --git a/internal/input/keymap.go b/internal/input/keymap.go index 070c1f9..f4c2b89 100644 --- a/internal/input/keymap.go +++ b/internal/input/keymap.go @@ -38,7 +38,7 @@ func NewNormalKeymap() *Keymap { operators: map[string]action.Operator{ "d": operator.DeleteOperator{}, "y": operator.YankOperator{}, - // "c": ChangeOp{}, + "c": operator.ChangeOperator{}, // TODO: Finish implementing // "s": SubstitueOp{}, // "~": SwapCaseOp{}, }, @@ -92,6 +92,7 @@ func NewVisualKeymap() *Keymap { // "~": SwapCaseOp{}, }, actions: map[string]action.Action{ + "p": action.VisualPaste{Count: 1}, // ":": action.EnterComandMode{}, // Different OP }, }