File: SplitStringLiteral\InterpolatedStringSplitter.cs
Web Access
Project: src\roslyn\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.

#nullable disable

using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Indentation;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.CSharp.SplitStringLiteral;

using static CSharpSyntaxTokens;
using static SyntaxFactory;

internal abstract partial class StringSplitter
{
    private sealed class InterpolatedStringSplitter(
        ParsedDocument document,
        int position,
        InterpolatedStringExpressionSyntax interpolatedStringExpression,
        IndentationOptions indentationOptions,
        CancellationToken cancellationToken) : StringSplitter(document, position, indentationOptions, cancellationToken)
    {
        private readonly InterpolatedStringExpressionSyntax _interpolatedStringExpression = interpolatedStringExpression;

        protected override SyntaxNode GetNodeToReplace() => _interpolatedStringExpression;

        // Don't offer on $@"" strings and raw string literals.  They support newlines directly in their content.
        protected override bool CheckToken()
            => _interpolatedStringExpression.StringStartToken.Kind() == SyntaxKind.InterpolatedStringStartToken;

        protected override BinaryExpressionSyntax CreateSplitString()
        {
            var contents = _interpolatedStringExpression.Contents.ToList();

            var beforeSplitContents = new List<InterpolatedStringContentSyntax>();
            var afterSplitContents = new List<InterpolatedStringContentSyntax>();

            foreach (var content in contents)
            {
                if (content.Span.End <= CursorPosition)
                {
                    // Content is entirely before the cursor.  Nothing needs to be done to it.
                    beforeSplitContents.Add(content);
                }
                else if (content.Span.Start >= CursorPosition)
                {
                    // Content is entirely after the cursor.  Nothing needs to be done to it.
                    afterSplitContents.Add(content);
                }
                else
                {
                    // Content crosses the cursor.  Need to split it.
                    beforeSplitContents.Add(CreateInterpolatedStringText(content.SpanStart, CursorPosition));
                    afterSplitContents.Insert(0, CreateInterpolatedStringText(CursorPosition, content.Span.End));
                }
            }

            var leftExpression = InterpolatedStringExpression(
                _interpolatedStringExpression.StringStartToken,
                [.. beforeSplitContents],
                InterpolatedStringEndToken
                             .WithTrailingTrivia(ElasticSpace));

            var rightExpression = InterpolatedStringExpression(
                InterpolatedStringStartToken,
                [.. afterSplitContents],
                _interpolatedStringExpression.StringEndToken);

            return BinaryExpression(
                SyntaxKind.AddExpression,
                leftExpression,
                PlusNewLineToken,
                rightExpression.WithAdditionalAnnotations(RightNodeAnnotation));
        }

        private InterpolatedStringTextSyntax CreateInterpolatedStringText(int start, int end)
        {
            var content = Document.Text.ToString(TextSpan.FromBounds(start, end));
            return InterpolatedStringText(
                Token(
                    leading: default,
                    kind: SyntaxKind.InterpolatedStringTextToken,
                    text: content,
                    valueText: "",
                    trailing: default));
        }

        protected override int StringOpenQuoteLength() => "$\"".Length;
    }
}