File: RawStringLiteral\CSharpRawStringLiteralOnAutoInsertService.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.Composition;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.RawStringLiteral;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CSharp.RawStringLiteral;
 
[ExportLanguageService(typeof(IRawStringLiteralAutoInsertService), LanguageNames.CSharp), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class CSharpRawStringLiteralOnAutoInsertService() : IRawStringLiteralAutoInsertService
{
    public TextChange? GetTextChangeForQuote(Document document, SourceText text, int caretPosition, CancellationToken cancellationToken)
    {
        return
            TryGenerateInitialEmptyRawString(text, document, caretPosition, cancellationToken) ??
            TryGrowInitialEmptyRawString(text, document, caretPosition, cancellationToken) ??
            TryGrowRawStringDelimiters(text, document, caretPosition, cancellationToken);
    }
 
    /// <summary>
    /// When typing <c>"</c> given a normal string like <c>""$$</c>, then update the text to be <c>"""$$"""</c>.
    /// Note that this puts the user in the position where TryGrowInitialEmptyRawString can now take effect.
    /// </summary>
    private static TextChange? TryGenerateInitialEmptyRawString(
        SourceText text,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        // if we have ""$$"   then typing `"` here should not be handled by this path but by TryGrowInitialEmptyRawString
        if (position + 1 < text.Length && text[position + 1] == '"')
            return null;
 
        var start = position;
        while (start - 1 >= 0 && text[start - 1] == '"')
            start--;
 
        // must have exactly `""`
        if (position - start != 2)
            return null;
 
        while (start - 1 >= 0 && text[start - 1] == '$')
            start--;
 
        // hitting `"` after `@""` shouldn't do anything
        if (start - 1 >= 0 && text[start - 1] == '@')
            return null;
 
        var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
        var token = root.FindToken(start);
        if (token.SpanStart != start)
            return null;
 
        if (token.Kind() is not (SyntaxKind.StringLiteralToken or
                                 SyntaxKind.InterpolatedStringStartToken or
                                 SyntaxKind.InterpolatedSingleLineRawStringStartToken or
                                 SyntaxKind.InterpolatedMultiLineRawStringStartToken))
        {
            return null;
        }
 
        return new TextChange(new TextSpan(position + 1, 0), "\"\"\"");
    }
 
    /// <summary>
    /// When typing <c>"</c> given a raw string like <c>"""$$"""</c> (or a similar multiline form), then update the
    /// text to be: <c>""""$$""""</c>.  i.e. grow both the start and end delimiters to keep the string properly
    /// balanced.  This differs from TryGrowRawStringDelimiters in that the language will consider that initial
    /// <c>""""""</c> text to be a single delimiter, while we want to treat it as two.
    /// </summary>
    private static TextChange? TryGrowInitialEmptyRawString(
        SourceText text,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        var start = position;
        while (start - 1 >= 0 && text[start - 1] == '"')
            start--;
 
        var end = position;
        while (end < text.Length && text[end] == '"')
            end++;
 
        // Have to have an even number of quotes.
        var quoteLength = end - start;
        if (quoteLength % 2 == 1)
            return null;
 
        // User position must be halfway through the quotes.
        if (position != (start + quoteLength / 2))
            return null;
 
        // have to at least have `"""$$"""`
        if (quoteLength < 6)
            return null;
 
        while (start - 1 >= 0 && text[start - 1] == '$')
            start--;
 
        var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
        var token = root.FindToken(start);
        if (token.SpanStart != start)
            return null;
 
        if (token.Kind() is not (SyntaxKind.SingleLineRawStringLiteralToken or
                                 SyntaxKind.MultiLineRawStringLiteralToken or
                                 SyntaxKind.InterpolatedSingleLineRawStringStartToken or
                                 SyntaxKind.InterpolatedMultiLineRawStringStartToken))
        {
            return null;
        }
 
        return new TextChange(new TextSpan(position + 1, 0), "\"");
    }
 
    /// <summary>
    /// When typing <c>"</c> given a raw string like <c>"""$$ goo bar """</c> (or a similar multiline form), then
    /// update the text to be: <c>"""" goo bar """"</c>.  i.e. grow both the start and end delimiters to keep the
    /// string properly balanced.
    /// </summary>
    private static TextChange? TryGrowRawStringDelimiters(
        SourceText text,
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        // if we have """$$"   then typing `"` here should not grow the start/end quotes.  we only want to grow them
        // if the user is at the end of the start delimiter.
        if (position < text.Length && text[position] == '"')
            return null;
 
        var start = position;
        while (start - 1 >= 0 && text[start - 1] == '"')
            start--;
 
        // must have at least three quotes for this to be a raw string
        var quoteCount = position - start;
        if (quoteCount < 3)
            return null;
 
        while (start - 1 >= 0 && text[start - 1] == '$')
            start--;
 
        var root = document.GetRequiredSyntaxRootSynchronously(cancellationToken);
        var token = root.FindToken(start);
        if (token.SpanStart != start)
            return null;
 
        if (token.Span.Length < (2 * quoteCount))
            return null;
 
        if (token.Kind() is SyntaxKind.InterpolatedSingleLineRawStringStartToken or SyntaxKind.InterpolatedMultiLineRawStringStartToken)
        {
            var interpolatedString = (InterpolatedStringExpressionSyntax)token.GetRequiredParent();
            var endToken = interpolatedString.StringEndToken;
            if (!endToken.Text.EndsWith(new string('"', quoteCount)))
                return null;
        }
        else if (token.Kind() is SyntaxKind.SingleLineRawStringLiteralToken or SyntaxKind.MultiLineRawStringLiteralToken)
        {
            if (!token.Text.EndsWith(new string('"', quoteCount)))
                return null;
        }
 
        return new TextChange(new TextSpan(token.GetRequiredParent().Span.End, 0), "\"");
    }
}