All checks were successful
Run Test Suite / test (push) Successful in 56s
Not sure if this is perfect, but it seems to be working
190 lines
5.1 KiB
Go
190 lines
5.1 KiB
Go
package operator
|
|
|
|
import (
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
"git.gophernest.net/azpect/TextEditor/internal/core"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// ChangeOperator implements Operator (c) - changes (deletes and enters insert mode) text.
|
|
type ChangeOperator struct{}
|
|
|
|
// ChangeOperator.Operate: Changes text based on the current mode and motion type.
|
|
func (o ChangeOperator) Operate(m action.Model, start, end core.Position, mtype core.MotionType) tea.Cmd {
|
|
switch m.Mode() {
|
|
case core.VisualMode:
|
|
changeCharSelection(m, start, end)
|
|
case core.VisualLineMode:
|
|
changeLineSelection(m, start, end)
|
|
case core.VisualBlockMode:
|
|
changeBlockSelection(m, start, end)
|
|
case core.NormalMode:
|
|
changeNormalMode(m, start, end, mtype)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// changeNormalMode: Changes text in normal mode based on motion type.
|
|
func changeNormalMode(m action.Model, start, end core.Position, mtype core.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 == core.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 == core.CharwiseExclusive {
|
|
m.SetMode(core.InsertMode)
|
|
return
|
|
}
|
|
// Exclusive motion: end position not included, so back up one
|
|
if mtype == core.CharwiseExclusive {
|
|
end.Col--
|
|
}
|
|
if end.Col >= start.Col {
|
|
changeCharSelection(m, start, end)
|
|
} else {
|
|
m.SetMode(core.InsertMode)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Charwise motion spanning multiple lines
|
|
changeCharSelection(m, start, end)
|
|
}
|
|
|
|
// changeCharSelection: Changes a character-wise selection and enters insert mode.
|
|
func changeCharSelection(m action.Model, start, end core.Position) {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
var deletedText string
|
|
|
|
if start.Line == end.Line {
|
|
line := buf.Line(start.Line)
|
|
endCol := min(end.Col+1, len(line))
|
|
deletedText = line[start.Col:endCol]
|
|
buf.SetLine(start.Line, line[:start.Col]+line[endCol:])
|
|
} else {
|
|
startLine := buf.Line(start.Line)
|
|
endLine := buf.Line(end.Line)
|
|
|
|
// Extract deleted text
|
|
deletedText = startLine[start.Col:] + "\n"
|
|
for y := start.Line + 1; y < end.Line; y++ {
|
|
deletedText += buf.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-- {
|
|
buf.DeleteLine(i)
|
|
}
|
|
buf.InsertLine(start.Line, prefix+suffix)
|
|
}
|
|
|
|
win.SetCursorLine(start.Line)
|
|
win.SetCursorCol(start.Col)
|
|
m.SetMode(core.InsertMode)
|
|
|
|
// Update register with deleted text
|
|
m.UpdateDefaultRegister(core.CharwiseRegister, []string{deletedText})
|
|
}
|
|
|
|
// changeLineSelection: Changes entire lines and enters insert mode.
|
|
func changeLineSelection(m action.Model, start, end core.Position) {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
var lines []string
|
|
|
|
for i := end.Line; i >= start.Line; i-- {
|
|
lines = append([]string{buf.Line(i)}, lines...)
|
|
buf.DeleteLine(i)
|
|
}
|
|
|
|
// Insert an empty line for editing
|
|
insertY := min(start.Line, buf.LineCount())
|
|
buf.InsertLine(insertY, "")
|
|
|
|
win.SetCursorLine(insertY)
|
|
win.SetCursorCol(0)
|
|
m.SetMode(core.InsertMode)
|
|
|
|
// Update registers
|
|
m.UpdateDefaultRegister(core.LinewiseRegister, lines)
|
|
}
|
|
|
|
// changeBlockSelection: Changes a rectangular block selection and enters insert mode.
|
|
func changeBlockSelection(m action.Model, start, end core.Position) {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
startCol := min(start.Col, end.Col)
|
|
endCol := max(start.Col, end.Col)
|
|
|
|
for y := start.Line; y <= end.Line; y++ {
|
|
line := buf.Line(y)
|
|
if startCol >= len(line) {
|
|
continue
|
|
}
|
|
ec := min(endCol+1, len(line))
|
|
buf.SetLine(y, line[:startCol]+line[ec:])
|
|
}
|
|
|
|
win.SetCursorLine(start.Line)
|
|
win.SetCursorCol(startCol)
|
|
m.SetMode(core.InsertMode)
|
|
}
|
|
|
|
// Verify ChangeOperator implements DoublePresser
|
|
var _ action.DoublePresser = ChangeOperator{}
|
|
|
|
// ChangeOperator.DoublePress: Handles cc - changes Count entire lines.
|
|
func (o ChangeOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
|
win := m.ActiveWindow()
|
|
buf := m.ActiveBuffer()
|
|
|
|
startY := win.Cursor.Line
|
|
|
|
// If we have a higher value than lines remaining, we can only run so many times
|
|
opCount := min(count, buf.LineCount()-startY)
|
|
|
|
var lines []string
|
|
|
|
// Collect lines to delete (always delete at startY since lines shift up)
|
|
for range opCount {
|
|
lines = append(lines, buf.Line(startY))
|
|
buf.DeleteLine(startY)
|
|
}
|
|
|
|
// Put deleted lines in register
|
|
m.UpdateDefaultRegister(core.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, buf.LineCount())
|
|
buf.InsertLine(insertY, "")
|
|
|
|
// Position cursor on the new empty line
|
|
win.SetCursorLine(insertY)
|
|
win.SetCursorCol(0)
|
|
m.SetMode(core.InsertMode)
|
|
|
|
return nil
|
|
}
|