File: StringCopyPaste\StringCopyPasteData.cs
Web Access
Project: src\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 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>
[method: JsonConstructor]
internal class StringCopyPasteData(ImmutableArray<StringCopyPasteContent> contents)
{
    public ImmutableArray<StringCopyPasteContent> Contents { get; } = 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([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());
    }
}