diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs b/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs index b75ccfe057becbae3cc5cd1fb3acd9c8253d4582..18f587e80c05d7327759ac0599469cc964c67f19 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs +++ b/src/InteractiveWindow/Editor/InteractiveWindow.UIThreadOnly.cs @@ -475,9 +475,26 @@ private void AppendLineNoPromptInjection(ITextBuffer buffer) } /// Implements . - internal void TypeChar(char typedChar) + public void TypeChar(char typedChar) { - InsertText(typedChar.ToString()); + using (var transaction = UndoHistory?.CreateTransaction(InteractiveWindowResources.TypeChar)) + { + if (transaction != null) + { + var mergeDirections = TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward; + // replacing selected text should be an atomic undo operation). + if ((!TextView.Selection.IsEmpty && !IsEmptyBoxSelection())) + { + mergeDirections = TextTransactionMergeDirections.Forward; + } + transaction.MergePolicy = new TextTransactionMergePolicy(mergeDirections); + } + + if (InsertText(typedChar.ToString())) + { + transaction?.Complete(); + } + } } /// Implements . @@ -498,44 +515,39 @@ public void InsertCode(string text) } } - private void InsertText(string text) + private bool InsertText(string text) { - using (var transaction = UndoHistory?.CreateTransaction(InteractiveWindowResources.TypeChar)) + var selection = TextView.Selection; + var caretPosition = TextView.Caret.Position.BufferPosition; + if (!TextView.Selection.IsEmpty) { - var selection = TextView.Selection; - var caretPosition = TextView.Caret.Position.BufferPosition; - if (!TextView.Selection.IsEmpty) + if (!IsSelectionInsideCurrentSubmission()) { - if (!IsSelectionInsideCurrentSubmission()) - { - return; - } + return false; + } - DeleteSelection(); + DeleteSelection(); - if (selection.Mode == TextSelectionMode.Box) - { - ReduceBoxSelectionToEditableBox(isDelete: true); - } - else - { - selection.Clear(); - MoveCaretToClosestEditableBuffer(); - } - } - else if (IsInActivePrompt(caretPosition)) + if (selection.Mode == TextSelectionMode.Box) { - MoveCaretToClosestEditableBuffer(); + ReduceBoxSelectionToEditableBox(isDelete: true); } - else if (MapToEditableBuffer(caretPosition) == null) + else { - return; + selection.Clear(); + MoveCaretToClosestEditableBuffer(); } - - EditorOperations.InsertText(text); - - transaction?.Complete(); } + else if (IsInActivePrompt(caretPosition)) + { + MoveCaretToClosestEditableBuffer(); + } + else if (MapToEditableBuffer(caretPosition) == null) + { + return false; + } + + return EditorOperations.InsertText(text); } /// Implements the core of . diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.csproj b/src/InteractiveWindow/Editor/InteractiveWindow.csproj index 491c0751a574e6d28111ca8b2fbfc15a7c6a9d1a..b12396bd505c6a84996d9227fc715297319718e1 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.csproj +++ b/src/InteractiveWindow/Editor/InteractiveWindow.csproj @@ -20,7 +20,7 @@ - + @@ -124,6 +124,7 @@ + @@ -137,6 +138,7 @@ ResXFileCodeGenerator InteractiveWindowResources.Designer.cs + Designer @@ -147,4 +149,4 @@ - + \ No newline at end of file diff --git a/src/InteractiveWindow/Editor/InteractiveWindowResources.Designer.cs b/src/InteractiveWindow/Editor/InteractiveWindowResources.Designer.cs index 42c51b359638f6325b8b9063e81871d8a6eb6318..cd5e97c2756ded724b88e3c1a0db3d82a9633c9a 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindowResources.Designer.cs +++ b/src/InteractiveWindow/Editor/InteractiveWindowResources.Designer.cs @@ -10,9 +10,8 @@ namespace Microsoft.VisualStudio.InteractiveWindow { using System; - using System.Reflection; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -40,7 +39,7 @@ internal class InteractiveWindowResources { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.InteractiveWindow.InteractiveWindowResources", typeof(InteractiveWindowResources).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.InteractiveWindow.InteractiveWindowResources", typeof(InteractiveWindowResources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/src/InteractiveWindow/Editor/TextTransactionMergePolicy.cs b/src/InteractiveWindow/Editor/TextTransactionMergePolicy.cs new file mode 100644 index 0000000000000000000000000000000000000000..1bcb39d9e0b36d52dab15686d02bf1ce7544a03e --- /dev/null +++ b/src/InteractiveWindow/Editor/TextTransactionMergePolicy.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.VisualStudio.Text.Operations; + +namespace Microsoft.VisualStudio.InteractiveWindow +{ + // The code here is copied from `Microsoft.VisualStudio.Text.Operations.Implementation` + // with minor modification to provide ability for merging undo transactions in InteractiveWindow . + + [Flags] + internal enum TextTransactionMergeDirections + { + Forward = 0x0001, + Backward = 0x0002 + } + + /// + /// This is the merge policy used for determining whether text's undo transactions can be merged. + /// + internal class TextTransactionMergePolicy : IMergeTextUndoTransactionPolicy + { + TextTransactionMergeDirections _allowableMergeDirections; + + public TextTransactionMergePolicy() : this(TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward) + { + } + + public TextTransactionMergePolicy(TextTransactionMergeDirections allowableMergeDirections) + { + _allowableMergeDirections = allowableMergeDirections; + } + + public bool CanMerge(ITextUndoTransaction newTransaction, ITextUndoTransaction oldTransaction) + { + // Validate + if (newTransaction == null) + { + throw new ArgumentNullException("newTransaction"); + } + + if (oldTransaction == null) + { + throw new ArgumentNullException("oldTransaction"); + } + + TextTransactionMergePolicy oldPolicy = oldTransaction.MergePolicy as TextTransactionMergePolicy; + TextTransactionMergePolicy newPolicy = newTransaction.MergePolicy as TextTransactionMergePolicy; + if (oldPolicy == null || newPolicy == null) + { + throw new InvalidOperationException("The MergePolicy for both transactions should be a TextTransactionMergePolicy."); + } + + // Make sure the merge policy directions permit merging these two transactions. + if ((oldPolicy._allowableMergeDirections & TextTransactionMergeDirections.Forward) == 0 || + (newPolicy._allowableMergeDirections & TextTransactionMergeDirections.Backward) == 0) + { + return false; + } + + // Only merge text transactions that have the same description + if (newTransaction.Description != oldTransaction.Description) + { + return false; + } + + return true; + } + + public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) + { + if (existingTransaction == null) + throw new ArgumentNullException("existingTransaction"); + if (newTransaction == null) + throw new ArgumentNullException("newTransaction"); + + // Remove trailing AfterTextBufferChangeUndoPrimitive from previous transaction and skip copying + // initial BeforeTextBufferChangeUndoPrimitive from newTransaction, as they are unnecessary. + int copyStartIndex = 0; + + // Copy items from newTransaction into existingTransaction. + for (int i = copyStartIndex; i < newTransaction.UndoPrimitives.Count; i++) + { + existingTransaction.UndoPrimitives.Add(newTransaction.UndoPrimitives[i]); + } + } + + public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other) + { + if (other == null) + { + throw new ArgumentNullException("other"); + } + + // Only merge transaction if they are both a text transaction + return this.GetType() == other.GetType(); + } + } +} diff --git a/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs b/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs index 547f1e3e76cee7da56120c47f28bfacc5fb7cd02..7603e045dfe504010c006efa58132bcf1976c384 100644 --- a/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs +++ b/src/InteractiveWindow/EditorTest/InteractiveWindowTests.cs @@ -1048,6 +1048,46 @@ private async Task SubmitAsync(params string[] submissions) AssertEx.Equal(submissions, actualSubmissions); } + [WorkItem(6397, "https://github.com/dotnet/roslyn/issues/6397")] + [WpfFact] + public void TypeCharWithUndoRedo() + { + Window.Operations.TypeChar('a'); + Window.Operations.TypeChar('b'); + Window.Operations.TypeChar('c'); + Assert.Equal("> abc", GetTextFromCurrentSnapshot()); + + // undo/redo for consecutive TypeChar's shold be a single action + + ((InteractiveWindow)Window).Undo_TestOnly(1); + Assert.Equal("> ", GetTextFromCurrentSnapshot()); + + ((InteractiveWindow)Window).Redo_TestOnly(1); + Assert.Equal("> abc", GetTextFromCurrentSnapshot()); + + // make a stream selection as follows: + // > |aaa| + Window.Operations.SelectAll(); + Window.Operations.TypeChar('1'); + Window.Operations.TypeChar('2'); + Window.Operations.TypeChar('3'); + + Assert.Equal("> 123", GetTextFromCurrentSnapshot()); + + ((InteractiveWindow)Window).Undo_TestOnly(1); + Assert.Equal("> abc", GetTextFromCurrentSnapshot()); + + ((InteractiveWindow)Window).Undo_TestOnly(1); + Assert.Equal("> ", GetTextFromCurrentSnapshot()); + + // type in active prompt + MoveCaretToPreviousPosition(2); + Window.Operations.TypeChar('x'); + Window.Operations.TypeChar('y'); + Window.Operations.TypeChar('z'); + Assert.Equal("> xyz", GetTextFromCurrentSnapshot()); + } + private string GetTextFromCurrentSnapshot() { return Window.TextView.TextBuffer.CurrentSnapshot.GetText(); @@ -1110,5 +1150,10 @@ internal static void CutLine(this IInteractiveWindowOperations operations) { ((IInteractiveWindowOperations2)operations).CutLine(); } + + internal static void TypeChar(this IInteractiveWindowOperations operations, char typedChar) + { + ((IInteractiveWindowOperations2)operations).TypeChar(typedChar); + } } } diff --git a/src/InteractiveWindow/VisualStudio/VsInteractiveWindowCommandFilter.cs b/src/InteractiveWindow/VisualStudio/VsInteractiveWindowCommandFilter.cs index b0364612e623fb1d5b365b8c0d4d4955fe6c6e1e..5c5a4913c7a868764f4ca747dbfb0389168169b8 100644 --- a/src/InteractiveWindow/VisualStudio/VsInteractiveWindowCommandFilter.cs +++ b/src/InteractiveWindow/VisualStudio/VsInteractiveWindowCommandFilter.cs @@ -215,6 +215,22 @@ private int PreEditorCommandFilterExec(ref Guid pguidCmdGroup, uint nCmdID, uint { switch ((VSConstants.VSStd2KCmdID)nCmdID) { + case VSConstants.VSStd2KCmdID.TYPECHAR: + { + var operations = _window.Operations as IInteractiveWindowOperations2; + if (operations != null) + { + char typedChar = (char)(ushort)System.Runtime.InteropServices.Marshal.GetObjectForNativeVariant(pvaIn); + operations.TypeChar(typedChar); + return VSConstants.S_OK; + } + else + { + _window.Operations.Delete(); + } + break; + } + case VSConstants.VSStd2KCmdID.RETURN: if (_window.Operations.Return()) { @@ -416,19 +432,6 @@ private int PreLanguageCommandFilterExec(ref Guid pguidCmdGroup, uint nCmdID, ui { switch ((VSConstants.VSStd2KCmdID)nCmdID) { - case VSConstants.VSStd2KCmdID.TYPECHAR: - { - var operations = _window.Operations as IInteractiveWindowOperations2; - if (operations != null) - { - char typedChar = (char)(ushort)System.Runtime.InteropServices.Marshal.GetObjectForNativeVariant(pvaIn); - operations.TypeChar(typedChar); - return VSConstants.S_OK; - } - _window.Operations.Delete(); - break; - } - case VSConstants.VSStd2KCmdID.RETURN: if (_window.Operations.TrySubmitStandardInput()) {