From 444eb49693792e2e8564d81a962a3e6e8419a3a5 Mon Sep 17 00:00:00 2001 From: Gen Lu Date: Tue, 20 Oct 2015 14:33:20 -0700 Subject: [PATCH] Add a command handler for pasting from interactive window --- .../InteractivePasteCommandHandler.cs | 105 +++++++++++++ src/EditorFeatures/Core/EditorFeatures.csproj | 7 + .../Core/EditorFeaturesResources.Designer.cs | 9 ++ .../Core/EditorFeaturesResources.resx | 3 + .../Commands/PredefinedCommandHandlerNames.cs | 5 + .../Test2/EditorServicesTest2.vbproj | 7 +- .../InteractivePasteCommandHandlerTests.vb | 139 ++++++++++++++++++ .../Editor/InteractiveWindow.csproj | 10 +- .../EditorTest/InteractiveWindowTest.csproj | 2 +- 9 files changed, 281 insertions(+), 6 deletions(-) create mode 100644 src/EditorFeatures/Core/CommandHandlers/InteractivePasteCommandHandler.cs create mode 100644 src/EditorFeatures/Test2/InteractivePaste/InteractivePasteCommandHandlerTests.vb diff --git a/src/EditorFeatures/Core/CommandHandlers/InteractivePasteCommandHandler.cs b/src/EditorFeatures/Core/CommandHandlers/InteractivePasteCommandHandler.cs new file mode 100644 index 00000000000..c69245532d7 --- /dev/null +++ b/src/EditorFeatures/Core/CommandHandlers/InteractivePasteCommandHandler.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +extern alias InteractiveWindow; + +using System; +using System.ComponentModel.Composition; +using System.Runtime.CompilerServices; +using System.Windows; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Operations; +using Microsoft.VisualStudio.Utilities; +using Microsoft.CodeAnalysis.Editor.Commands; + +namespace Microsoft.CodeAnalysis.Editor.CommandHandlers +{ + [ExportCommandHandler(PredefinedCommandHandlerNames.InteractivePaste, ContentTypeNames.RoslynContentType)] + [Order(After = PredefinedCommandHandlerNames.Rename)] + [Order(After = PredefinedCommandHandlerNames.FormatDocument)] + [Order(After = PredefinedCommandHandlerNames.Commit)] + [Order(Before = PredefinedCommandHandlerNames.Completion)] + internal sealed class InteractivePasteCommandHandler : ICommandHandler + { + // Duplicated string, originally defined at `Microsoft.VisualStudio.InteractiveWindow.PredefinedInteractiveContentTypes` + private const string InteractiveContentTypeName = "Interactive Content"; + // Duplicated string, originally defined at `Microsoft.VisualStudio.InteractiveWindow.InteractiveWindow` + private const string InteractiveClipboardFormat = "89344A36-9821-495A-8255-99A63969F87D"; + + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry; + + // This is for unit test purpose only, do not explicitly set this field otherwise. + internal IRoslynClipboard RoslynClipBoard; + + [ImportingConstructor] + public InteractivePasteCommandHandler(IEditorOperationsFactoryService editorOperationsFactoryService, ITextUndoHistoryRegistry textUndoHistoryRegistry) + { + _editorOperationsFactoryService = editorOperationsFactoryService; + _textUndoHistoryRegistry = textUndoHistoryRegistry; + RoslynClipBoard = new SystemClipboardWrapper(); + } + + public void ExecuteCommand(PasteCommandArgs args, Action nextHandler) + { + // InteractiveWindow handles pasting by itself, which including checks for buffer types, etc. + if (!args.TextView.TextBuffer.ContentType.IsOfType(InteractiveContentTypeName) && + RoslynClipBoard.ContainsData(InteractiveClipboardFormat)) + { + PasteInteractiveFormat(args.TextView); + } + else + { + nextHandler(); + } + } + + public CommandState GetCommandState(PasteCommandArgs args, Func nextHandler) + { + return nextHandler(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // Avoid loading InteractiveWindow unless necessary + private void PasteInteractiveFormat(ITextView textView) + { + var editorOperation = _editorOperationsFactoryService.GetEditorOperations(textView); + var blocks = InteractiveWindow::Microsoft.VisualStudio.InteractiveWindow.BufferBlock.Deserialize((string)RoslynClipBoard.GetData(InteractiveClipboardFormat)); + using (var transaction = _textUndoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.InteractivePaste)) + { + foreach (var block in blocks) + { + switch (block.Kind) + { + case InteractiveWindow::Microsoft.VisualStudio.InteractiveWindow.ReplSpanKind.Input: + case InteractiveWindow::Microsoft.VisualStudio.InteractiveWindow.ReplSpanKind.Output: + case InteractiveWindow::Microsoft.VisualStudio.InteractiveWindow.ReplSpanKind.StandardInput: + editorOperation.InsertText(block.Content); + break; + } + } + transaction.Complete(); + } + } + + // The mock clipboard used in tests will implement this interface + internal interface IRoslynClipboard + { + bool ContainsData(string format); + object GetData(string format); + } + + // In product code, we use this simple wrapper around system clipboard. + // Maybe at some point we can elevate this class and interface so they could be shared among Roslyn code base. + private class SystemClipboardWrapper : IRoslynClipboard + { + public bool ContainsData(string format) + { + return Clipboard.ContainsData(format); + } + + public object GetData(string format) + { + return Clipboard.GetData(format); + } + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index e53ba9dc4ab..caf926b17d5 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -41,6 +41,11 @@ {18F5FBB8-7570-4412-8CC7-0A86FF13B7BA} TextEditorFeatures + + {01E9BD68-0339-4A13-B42F-A3CA84D164F3} + InteractiveWindow + InteractiveWindow + true @@ -86,6 +91,7 @@ + @@ -137,6 +143,7 @@ + diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs b/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs index 02edd6c4d23..ef222112bba 100644 --- a/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs +++ b/src/EditorFeatures/Core/EditorFeaturesResources.Designer.cs @@ -1195,6 +1195,15 @@ internal class EditorFeaturesResources { } } + /// + /// Looks up a localized string similar to Paste From Interactive Window. + /// + internal static string InteractivePaste { + get { + return ResourceManager.GetString("InteractivePaste", resourceCulture); + } + } + /// /// Looks up a localized string similar to Interface Parts. /// diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.resx b/src/EditorFeatures/Core/EditorFeaturesResources.resx index 2fe5836a95d..1b7053b1607 100644 --- a/src/EditorFeatures/Core/EditorFeaturesResources.resx +++ b/src/EditorFeatures/Core/EditorFeaturesResources.resx @@ -745,4 +745,7 @@ Do you want to proceed? Modify any highlighted location to begin renaming. + + Paste From Interactive Window + \ No newline at end of file diff --git a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs index fc30691328b..7aff71839d3 100644 --- a/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs +++ b/src/EditorFeatures/Core/Extensibility/Commands/PredefinedCommandHandlerNames.cs @@ -134,5 +134,10 @@ internal static class PredefinedCommandHandlerNames /// handled by the command handler. /// public const string SignatureHelp = "Signature Help Command Handler"; + + /// + /// Command handler name for Paste Content in Interactive Format. + /// + public const string InteractivePaste = "Interactive Paste Command Handler"; } } diff --git a/src/EditorFeatures/Test2/EditorServicesTest2.vbproj b/src/EditorFeatures/Test2/EditorServicesTest2.vbproj index 7b7626d1642..4d1edddf4a6 100644 --- a/src/EditorFeatures/Test2/EditorServicesTest2.vbproj +++ b/src/EditorFeatures/Test2/EditorServicesTest2.vbproj @@ -37,6 +37,10 @@ {2523D0E6-DF32-4A3E-8AE0-A19BFFAE2EF6} BasicCodeAnalysis + + {01e9bd68-0339-4a13-b42f-a3ca84d164f3} + InteractiveWindow + {92412d1a-0f23-45b5-b196-58839c524917} InteractiveEditorFeatures @@ -169,6 +173,7 @@ + @@ -317,4 +322,4 @@ - + \ No newline at end of file diff --git a/src/EditorFeatures/Test2/InteractivePaste/InteractivePasteCommandHandlerTests.vb b/src/EditorFeatures/Test2/InteractivePaste/InteractivePasteCommandHandlerTests.vb new file mode 100644 index 00000000000..9d2897e5f4a --- /dev/null +++ b/src/EditorFeatures/Test2/InteractivePaste/InteractivePasteCommandHandlerTests.vb @@ -0,0 +1,139 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports System.Text +Imports System.Windows +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Editor.Commands +Imports Microsoft.CodeAnalysis.Editor.Host +Imports Microsoft.CodeAnalysis.Editor.Implementation.Interactive +Imports Microsoft.CodeAnalysis.Editor.Shared.Extensions +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces +Imports Microsoft.CodeAnalysis.Text.Shared.Extensions +Imports Microsoft.VisualStudio.Text +Imports Microsoft.VisualStudio.Text.Editor +Imports Microsoft.VisualStudio.Text.Operations +Imports Microsoft.CodeAnalysis.Editor.CommandHandlers +Imports Microsoft.VisualStudio.InteractiveWindow + +Namespace Microsoft.CodeAnalysis.Editor.UnitTests.InteractivePaste + Public Class InteractivePasteCommandhandlerTests + Private Function CreateCommandHandler(workspace As TestWorkspace) As InteractivePasteCommandHandler + Dim handler = New InteractivePasteCommandHandler(workspace.GetService(Of IEditorOperationsFactoryService), + workspace.GetService(Of ITextUndoHistoryRegistry)) + handler.RoslynClipBoard = New MockClipboard() + Return handler + End Function + + + + Public Sub PasteCommandWithInteractiveFormat() + Using workspace = TestWorkspaceFactory.CreateWorkspace( + + + + + ) + + ' Force initialization. + workspace.GetOpenDocumentIds().Select(Function(id) workspace.GetTestDocument(id).GetTextView()).ToList() + + Dim textView = workspace.Documents.Single().GetTextView() + + Dim handler = CreateCommandHandler(workspace) + Dim clipboard = DirectCast(handler.RoslynClipBoard, MockClipboard) + + Dim blocks = New BufferBlock() _ + { + New BufferBlock(ReplSpanKind.Output, "a" & vbCr & vbLf & "bc"), + New BufferBlock(ReplSpanKind.Prompt, "> "), + New BufferBlock(ReplSpanKind.Prompt, "< "), + New BufferBlock(ReplSpanKind.Input, "12"), + New BufferBlock(ReplSpanKind.StandardInput, "3") + } + CopyToClipboard(clipboard, blocks, includeRepl:=True) + + handler.ExecuteCommand(New PasteCommandArgs(textView, textView.TextBuffer), Sub() Throw New Exception("The operation should have been handled.")) + + Assert.Equal("a" & vbCr & vbLf & "bc123", textView.TextBuffer.CurrentSnapshot.GetText()) + End Using + End Sub + + + + Public Sub PasteCommandWithOutInteractiveFormat() + Using workspace = TestWorkspaceFactory.CreateWorkspace( + + + + + ) + + ' Force initialization. + workspace.GetOpenDocumentIds().Select(Function(id) workspace.GetTestDocument(id).GetTextView()).ToList() + + Dim textView = workspace.Documents.Single().GetTextView() + Dim editorOperations = workspace.GetService(Of IEditorOperationsFactoryService)().GetEditorOperations(textView) + + Dim handler = CreateCommandHandler(workspace) + Dim clipboard = DirectCast(handler.RoslynClipBoard, MockClipboard) + + Dim blocks = New BufferBlock() _ + { + New BufferBlock(ReplSpanKind.Output, "a" & vbCr & vbLf & "bc"), + New BufferBlock(ReplSpanKind.Prompt, "> "), + New BufferBlock(ReplSpanKind.Prompt, "< "), + New BufferBlock(ReplSpanKind.Input, "12"), + New BufferBlock(ReplSpanKind.StandardInput, "3") + } + CopyToClipboard(clipboard, blocks, includeRepl:=False) + + handler.ExecuteCommand(New PasteCommandArgs(textView, textView.TextBuffer), Sub() editorOperations.InsertText("p")) + + Assert.Equal("p", textView.TextBuffer.CurrentSnapshot.GetText()) + End Using + End Sub + + Private Sub CopyToClipboard(clipboard As MockClipboard, blocks As BufferBlock(), includeRepl As Boolean) + clipboard.Clear() + Dim data = New DataObject() + Dim builder = New StringBuilder() + For Each block As BufferBlock In blocks + builder.Append(block.Content) + Next + Dim text = builder.ToString() + data.SetData(DataFormats.UnicodeText, text) + data.SetData(DataFormats.StringFormat, text) + If includeRepl Then + data.SetData(InteractiveWindow.ClipboardFormat, BufferBlock.Serialize(blocks)) + End If + clipboard.SetDataObject(data) + End Sub + + Private Class MockClipboard + Implements InteractivePasteCommandHandler.IRoslynClipboard + + Private _data As DataObject + + Friend Sub Clear() + _data = Nothing + End Sub + + Friend Sub SetDataObject(data As Object) + _data = DirectCast(data, DataObject) + End Sub + + Public Function ContainsData(format As String) As Boolean Implements InteractivePasteCommandHandler.IRoslynClipboard.ContainsData + Return _data IsNot Nothing And _data.GetData(format) IsNot Nothing + End Function + + Public Function GetData(format As String) As Object Implements InteractivePasteCommandHandler.IRoslynClipboard.GetData + If _data Is Nothing Then + Return Nothing + Else + Return _data.GetData(format) + End If + End Function + End Class + End Class +End Namespace + diff --git a/src/InteractiveWindow/Editor/InteractiveWindow.csproj b/src/InteractiveWindow/Editor/InteractiveWindow.csproj index 925e353486e..f8fc6a4df37 100644 --- a/src/InteractiveWindow/Editor/InteractiveWindow.csproj +++ b/src/InteractiveWindow/Editor/InteractiveWindow.csproj @@ -23,8 +23,10 @@ + + @@ -74,6 +76,7 @@ + @@ -90,20 +93,17 @@ - + - - - @@ -120,7 +120,9 @@ + + diff --git a/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj b/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj index bea79b2b3fd..941d144aa8a 100644 --- a/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj +++ b/src/InteractiveWindow/EditorTest/InteractiveWindowTest.csproj @@ -98,4 +98,4 @@ - + \ No newline at end of file -- GitLab