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())
{