package editor import tea "github.com/charmbracelet/bubbletea" // Undo/redo is snapshot-based: each checkpoint captures the buffer, cursor, // scroll, and selection anchor before a change. Consecutive typing coalesces // into one group; every structural op (newline, delete, kill, paste, cut) is // its own group, per TASK-006. // maxUndo bounds the history so a long session can't grow it without limit. const maxUndo = 500 // editKind classifies a key for grouping. kindType groups coalesce; kindStructural // groups never do. type editKind int const ( kindNone editKind = iota kindType kindStructural ) // snapshot is a restorable point-in-time copy of the editable state. type snapshot struct { lines []string cursor Position scroll int anchor *Position } func (e *Editor) snapshot() snapshot { ls := make([]string, len(e.Lines)) copy(ls, e.Lines) var a *Position if e.anchor != nil { c := *e.anchor a = &c } return snapshot{lines: ls, cursor: e.Cursor, scroll: e.Scroll, anchor: a} } func (e *Editor) restore(s snapshot) { ls := make([]string, len(s.lines)) copy(ls, s.lines) e.Lines = ls e.invalidate() e.Cursor = s.cursor e.Scroll = s.scroll if s.anchor != nil { c := *s.anchor e.anchor = &c } else { e.anchor = nil } e.Dirty = true e.setGoal() e.followCursor() } // sameContent reports whether the buffer text matches s (cursor/selection ignored). func (e *Editor) sameContent(s snapshot) bool { if len(s.lines) != len(e.Lines) { return false } for i := range s.lines { if s.lines[i] != e.Lines[i] { return false } } return true } // pushUndoSnapshot appends s to the bounded undo stack. func (e *Editor) pushUndoSnapshot(s snapshot) { e.undo = append(e.undo, s) if len(e.undo) > maxUndo { e.undo = e.undo[len(e.undo)-maxUndo:] } } // resetHistory clears all undo/redo state (called when the buffer is replaced). func (e *Editor) resetHistory() { e.undo = nil e.redo = nil e.lastKind = kindNone } // PushUndo records the current state as a structural checkpoint. The app calls // it before driving an edit through InsertText/DeleteSelection (paste, cut). func (e *Editor) PushUndo() { e.pushUndoSnapshot(e.snapshot()) e.redo = nil e.lastKind = kindStructural } // Undo reverts the most recent edit group, restoring cursor and selection. func (e *Editor) Undo() { if len(e.undo) == 0 { return } cur := e.snapshot() s := e.undo[len(e.undo)-1] e.undo = e.undo[:len(e.undo)-1] e.redo = append(e.redo, cur) e.restore(s) e.lastKind = kindNone } // Redo reapplies the most recently undone edit group. func (e *Editor) Redo() { if len(e.redo) == 0 { return } cur := e.snapshot() s := e.redo[len(e.redo)-1] e.redo = e.redo[:len(e.redo)-1] e.pushUndoSnapshot(cur) e.restore(s) e.lastKind = kindNone } // editKindOf reports a key's grouping kind and whether it mutates the buffer. func editKindOf(k tea.KeyMsg) (editKind, bool) { switch k.Type { case tea.KeyRunes: if k.Alt { switch string(k.Runes) { case "d": // Alt+D deletes the next word return kindStructural, true case "s", "i", "c", "k": // inline markdown formatting (TASK-009) return kindStructural, true case "x": // toggle the checkbox on the current line (TASK-023) return kindStructural, true } return kindNone, false // Alt+b/f are word motions } return kindType, true case tea.KeySpace: return kindType, true case tea.KeyEnter, tea.KeyTab, tea.KeyShiftTab: return kindStructural, true case tea.KeyUp, tea.KeyDown: if k.Alt { // Alt+Up/Down move (reorder) the current line (TASK-024) return kindStructural, true } return kindNone, false case tea.KeyBackspace, tea.KeyDelete, tea.KeyCtrlU, tea.KeyCtrlK, tea.KeyCtrlW: return kindStructural, true default: return kindNone, false } }