Gim/internal/action/paste.go
Hayden Hargreaves 10e37b82af
All checks were successful
Run Test Suite / test (push) Successful in 13s
feat: implemented the command window! Not tested. Maybe we need some?
2026-03-14 23:13:59 -07:00

482 lines
12 KiB
Go

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
// Shouldn't happen, just a check
if len(lines) != 1 {
out := core.CommandOutput{
Lines: []string{"Charwise register should only have a single line of content."},
Inline: true,
IsError: true,
}
m.SetCommandOutput(&out)
break
}
x := win.Cursor.Col
y := win.Cursor.Line
cnt := strings.Repeat(lines[0], max(1, a.Count))
curLine := buf.Lines[y]
// Catch edge cases, end of line, start of blank line
insertAt := min(x+1, len(curLine))
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
buf.SetLine(y, newLine)
win.SetCursorCol(x + len(cnt))
}
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
// Shouldn't happen, just a check
if len(lines) != 1 {
out := core.CommandOutput{
Lines: []string{"Charwise register should only have a single line of content."},
Inline: true,
IsError: true,
}
m.SetCommandOutput(&out)
break
}
x := win.Cursor.Col
y := win.Cursor.Line
cnt := strings.Repeat(lines[0], max(1, a.Count))
curLine := buf.Lines[y]
// Catch edge cases, end of line, start of blank line
insertAt := min(x, len(curLine))
newLine := curLine[:insertAt] + cnt + curLine[insertAt:]
buf.SetLine(y, newLine)
win.SetCursorCol(x + len(cnt))
}
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}
}