Editor undo system types. Defines UndoScope which represents a nested undo capture that commits on dispose, UndoEntry storing before/after snapshots and metadata, and UndoHistoryItem used for presenting the history list.
using Editor.Prism.Core;
namespace Editor.Prism.Undo;
/// <summary>
/// One bracketed edit. Opening a scope captures the document as it is now; disposing it captures the
/// document as it became and turns the pair into a single undo entry.
/// <para>
/// Scopes nest. Only the outermost one produces an entry, so a compound operation — paste, which adds
/// nodes and then edges and then parameters — is one step in the History panel rather than a dozen.
/// The nesting depth is asserted on the way out: an unbalanced dispose is reported loudly and then
/// repaired, because the alternative is a silently corrupt stack that only shows up three edits later.
/// </para>
/// </summary>
public sealed class UndoScope : IDisposable
{
PrismUndoStack _stack;
internal UndoScope( PrismUndoStack stack, string name, int depth )
{
_stack = stack;
Name = name;
Depth = depth;
}
/// <summary>The label this edit shows in the History panel.</summary>
public string Name { get; }
/// <summary>Nesting depth. Zero is the outermost scope, which is the one that commits.</summary>
public int Depth { get; }
/// <summary>True when this is the scope that will produce the undo entry.</summary>
public bool IsOutermost => Depth == 0;
/// <summary>True once <see cref="Cancel"/> has been called.</summary>
public bool Cancelled { get; private set; }
/// <summary>True once the scope has been disposed.</summary>
public bool Disposed => _stack is null;
/// <summary>
/// Abandon this edit: the pending snapshot is discarded and no entry is produced. The document is
/// <em>not</em> rolled back — cancel means "this was not worth recording", not "undo it".
/// </summary>
public void Cancel()
{
Cancelled = true;
if ( _stack is null ) return;
_stack.CancelPending();
_stack = null;
}
/// <summary>Close the scope, committing the edit when this is the outermost one.</summary>
public void Dispose()
{
var stack = _stack;
if ( stack is null ) return;
_stack = null;
if ( Cancelled ) return;
stack.EndScope( this );
}
/// <inheritdoc/>
public override string ToString() => $"UndoScope '{Name}' depth {Depth}{( Cancelled ? " (cancelled)" : "" )}";
/// <summary>A scope that records nothing, returned when there is no undo stack to record into.</summary>
public static readonly IDisposable None = new NullScope();
sealed class NullScope : IDisposable
{
public void Dispose() { }
}
}
/// <summary>One recorded edit: the document before it and the document after it.</summary>
public sealed class UndoEntry
{
/// <summary>The label shown in menus and in the History panel.</summary>
public string Name { get; init; } = "Edit";
/// <summary>The serialized document as it was before the edit.</summary>
public string Before { get; internal set; }
/// <summary>The serialized document as it was after the edit.</summary>
public string After { get; internal set; }
/// <summary>When the edit was committed. Drives coalescing and the History panel's timestamps.</summary>
public DateTime Time { get; internal set; } = DateTime.UtcNow;
/// <summary>How many characters this entry holds, so the stack's memory cost is inspectable.</summary>
public int Size => ( Before?.Length ?? 0 ) + ( After?.Length ?? 0 );
/// <inheritdoc/>
public override string ToString() => $"{Name} ({Size} chars)";
}
/// <summary>One row of the History panel's projection of the undo stack.</summary>
public readonly record struct UndoHistoryItem( int Level, string Name, bool IsCurrent, DateTime Time, int Size )
{
/// <summary>True when this row is the document's original state rather than an edit.</summary>
public bool IsBaseline => Level == 0;
/// <inheritdoc/>
public override string ToString() => $"{( IsCurrent ? "> " : " " )}{Level}. {Name}";
}