File: Snippets\SnippetProviders\AbstractConsoleSnippetProvider.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.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Snippets.SnippetProviders;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Snippets;
 
internal abstract class AbstractConsoleSnippetProvider<
    TExpressionStatementSyntax,
    TExpressionSyntax,
    TArgumentListSyntax,
    TLambdaExpressionSyntax> : AbstractSingleChangeSnippetProvider<TExpressionSyntax>
    where TExpressionStatementSyntax : SyntaxNode
    where TExpressionSyntax : SyntaxNode
    where TArgumentListSyntax : SyntaxNode
    where TLambdaExpressionSyntax : TExpressionSyntax
{
    public sealed override string Identifier => CommonSnippetIdentifiers.ConsoleWriteLine;
 
    public sealed override string Description => FeaturesResources.console_writeline;
 
    public sealed override ImmutableArray<string> AdditionalFilterTexts { get; } = ["WriteLine"];
 
    protected abstract TArgumentListSyntax GetArgumentList(TExpressionSyntax expression);
    protected abstract SyntaxToken GetOpenParenToken(TArgumentListSyntax argumentList);
 
    protected sealed override async Task<TextChange> GenerateSnippetTextChangeAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var generator = SyntaxGenerator.GetGenerator(document);
 
        var resultingNode = generator.InvocationExpression(generator.MemberAccessExpression(generator.IdentifierName(nameof(Console)), nameof(Console.WriteLine)));
 
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var syntaxContext = document.GetRequiredLanguageService<ISyntaxContextService>().CreateContext(document, semanticModel, position, cancellationToken);
 
        // In case we are after an arrow token in lambda, Console.WriteLine acts like an expression,
        // so it doesn't need to be wrapped into a statement
        if (syntaxContext.TargetToken.Parent is not TLambdaExpressionSyntax)
        {
            resultingNode = generator.ExpressionStatement(resultingNode);
        }
 
        var change = new TextChange(TextSpan.FromBounds(position, position), resultingNode.ToFullString());
        return change;
    }
 
    /// <summary>
    /// Tries to get the location after the open parentheses in the argument list.
    /// If it can't, then we default to the end of the snippet's span.
    /// </summary>
    protected sealed override int GetTargetCaretPosition(TExpressionSyntax caretTarget, SourceText sourceText)
    {
        var argumentListNode = GetArgumentList(caretTarget);
        if (argumentListNode is null)
            return caretTarget.Span.End;
 
        var openParenToken = GetOpenParenToken(argumentListNode);
        return openParenToken.Span.End;
    }
 
    protected sealed override 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 compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
        var consoleSymbol = GetConsoleSymbolFromMetaDataName(compilation);
        var reformatSnippetNode = snippetExpressionNode.WithAdditionalAnnotations(FindSnippetAnnotation, Simplifier.Annotation, SymbolAnnotation.Create(consoleSymbol!), Formatter.Annotation);
        return root.ReplaceNode(snippetExpressionNode, reformatSnippetNode);
    }
 
    protected sealed override ImmutableArray<SnippetPlaceholder> GetPlaceHolderLocationsList(TExpressionSyntax node, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken)
        => [];
 
    protected static INamedTypeSymbol? GetConsoleSymbolFromMetaDataName(Compilation compilation)
        => compilation.GetBestTypeByMetadataName(typeof(Console).FullName!);
 
    protected sealed override TExpressionSyntax? FindAddedSnippetSyntaxNode(SyntaxNode root, int position)
    {
        var closestNode = root.FindNode(TextSpan.FromBounds(position, position));
        var nearestExpression = closestNode.FirstAncestorOrSelf<TExpressionSyntax>(static exp => exp.Parent is TExpressionStatementSyntax or TLambdaExpressionSyntax);
        if (nearestExpression is null)
            return null;
 
        // Checking to see if that expression that we found is
        // starting at the same position as the position we inserted
        // the Console WriteLine expression.
        if (nearestExpression.SpanStart != position)
            return null;
 
        return nearestExpression;
    }
}