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.CodeCleanup;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
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 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);
 
    private readonly SyntaxAnnotation _annotation = new();
    private readonly SyntaxAnnotation _replaceStartAnnotation = new();
    private readonly SyntaxAnnotation _replaceEndAnnotation = new();
 
    protected abstract SyntaxToken GetToken(CompletionItem completionItem, SyntaxTree tree, CancellationToken cancellationToken);
 
    protected abstract Task<ISymbol> GenerateMemberAsync(
        Document document, CompletionItem item, Compilation compilation, ISymbol member, INamedTypeSymbol containingType, CancellationToken cancellationToken);
    protected abstract int GetTargetCaretPosition(SyntaxNode caretTarget);
    protected abstract SyntaxNode GetSyntax(SyntaxToken commonSyntaxToken);
 
    protected static CompletionItemRules GetRules()
        => s_defaultRules;
 
    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);
 
                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
        // We annotate the line in order to handle adding the generated code before our annotated token in the same line
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var treeRoot = tree.GetRoot(cancellationToken);
 
        // 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 lineStart = line.GetFirstNonWhitespacePosition();
        Contract.ThrowIfNull(lineStart);
        var endToken = GetToken(completionItem, tree, cancellationToken);
        var annotatedRoot = treeRoot.ReplaceToken(
            endToken, endToken.WithAdditionalAnnotations(_replaceEndAnnotation));
 
        var startToken = annotatedRoot.FindTokenOnRightOfPosition(lineStart.Value);
        annotatedRoot = annotatedRoot.ReplaceToken(
            startToken, startToken.WithAdditionalAnnotations(_replaceStartAnnotation));
 
        // 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.  Pass in 'forceFreeze: true' to
        // ensure all further transformations we make do not run generators either.
        document = document.WithSyntaxRoot(annotatedRoot).WithFrozenPartialSemantics(forceFreeze: true, 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);
 
        document = await RemoveDestinationNodeAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false);
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
        document = await Simplifier.ReduceAsync(document, _annotation, memberContainingDocumentCleanupOptions.SimplifierOptions, 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 member = symbols.FirstOrDefault();
 
        // If SymbolKey resolution failed, then bail.
        if (member == null)
            return null;
 
        // CodeGenerationOptions containing before and after
        var context = new CodeGenerationSolutionContext(
            document.Project.Solution,
            new CodeGenerationContext(
                autoInsertionLocation: false,
                beforeThisLocation: semanticModel.SyntaxTree.GetLocation(TextSpan.FromBounds(line.Start, line.Start))));
 
        var generatedMember = await GenerateMemberAsync(
            document, completionItem, semanticModel.Compilation, member, containingType, cancellationToken).ConfigureAwait(false);
        generatedMember = _annotation.AddAnnotationToSymbol(generatedMember);
 
        return generatedMember switch
        {
            IMethodSymbol method => await codeGenService.AddMethodAsync(context, containingType, method, cancellationToken).ConfigureAwait(false),
            IPropertySymbol property => await codeGenService.AddPropertyAsync(context, containingType, property, cancellationToken).ConfigureAwait(false),
            IEventSymbol @event => await codeGenService.AddEventAsync(context, containingType, @event, cancellationToken).ConfigureAwait(false),
            _ => document
        };
    }
 
    private TextSpan ComputeDestinationSpan(SyntaxNode insertionRoot)
    {
        var startToken = insertionRoot.GetAnnotatedTokens(_replaceStartAnnotation).FirstOrNull();
        Contract.ThrowIfNull(startToken);
        var endToken = insertionRoot.GetAnnotatedTokens(_replaceEndAnnotation).FirstOrNull();
        Contract.ThrowIfNull(endToken);
 
        var text = insertionRoot.GetText();
        var line = text.Lines.GetLineFromPosition(endToken.Value.Span.End);
 
        return TextSpan.FromBounds(startToken.Value.SpanStart, line.EndIncludingLineBreak);
    }
 
    private async Task<Document> RemoveDestinationNodeAsync(
        Document memberContainingDocument, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken)
    {
        // At this stage we have created the replacing node, but we also have the source node that triggered the completion
        // To remove the old node, we need to port over the trivia and then recalculate the node to remove
        // since we may have adjusted the position of the node by inserting trivia before the new node
 
        var root = await memberContainingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var destinationSpan = ComputeDestinationSpan(root);
        var destinationNode = root.FindNode(destinationSpan, true);
        var replacingNode = root.GetAnnotatedNodes(_annotation).Single();
        // WithTriviaFrom does not completely fit our purpose because we could be missing trivia from interior missing tokens,
        // with all the last tokens being missing, and thus only having part of the picture
        root = root.ReplaceNode(replacingNode, replacingNode.WithTriviaFromIncludingMissingTokens(destinationNode));
 
        // Now that we have replaced the node, find the destination node again
        destinationSpan = ComputeDestinationSpan(root);
        destinationNode = root.FindNode(destinationSpan);
        SyntaxNode newRoot;
        if (destinationSpan.Contains(destinationNode.Span))
        {
            newRoot = root.RemoveNode(destinationNode, SyntaxRemoveOptions.KeepNoTrivia)!;
        }
        else
        {
            var tokens = destinationNode.DescendantTokens(destinationSpan);
            newRoot = root.ReplaceTokens(tokens, static (_, _) => default);
        }
 
        var document = memberContainingDocument.WithSyntaxRoot(newRoot);
 
        document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
        document = await Formatter.FormatAsync(document, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false);
 
        return document;
    }
 
    internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
        => MemberInsertionCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);
}