File: StringCopyPaste\StringCopyPasteData.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.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using Microsoft.CodeAnalysis.PooledObjects;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.StringCopyPaste
{
    /// <summary>
    /// Data about a string that a user has copied a subsection of. This will itself be placed on the clipboard so that
    /// it can be retrieved later on if the user pastes.
    /// </summary>
    internal class StringCopyPasteData
    {
        public ImmutableArray<StringCopyPasteContent> Contents { get; }
 
        [JsonConstructor]
        public StringCopyPasteData(ImmutableArray<StringCopyPasteContent> contents)
        {
            Contents = contents;
        }
 
        public string? ToJson()
        {
            try
            {
                return JsonSerializer.Serialize(this, typeof(StringCopyPasteData));
            }
            catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
            {
            }
 
            return null;
        }
 
        public static StringCopyPasteData? FromJson(string? json)
        {
            if (string.IsNullOrWhiteSpace(json))
                return null;
 
            try
            {
                var value = JsonSerializer.Deserialize(JsonDocument.Parse(json), typeof(StringCopyPasteData));
                if (value is null)
                    return null;
 
                return (StringCopyPasteData)value;
            }
            catch (Exception ex) when (FatalError.ReportAndCatch(ex, ErrorSeverity.Critical))
            {
            }
 
            return null;
        }
 
        /// <summary>
        /// Given a <paramref name="stringExpression"/> for a string literal or interpolated string, and the <paramref
        /// name="selectionSpan"/> the user has selected in it, tries to determine the interpreted content within that
        /// expression that has been copied.  "interpreted" in this context means the actual value of the content that
        /// was selected, with things like escape characters embedded as the actual characters they represent.
        /// </summary>
        public static StringCopyPasteData? TryCreate(IVirtualCharLanguageService virtualCharService, ExpressionSyntax stringExpression, TextSpan selectionSpan)
            => stringExpression switch
            {
                LiteralExpressionSyntax literal => TryCreateForLiteral(virtualCharService, literal, selectionSpan),
                InterpolatedStringExpressionSyntax interpolatedString => TryCreateForInterpolatedString(virtualCharService, interpolatedString, selectionSpan),
                _ => throw ExceptionUtilities.UnexpectedValue(stringExpression.Kind()),
            };
 
        private static StringCopyPasteData? TryCreateForLiteral(IVirtualCharLanguageService virtualCharService, LiteralExpressionSyntax literal, TextSpan span)
            => TryGetContentForSpan(virtualCharService, literal.Token, span, out var content)
                ? new StringCopyPasteData(ImmutableArray.Create(content))
                : null;
 
        /// <summary>
        /// Given a string <paramref name="token"/>, and the <paramref name="selectionSpan"/> the user has selected that
        /// overlaps with it, tries to determine the interpreted content within that token that has been copied.
        /// "interpreted" in this context means the actual value of the content that was selected, with things like
        /// escape characters embedded as the actual characters they represent.
        /// </summary>
        private static bool TryGetNormalizedStringForSpan(
            IVirtualCharLanguageService virtualCharService,
            SyntaxToken token,
            TextSpan selectionSpan,
            [NotNullWhen(true)] out string? normalizedText)
        {
            normalizedText = null;
 
            // First, try to convert this token to a sequence of virtual chars.
            var virtualChars = virtualCharService.TryConvertToVirtualChars(token);
            if (virtualChars.IsDefaultOrEmpty)
                return false;
 
            // Then find the start/end of the token's characters that overlap with the selection span.
            var firstOverlappingChar = virtualChars.FirstOrNull(vc => vc.Span.OverlapsWith(selectionSpan));
            var lastOverlappingChar = virtualChars.LastOrNull(vc => vc.Span.OverlapsWith(selectionSpan));
 
            if (firstOverlappingChar is null || lastOverlappingChar is null)
                return false;
 
            // Don't allow partial selection of an escaped character.  e.g. if they select 'n' in '\n'
            if (selectionSpan.Start > firstOverlappingChar.Value.Span.Start)
                return false;
 
            if (selectionSpan.End < lastOverlappingChar.Value.Span.End)
                return false;
 
            var firstCharIndexInclusive = virtualChars.IndexOf(firstOverlappingChar.Value);
            var lastCharIndexInclusive = virtualChars.IndexOf(lastOverlappingChar.Value);
 
            // Grab that subsequence of characters and get the final interpreted string for it.
            var subsequence = virtualChars.GetSubSequence(TextSpan.FromBounds(firstCharIndexInclusive, lastCharIndexInclusive + 1));
            normalizedText = subsequence.CreateString();
            return true;
        }
 
        private static bool TryGetContentForSpan(
            IVirtualCharLanguageService virtualCharService,
            SyntaxToken token,
            TextSpan selectionSpan,
            out StringCopyPasteContent content)
        {
            if (!TryGetNormalizedStringForSpan(virtualCharService, token, selectionSpan, out var text))
            {
                content = default;
                return false;
            }
            else
            {
                content = StringCopyPasteContent.ForText(text);
                return true;
            }
        }
 
        private static StringCopyPasteData? TryCreateForInterpolatedString(
            IVirtualCharLanguageService virtualCharService,
            InterpolatedStringExpressionSyntax interpolatedString,
            TextSpan selectionSpan)
        {
            using var _ = ArrayBuilder<StringCopyPasteContent>.GetInstance(out var result);
 
            foreach (var interpolatedContent in interpolatedString.Contents)
            {
                // Only consider portions of the interpolated string that overlap the selection.
                if (interpolatedContent.Span.OverlapsWith(selectionSpan))
                {
                    if (interpolatedContent is InterpolationSyntax interpolation)
                    {
                        // If the user copies a portion of an interpolation, just treat this as a non-smart copy paste
                        // for simplicity.
                        if (!selectionSpan.Contains(interpolation.Span))
                            return null;
 
                        // The format clause needs to be written differently depending on what sort of interpolated
                        // string we have (normal, verbatim, raw).  So grab the token for it and determine it's actual
                        // interpreted value so we can paste it properly at the destination side.
                        var formatClause = (string?)null;
                        if (interpolation.FormatClause != null &&
                            !TryGetNormalizedStringForSpan(virtualCharService, interpolation.FormatClause.FormatStringToken, selectionSpan, out formatClause))
                        {
                            return null;
                        }
 
                        // Can grab the expression and alignment-clause as is.  That's just normal C# code, and will
                        // remain the same no matter what we past into.
                        result.Add(StringCopyPasteContent.ForInterpolation(
                            interpolation.Expression.ToFullString(),
                            interpolation.AlignmentClause?.ToFullString(),
                            formatClause));
                    }
                    else if (interpolatedContent is InterpolatedStringTextSyntax stringText)
                    {
                        if (!TryGetContentForSpan(virtualCharService, stringText.TextToken, selectionSpan, out var content))
                            return null;
 
                        result.Add(content);
                    }
                }
            }
 
            return new StringCopyPasteData(result.ToImmutable());
        }
    }
}