File: Completion\Providers\AbstractMemberInsertingCompletionProvider.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.CodeActions;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Completion.Providers;
 
internal abstract partial class AbstractMemberInsertingCompletionProvider : LSPCompletionProvider
{
    private readonly SyntaxAnnotation _annotation = new();
    private readonly SyntaxAnnotation _otherAnnotation = new();
 
    protected abstract SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken);
 
    protected abstract Task<ISymbol> GenerateMemberAsync(ISymbol member, INamedTypeSymbol containingType, Document document, CompletionItem item, CancellationToken cancellationToken);
    protected abstract int GetTargetCaretPosition(SyntaxNode caretTarget);
    protected abstract SyntaxNode GetSyntax(SyntaxToken commonSyntaxToken);
 
    public AbstractMemberInsertingCompletionProvider()
    {
    }
 
    public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default)
    {
        var newDocument = await DetermineNewDocumentAsync(document, item, cancellationToken).ConfigureAwait(false);
        var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        int? newPosition = null;
 
        // Attempt to find the inserted node and move the caret appropriately
        if (newRoot != null)
        {
            var caretTarget = newRoot.GetAnnotatedNodes(_annotation).FirstOrDefault();
            if (caretTarget != null)
            {
                var targetPosition = GetTargetCaretPosition(caretTarget);
 
                // Something weird happened and we failed to get a valid position.
                // Bail on moving the caret.
                if (targetPosition > 0 && targetPosition <= newText.Length)
                {
                    newPosition = targetPosition;
                }
            }
        }
 
        var changes = await newDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
        var changesArray = changes.ToImmutableArray();
        var change = Utilities.Collapse(newText, changesArray);
 
        return CompletionChange.Create(change, changesArray, newPosition, includesCommitCharacter: true);
    }
 
    private async Task<Document> DetermineNewDocumentAsync(
        Document document,
        CompletionItem completionItem,
        CancellationToken cancellationToken)
    {
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        // The span we're going to replace
        var line = text.Lines[MemberInsertionCompletionItem.GetLine(completionItem)];
 
        // Annotate the line we care about so we can find it after adding usings
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var token = GetToken(completionItem, tree, cancellationToken);
        var annotatedRoot = tree.GetRoot(cancellationToken).ReplaceToken(token, token.WithAdditionalAnnotations(_otherAnnotation));
        // Make sure the new document is frozen before we try to get the semantic model. This is to 
        // avoid trigger source generator, which is expensive and not needed for calculating the change.
        document = document.WithSyntaxRoot(annotatedRoot).WithFrozenPartialSemantics(cancellationToken);
 
        var memberContainingDocument = await GenerateMemberAndUsingsAsync(document, completionItem, line, cancellationToken).ConfigureAwait(false);
        if (memberContainingDocument == null)
        {
            // Generating the new document failed because we somehow couldn't resolve
            // the underlying symbol's SymbolKey. At this point, we won't be able to 
            // make any changes, so just return the document we started with.
            return document;
        }
 
        var memberContainingDocumentCleanupOptions = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
        var insertionRoot = await GetTreeWithAddedSyntaxNodeRemovedAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false);
        var insertionText = await GenerateInsertionTextAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false);
 
        var destinationSpan = ComputeDestinationSpan(insertionRoot);
 
        var finalText = insertionRoot.GetText(text.Encoding)
            .Replace(destinationSpan, insertionText.Trim());
 
        document = document.WithText(finalText);
        var newRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var declaration = GetSyntax(newRoot.FindToken(destinationSpan.End));
 
        document = document.WithSyntaxRoot(newRoot.ReplaceNode(declaration, declaration.WithAdditionalAnnotations(_annotation)));
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        return await Formatter.FormatAsync(document, _annotation, formattingOptions, cancellationToken).ConfigureAwait(false);
    }
 
    private async Task<Document?> GenerateMemberAndUsingsAsync(
        Document document,
        CompletionItem completionItem,
        TextLine line,
        CancellationToken cancellationToken)
    {
        var codeGenService = document.GetRequiredLanguageService<ICodeGenerationService>();
 
        // Resolve member and type in our new, forked, solution
        var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
 
        var containingType = semanticModel.GetEnclosingSymbol<INamedTypeSymbol>(line.Start, cancellationToken);
        Contract.ThrowIfNull(containingType);
 
        var symbols = await SymbolCompletionItem.GetSymbolsAsync(completionItem, document, cancellationToken).ConfigureAwait(false);
        var overriddenMember = symbols.FirstOrDefault();
 
        if (overriddenMember == null)
        {
            // Unfortunately, SymbolKey resolution failed. Bail.
            return null;
        }
 
        // CodeGenerationOptions containing before and after
        var context = new CodeGenerationSolutionContext(
            document.Project.Solution,
            new CodeGenerationContext(
                contextLocation: semanticModel.SyntaxTree.GetLocation(TextSpan.FromBounds(line.Start, line.Start))));
 
        var generatedMember = await GenerateMemberAsync(overriddenMember, containingType, document, completionItem, cancellationToken).ConfigureAwait(false);
        generatedMember = _annotation.AddAnnotationToSymbol(generatedMember);
 
        Document? memberContainingDocument = null;
        if (generatedMember.Kind == SymbolKind.Method)
        {
            memberContainingDocument = await codeGenService.AddMethodAsync(context, containingType, (IMethodSymbol)generatedMember, cancellationToken).ConfigureAwait(false);
        }
        else if (generatedMember.Kind == SymbolKind.Property)
        {
            memberContainingDocument = await codeGenService.AddPropertyAsync(context, containingType, (IPropertySymbol)generatedMember, cancellationToken).ConfigureAwait(false);
        }
        else if (generatedMember.Kind == SymbolKind.Event)
        {
            memberContainingDocument = await codeGenService.AddEventAsync(context, containingType, (IEventSymbol)generatedMember, cancellationToken).ConfigureAwait(false);
        }
 
        return memberContainingDocument;
    }
 
    private TextSpan ComputeDestinationSpan(SyntaxNode insertionRoot)
    {
        var targetToken = insertionRoot.GetAnnotatedTokens(_otherAnnotation).FirstOrNull();
        Contract.ThrowIfNull(targetToken);
 
        var text = insertionRoot.GetText();
        var line = text.Lines.GetLineFromPosition(targetToken.Value.Span.End);
 
        // DevDiv 958235: 
        //
        // void goo()
        // {
        // }
        // override $$
        //
        // If our text edit includes the trailing trivia of the close brace of goo(),
        // that token will be reconstructed. The ensuing tree diff will then count
        // the { } as replaced even though we didn't want it to. If the user
        // has collapsed the outline for goo, that means we'll edit the outlined 
        // region and weird stuff will happen. Therefore, we'll start with the first
        // token on the line in order to leave the token and its trivia alone.
        var position = line.GetFirstNonWhitespacePosition();
        Contract.ThrowIfNull(position);
 
        var firstToken = insertionRoot.FindToken(position.Value);
        return TextSpan.FromBounds(firstToken.SpanStart, line.End);
    }
 
    private async Task<string> GenerateInsertionTextAsync(
        Document memberContainingDocument, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken)
    {
        memberContainingDocument = await Simplifier.ReduceAsync(memberContainingDocument, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
        memberContainingDocument = await Formatter.FormatAsync(memberContainingDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false);
 
        var root = await memberContainingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        return root.GetAnnotatedNodes(_annotation).Single().ToString().Trim();
    }
 
    private async Task<SyntaxNode> GetTreeWithAddedSyntaxNodeRemovedAsync(
        Document document, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken)
    {
        // Added imports are annotated for simplification too. Therefore, we simplify the document
        // before removing added member node to preserve those imports in the document.
        document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
 
        var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var members = root.GetAnnotatedNodes(_annotation).AsImmutable();
 
        root = root.RemoveNodes(members, SyntaxRemoveOptions.KeepUnbalancedDirectives);
        Contract.ThrowIfNull(root);
 
        var dismemberedDocument = document.WithSyntaxRoot(root);
 
        dismemberedDocument = await Formatter.FormatAsync(dismemberedDocument, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false);
        return await dismemberedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
    }
 
    private static readonly ImmutableArray<CharacterSetModificationRule> s_commitRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Replace, '(')];
 
    private static readonly ImmutableArray<CharacterSetModificationRule> s_filterRules = [CharacterSetModificationRule.Create(CharacterSetModificationKind.Remove, '(')];
 
    private static readonly CompletionItemRules s_defaultRules =
        CompletionItemRules.Create(
            commitCharacterRules: s_commitRules,
            filterCharacterRules: s_filterRules,
            enterKeyRule: EnterKeyRule.Never);
 
    protected static CompletionItemRules GetRules()
        => s_defaultRules;
 
    internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
        => MemberInsertionCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);
}