|
// 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;
}
}
|