File: Snippets\RoslynLSPSnippetConverter.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Snippets;
 
internal static class RoslynLSPSnippetConverter
{
    /// <summary>
    /// Extends the TextChange to encompass all placeholder positions as well as caret position.
    /// Generates a LSP formatted snippet from a TextChange, list of placeholders, and caret position.
    /// </summary>
    public static async Task<string> GenerateLSPSnippetAsync(Document document, int caretPosition, ImmutableArray<SnippetPlaceholder> placeholders, TextChange textChange, int triggerLocation, CancellationToken cancellationToken)
    {
        var extendedTextChange = await ExtendSnippetTextChangeAsync(document, textChange, placeholders, caretPosition, triggerLocation, cancellationToken).ConfigureAwait(false);
        return ConvertToLSPSnippetString(extendedTextChange, placeholders, caretPosition);
    }
 
    /// <summary>
    /// Iterates through every index in the snippet string and determines where the
    /// LSP formatted chunks should be inserted for each placeholder.
    /// </summary>
    private static string ConvertToLSPSnippetString(TextChange textChange, ImmutableArray<SnippetPlaceholder> placeholders, int caretPosition)
    {
        var textChangeStart = textChange.Span.Start;
        var textChangeText = textChange.NewText;
        Contract.ThrowIfNull(textChangeText);
 
        using var _1 = PooledStringBuilder.GetInstance(out var lspSnippetString);
        using var _2 = PooledDictionary<int, (string text, int priority)>.GetInstance(out var dictionary);
        PopulateMapOfSpanStartsToLSPStringItem(dictionary, placeholders, textChangeStart);
 
        // Need to go through the length + 1 since caret positions occur before and after the
        // character position.
        // If there is a caret at the end of the line, then it's position
        // will be equivalent to the length of the TextChange.
        for (var i = 0; i < textChange.Span.Length + 1;)
        {
            if (i == caretPosition - textChangeStart)
            {
                lspSnippetString.Append("$0");
            }
 
            // Tries to see if a value exists at that position in the map, and if so it
            // generates a string that is LSP formatted.
            if (dictionary.TryGetValue(i, out var placeholderInfo))
            {
                var str = $"${{{placeholderInfo.priority}:{placeholderInfo.text}}}";
                lspSnippetString.Append(str);
 
                // Skip past the entire identifier in the TextChange text
                i += placeholderInfo.text.Length;
            }
            else
            {
                if (i < textChangeText.Length)
                {
                    lspSnippetString.Append(textChangeText[i]);
                    i++;
                }
                else
                {
                    break;
                }
            }
        }
 
        return lspSnippetString.ToString();
    }
 
    /// <summary>
    /// Preprocesses the list of placeholders into a dictionary that maps the insertion position
    /// in the string to the placeholder's identifier and the priority associated with it.
    /// </summary>
    private static void PopulateMapOfSpanStartsToLSPStringItem(Dictionary<int, (string identifier, int priority)> dictionary, ImmutableArray<SnippetPlaceholder> placeholders, int textChangeStart)
    {
        for (var i = 0; i < placeholders.Length; i++)
        {
            var placeholder = placeholders[i];
            foreach (var position in placeholder.StartingPositions)
            {
                // i + 1 since the placeholder priority is set according to the index in the
                // placeholders array, starting at 1.
                // We should never be adding two placeholders in the same position since identifiers
                // must have a length greater than 0.
                dictionary.Add(position - textChangeStart, (placeholder.Text, i + 1));
            }
        }
    }
 
    /// <summary>
    /// We need to extend the snippet's TextChange if any of the placeholders or
    /// if the caret position comes before or after the span of the TextChange.
    /// If so, then find the new string that encompasses all of the placeholders
    /// and caret position.
    /// This is important for the cases in which the document does not determine the TextChanges from
    /// the original document accurately.
    /// </summary>
    private static async Task<TextChange> ExtendSnippetTextChangeAsync(Document document, TextChange textChange, ImmutableArray<SnippetPlaceholder> placeholders, int caretPosition, int triggerLocation, CancellationToken cancellationToken)
    {
        var extendedSpan = GetUpdatedTextSpan(textChange, placeholders, caretPosition, triggerLocation);
        var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var newString = documentText.ToString(extendedSpan);
        var newTextChange = new TextChange(extendedSpan, newString);
 
        return newTextChange;
    }
 
    /// <summary>
    /// Iterates through the placeholders and determines if any of the positions
    /// come before or after what is indicated by the snippet's TextChange.
    /// If so, adjust the starting and ending position accordingly.
    /// </summary>
    private static TextSpan GetUpdatedTextSpan(TextChange textChange, ImmutableArray<SnippetPlaceholder> placeholders, int caretPosition, int triggerLocation)
    {
        var textChangeText = textChange.NewText;
        Contract.ThrowIfNull(textChangeText);
 
        var startPosition = textChange.Span.Start;
        var endPosition = textChange.Span.Start + textChangeText.Length;
 
        if (placeholders.Length > 0)
        {
            startPosition = Math.Min(startPosition, placeholders.Min(placeholder => placeholder.StartingPositions.Min()));
            endPosition = Math.Max(endPosition, placeholders.Max(placeholder => placeholder.StartingPositions.Max()));
        }
 
        startPosition = Math.Min(startPosition, caretPosition);
        endPosition = Math.Max(endPosition, caretPosition);
 
        startPosition = Math.Min(startPosition, triggerLocation);
 
        return TextSpan.FromBounds(startPosition, endPosition);
    }
}