File: ConvertBetweenRegularAndVerbatimString\AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.ConvertBetweenRegularAndVerbatimString;
 
internal abstract class AbstractConvertBetweenRegularAndVerbatimStringCodeRefactoringProvider<
    TStringExpressionSyntax>
    : CodeRefactoringProvider
    where TStringExpressionSyntax : ExpressionSyntax
{
    private const char OpenBrace = '{';
    private const char CloseBrace = '}';
    protected const char DoubleQuote = '"';
 
    protected abstract bool IsInterpolation { get; }
    protected abstract bool IsAppropriateLiteralKind(TStringExpressionSyntax literalExpression);
    protected abstract void AddSubStringTokens(TStringExpressionSyntax literalExpression, ArrayBuilder<SyntaxToken> subTokens);
    protected abstract bool IsVerbatim(TStringExpressionSyntax literalExpression);
    protected abstract TStringExpressionSyntax CreateVerbatimStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression);
    protected abstract TStringExpressionSyntax CreateRegularStringExpression(IVirtualCharService charService, StringBuilder sb, TStringExpressionSyntax stringExpression);
 
    public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        var literalExpression = await context.TryGetRelevantNodeAsync<TStringExpressionSyntax>().ConfigureAwait(false);
        if (literalExpression == null || !IsAppropriateLiteralKind(literalExpression))
            return;
 
        var (document, _, cancellationToken) = context;
 
        var charService = document.GetRequiredLanguageService<IVirtualCharLanguageService>();
 
        using var _ = ArrayBuilder<SyntaxToken>.GetInstance(out var subStringTokens);
 
        // First, ensure that we understand all text parts of the interpolation.
        AddSubStringTokens(literalExpression, subStringTokens);
        foreach (var subToken in subStringTokens)
        {
            var chars = charService.TryConvertToVirtualChars(subToken);
            if (chars.IsDefault)
                return;
        }
 
        // Note: This is a generally useful feature on strings.  But it's not likely to be something
        // people want to use a lot.  Make low priority so it doesn't interfere with more
        // commonly useful refactorings.
 
        if (IsVerbatim(literalExpression))
        {
            // always offer to convert from verbatim string to normal string.
            context.RegisterRefactoring(CodeAction.Create(
                CSharpFeaturesResources.Convert_to_regular_string,
                c => ConvertToRegularStringAsync(document, literalExpression, c),
                nameof(CSharpFeaturesResources.Convert_to_regular_string),
                CodeActionPriority.Low));
        }
        else if (ContainsSimpleEscape(charService, subStringTokens))
        {
            // Offer to convert to a verbatim string if the normal string contains simple
            // escapes that can be directly embedded in the verbatim string.
            context.RegisterRefactoring(CodeAction.Create(
                CSharpFeaturesResources.Convert_to_verbatim_string,
                c => ConvertToVerbatimStringAsync(document, literalExpression, c),
                nameof(CSharpFeaturesResources.Convert_to_verbatim_string),
                CodeActionPriority.Low));
        }
    }
 
    private static async Task<Document> ConvertAsync(
        Func<IVirtualCharService, StringBuilder, TStringExpressionSyntax, TStringExpressionSyntax> convert,
        Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken)
    {
        using var _ = PooledStringBuilder.GetInstance(out var sb);
 
        var charService = document.GetRequiredLanguageService<IVirtualCharLanguageService>();
        var newStringExpression = convert(charService, sb, stringExpression).WithTriviaFrom(stringExpression);
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        return document.WithSyntaxRoot(root.ReplaceNode(stringExpression, newStringExpression));
    }
 
    private Task<Document> ConvertToVerbatimStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken)
        => ConvertAsync(CreateVerbatimStringExpression, document, stringExpression, cancellationToken);
 
    private Task<Document> ConvertToRegularStringAsync(Document document, TStringExpressionSyntax stringExpression, CancellationToken cancellationToken)
        => ConvertAsync(CreateRegularStringExpression, document, stringExpression, cancellationToken);
 
    protected void AddVerbatimStringText(
        IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken)
    {
        var isInterpolation = IsInterpolation;
        var chars = charService.TryConvertToVirtualChars(stringToken);
 
        foreach (var ch in chars)
        {
            // just build the verbatim string by concatenating all the chars in the original
            // string.  The only exceptions are double-quotes which need to be doubled up in the
            // final string, and curlies which need to be doubled in interpolations.
            ch.AppendTo(sb);
 
            if (ShouldDouble(ch, isInterpolation))
                ch.AppendTo(sb);
        }
 
        static bool ShouldDouble(VirtualChar ch, bool isInterpolation)
        {
            if (ch == DoubleQuote)
                return true;
 
            if (isInterpolation)
                return IsOpenOrCloseBrace(ch);
 
            return false;
        }
    }
 
    private static bool IsOpenOrCloseBrace(VirtualChar ch)
        => ch == OpenBrace || ch == CloseBrace;
 
    protected void AddRegularStringText(
        IVirtualCharService charService, StringBuilder sb, SyntaxToken stringToken)
    {
        var isInterpolation = IsInterpolation;
        var chars = charService.TryConvertToVirtualChars(stringToken);
 
        foreach (var ch in chars)
        {
            if (charService.TryGetEscapeCharacter(ch, out var escaped))
            {
                sb.Append('\\');
                sb.Append(escaped);
            }
            else
            {
                ch.AppendTo(sb);
 
                // if it's an interpolation, we need to double-up open/close braces.
                if (isInterpolation && IsOpenOrCloseBrace(ch))
                    ch.AppendTo(sb);
            }
        }
    }
 
    private static bool ContainsSimpleEscape(
        IVirtualCharService charService, ArrayBuilder<SyntaxToken> subTokens)
    {
        foreach (var subToken in subTokens)
        {
            var chars = charService.TryConvertToVirtualChars(subToken);
 
            // This was checked above.
            Debug.Assert(!chars.IsDefault);
            if (ContainsSimpleEscape(chars))
                return true;
        }
 
        return false;
    }
 
    private static bool ContainsSimpleEscape(VirtualCharSequence chars)
    {
        foreach (var ch in chars)
        {
            // look for two-character escapes that start with  \  .  i.e.  \n  . Note:  \0
            // cannot be encoded into a verbatim string, so don't offer to convert if we have
            // that.
            if (ch.Span.Length == 2 && ch.Rune.Value != 0)
            {
                return true;
            }
        }
 
        return false;
    }
}