package action import ( "fmt" "strings" tea "github.com/charmbracelet/bubbletea" ) // Paste implements Action (p) - pastes after cursor type Paste struct { Count int } func (a Paste) Execute(m Model) tea.Cmd { // Get reg reg, found := m.GetRegister('"') if !found { m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh.")) return nil } // Exit if blank if len(reg.Content) == 0 { return nil } switch reg.Type { case LinewiseRegister: { initY := m.CursorY() lines := reg.Content insertPos := initY + 1 // Run count times for range a.Count { for _, line := range lines { m.InsertLine(insertPos, line) insertPos++ } } if m.LineCount() > 1 { m.SetCursorY(initY + 1) } } case CharwiseRegister: { lines := reg.Content // Shouldn't happen, just a check if len(lines) != 1 { m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content.")) break } x := m.CursorX() y := m.CursorY() cnt := strings.Repeat(lines[0], max(1, a.Count)) curLine := m.Line(y) // Catch edge cases, end of line, start of blank line insertAt := min(x+1, len(curLine)) newLine := curLine[:insertAt] + cnt + curLine[insertAt:] m.SetLine(y, newLine) m.SetCursorX(x + len(cnt)) m.ClampCursorX() } default: m.SetCommandError(fmt.Errorf("Register type is not implemented.")) } return nil } // Ensure Paste implements Repeatable var _ Repeatable = Paste{} func (a Paste) WithCount(n int) Action { return Paste{Count: n} } // PasteBefore implements Action (P) - pastes before cursor type PasteBefore struct { Count int } func (a PasteBefore) Execute(m Model) tea.Cmd { // Get reg reg, found := m.GetRegister('"') if !found { m.SetCommandError(fmt.Errorf("\"\" register is broken. Uh oh.")) return nil } switch reg.Type { case LinewiseRegister: { initY := m.CursorY() lines := reg.Content insertPos := initY // Leave here, this will effectively move the lines below // Run count times for range a.Count { for _, line := range lines { m.InsertLine(insertPos, line) insertPos++ } } } case CharwiseRegister: { lines := reg.Content // Shouldn't happen, just a check if len(lines) != 1 { m.SetCommandError(fmt.Errorf("Charwise register should only have a single line of content.")) break } x := m.CursorX() y := m.CursorY() cnt := strings.Repeat(lines[0], max(1, a.Count)) curLine := m.Line(y) // Catch edge cases, end of line, start of blank line insertAt := min(x, len(curLine)) newLine := curLine[:insertAt] + cnt + curLine[insertAt:] m.SetLine(y, newLine) m.SetCursorX(x + len(cnt)) m.ClampCursorX() } default: m.SetCommandError(fmt.Errorf("Register type is not implemented.")) } return nil } // Ensure PasteBefore implements Repeatable 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} }