File: Completion\Providers\ImportCompletionProvider\AbstractImportCompletionProvider.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.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Completion.Log;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Completion.Providers;
 
internal abstract class AbstractImportCompletionProvider : LSPCompletionProvider, INotifyCommittingItemCompletionProvider
{
    protected abstract bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext);
    protected abstract void WarmUpCacheInBackground(Document document);
    protected abstract Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet<string> namespacesInScope, CancellationToken cancellationToken);
    protected abstract bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token);
    protected abstract Task<bool> ShouldProvideParenthesisCompletionAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken);
    protected abstract void LogCommit();
 
    public Task NotifyCommittingItemAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
    {
        LogCommit();
        return Task.CompletedTask;
    }
 
    internal override bool IsExpandItemProvider => true;
 
    public override async Task ProvideCompletionsAsync(CompletionContext completionContext)
    {
        if (!completionContext.CompletionOptions.ShouldShowItemsFromUnimportedNamespaces)
            return;
 
        var cancellationToken = completionContext.CancellationToken;
        var document = completionContext.Document;
 
        var syntaxContext = await completionContext.GetSyntaxContextWithExistingSpeculativeModelAsync(document, cancellationToken).ConfigureAwait(false);
 
        if (!ShouldProvideCompletion(completionContext, syntaxContext))
        {
            // Queue a background task to warm up cache and return immediately if this is not the context to trigger this provider.
            // `ForceExpandedCompletionIndexCreation` and `UpdateImportCompletionCacheInBackground` are both test only options to
            // make test behavior deterministic.
            var options = completionContext.CompletionOptions;
            if (options.UpdateImportCompletionCacheInBackground && !options.ForceExpandedCompletionIndexCreation)
                WarmUpCacheInBackground(document);
 
            return;
        }
 
        // Find all namespaces in scope at current cursor location, 
        // which will be used to filter so the provider only returns out-of-scope types.
        var namespacesInScope = GetNamespacesInScope(syntaxContext, cancellationToken);
        await AddCompletionItemsAsync(completionContext, syntaxContext, namespacesInScope, cancellationToken).ConfigureAwait(false);
    }
 
    private static HashSet<string> GetNamespacesInScope(SyntaxContext syntaxContext, CancellationToken cancellationToken)
    {
        var semanticModel = syntaxContext.SemanticModel;
        var document = syntaxContext.Document;
 
        var importedNamespaces = GetImportedNamespaces(syntaxContext, cancellationToken);
 
        // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language.
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var namespacesInScope = new HashSet<string>(importedNamespaces, syntaxFacts.StringComparer);
 
        // Get containing namespaces.
        var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken);
        while (namespaceSymbol != null)
        {
            namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat));
            namespaceSymbol = namespaceSymbol.ContainingNamespace;
        }
 
        return namespacesInScope;
    }
 
    private static ImmutableArray<string> GetImportedNamespaces(SyntaxContext context, CancellationToken cancellationToken)
    {
        var position = context.Position;
        var targetToken = context.TargetToken;
 
        // If we are immediately after `using` directive adjust position to the start of the next token.
        // This is a workaround for an issue, when immediately after a `using` directive it is not included into the import scope.
        // See https://github.com/dotnet/roslyn/issues/67447 for more info.
        if (context.IsRightAfterUsingOrImportDirective)
            position = targetToken.GetNextToken(includeZeroWidth: true).SpanStart;
 
        var scopes = context.SemanticModel.GetImportScopes(position, cancellationToken);
 
        using var _ = ArrayBuilder<string>.GetInstance(out var usingsBuilder);
 
        foreach (var scope in scopes)
        {
            foreach (var import in scope.Imports)
            {
                if (import.NamespaceOrType is INamespaceSymbol @namespace)
                {
                    usingsBuilder.Add(@namespace.ToDisplayString(SymbolDisplayFormats.NameFormat));
                }
            }
        }
 
        return usingsBuilder.ToImmutableAndClear();
    }
 
    public override async Task<CompletionChange> GetChangeAsync(
        Document document, CompletionItem completionItem, char? commitKey, CancellationToken cancellationToken)
    {
        var containingNamespace = ImportCompletionItem.GetContainingNamespace(completionItem);
        var provideParenthesisCompletion = await ShouldProvideParenthesisCompletionAsync(
            document,
            completionItem,
            commitKey,
            cancellationToken).ConfigureAwait(false);
 
        var insertText = completionItem.DisplayText;
        if (provideParenthesisCompletion)
        {
            insertText += "()";
            CompletionProvidersLogger.LogCustomizedCommitToAddParenthesis(commitKey);
        }
 
        if (await ShouldCompleteWithFullyQualifyTypeNameAsync().ConfigureAwait(false))
        {
            var completionText = $"{containingNamespace}.{insertText}";
            return CompletionChange.Create(new TextChange(completionItem.Span, completionText));
        }
 
        // Find context node so we can use it to decide where to insert using/imports.
        var tree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
        var addImportContextNode = root.FindToken(completionItem.Span.Start, findInsideTrivia: true).Parent;
 
        // Add required using/imports directive.
        var addImportService = document.GetRequiredLanguageService<IAddImportsService>();
        var generator = document.GetRequiredLanguageService<SyntaxGenerator>();
 
        var addImportsOptions = await document.GetAddImportPlacementOptionsAsync(cancellationToken).ConfigureAwait(false);
        var formattingOptions = await document.GetSyntaxFormattingOptionsAsync(cancellationToken).ConfigureAwait(false);
 
        var importNode = CreateImport(document, containingNamespace);
 
        var compilation = await document.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false);
        var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode!, importNode, generator, addImportsOptions, cancellationToken);
        var documentWithImport = document.WithSyntaxRoot(rootWithImport);
        // This only formats the annotated import we just added, not the entire document.
        var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, formattingOptions, cancellationToken).ConfigureAwait(false);
 
        using var _ = ArrayBuilder<TextChange>.GetInstance(out var builder);
 
        // Get text change for add import
        var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
        builder.AddRange(importChanges);
 
        // Create text change for complete type name.
        //
        // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, 
        //       then use Document.GetTextChangesAsync on document created from the changed text. This is
        //       because it will do a diff and return TextChanges with minimum span instead of actual 
        //       replacement span.
        //
        //       For example: If I'm typing "asd", the completion provider could be triggered after "a"
        //       is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described 
        //       above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of 
        //       the full display text with a span of length 1. This will later mess up span-tracking and end up 
        //       with "AsnEncodedDatasd" in the code.
        builder.Add(new TextChange(completionItem.Span, insertText));
 
        // Then get the combined change
        var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
        var newText = text.WithChanges(builder);
 
        var changes = builder.ToImmutable();
        var change = Utilities.Collapse(newText, changes);
        return CompletionChange.Create(change, changes);
 
        async Task<bool> ShouldCompleteWithFullyQualifyTypeNameAsync()
        {
            if (ImportCompletionItem.ShouldAlwaysFullyQualify(completionItem))
                return true;
 
            if (!IsAddingImportsSupported(document, completionOptions: null))
                return true;
 
            // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing
            // import container (e.g. namespace/class/etc. declarations).
            //
            // For example, `List` and `StringBuilder` both need to be fully qualified below: 
            // 
            //      using CollectionOfStringBuilders = System.Collections.Generic.List<System.Text.StringBuilder>;
            //
            // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), 
            // then we can add an using in the outer import container instead (this is not allowed in VB). 
            //
            // For example:
            //
            //      using System.Collections.Generic;
            //      using System.Text;
            //
            //      namespace Foo
            //      {
            //          using CollectionOfStringBuilders = List<StringBuilder>;
            //      }
            //
            // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple.
            return await IsInImportsDirectiveAsync(document, completionItem.Span.Start, cancellationToken).ConfigureAwait(false);
        }
    }
 
    private async Task<bool> IsInImportsDirectiveAsync(Document document, int position, CancellationToken cancellationToken)
    {
        var syntaxFacts = document.GetRequiredLanguageService<ISyntaxFactsService>();
        var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
        var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true);
        return leftToken.GetAncestor(syntaxFacts.IsUsingOrExternOrImport) is { } node
            && !IsFinalSemicolonOfUsingOrExtern(node, leftToken);
    }
 
    protected static bool IsAddingImportsSupported(Document document, CompletionOptions? completionOptions)
    {
        // Certain documents, e.g. Razor document, don't support adding imports
        return completionOptions?.CanAddImportStatement != false &&
            document.Project.Solution.Services.GetRequiredService<IDocumentSupportsFeatureService>().SupportsRefactorings(document);
    }
 
    private static SyntaxNode CreateImport(Document document, string namespaceName)
    {
        var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
        return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation);
    }
 
    internal override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken)
        => ImportCompletionItem.GetCompletionDescriptionAsync(document, item, displayOptions, cancellationToken);
}