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, s, R) - 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 }