feat: fixed and finished pasting in visual mode, testing
This commit is contained in:
parent
37ad0ecb2b
commit
63fec65be4
@ -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}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user