using System; using System.Collections.Generic; namespace codelanguage; /// Classification of an edit, used for typing coalescing. public enum EditKind { /// Anything else (paste, enter, block indent, replace…). Other, /// A single character typed at the caret. InsertChar, /// A single character removed before the caret (Backspace). DeleteBackward, /// A single character removed after the caret (Delete). DeleteForward, } /// Immutable snapshot of the buffer plus caret/selection state. public sealed record EditorSnapshot( string Text, int CaretLine, int CaretColumn, bool HasSelection, int SelStartLine, int SelStartCol, int SelEndLine, int SelEndCol); /// /// Snapshot-based undo/redo history with typing coalescing: consecutive single /// character inserts (or single character backspaces/deletes) performed one /// after another collapse into one undo step, so undoing after typing a word /// reverts the whole word instead of one letter. /// public sealed class UndoStack { private const int MaxDepth = 200; private readonly List _undo = new(); private readonly List _redo = new(); private EditKind _mergeKind = EditKind.Other; private int _mergeLine; private int _mergeCol; private bool _hasMergeAnchor; public bool CanUndo => _undo.Count > 0; public bool CanRedo => _redo.Count > 0; public int UndoCount => _undo.Count; public int RedoCount => _redo.Count; public void Clear() { _undo.Clear(); _redo.Clear(); ResetMerge(); } /// /// Records the state from before an edit that was just applied. /// plus the caret position after the edit decide /// whether this step merges into the previous one. Any new edit clears the /// redo stack. /// public void Push(EditorSnapshot before, EditKind kind, int caretLineAfter, int caretColumnAfter) { bool canMerge = _hasMergeAnchor && kind != EditKind.Other && kind == _mergeKind && !before.HasSelection && before.CaretLine == _mergeLine && before.CaretColumn == _mergeCol; if (!canMerge) { _undo.Add(before); if (_undo.Count > MaxDepth) _undo.RemoveAt(0); } _redo.Clear(); if (kind == EditKind.Other) { ResetMerge(); } else { _mergeKind = kind; _mergeLine = caretLineAfter; _mergeCol = caretColumnAfter; _hasMergeAnchor = true; } } /// /// Undoes the most recent step and returns the snapshot to restore, or null /// when history is empty. becomes the redo state. /// public EditorSnapshot? Undo(EditorSnapshot current) { if (_undo.Count == 0) return null; _redo.Add(current); var snapshot = _undo[^1]; _undo.RemoveAt(_undo.Count - 1); ResetMerge(); return snapshot; } /// /// Redoes the most recent undo and returns the snapshot to restore, or null /// when the redo stack is empty. becomes the undo state. /// public EditorSnapshot? Redo(EditorSnapshot current) { if (_redo.Count == 0) return null; _undo.Add(current); var snapshot = _redo[^1]; _redo.RemoveAt(_redo.Count - 1); ResetMerge(); return snapshot; } private void ResetMerge() { _mergeKind = EditKind.Other; _hasMergeAnchor = false; _mergeLine = 0; _mergeCol = 0; } }