File: Interactive\InteractivePasteCommandHandler.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_a0rtafw3_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.Interactive;
 
// This command handler must be invoked after the handlers specified in `Order` attribute
// (those handlers also implement `ICommandHandler<PasteCommandArgs>`),
// because it will intercept the paste command and skip the rest of handlers in chain.  
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.InteractivePaste)]
[Order(After = PredefinedCommandHandlerNames.Rename)]
[Order(After = PredefinedCommandHandlerNames.FormatDocument)]
[Order(After = PredefinedCommandHandlerNames.Commit)]
[Order(After = PredefinedCompletionNames.CompletionCommandHandler)]
internal sealed class InteractivePasteCommandHandler : ICommandHandler<PasteCommandArgs>
{
    // The following two field definitions have to stay in sync with VS editor implementation
 
    /// <summary>
    /// A data format used to tag the contents of the clipboard so that it's clear
    /// the data has been put in the clipboard by our editor
    /// </summary>
    internal const string ClipboardLineBasedCutCopyTag = "VisualStudioEditorOperationsLineCutCopyClipboardTag";
 
    /// <summary>
    /// A data format used to tag the contents of the clipboard as a box selection.
    /// This is the same string that was used in VS9 and previous versions.
    /// </summary>
    internal const string BoxSelectionCutCopyTag = "MSDEVColumnSelect";
 
    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;
 
    public string DisplayName => EditorFeaturesResources.Paste_in_Interactive;
 
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public InteractivePasteCommandHandler(IEditorOperationsFactoryService editorOperationsFactoryService, ITextUndoHistoryRegistry textUndoHistoryRegistry)
    {
        _editorOperationsFactoryService = editorOperationsFactoryService;
        _textUndoHistoryRegistry = textUndoHistoryRegistry;
        RoslynClipboard = new SystemClipboardWrapper();
    }
 
    public bool ExecuteCommand(PasteCommandArgs args, CommandExecutionContext context)
    {
        // InteractiveWindow handles pasting by itself, which including checks for buffer types, etc.
        if (!args.TextView.TextBuffer.ContentType.IsOfType(PredefinedInteractiveContentTypes.InteractiveContentTypeName) &&
            RoslynClipboard.ContainsData(InteractiveClipboardFormat.Tag))
        {
            PasteInteractiveFormat(args.TextView);
            return true;
        }
        else
        {
            return false;
        }
    }
 
    public CommandState GetCommandState(PasteCommandArgs args)
        => CommandState.Unspecified;
 
    [MethodImpl(MethodImplOptions.NoInlining)]  // Avoid loading InteractiveWindow unless necessary
    private void PasteInteractiveFormat(ITextView textView)
    {
        var editorOperations = _editorOperationsFactoryService.GetEditorOperations(textView);
 
        var data = RoslynClipboard.GetDataObject();
        Debug.Assert(data != null);
 
        var dataHasLineCutCopyTag = data.GetDataPresent(ClipboardLineBasedCutCopyTag);
        var dataHasBoxCutCopyTag = data.GetDataPresent(BoxSelectionCutCopyTag);
        Debug.Assert(!(dataHasLineCutCopyTag && dataHasBoxCutCopyTag));
 
        string text;
        try
        {
            text = InteractiveClipboardFormat.Deserialize(RoslynClipboard.GetData(InteractiveClipboardFormat.Tag));
        }
        catch (InvalidDataException)
        {
            text = "<bad clipboard data>";
        }
 
        using var transaction = _textUndoHistoryRegistry.GetHistory(textView.TextBuffer).CreateTransaction(EditorFeaturesResources.Paste);
        editorOperations.AddBeforeTextBufferChangePrimitive();
        if (dataHasLineCutCopyTag && textView.Selection.IsEmpty)
        {
            editorOperations.MoveToStartOfLine(extendSelection: false);
            editorOperations.InsertText(text);
        }
        else if (dataHasBoxCutCopyTag)
        {
            // If the caret is on a blank line, treat this like a normal stream insertion
            if (textView.Selection.IsEmpty && !HasNonWhiteSpaceCharacter(textView.Caret.Position.BufferPosition.GetContainingLine()))
            {
                // trim the last newline before paste
                var trimmed = text.Remove(text.LastIndexOf(textView.Options.GetNewLineCharacter()));
                editorOperations.InsertText(trimmed);
            }
            else
            {
                editorOperations.InsertTextAsBox(text, out _, out _);
            }
        }
        else
        {
            editorOperations.InsertText(text);
        }
 
        editorOperations.AddAfterTextBufferChangePrimitive();
        transaction.Complete();
    }
 
    private static bool HasNonWhiteSpaceCharacter(ITextSnapshotLine line)
    {
        var snapshot = line.Snapshot;
        var start = line.Start.Position;
        var count = line.Length;
        for (var i = 0; i < count; i++)
        {
            if (!char.IsWhiteSpace(snapshot[start + i]))
            {
                return true;
            }
        }
 
        return false;
    }
 
    // The mock clipboard used in tests will implement this interface 
    internal interface IRoslynClipboard
    {
        bool ContainsData(string format);
        object GetData(string format);
        IDataObject GetDataObject();
    }
 
    // 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 sealed class SystemClipboardWrapper : IRoslynClipboard
    {
        public bool ContainsData(string format)
            => Clipboard.ContainsData(format);
 
        public object GetData(string format)
            => Clipboard.GetData(format);
 
        public IDataObject GetDataObject()
            => Clipboard.GetDataObject();
    }
}