File: StringCopyPaste\AbstractPasteProcessor.cs
Web Access
Project: ..\..\..\src\EditorFeatures\CSharp\Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures)
// 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.
 
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste
{
    using static StringCopyPasteHelpers;
 
    /// <summary>
    /// Holds core before/after state related to the paste to allow subclasses to decide what text changes to make
    /// without having to pass around tons of common values.
    /// </summary>
    internal abstract class AbstractPasteProcessor
    {
        /// <summary>
        /// The buffer's snapshot prior to the paste application.
        /// </summary>
        protected readonly ITextSnapshot SnapshotBeforePaste;
 
        /// <summary>
        /// The buffer's snapshot right after the paste application.  Guaranteed to be exactly one version ahead of <see
        /// cref="SnapshotBeforePaste"/>.
        /// </summary>
        protected readonly ITextSnapshot SnapshotAfterPaste;
 
        /// <summary>
        /// Roslyn SourceText corresponding to <see cref="SnapshotBeforePaste"/>.
        /// </summary>
        protected readonly SourceText TextBeforePaste;
 
        /// <summary>
        /// Roslyn SourceText corresponding to <see cref="SnapshotAfterPaste"/>.
        /// </summary>
        protected readonly SourceText TextAfterPaste;
 
        /// <summary>
        /// Roslyn document corresponding to <see cref="SnapshotBeforePaste"/>.
        /// </summary>
        protected readonly Document DocumentBeforePaste;
 
        /// <summary>
        /// Roslyn document corresponding to <see cref="SnapshotAfterPaste"/>.
        /// </summary>
        protected readonly Document DocumentAfterPaste;
 
        /// <summary>
        /// The <see cref="LiteralExpressionSyntax"/> or <see cref="InterpolatedStringExpressionSyntax"/> that the
        /// changes were pasted into.  All changes in the paste will be in the same 'content text span' in that string
        /// expression.
        /// </summary>
        protected readonly ExpressionSyntax StringExpressionBeforePaste;
 
        /// <summary>
        /// Information about the relevant pieces of <see cref="StringExpressionBeforePaste"/> (like where its
        /// delimiters are).
        /// </summary>
        protected readonly StringInfo StringExpressionBeforePasteInfo;
 
        /// <summary>
        /// All the spans of <see cref="StringExpressionBeforePasteInfo"/>'s <see cref="StringInfo.ContentSpans"/>
        /// mapped forward (<see cref="MapSpanForward(TextSpan)"/>) to <see cref="SnapshotAfterPaste"/> in an inclusive
        /// manner. This can be used to determine what content exists post paste, and if that content requires the
        /// literal to revised to be legal.  For example, if the text content in a raw-literal contains a longer
        /// sequence of quotes after pasting, then the delimiters of the raw literal may need to be increased
        /// accordingly.
        /// </summary>
        protected readonly ImmutableArray<TextSpan> TextContentsSpansAfterPaste;
 
        /// <summary>
        /// User's desired new-line sequence if we need to add newlines to our text changes.
        /// </summary>
        protected readonly string NewLine;
 
        /// <summary>
        /// Amount to indent content in a multi-line raw string literal.
        /// </summary>
        protected readonly string IndentationWhitespace;
 
        /// <summary>
        /// The set of <see cref="ITextChange"/>'s that produced <see cref="SnapshotAfterPaste"/> from <see
        /// cref="SnapshotBeforePaste"/>.
        /// </summary>
        protected INormalizedTextChangeCollection Changes => SnapshotBeforePaste.Version.Changes;
 
        protected AbstractPasteProcessor(
            string newLine,
            string indentationWhitespace,
            ITextSnapshot snapshotBeforePaste,
            ITextSnapshot snapshotAfterPaste,
            Document documentBeforePaste,
            Document documentAfterPaste,
            ExpressionSyntax stringExpressionBeforePaste)
        {
            NewLine = newLine;
            IndentationWhitespace = indentationWhitespace;
 
            SnapshotBeforePaste = snapshotBeforePaste;
            SnapshotAfterPaste = snapshotAfterPaste;
 
            TextBeforePaste = SnapshotBeforePaste.AsText();
            TextAfterPaste = SnapshotAfterPaste.AsText();
 
            DocumentBeforePaste = documentBeforePaste;
            DocumentAfterPaste = documentAfterPaste;
 
            StringExpressionBeforePaste = stringExpressionBeforePaste;
            StringExpressionBeforePasteInfo = StringInfo.GetStringInfo(TextBeforePaste, stringExpressionBeforePaste);
            TextContentsSpansAfterPaste = StringExpressionBeforePasteInfo.ContentSpans.SelectAsArray(MapSpanForward);
 
            Contract.ThrowIfTrue(StringExpressionBeforePasteInfo.ContentSpans.IsEmpty);
        }
 
        /// <summary>
        /// Determine the edits that should be made to smartly handle pasting hte data that is on the clipboard._selectionBeforePaste
        /// </summary>
        public abstract ImmutableArray<TextChange> GetEdits();
 
        /// <summary>
        /// Takes a span in <see cref="SnapshotBeforePaste"/> and maps it appropriately (in an <see
        /// cref="SpanTrackingMode.EdgeInclusive"/> manner) to <see cref="SnapshotAfterPaste"/>.
        /// </summary>
        protected TextSpan MapSpanForward(TextSpan span)
            => MapSpan(span, SnapshotBeforePaste, SnapshotAfterPaste);
 
        /// <summary>
        /// Given an initial raw string literal, and the changes made to it by the paste, determines how many quotes to
        /// add to the start and end to keep things parsing properly.
        /// </summary>
        protected string? GetQuotesToAddToRawString(
            SourceText textAfterChange, ImmutableArray<TextSpan> textContentSpansAfterChange)
        {
            Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste));
 
            var longestQuoteSequence = textContentSpansAfterChange.Max(ts => GetLongestQuoteSequence(textAfterChange, ts));
 
            var quotesToAddCount = (longestQuoteSequence - StringExpressionBeforePasteInfo.DelimiterQuoteCount) + 1;
            return quotesToAddCount <= 0 ? null : new string('"', quotesToAddCount);
        }
 
        /// <summary>
        /// Given an initial raw string literal, and the changes made to it by the paste, determines how many dollar
        /// signs to add to the start to keep things parsing properly.
        /// </summary>
        protected string? GetDollarSignsToAddToRawString(
            SourceText textAfterChange, ImmutableArray<TextSpan> textContentSpansAfterChange)
        {
            Contract.ThrowIfFalse(IsAnyRawStringExpression(StringExpressionBeforePaste));
 
            // Only have to do this for interpolated strings.  Other strings never have a $ in their starting delimiter.
            if (StringExpressionBeforePaste is not InterpolatedStringExpressionSyntax)
                return null;
 
            var longestBraceSequence = textContentSpansAfterChange.Max(
                ts => Math.Max(
                    GetLongestOpenBraceSequence(textAfterChange, ts),
                    GetLongestCloseBraceSequence(textAfterChange, ts)));
 
            var dollarsToAddCount = (longestBraceSequence - StringExpressionBeforePasteInfo.DelimiterDollarCount) + 1;
            return dollarsToAddCount <= 0 ? null : new string('$', dollarsToAddCount);
        }
    }
}