//
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
//
// This file contain implementations details that are subject to change without notice.
// Use at your own risk.
//
namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation
{
using System;
using System.Diagnostics;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Operations;
///
/// The UndoPrimitive for a text buffer change operation.
///
public class TextBufferChangeUndoPrimitive : TextUndoPrimitive, IEditOnlyTextUndoPrimitive
{
#region Private Data Members
private bool _canUndo;
private readonly ITextUndoHistory _undoHistory;
private WeakReference _weakBufferReference;
public INormalizedTextChangeCollection Changes { get; }
public int? BeforeReiteratedVersionNumber { get; private set; }
public int? AfterReiteratedVersionNumber { get; private set; }
#if DEBUG
private int _bufferLengthAfterChange;
#endif
#endregion // Private Data Members
///
/// Constructs a TextBufferChangeUndoPrimitive.
///
///
/// The ITextUndoHistory this change will be added to.
///
///
/// The representing this change.
/// This is actually the version associated with the snapshot prior to the change.
///
/// is null.
/// is null.
public TextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, ITextVersion textVersion)
{
// Verify input parameters
if (undoHistory == null)
{
throw new ArgumentNullException(nameof(undoHistory));
}
if (textVersion == null)
{
throw new ArgumentNullException(nameof(textVersion));
}
this.Changes = textVersion.Changes;
this.BeforeReiteratedVersionNumber = textVersion.ReiteratedVersionNumber;
this.AfterReiteratedVersionNumber = textVersion.Next.VersionNumber;
Debug.Assert(textVersion.Next.VersionNumber == textVersion.Next.ReiteratedVersionNumber,
"Creating a TextBufferChangeUndoPrimitive for a change that has previously been undone? This is probably wrong.");
_undoHistory = undoHistory;
TextBuffer = textVersion.TextBuffer;
AttachedToNewBuffer = false;
_canUndo = true;
#if DEBUG
// for debug sanity checks
_bufferLengthAfterChange = textVersion.Next.Length;
#endif
}
#region UndoPrimitive Members
///
/// Returns true if operation can be undone, false otherwise.
///
public override bool CanUndo
{
get
{
// NOTE: We don't know for sure if we can undo (it might get blocked by a readonly region or a
// canceled edit), in which case the actual Undo() will fail.
return _canUndo;
}
}
///
/// Returns true if operation can be redone, false otherwise.
///
public override bool CanRedo
{
get
{
// NOTE: We don't know for sure if we can redo (it might get blocked by a readonly region or a
// canceled edit), in which case the actual Do() will fail.
return !_canUndo;
}
}
///
/// Redo the text buffer change action.
///
/// Operation cannot be redone.
public override void Do()
{
// Validate, we shouldn't be allowed to undo
if (!CanRedo)
{
throw new InvalidOperationException(Strings.CannotRedo);
}
// For undo-in-closed-files scenarios where we are done/undone on a buffer other
// than the one we were originally created on.
if (AttachedToNewBuffer)
{
AttachedToNewBuffer = false;
this.BeforeReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber;
this.AfterReiteratedVersionNumber = null;
}
bool editCanceled = false;
using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, this.AfterReiteratedVersionNumber, UndoTag.Tag))
{
foreach (ITextChange textChange in this.Changes)
{
if (!edit.Replace(new Span(textChange.OldPosition, textChange.OldLength), textChange.NewText))
{
// redo canceled by readonly region
editCanceled = true;
break;
}
}
if (!editCanceled)
{
edit.Apply();
if (edit.Canceled)
{
editCanceled = true;
}
}
}
if (editCanceled)
{
throw new OperationCanceledException("Redo failed due to readonly regions or canceled edit.");
}
if (this.AfterReiteratedVersionNumber == null)
{
this.AfterReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber;
}
#if DEBUG
// sanity check
Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange,
"The buffer is in a different state than when this TextBufferChangeUndoPrimitive was created!");
#endif
_canUndo = true;
}
///
/// Undo the text buffer change action.
///
/// Operation cannot be undone.
public override void Undo()
{
// Validate that we can undo this change
if (!CanUndo)
{
throw new InvalidOperationException(Strings.CannotUndo);
}
#if DEBUG
// sanity check
Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange,
"The buffer is in a different state than when this TextBufferUndoChangePrimitive was created!");
#endif
// For undo-in-closed-files scenarios where we are done/undone on a buffer other
// than the one we were originally created on.
if (AttachedToNewBuffer)
{
AttachedToNewBuffer = false;
this.BeforeReiteratedVersionNumber = null;
this.AfterReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber;
}
bool editCanceled = false;
using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, this.BeforeReiteratedVersionNumber, UndoTag.Tag))
{
foreach (ITextChange textChange in this.Changes)
{
if (!edit.Replace(new Span(textChange.NewPosition, textChange.NewLength), textChange.OldText))
{
// undo canceled by readonly region
editCanceled = true;
break;
}
}
if (!editCanceled)
{
edit.Apply();
if (edit.Canceled)
{
editCanceled = true;
}
}
}
if (editCanceled)
{
throw new OperationCanceledException("Undo failed due to readonly regions or canceled edit.");
}
if (this.BeforeReiteratedVersionNumber == null)
{
this.BeforeReiteratedVersionNumber = TextBuffer.CurrentSnapshot.Version.VersionNumber;
}
_canUndo = false;
}
public override bool CanMerge(ITextUndoPrimitive older)
{
return false;
}
#endregion
#region Private Helpers
///
/// We track our ITextBuffer in ITextUndoHistory.Properties so that we can be redirected to act on a
/// different ITextBuffer in the undo-in-closed-files scenario.
///
private ITextBuffer TextBuffer
{
get
{
ITextBuffer buffer;
if (!_undoHistory.Properties.TryGetProperty(typeof(ITextBuffer), out buffer))
{
Debug.Assert(false);
throw new InvalidOperationException("ITextUndoHistory.Properties must contain an entry for the ITextBuffer this TextBufferChangeUndoPrimitive should act against.");
}
return buffer;
}
set
{
_undoHistory.Properties[typeof(ITextBuffer)] = value;
}
}
private bool AttachedToNewBuffer
{
get
{
return _weakBufferReference.Target != TextBuffer;
}
set
{
if (value != false)
{
throw new InvalidOperationException("AttachedToNewBuffer can only be reset to false.");
}
Debug.Assert(TextBuffer != null);
_weakBufferReference = new WeakReference(TextBuffer);
}
}
#endregion
internal class UndoTag : IUndoEditTag
{
public static readonly UndoTag Tag = new UndoTag();
}
}
}