Gim/internal/operator/delete.go
Hayden Hargreaves b1b3edf810 feat: this is huge, and needs some review, but for now its good
The tests are not passing, something to do with view I think.
2026-02-26 23:18:18 -07:00

167 lines
3.8 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
}
// Verify DeleteOperator implements DoublePresser
var _ action.DoublePresser = DeleteOperator{}
// Double press handles dd - delete the entire line
func (o DeleteOperator) DoublePress(m action.Model, count int) tea.Cmd {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
// If we have a higher value than lines remaining, we can only run so many times
opCount := min(count, buf.LineCount()-win.Cursor.Line)
var lines []string
for range opCount {
y := win.Cursor.Line
lines = append(lines, buf.Lines[y])
buf.DeleteLine(y)
if buf.LineCount() == 0 {
buf.InsertLine(0, "")
}
if y >= buf.LineCount() {
y = buf.LineCount() - 1
}
win.SetCursorLine(y)
}
// Put her in the register!
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
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 && mtype == action.CharwiseExclusive {
return
}
// Exclusive motion: end position not included, so back up one
if mtype == action.CharwiseExclusive {
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) {
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 := 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)
}
func deleteLineSelection(m action.Model, start, end action.Position) {
win := m.ActiveWindow()
buf := m.ActiveBuffer()
var lines []string
for i := end.Line; i >= start.Line; i-- {
lines = append(lines, buf.Lines[i])
buf.DeleteLine(i)
}
if buf.LineCount() == 0 {
buf.InsertLine(0, "")
}
y := start.Line
if y >= buf.LineCount() {
y = buf.LineCount() - 1
}
win.SetCursorLine(y)
// Update registers
m.UpdateDefaultRegister(action.LinewiseRegister, lines)
}
func deleteBlockSelection(m action.Model, start, end action.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.Lines[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)
}