Compare commits
2 Commits
37ad0ecb2b
...
db52b63db1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db52b63db1 | ||
|
|
63fec65be4 |
109
internal/action/change.go
Normal file
109
internal/action/change.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package action
|
||||||
|
|
||||||
|
import tea "github.com/charmbracelet/bubbletea"
|
||||||
|
|
||||||
|
// ChangeToEndOfLine implements Action (C) - changes from cursor to end of line
|
||||||
|
type ChangeToEndOfLine struct {
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ChangeToEndOfLine) Execute(m Model) tea.Cmd {
|
||||||
|
pos := m.CursorX()
|
||||||
|
line := m.Line(m.CursorY())
|
||||||
|
|
||||||
|
// Save deleted text to register
|
||||||
|
if pos < len(line) {
|
||||||
|
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos:]})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete to end of line
|
||||||
|
m.SetLine(m.CursorY(), line[:pos])
|
||||||
|
|
||||||
|
// Enter insert mode
|
||||||
|
m.SetMode(InsertMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure ChangeToEndOfLine implements Repeatable
|
||||||
|
var _ Repeatable = ChangeToEndOfLine{}
|
||||||
|
|
||||||
|
func (a ChangeToEndOfLine) WithCount(n int) Action {
|
||||||
|
return ChangeToEndOfLine{Count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubstituteChar implements Action (s) - deletes character and enters insert mode
|
||||||
|
type SubstituteChar struct {
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SubstituteChar) Execute(m Model) tea.Cmd {
|
||||||
|
pos := m.CursorX()
|
||||||
|
line := m.Line(m.CursorY())
|
||||||
|
|
||||||
|
// Calculate how many chars to delete (limited by line length)
|
||||||
|
count := min(a.Count, len(line)-pos)
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
// Save deleted text to register
|
||||||
|
m.UpdateDefaultRegister(CharwiseRegister, []string{line[pos : pos+count]})
|
||||||
|
|
||||||
|
// Delete the characters
|
||||||
|
m.SetLine(m.CursorY(), line[:pos]+line[pos+count:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter insert mode
|
||||||
|
m.SetMode(InsertMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure SubstituteChar implements Repeatable
|
||||||
|
var _ Repeatable = SubstituteChar{}
|
||||||
|
|
||||||
|
func (a SubstituteChar) WithCount(n int) Action {
|
||||||
|
return SubstituteChar{Count: n}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubstituteLine implements Action (S) - clears line and enters insert mode
|
||||||
|
type SubstituteLine struct {
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a SubstituteLine) Execute(m Model) tea.Cmd {
|
||||||
|
y := m.CursorY()
|
||||||
|
|
||||||
|
// Calculate how many lines to substitute
|
||||||
|
count := min(a.Count, m.LineCount()-y)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
// Collect and delete lines
|
||||||
|
for range count {
|
||||||
|
lines = append(lines, m.Line(y))
|
||||||
|
m.DeleteLine(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save deleted lines to register
|
||||||
|
m.UpdateDefaultRegister(LinewiseRegister, lines)
|
||||||
|
|
||||||
|
// Insert empty line at original position
|
||||||
|
insertY := min(y, m.LineCount())
|
||||||
|
m.InsertLine(insertY, "")
|
||||||
|
|
||||||
|
// Position cursor
|
||||||
|
m.SetCursorY(insertY)
|
||||||
|
m.SetCursorX(0)
|
||||||
|
|
||||||
|
// Enter insert mode
|
||||||
|
m.SetMode(InsertMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure SubstituteLine implements Repeatable
|
||||||
|
var _ Repeatable = SubstituteLine{}
|
||||||
|
|
||||||
|
func (a SubstituteLine) WithCount(n int) Action {
|
||||||
|
return SubstituteLine{Count: n}
|
||||||
|
}
|
||||||
@ -147,3 +147,281 @@ var _ Repeatable = PasteBefore{}
|
|||||||
func (a PasteBefore) WithCount(n int) Action {
|
func (a PasteBefore) WithCount(n int) Action {
|
||||||
return PasteBefore{Count: n}
|
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}
|
||||||
|
}
|
||||||
|
|||||||
1014
internal/editor/integration_operator_change_test.go
Normal file
1014
internal/editor/integration_operator_change_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -152,7 +152,10 @@ func (h *Handler) handleInitial(m action.Model, kind string, binding any, key st
|
|||||||
mtype = action.Linewise
|
mtype = action.Linewise
|
||||||
}
|
}
|
||||||
cmd := op.Operate(m, start, end, mtype)
|
cmd := op.Operate(m, start, end, mtype)
|
||||||
m.SetMode(action.NormalMode)
|
// Only reset to normal mode if operator didn't enter insert mode
|
||||||
|
if m.Mode() != action.InsertMode {
|
||||||
|
m.SetMode(action.NormalMode)
|
||||||
|
}
|
||||||
h.Reset()
|
h.Reset()
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func NewNormalKeymap() *Keymap {
|
|||||||
operators: map[string]action.Operator{
|
operators: map[string]action.Operator{
|
||||||
"d": operator.DeleteOperator{},
|
"d": operator.DeleteOperator{},
|
||||||
"y": operator.YankOperator{},
|
"y": operator.YankOperator{},
|
||||||
// "c": ChangeOp{},
|
"c": operator.ChangeOperator{}, // TODO: Finish implementing
|
||||||
// "s": SubstitueOp{},
|
// "s": SubstitueOp{},
|
||||||
// "~": SwapCaseOp{},
|
// "~": SwapCaseOp{},
|
||||||
},
|
},
|
||||||
@ -55,6 +55,9 @@ func NewNormalKeymap() *Keymap {
|
|||||||
"V": action.EnterVisualLineMode{},
|
"V": action.EnterVisualLineMode{},
|
||||||
"ctrl+v": action.EnterVisualBlockMode{},
|
"ctrl+v": action.EnterVisualBlockMode{},
|
||||||
"D": action.DeleteToEndOfLine{Count: 1},
|
"D": action.DeleteToEndOfLine{Count: 1},
|
||||||
|
"C": action.ChangeToEndOfLine{Count: 1},
|
||||||
|
"s": action.SubstituteChar{Count: 1},
|
||||||
|
"S": action.SubstituteLine{Count: 1},
|
||||||
"p": action.Paste{Count: 1},
|
"p": action.Paste{Count: 1},
|
||||||
"P": action.PasteBefore{Count: 1},
|
"P": action.PasteBefore{Count: 1},
|
||||||
},
|
},
|
||||||
@ -85,13 +88,10 @@ func NewVisualKeymap() *Keymap {
|
|||||||
"d": operator.DeleteOperator{},
|
"d": operator.DeleteOperator{},
|
||||||
"x": operator.DeleteOperator{},
|
"x": operator.DeleteOperator{},
|
||||||
"y": operator.YankOperator{},
|
"y": operator.YankOperator{},
|
||||||
// "c": ChangeOp{},
|
"c": operator.ChangeOperator{},
|
||||||
// "y": YankOp{},
|
|
||||||
// "p": PasteOp{},
|
|
||||||
// "s": SubstitueOp{},
|
|
||||||
// "~": SwapCaseOp{},
|
|
||||||
},
|
},
|
||||||
actions: map[string]action.Action{
|
actions: map[string]action.Action{
|
||||||
|
"p": action.VisualPaste{Count: 1},
|
||||||
// ":": action.EnterComandMode{}, // Different OP
|
// ":": action.EnterComandMode{}, // Different OP
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
173
internal/operator/change.go
Normal file
173
internal/operator/change.go
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package operator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implements Operator (c)
|
||||||
|
type ChangeOperator struct{}
|
||||||
|
|
||||||
|
func (o ChangeOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
||||||
|
switch m.Mode() {
|
||||||
|
case action.VisualMode:
|
||||||
|
changeCharSelection(m, start, end)
|
||||||
|
case action.VisualLineMode:
|
||||||
|
changeLineSelection(m, start, end)
|
||||||
|
case action.VisualBlockMode:
|
||||||
|
changeBlockSelection(m, start, end)
|
||||||
|
case action.NormalMode:
|
||||||
|
changeNormalMode(m, start, end, mtype)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeNormalMode(m action.Model, start, end action.Position, mtype action.MotionType) {
|
||||||
|
// Normalize so start is always before or equal to end
|
||||||
|
if start.Line > end.Line || (start.Line == end.Line && start.Col > end.Col) {
|
||||||
|
start, end = end, start
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linewise motions (j, k, G, gg) always operate on whole lines
|
||||||
|
if mtype == action.Linewise {
|
||||||
|
changeLineSelection(m, start, end)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charwise motions on same line
|
||||||
|
if start.Line == end.Line {
|
||||||
|
// No movement = nothing to change
|
||||||
|
if start.Col == end.Col && mtype == action.CharwiseExclusive {
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Exclusive motion: end position not included, so back up one
|
||||||
|
if mtype == action.CharwiseExclusive {
|
||||||
|
end.Col--
|
||||||
|
}
|
||||||
|
if end.Col >= start.Col {
|
||||||
|
changeCharSelection(m, start, end)
|
||||||
|
} else {
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charwise motion spanning multiple lines
|
||||||
|
changeCharSelection(m, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeCharSelection(m action.Model, start, end action.Position) {
|
||||||
|
var deletedText string
|
||||||
|
|
||||||
|
if start.Line == end.Line {
|
||||||
|
line := m.Line(start.Line)
|
||||||
|
endCol := min(end.Col+1, len(line))
|
||||||
|
deletedText = line[start.Col:endCol]
|
||||||
|
m.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
||||||
|
} else {
|
||||||
|
startLine := m.Line(start.Line)
|
||||||
|
endLine := m.Line(end.Line)
|
||||||
|
|
||||||
|
// Extract deleted text
|
||||||
|
deletedText = startLine[start.Col:] + "\n"
|
||||||
|
for y := start.Line + 1; y < end.Line; y++ {
|
||||||
|
deletedText += m.Line(y) + "\n"
|
||||||
|
}
|
||||||
|
endCol := min(end.Col+1, len(endLine))
|
||||||
|
deletedText += endLine[:endCol]
|
||||||
|
|
||||||
|
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)
|
||||||
|
m.ClampCursorX()
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
|
||||||
|
// Update register with deleted text
|
||||||
|
m.UpdateDefaultRegister(action.CharwiseRegister, []string{deletedText})
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeLineSelection(m action.Model, start, end action.Position) {
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
for i := end.Line; i >= start.Line; i-- {
|
||||||
|
lines = append([]string{m.Line(i)}, lines...)
|
||||||
|
m.DeleteLine(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert an empty line for editing
|
||||||
|
insertY := min(start.Line, m.LineCount())
|
||||||
|
m.InsertLine(insertY, "")
|
||||||
|
|
||||||
|
m.SetCursorY(insertY)
|
||||||
|
m.SetCursorX(0)
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
|
||||||
|
// Update registers
|
||||||
|
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func changeBlockSelection(m action.Model, start, end action.Position) {
|
||||||
|
startCol := min(start.Col, end.Col)
|
||||||
|
endCol := max(start.Col, end.Col)
|
||||||
|
|
||||||
|
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:])
|
||||||
|
}
|
||||||
|
|
||||||
|
m.SetCursorY(start.Line)
|
||||||
|
m.SetCursorX(startCol)
|
||||||
|
m.ClampCursorX()
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify ChangeOperator implements DoublePresser
|
||||||
|
var _ action.DoublePresser = ChangeOperator{}
|
||||||
|
|
||||||
|
// Double press handles cc - change the entire line
|
||||||
|
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
||||||
|
startY := m.CursorY()
|
||||||
|
|
||||||
|
// If we have a higher value than lines remaining, we can only run so many times
|
||||||
|
opCount := min(count, m.LineCount()-startY)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
// Collect lines to delete (always delete at startY since lines shift up)
|
||||||
|
for range opCount {
|
||||||
|
lines = append(lines, m.Line(startY))
|
||||||
|
m.DeleteLine(startY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put deleted lines in register
|
||||||
|
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||||
|
|
||||||
|
// Insert empty line at the original position for editing
|
||||||
|
// If we deleted everything, startY might be past end, so clamp it
|
||||||
|
insertY := min(startY, m.LineCount())
|
||||||
|
m.InsertLine(insertY, "")
|
||||||
|
|
||||||
|
// Position cursor on the new empty line
|
||||||
|
m.SetCursorY(insertY)
|
||||||
|
m.SetCursorX(0)
|
||||||
|
m.SetMode(action.InsertMode)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -52,7 +52,6 @@ func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
|||||||
|
|
||||||
// Put her in the register!
|
// Put her in the register!
|
||||||
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
|
||||||
// m.SetRegister('"', action.LinewiseRegister, lines)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user