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.Diagnostics;
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 TextSpan GetTargetSelectionSpan(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, newSpan) = await DetermineNewDocumentAsync(document, item, cancellationToken).ConfigureAwait(false);
        var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
 
        var changes = await newDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
        var changesArray = changes.ToImmutableArray();
        var change = Utilities.Collapse(newText, changesArray);
 
        return CompletionChange.Create(change, changesArray, properties: ImmutableDictionary<string, string>.Empty, newSpan, includesCommitCharacter: true);
    }
 
    private async Task<(Document, TextSpan? caretPosition)> 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, null);
        }
 
        var memberContainingDocumentCleanupOptions = await document.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        var result = await RemoveDestinationNodeAsync(memberContainingDocument, memberContainingDocumentCleanupOptions, cancellationToken).ConfigureAwait(false);
        return result;
    }
 
    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, SourceText text)
    {
        var startToken = insertionRoot.GetAnnotatedTokens(_replaceStartAnnotation).FirstOrNull();
        Contract.ThrowIfNull(startToken);
        var endToken = insertionRoot.GetAnnotatedTokens(_replaceEndAnnotation).FirstOrNull();
        Contract.ThrowIfNull(endToken);
 
        var line = text.Lines.GetLineFromPosition(endToken.Value.Span.End);
 
        return TextSpan.FromBounds(startToken.Value.SpanStart, line.EndIncludingLineBreak);
    }
 
    private async Task<(Document Document, TextSpan? Selection)> RemoveDestinationNodeAsync(
        Document memberContainingDocument, CodeCleanupOptions cleanupOptions, CancellationToken cancellationToken)
    {
        // We now have a replacement node inserted into the document, but we still have the source code that triggered completion (with associated trivia).
        // We need to move the trivia to the new replacement and remove the original code.
        var root = await memberContainingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
 
        // Compute the destination span and node in the new tree.  Depending on the context, the destination node can end up
        // containing more code (and trivia) than what we want to remove.  For example
        // ```
        //     override Eq
        //     [Attribute] public void M() {}
        // ```
        // returns a destination node (array syntax) containing both the `override Eq` and the `[Attribute]` on the line below.
        var text = await memberContainingDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var destinationSpan = ComputeDestinationSpan(root, text);
        var destinationNode = root.FindNode(destinationSpan, true);
        var syntaxFacts = memberContainingDocument.Project.Services.GetRequiredService<ISyntaxFactsService>();
 
        // Given that the destination node can contain code we want to keep, we can't directly remove it using syntax tree manipulations.
        // It is difficult to create a valid syntax tree manipulation that does only a partial removal of the destination node / tokens.
        //
        // Instead it is much easier to remove the old completion source with a direct text edit (we know the exact span).
        // But in order to do that, we first must move trivia to the replacement node and format/simplify (requires tree annotations).
 
        // First find all tokens inside the node that intersect the span being deleted.  Move trivia from these tokens to the replacement node.
        // Tokens from the destination node, but outside the destination span should not be touched.
        var destinationTokens = destinationNode.DescendantTokens(destinationSpan);
 
        SyntaxTriviaList leadingTriviaToCopy = [];
        SyntaxTriviaList trailingTriviaToCopy = [];
        root = root.ReplaceTokens(destinationTokens, (original, originalAdjusted) =>
        {
            // Save the trivia for later application onto the replacement node and then delete.
            leadingTriviaToCopy = leadingTriviaToCopy.AddRange(original.LeadingTrivia);
            trailingTriviaToCopy = trailingTriviaToCopy.AddRange(original.TrailingTrivia);
 
            var trailingEndOfLine = originalAdjusted.TrailingTrivia.FirstOrNull(t => syntaxFacts.IsEndOfLineTrivia(t));
            var destinationWithoutTrivia = originalAdjusted.WithoutTrivia();
            // If there was an end of line attached to the destination token, keep it as otherwise lines below
            // may get moved up to the same line as the destination span line we're removing later.
            if (trailingEndOfLine is not null)
                destinationWithoutTrivia = destinationWithoutTrivia.WithTrailingTrivia(trailingEndOfLine.Value);
            return destinationWithoutTrivia;
        });
 
        // Add the saved trivia on to the replacement node.
        var replacingNode = root.GetAnnotatedNodes(_annotation).Single();
        root = root.ReplaceNode(replacingNode, replacingNode.WithLeadingTrivia(leadingTriviaToCopy).WithTrailingTrivia(trailingTriviaToCopy));
 
        // We've finished the major modifications, we can now format and simplify.
        var document = memberContainingDocument.WithSyntaxRoot(root);
        document = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cleanupOptions.SimplifierOptions, cancellationToken).ConfigureAwait(false);
        document = await Formatter.FormatAsync(document, Formatter.Annotation, cleanupOptions.FormattingOptions, cancellationToken).ConfigureAwait(false);
 
        // Formatting/simplification changed the tree, so recompute the destination span.
        root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        destinationSpan = ComputeDestinationSpan(root, text);
 
        // We have basically the final tree.  Calculate the new caret position while we still have the annotations.
        TextSpan? newSpan = null;
        var caretTarget = root.GetAnnotatedNodes(_annotation).FirstOrDefault();
        if (caretTarget != null)
        {
            var targetSelectionSpan = GetTargetSelectionSpan(caretTarget);
 
            if (targetSelectionSpan.Start > 0 && targetSelectionSpan.End <= text.Length)
            {
                // The new replacement method should always be inserted before the destination span we're removing.
                // This means the end selection position in the inserted method should be safe to return as-is.
                Debug.Assert(targetSelectionSpan.End < destinationSpan.Start);
                newSpan = targetSelectionSpan;
            }
        }
 
        // Now we can finally delete the destination span.  It is safe to delete the whole line here (instead of just the destination span)
        // as override completion will not be shown with unrelated text preceding or following the override trigger.
        text.GetLineAndOffset(destinationSpan.Start, out var lineNumber, out _);
        var textChange = new TextChange(text.Lines[lineNumber].SpanIncludingLineBreak, string.Empty);
 
        text = text.WithChanges(textChange);
        return (document.WithText(text), newSpan);
    }
 
    internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
        => MemberInsertionCompletionItem.GetDescriptionAsync(item, document, displayOptions, cancellationToken);
}