File: Snippets\SnippetProviders\AbstractSnippetProvider.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.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Snippets.SnippetProviders;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Snippets;
 
internal abstract class AbstractSnippetProvider<TSnippetSyntax> : ISnippetProvider
    where TSnippetSyntax : SyntaxNode
{
    public abstract string Identifier { get; }
    public abstract string Description { get; }
 
    public virtual ImmutableArray<string> AdditionalFilterTexts => [];
 
    protected static readonly SyntaxAnnotation FindSnippetAnnotation = new();
 
    /// <summary>
    /// Implemented by each SnippetProvider to determine if that particular position is a valid
    /// location for the snippet to be inserted.
    /// </summary>
    protected abstract bool IsValidSnippetLocationCore(SnippetContext context, CancellationToken cancellationToken);
 
    /// <summary>
    /// Generates the new snippet's TextChanges that are being inserted into the document.
    /// </summary>
    protected abstract Task<ImmutableArray<TextChange>> GenerateSnippetTextChangesAsync(Document document, int position, CancellationToken cancellationToken);
 
    /// <summary>
    /// Gets the position that we want the caret to be at after all of the indentation/formatting has been done.
    /// </summary>
    protected abstract int GetTargetCaretPosition(TSnippetSyntax caretTarget, SourceText sourceText);
 
    /// <summary>
    /// Method to find the locations that must be renamed and where tab stops must be inserted into the snippet.
    /// </summary>
    protected abstract ImmutableArray<SnippetPlaceholder> GetPlaceHolderLocationsList(TSnippetSyntax node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken);
 
    public bool IsValidSnippetLocation(SnippetContext context, CancellationToken cancellationToken)
    {
        var syntaxFacts = context.Document.GetRequiredLanguageService<ISyntaxFactsService>();
        var syntaxTree = context.SyntaxContext.SyntaxTree;
        if (syntaxFacts.IsInNonUserCode(syntaxTree, context.Position, cancellationToken))
        {
            return false;
        }
 
        return IsValidSnippetLocationCore(context, cancellationToken);
    }
 
    /// <summary>
    /// Handles all the work to generate the Snippet.
    /// Reformats the document with the snippet TextChange and annotates 
    /// appropriately for the cursor to get the target cursor position.
    /// </summary>
    public async Task<SnippetChange> GetSnippetChangeAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
 
        // Generates the snippet as a list of text changes
        var textChanges = await GenerateSnippetTextChangesAsync(document, position, cancellationToken).ConfigureAwait(false);
 
        // Applies the snippet text changes to the document 
        var snippetDocument = await GetDocumentWithSnippetAsync(document, textChanges, cancellationToken).ConfigureAwait(false);
 
        // Finds the inserted snippet and replaces the node in the document with a node that has added trivia
        // since all trivia is removed when converted to a TextChange.
        var snippetWithTriviaDocument = await GetDocumentWithSnippetAndTriviaAsync(snippetDocument, position, syntaxFacts, cancellationToken).ConfigureAwait(false);
 
        // Adds annotations to inserted snippet to be formatted, simplified, add imports if needed, etc.
        var formatAnnotatedSnippetDocument = await AddFormatAnnotationAsync(snippetWithTriviaDocument, position, cancellationToken).ConfigureAwait(false);
 
        // Goes through and calls upon the formatting engines that the previous step annotated.
        var reformattedDocument = await CleanupDocumentAsync(formatAnnotatedSnippetDocument, cancellationToken).ConfigureAwait(false);
 
        // Finds the added snippet and adds indentation where necessary (braces).
        var documentWithIndentation = await AddIndentationToDocumentAsync(reformattedDocument, cancellationToken).ConfigureAwait(false);
 
        var reformattedRoot = await documentWithIndentation.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var mainChangeNode = (TSnippetSyntax)reformattedRoot.GetAnnotatedNodes(FindSnippetAnnotation).First();
 
        var annotatedReformattedDocument = documentWithIndentation.WithSyntaxRoot(reformattedRoot);
 
        // All the TextChanges from the original document. Will include any imports (if necessary) and all snippet associated
        // changes after having been formatted.
        var changes = await annotatedReformattedDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
 
        // Gets a listing of the identifiers that need to be found in the snippet TextChange
        // and their associated TextSpan so they can later be converted into an LSP snippet format.
        var placeholders = GetPlaceHolderLocationsList(mainChangeNode, syntaxFacts, cancellationToken);
 
        // All the changes from the original document to the most updated. Will later be
        // collapsed into one collapsed TextChange.
        var changesArray = changes.ToImmutableArray();
        var sourceText = await annotatedReformattedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        return new SnippetChange(
            textChanges: changesArray,
            placeholders: placeholders,
            finalCaretPosition: GetTargetCaretPosition(mainChangeNode, sourceText));
    }
 
    /// <summary>
    /// Descends into the inserted snippet to add back trivia on every token.
    /// </summary>
    private static SyntaxNode? GenerateElasticTriviaForSyntax(ISyntaxFacts syntaxFacts, SyntaxNode? node)
    {
        if (node is null)
        {
            return null;
        }
 
        var allTokens = node.DescendantTokens(descendIntoTrivia: true).ToList();
 
        // Skips the first and last token since
        // those do not need elastic trivia added to them.
        var nodeWithTrivia = node.ReplaceTokens(allTokens.Skip(1).Take(allTokens.Count - 2),
            (oldToken, _) => oldToken.WithAdditionalAnnotations(SyntaxAnnotation.ElasticAnnotation)
            .WithAppendedTrailingTrivia(syntaxFacts.ElasticMarker)
            .WithPrependedLeadingTrivia(syntaxFacts.ElasticMarker));
 
        return nodeWithTrivia;
    }
 
    private static async Task<Document> CleanupDocumentAsync(
        Document document, CancellationToken cancellationToken)
    {
        if (document.SupportsSyntaxTree)
        {
            var addImportPlacementOptions = await document.GetAddImportPlacementOptionsAsync(cancellationToken).ConfigureAwait(false);
            var simplifierOptions = await document.GetSimplifierOptionsAsync(cancellationToken).ConfigureAwait(false);
            var syntaxFormattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
 
            document = await ImportAdder.AddImportsFromSymbolAnnotationAsync(
                document, FindSnippetAnnotation, addImportPlacementOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
 
            document = await Simplifier.ReduceAsync(document, FindSnippetAnnotation, simplifierOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
 
            // format any node with explicit formatter annotation
            document = await Formatter.FormatAsync(document, FindSnippetAnnotation, syntaxFormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
 
            // format any elastic whitespace
            document = await Formatter.FormatAsync(document, SyntaxAnnotation.ElasticAnnotation, syntaxFormattingOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
        }
 
        return document;
    }
 
    /// <summary>
    /// Locates the snippet that was inserted. Generates trivia for every token in that SyntaxNode.
    /// Replaces the SyntaxNodes and gets back the new document.
    /// </summary>
    private async Task<Document> GetDocumentWithSnippetAndTriviaAsync(Document snippetDocument, int position, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken)
    {
        var root = await snippetDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var nearestStatement = FindAddedSnippetSyntaxNode(root, position);
 
        if (nearestStatement is null)
        {
            return snippetDocument;
        }
 
        var nearestStatementWithTrivia = GenerateElasticTriviaForSyntax(syntaxFacts, nearestStatement);
 
        if (nearestStatementWithTrivia is null)
        {
            return snippetDocument;
        }
 
        root = root.ReplaceNode(nearestStatement, nearestStatementWithTrivia);
        return snippetDocument.WithSyntaxRoot(root);
    }
 
    private static async Task<Document> GetDocumentWithSnippetAsync(Document document, ImmutableArray<TextChange> snippets, CancellationToken cancellationToken)
    {
        var originalText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        originalText = originalText.WithChanges(snippets);
        var snippetDocument = document.WithText(originalText);
 
        return snippetDocument;
    }
 
    private async Task<Document> AddFormatAnnotationAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var annotatedSnippetRoot = await AnnotateNodesToReformatAsync(document, position, cancellationToken).ConfigureAwait(false);
        document = document.WithSyntaxRoot(annotatedSnippetRoot);
        return document;
    }
 
    /// <summary>
    /// Method to added formatting annotations to the created snippet.
    /// </summary>
    protected virtual async Task<SyntaxNode> AnnotateNodesToReformatAsync(
        Document document, int position, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var snippetExpressionNode = FindAddedSnippetSyntaxNode(root, position);
        Contract.ThrowIfNull(snippetExpressionNode);
 
        var reformatSnippetNode = snippetExpressionNode.WithAdditionalAnnotations(FindSnippetAnnotation, Simplifier.Annotation, Formatter.Annotation);
        return root.ReplaceNode(snippetExpressionNode, reformatSnippetNode);
    }
 
    protected virtual TSnippetSyntax? FindAddedSnippetSyntaxNode(SyntaxNode root, int position)
        => root.FindNode(TextSpan.FromBounds(position, position), getInnermostNodeForTie: true) as TSnippetSyntax;
 
    /// <summary>
    /// Certain snippets require more indentation - snippets with blocks.
    /// The SyntaxGenerator does not insert this space for us nor does the LSP Snippet Expander.
    /// We need to manually add that spacing to snippets containing blocks.
    /// </summary>
    private async Task<Document> AddIndentationToDocumentAsync(Document document, CancellationToken cancellationToken)
    {
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var snippetNode = root.GetAnnotatedNodes(FindSnippetAnnotation).FirstOrDefault();
 
        if (snippetNode is not TSnippetSyntax snippet)
            return document;
 
        return await AddIndentationToDocumentAsync(document, snippet, cancellationToken).ConfigureAwait(false);
    }
 
    protected virtual Task<Document> AddIndentationToDocumentAsync(Document document, TSnippetSyntax snippet, CancellationToken cancellationToken)
        => Task.FromResult(document);
}