Not totally complete WRT tests, but lots of progress. These interfaces make everything easy.
141 lines
3.1 KiB
Go
141 lines
3.1 KiB
Go
package operator
|
|
|
|
import (
|
|
"git.gophernest.net/azpect/TextEditor/internal/action"
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// Implements Operator (d)
|
|
type DeleteOperator struct{}
|
|
|
|
func (o DeleteOperator) Operate(m action.Model, start, end action.Position, mtype action.MotionType) tea.Cmd {
|
|
switch m.Mode() {
|
|
case action.VisualMode:
|
|
deleteCharSelection(m, start, end)
|
|
case action.VisualLineMode:
|
|
deleteLineSelection(m, start, end)
|
|
case action.VisualBlockMode:
|
|
deleteBlockSelection(m, start, end)
|
|
case action.NormalMode:
|
|
deleteNormalMode(m, start, end, mtype)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Double press handles dd - delete the entire line
|
|
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
|
|
// If we have a higher value than lines remaining, we can only run so many times
|
|
opCount := min(count, m.LineCount()-m.CursorY())
|
|
|
|
for range opCount {
|
|
y := m.CursorY()
|
|
m.DeleteLine(y)
|
|
|
|
if m.LineCount() == 0 {
|
|
m.InsertLine(0, "")
|
|
}
|
|
|
|
if y >= m.LineCount() {
|
|
y = m.LineCount() - 1
|
|
}
|
|
|
|
m.SetCursorY(y)
|
|
m.ClampCursorX()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteNormalMode(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 {
|
|
deleteLineSelection(m, start, end)
|
|
return
|
|
}
|
|
|
|
// Charwise motions on same line
|
|
if start.Line == end.Line {
|
|
// No movement = nothing to delete
|
|
if start.Col == end.Col {
|
|
return
|
|
}
|
|
// Exclusive motion: delete [start.Col, end.Col)
|
|
end.Col--
|
|
if end.Col >= start.Col {
|
|
deleteCharSelection(m, start, end)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Charwise motion spanning multiple lines (e.g., d/search)
|
|
deleteCharSelection(m, start, end)
|
|
}
|
|
|
|
func deleteCharSelection(m action.Model, start, end action.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 := 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()
|
|
}
|
|
|
|
func deleteLineSelection(m action.Model, start, end action.Position) {
|
|
for i := end.Line; i >= start.Line; i-- {
|
|
m.DeleteLine(i)
|
|
}
|
|
|
|
if m.LineCount() == 0 {
|
|
m.InsertLine(0, "")
|
|
}
|
|
|
|
y := start.Line
|
|
if y >= m.LineCount() {
|
|
y = m.LineCount() - 1
|
|
}
|
|
|
|
m.SetCursorY(y)
|
|
m.ClampCursorX()
|
|
}
|
|
|
|
func deleteBlockSelection(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()
|
|
}
|