diff options
Diffstat (limited to 'src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs')
-rw-r--r-- | src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs | 245 |
1 files changed, 135 insertions, 110 deletions
diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs index 77f526c..646a47e 100644 --- a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs @@ -8,159 +8,165 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { using System; - using System.Collections.Generic; - using System.Text; + using System.Diagnostics; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Operations; - using System.Diagnostics; - class TextBufferUndoManager : ITextBufferUndoManager, IDisposable + internal sealed class TextBufferUndoManager : ITextBufferUndoManager, IDisposable { #region Private Members - ITextBuffer _textBuffer; - ITextUndoHistoryRegistry _undoHistoryRegistry; - ITextUndoHistory _undoHistory; - Queue<ITextVersion> _editVersionList = new Queue<ITextVersion>(); - bool _inPostChanged; + private ITextBuffer _textBuffer; + private readonly ITextUndoHistoryRegistry _undoHistoryRegistry; + private ITextUndoHistory _undoHistory; + + // The plan had been to add the IUndoMetadataEditTag to allow people to create simple edits + // that would restore carets. That is being pushed back to 16.0 (maybe) but I didn't want to + // abandon the work in progress. +#if false + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; - #endregion + IEditorOperations _initiatingOperations = null; +#endif + ITextUndoTransaction _createdTransaction = null; +#endregion public TextBufferUndoManager(ITextBuffer textBuffer, ITextUndoHistoryRegistry undoHistoryRegistry) { if (textBuffer == null) { - throw new ArgumentNullException("textBuffer"); + throw new ArgumentNullException(nameof(textBuffer)); } if (undoHistoryRegistry == null) { - throw new ArgumentNullException("undoHistoryRegistry"); + throw new ArgumentNullException(nameof(undoHistoryRegistry)); } _textBuffer = textBuffer; - _undoHistoryRegistry = undoHistoryRegistry; +#if false + if (editorOperationsFactoryService == null) + { + throw new ArgumentNullException(nameof(editorOperationsFactoryService)); + } + + _editorOperationsFactoryService = editorOperationsFactoryService; +#endif + // Register the undo history - _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + this.EnsureTextBufferUndoHistory(); // Listen for the buffer changed events so that we can make them undo/redo-able + _textBuffer.Changing += TextBufferChanging; _textBuffer.Changed += TextBufferChanged; _textBuffer.PostChanged += TextBufferPostChanged; - _textBuffer.Changing += TextBufferChanging; } - #region Private Methods +#region Private Methods private void TextBufferChanged(object sender, TextContentChangedEventArgs e) { - Debug.Assert((e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive) || - (_undoHistory.State != TextUndoHistoryState.Idle), - "We are undoing/redoing a change while UndoHistory.State is Idle. Something is wrong with the state."); - - // If this change didn't originate from undo, add a TextBufferChangeUndoPrimitive to our history. - if (_undoHistory.State == TextUndoHistoryState.Idle && - (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + if (!(e.EditTag is IUndoEditTag)) { - // With projection, we sometimes get Changed events with no changes, or for "" -> "". - // We don't want to create undo actions for these. - bool nonNullChange = false; - foreach (ITextChange c in e.BeforeVersion.Changes) + if (this.TextBufferUndoHistory.State != TextUndoHistoryState.Idle) { - if (c.OldLength != 0 || c.NewLength != 0) + Debug.Fail("We are doing a normal edit in a non-idle undo state. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); + } + else + { + // With projection, we sometimes get Changed events with no changes, or for "" -> "". + // We don't want to create undo actions for these. + bool nonNullChange = false; + foreach (ITextChange c in e.BeforeVersion.Changes) { - nonNullChange = true; - break; + if (c.OldLength != 0 || c.NewLength != 0) + { + nonNullChange = true; + break; + } } - } - if (nonNullChange) - { - // Queue the edit, and actually add an undo primitive later (see comment on PostChanged). - _editVersionList.Enqueue(e.BeforeVersion); + if (nonNullChange) + { + // If there's an open undo transaction, add our edit (turned into a primitive) to it. Otherwise, create and undo transaction. + var currentTransaction = _undoHistory.CurrentTransaction; + if (currentTransaction == null) + { + // TODO remove this + // Hack to allow Cascade's local undo to light up if using v15.7 but behave using the old -- non-local -- undo before if running on 15.6. + // Cascade should really be marking its edits with IInvisibleEditTag (and will once it can take a hard requirement of VS 15.7). + if ((e.EditTag is IInvisibleEditTag) || ((e.EditTag != null) && (string.Equals(e.EditTag.ToString(), "CascadeRemoteEdit", StringComparison.Ordinal)))) + { + _createdTransaction = ((ITextUndoHistory2)_undoHistory).CreateInvisibleTransaction("<invisible>"); + } +#if false + else if (e.EditTag is IUndoMetadataEditTag metadata) + { + _createdTransaction = _undoHistory.CreateTransaction(metadata.Description); + if (_initiatingOperations == null) + { + var view = metadata.InitiatingView; + if (view != null) + { + _initiatingOperations = _editorOperationsFactoryService.GetEditorOperations(view); + _initiatingOperations.AddBeforeTextBufferChangePrimitive(); + } + } + } +#endif + else + { + _createdTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged); + } + + currentTransaction = _createdTransaction; + } + + currentTransaction.AddUndo(new TextBufferChangeUndoPrimitive(_undoHistory, e.BeforeVersion)); + } } } } - /// <remarks> - /// Edits are queued up by our TextBufferChanged handler and then we finally add them to the - /// undo stack here in response to PostChanged. The reason and history behind why we do this - /// is as follows: - /// - /// Originally this was done for VB commit, which uses undo events (i.e. TransactionCompleted) to - /// trigger commit. Their commit logic relies on the buffer being in a state such that applying - /// an edit synchronously raises a Changed event (which is always the case for PostChanged, but - /// not for Changed if there are nested edits). - /// - /// JaredPar made a change (CS 1182244) that allowed VB to detect that UndoTransactionCompleted - /// was being fired from a nested edit, and therefore delay the actual commit until the following - /// PostChanged event. - /// - /// So this allowed us to move TextBufferUndoManager back to adding undo actions directly - /// from the TextBufferChanged handler (CS 1285117). This is preferable, as otherwise there's a - /// "delay" between when the edit happens and when we record the edit on the undo stack, - /// allowing other people to stick something on the undo stack (i.e. from - /// their ITextBuffer.Changed handler) in between. The result is actions being "out-of-order" - /// on the undo stack. - /// - /// Unfortunately, it turns out VB snippets actually rely on this "out-of-order" behavior - /// (see Dev10 834740) and so we are forced to revert CS 1285117) and return to the model - /// where we queue up edits and delay adding them to the undo stack until PostChanged. - /// - /// It would be good to revisit this at again, but we would need to work with VB - /// to fix their snippets / undo behavior, and verify that VB commit is also unaffected. - /// </remarks> - private void TextBufferPostChanged(object sender, EventArgs e) + void TextBufferChanging(object sender, TextContentChangingEventArgs e) { - // Only process a top level PostChanged event. Nested events will continue to process TextChange events - // which are added to the queue and will be processed below - if ( _inPostChanged ) - { - return; - } - - _inPostChanged = true; - try + // Note that VB explicitly forces undo edits to happen while the history is idle so we need to allow this here + // by always doing nothing for undo edits). This may be a bug in our code (e.g. not properly cleaning up when + // an undo transaction is cancelled in mid-flight) but changing that will require coordination with Roslyn. + if (!(e.EditTag is IUndoEditTag)) { - // Do not do a foreach loop here. It's perfectly possible, and in fact expected, that the Complete - // method below can trigger a series of events which leads to a nested edit and another - // ITextBuffer::Changed. That event will add to the _editVersionList queue and hence break a - // foreach loop - while ( _editVersionList.Count > 0 ) + if (this.TextBufferUndoHistory.State != TextUndoHistoryState.Idle) { - var cur = _editVersionList.Dequeue(); - using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged)) - { - TextBufferChangeUndoPrimitive undoPrimitive = new TextBufferChangeUndoPrimitive(_undoHistory, cur); - undoTransaction.AddUndo(undoPrimitive); - - undoTransaction.Complete(); - } + Debug.Fail("We are doing a normal edit in a non-idle undo state. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); + e.Cancel(); } } - finally - { - _editVersionList.Clear(); // Ensure we cleanup state in the face of an exception - _inPostChanged = false; - } } - void TextBufferChanging(object sender, TextContentChangingEventArgs e) + private void TextBufferPostChanged(object sender, EventArgs e) { - // See if somebody (other than us) is trying to edit the buffer during undo/redo. - if (_undoHistory.State != TextUndoHistoryState.Idle && - (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + if (_createdTransaction != null) { - Debug.Fail("Attempt to edit the buffer during undo/redo has been denied. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); - e.Cancel(); +#if false + if (_initiatingOperations != null) + { + _initiatingOperations.AddAfterTextBufferChangePrimitive(); + } + + _initiatingOperations = null; +#endif + + _createdTransaction.Complete(); + _createdTransaction.Dispose(); + _createdTransaction = null; } } +#endregion - #endregion - - #region ITextBufferUndoManager Members +#region ITextBufferUndoManager Members public ITextBuffer TextBuffer { @@ -175,31 +181,50 @@ namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation // we are robust, always register the undo history. get { - _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); - return _undoHistory; + this.EnsureTextBufferUndoHistory(); + return _undoHistory; } } public void UnregisterUndoHistory() { // Unregister the undo history - _undoHistoryRegistry.RemoveHistory(_undoHistory); + if (_undoHistory != null) + { + _undoHistoryRegistry.RemoveHistory(_undoHistory); + _undoHistory = null; + } } - #endregion +#endregion + + private void EnsureTextBufferUndoHistory() + { + if (_textBuffer == null) + throw new ObjectDisposedException("TextBufferUndoManager"); + + // Note, right now, there is no way for us to know if an ITextUndoHistory + // has been unregistered (ie it can be unregistered by a third party) + // An issue has been logged with the Undo team, but in the mean time, to ensure that + // we are robust, always register the undo history. + _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + } - #region IDisposable Members +#region IDisposable Members public void Dispose() { - UnregisterUndoHistory(); - _textBuffer.Changed -= TextBufferChanged; - _textBuffer.PostChanged -= TextBufferPostChanged; - _textBuffer.Changing -= TextBufferChanging; + if (_textBuffer != null) + { + _textBuffer.PostChanged -= TextBufferPostChanged; + _textBuffer.Changed -= TextBufferChanged; + _textBuffer.Changing -= TextBufferChanging; + _textBuffer = null; + } GC.SuppressFinalize(this); } - #endregion +#endregion } } |