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) }