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