All checks were successful
Run Test Suite / test (push) Successful in 46s
We means me and Claude (heavy on the Claude). Originally, if we copied a many line segment into a charwise register, the paste op would error, this is not right, it should paste, just differently.
536 lines
14 KiB
Go
536 lines
14 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
|
|
|
|
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}
|
|
}
|