package action import ( "strings" "git.gophernest.net/azpect/TextEditor/internal/core" tea "github.com/charmbracelet/bubbletea" ) // Paste implements Action (p) - pastes after cursor type Paste struct { Count int } // Paste.Execute: Pastes register content after the cursor position (p key). func (a Paste) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() // Get reg reg, found := m.GetRegister('"') if !found { out := core.CommandOutput{ Lines: []string{"\"\" register is broken. Uh oh."}, Inline: true, IsError: true, } m.SetCommandOutput(&out) return nil } // Exit if blank if len(reg.Content) == 0 { return nil } switch reg.Type { case core.LinewiseRegister: { initY := win.Cursor.Line lines := reg.Content insertPos := initY + 1 // Run count times for range a.Count { for _, line := range lines { buf.InsertLine(insertPos, line) insertPos++ } } if buf.LineCount() > 1 { win.SetCursorLine(initY + 1) } } case core.CharwiseRegister: { lines := reg.Content if len(lines) == 0 { break } x := win.Cursor.Col y := win.Cursor.Line curLine := buf.Lines[y] insertAt := min(x+1, len(curLine)) if len(lines) == 1 { // Single-line charwise paste cnt := strings.Repeat(lines[0], max(1, a.Count)) newLine := curLine[:insertAt] + cnt + curLine[insertAt:] buf.SetLine(y, newLine) win.SetCursorCol(x + len(cnt)) } else { // Multi-line charwise paste (e.g., from vi{ yank) suffix := curLine[insertAt:] // Save the part after cursor // For count > 1, we paste the content multiple times // Each paste continues from where the previous one ended var content strings.Builder for i := 0; i < a.Count; i++ { for j, line := range lines { if j > 0 { content.WriteString("\n") } content.WriteString(line) } } // Split the pasted content into lines pastedLines := strings.Split(content.String(), "\n") // First line: append to current line buf.SetLine(y, curLine[:insertAt]+pastedLines[0]) // Middle lines: insert as new lines for i := 1; i < len(pastedLines); i++ { buf.InsertLine(y+i, pastedLines[i]) } // Last line: append the suffix lastLineIdx := y + len(pastedLines) - 1 buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix) // Set cursor to end of last pasted content (before suffix) win.SetCursorLine(lastLineIdx) win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1) } } default: out := core.CommandOutput{ Lines: []string{"core.Register type is not implemented."}, Inline: true, IsError: true, } m.SetCommandOutput(&out) } return nil } // Ensure Paste implements Repeatable var _ Repeatable = Paste{} // Paste.WithCount: Returns a new Paste with the given count. func (a Paste) WithCount(n int) Action { return Paste{Count: n} } // PasteBefore implements Action (P) - pastes before cursor type PasteBefore struct { Count int } // PasteBefore.Execute: Pastes register content before the cursor position (P key). func (a PasteBefore) Execute(m Model) tea.Cmd { win := m.ActiveWindow() buf := m.ActiveBuffer() // Get reg reg, found := m.GetRegister('"') if !found { out := core.CommandOutput{ Lines: []string{"\"\" register is broken. Uh oh."}, Inline: true, IsError: true, } m.SetCommandOutput(&out) return nil } switch reg.Type { case core.LinewiseRegister: { initY := win.Cursor.Line 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 { buf.InsertLine(insertPos, line) insertPos++ } } } case core.CharwiseRegister: { lines := reg.Content if len(lines) == 0 { break } x := win.Cursor.Col y := win.Cursor.Line curLine := buf.Lines[y] insertAt := min(x, len(curLine)) if len(lines) == 1 { // Single-line charwise paste before cursor cnt := strings.Repeat(lines[0], max(1, a.Count)) newLine := curLine[:insertAt] + cnt + curLine[insertAt:] buf.SetLine(y, newLine) win.SetCursorCol(x + len(cnt)) } else { // Multi-line charwise paste before cursor suffix := curLine[insertAt:] // Save the part after cursor // For count > 1, we paste the content multiple times // Each paste continues from where the previous one ended var content strings.Builder for i := 0; i < a.Count; i++ { for j, line := range lines { if j > 0 { content.WriteString("\n") } content.WriteString(line) } } // Split the pasted content into lines pastedLines := strings.Split(content.String(), "\n") // First line: insert at cursor position buf.SetLine(y, curLine[:insertAt]+pastedLines[0]) // Middle lines: insert as new lines for i := 1; i < len(pastedLines); i++ { buf.InsertLine(y+i, pastedLines[i]) } // Last line: append the suffix lastLineIdx := y + len(pastedLines) - 1 buf.SetLine(lastLineIdx, buf.Lines[lastLineIdx]+suffix) // Set cursor to end of last pasted content (before suffix) win.SetCursorLine(lastLineIdx) win.SetCursorCol(len(buf.Lines[lastLineIdx]) - len(suffix) - 1) } } default: out := core.CommandOutput{ Lines: []string{"core.Register type is not implemented."}, Inline: true, IsError: true, } m.SetCommandOutput(&out) } return nil } // Ensure PasteBefore implements Repeatable var _ Repeatable = PasteBefore{} // PasteBefore.WithCount: Returns a new PasteBefore with the given count. 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 } // VisualPaste.Execute: Replaces visual selection with register content (p in visual mode). func (a VisualPaste) Execute(m Model) tea.Cmd { // Get register content to paste reg, found := m.GetRegister('"') if !found { out := core.CommandOutput{ Lines: []string{"\"\" register is broken. Uh oh."}, Inline: true, IsError: true, } m.SetCommandOutput(&out) return nil } mode := m.Mode() // Get selection bounds (normalize anchor and cursor) start, end := normalizeSelection(m) switch mode { case core.VisualMode: visualCharPaste(m, reg, start, end) case core.VisualBlockMode: visualBlockPaste(m, reg, start, end) case core.VisualLineMode: visualLinePaste(m, reg, start, end) } // Exit visual mode m.SetMode(core.NormalMode) return nil } // normalizeSelection: Returns start and end positions with start always before end. func normalizeSelection(m Model) (core.Position, core.Position) { win := m.ActiveWindow() start := core.Position{Line: win.Anchor.Line, Col: win.Anchor.Col} end := core.Position{Line: win.Cursor.Line, Col: win.Cursor.Col} // 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 operation in visual (character) mode. func visualCharPaste(m Model, reg core.Register, start, end core.Position) { win := m.ActiveWindow() buf := m.ActiveBuffer() // 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 == core.CharwiseRegister { // Charwise paste: insert text at cursor position if len(reg.Content) == 1 { line := buf.Lines[start.Line] insertAt := min(start.Col, len(line)) newLine := line[:insertAt] + reg.Content[0] + line[insertAt:] buf.SetLine(start.Line, newLine) // Cursor at end of pasted text win.SetCursorCol(insertAt + len(reg.Content[0]) - 1) win.SetCursorLine(start.Line) } } else if reg.Type == core.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 := buf.Lines[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:] } buf.SetLine(start.Line, newLine) } else { // Subsequent lines: insert new lines buf.InsertLine(start.Line+i, content) } } win.SetCursorLine(start.Line) win.SetCursorCol(start.Col) } // Update register with deleted text 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) { win := m.ActiveWindow() buf := m.ActiveBuffer() 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 := buf.Lines[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 := buf.Lines[y] if startCol >= len(line) { continue } ec := min(endCol+1, len(line)) buf.SetLine(y, line[:startCol]+line[ec:]) } // Insert register content if len(reg.Content) > 0 { pasteContent := reg.Content[0] if reg.Type == core.LinewiseRegister && len(reg.Content) > 0 { pasteContent = reg.Content[0] } for y := start.Line; y <= end.Line; y++ { line := buf.Lines[y] insertAt := min(startCol, len(line)) // Pad with spaces if needed for len(line) < insertAt { line += " " } newLine := line[:insertAt] + pasteContent + line[insertAt:] buf.SetLine(y, newLine) } } win.SetCursorLine(start.Line) win.SetCursorCol(startCol) // Update register with deleted block text (joined) 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) { win := m.ActiveWindow() buf := m.ActiveBuffer() // Extract deleted lines (for register) var deletedLines []string for y := start.Line; y <= end.Line; y++ { deletedLines = append(deletedLines, buf.Lines[y]) } // Delete the selected lines (from end to start to preserve indices) for y := end.Line; y >= start.Line; y-- { buf.DeleteLine(y) } // Insert register content if len(reg.Content) == 0 { // Empty register - ensure at least one empty line exists if buf.LineCount() == 0 { buf.InsertLine(0, "") } } else if reg.Type == core.LinewiseRegister { // Linewise register: insert each line insertPos := start.Line for _, content := range reg.Content { buf.InsertLine(insertPos, content) insertPos++ } } else { // Charwise register: insert as a single line buf.InsertLine(start.Line, reg.Content[0]) } // Ensure we have at least one line if buf.LineCount() == 0 { buf.InsertLine(0, "") } // core.Position cursor at start of pasted content y := start.Line if y >= buf.LineCount() { y = buf.LineCount() - 1 } win.SetCursorLine(y) win.SetCursorCol(0) // Update register with deleted lines m.UpdateDefaultRegister(core.LinewiseRegister, deletedLines) } // extractCharSelection: Extracts text from a character selection range. func extractCharSelection(m Model, start, end core.Position) string { buf := m.ActiveBuffer() if start.Line == end.Line { line := buf.Lines[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 := buf.Lines[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(buf.Lines[y]) result.WriteString("\n") } // Last line: from beginning to end.Col lastLine := buf.Lines[end.Line] endCol := min(end.Col+1, len(lastLine)) result.WriteString(lastLine[:endCol]) return result.String() } // deleteCharSelectionForPaste: Deletes a character selection for paste operations. func deleteCharSelectionForPaste(m Model, start, end core.Position) { win := m.ActiveWindow() buf := m.ActiveBuffer() if start.Line == end.Line { line := buf.Lines[start.Line] endCol := min(end.Col+1, len(line)) buf.SetLine(start.Line, line[:start.Col]+line[endCol:]) } else { startLine := buf.Lines[start.Line] endLine := buf.Lines[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-- { buf.DeleteLine(i) } buf.InsertLine(start.Line, prefix+suffix) } win.SetCursorLine(start.Line) win.SetCursorCol(start.Col) } // Ensure VisualPaste implements Repeatable var _ Repeatable = VisualPaste{} // VisualPaste.WithCount: Returns a new VisualPaste with the given count. func (a VisualPaste) WithCount(n int) Action { return VisualPaste{Count: n} }