File: Completion\CompletionService.ProviderManager.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.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.Completion.Providers;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Completion;
 
public abstract partial class CompletionService
{
    private sealed class ProviderManager : IEqualityComparer<ImmutableHashSet<string>>
    {
        private readonly object _gate = new();
        private readonly Lazy<ImmutableDictionary<string, CompletionProvider>> _nameToProvider;
        private readonly Dictionary<ImmutableHashSet<string>, ImmutableArray<CompletionProvider>> _rolesToProviders;
        private IReadOnlyList<Lazy<CompletionProvider, CompletionProviderMetadata>>? _lazyImportedProviders;
        private readonly CompletionService _service;
 
        private readonly AsyncBatchingWorkQueue<IReadOnlyList<AnalyzerReference>> _projectProvidersWorkQueue;
 
        public ProviderManager(CompletionService service, IAsynchronousOperationListenerProvider listenerProvider)
        {
            _service = service;
            _rolesToProviders = new Dictionary<ImmutableHashSet<string>, ImmutableArray<CompletionProvider>>(this);
            _nameToProvider = new Lazy<ImmutableDictionary<string, CompletionProvider>>(LoadImportedProvidersAndCreateNameMap, LazyThreadSafetyMode.PublicationOnly);
 
            _projectProvidersWorkQueue = new AsyncBatchingWorkQueue<IReadOnlyList<AnalyzerReference>>(
                    TimeSpan.FromSeconds(1),
                    ProcessBatchAsync,
                    EqualityComparer<IReadOnlyList<AnalyzerReference>>.Default,
                    listenerProvider.GetListener(FeatureAttribute.CompletionSet),
                    CancellationToken.None);
        }
 
        private ImmutableDictionary<string, CompletionProvider> LoadImportedProvidersAndCreateNameMap()
        {
            var builder = ImmutableDictionary.CreateBuilder<string, CompletionProvider>();
 
            foreach (var lazyImportedProvider in GetLazyImportedProviders())
                builder[lazyImportedProvider.Value.Name] = lazyImportedProvider.Value;
 
#pragma warning disable CS0618
            // We need to keep supporting built-in providers for a while longer since this is a public API.
            foreach (var builtinProvider in _service.GetBuiltInProviders())
                builder[builtinProvider.Name] = builtinProvider;
#pragma warning restore CS0618
 
            return builder.ToImmutable();
        }
 
        private IReadOnlyList<Lazy<CompletionProvider, CompletionProviderMetadata>> GetLazyImportedProviders()
        {
            if (_lazyImportedProviders == null)
            {
                var language = _service.Language;
                var mefExporter = _service._services.ExportProvider;
 
                var providers = ExtensionOrderer.Order(
                        mefExporter.GetExports<CompletionProvider, CompletionProviderMetadata>()
                        .Where(lz => lz.Metadata.Language == language)
                        ).ToList();
 
                Interlocked.CompareExchange(ref _lazyImportedProviders, providers, null);
            }
 
            return _lazyImportedProviders;
        }
 
        private ValueTask ProcessBatchAsync(ImmutableSegmentedList<IReadOnlyList<AnalyzerReference>> referencesList, CancellationToken cancellationToken)
        {
            foreach (var references in referencesList)
            {
                cancellationToken.ThrowIfCancellationRequested();
                // Go through the potentially slow path to ensure project providers are loaded.
                // We only do this in background here to avoid UI delays.
                _ = ProjectCompletionProvider.GetExtensions(_service.Language, references);
            }
 
            return ValueTaskFactory.CompletedTask;
        }
 
        public ImmutableArray<CompletionProvider> GetCachedProjectCompletionProvidersOrQueueLoadInBackground(Project? project, CompletionOptions options)
        {
            if (project is null || project.Solution.WorkspaceKind == WorkspaceKind.Interactive)
            {
                // TODO (https://github.com/dotnet/roslyn/issues/4932): Don't restrict completions in Interactive
                return [];
            }
 
            // On primary completion paths, don't load providers if they are not already cached,
            // return immediately and load them in background instead. If the test hook
            // 'ForceExpandedCompletionIndexCreation' is set, calculate the values immediately.
            // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1620947
            if (options.ForceExpandedCompletionIndexCreation)
            {
                return ProjectCompletionProvider.GetExtensions(_service.Language, project.AnalyzerReferences);
            }
 
            if (ProjectCompletionProvider.TryGetCachedExtensions(project.AnalyzerReferences, out var providers))
                return providers;
 
            _projectProvidersWorkQueue.AddWork(project.AnalyzerReferences);
            return [];
        }
 
        private ImmutableArray<CompletionProvider> GetImportedAndBuiltInProviders(ImmutableHashSet<string>? roles)
        {
            roles ??= [];
 
            lock (_gate)
            {
                if (!_rolesToProviders.TryGetValue(roles, out var providers))
                {
                    providers = GetImportedAndBuiltInProvidersWorker(roles);
                    _rolesToProviders.Add(roles, providers);
                }
 
                return providers;
            }
 
            ImmutableArray<CompletionProvider> GetImportedAndBuiltInProvidersWorker(ImmutableHashSet<string> roles)
            {
                return
                [
                    .. GetLazyImportedProviders()
                        .Where(lz => lz.Metadata.Roles == null || lz.Metadata.Roles.Length == 0 || roles.Overlaps(lz.Metadata.Roles))
                        .Select(lz => lz.Value),
                    // We need to keep supporting built-in providers for a while longer since this is a public API.
                    // https://github.com/dotnet/roslyn/issues/42367
#pragma warning disable 0618
                    .. _service.GetBuiltInProviders(),
#pragma warning restore 0618
                ];
            }
        }
 
        public CompletionProvider? GetProvider(CompletionItem item, Project? project)
        {
            if (item.ProviderName == null)
                return null;
 
            if (_nameToProvider.Value.TryGetValue(item.ProviderName, out var provider))
                return provider;
 
            using var _ = PooledDelegates.GetPooledFunction(static (p, n) => p.Name == n, item.ProviderName, out Func<CompletionProvider, bool> isNameMatchingProviderPredicate);
 
            // Publicly available options will not impact this call, since the completion item must have already
            // existed if it produced the input completion item.
            return GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, CompletionOptions.Default).FirstOrDefault(isNameMatchingProviderPredicate);
        }
 
        public ConcatImmutableArray<CompletionProvider> GetFilteredProviders(
            Project? project, ImmutableHashSet<string>? roles, CompletionTrigger trigger, in CompletionOptions options)
        {
            var allCompletionProviders = FilterProviders(GetImportedAndBuiltInProviders(roles), trigger, options);
            var projectCompletionProviders = FilterProviders(GetCachedProjectCompletionProvidersOrQueueLoadInBackground(project, options), trigger, options);
            return allCompletionProviders.ConcatFast(projectCompletionProviders);
        }
 
        private ImmutableArray<CompletionProvider> FilterProviders(
            ImmutableArray<CompletionProvider> providers,
            CompletionTrigger trigger,
            in CompletionOptions options)
        {
            providers = options.ExpandedCompletionBehavior switch
            {
                ExpandedCompletionMode.NonExpandedItemsOnly => providers.WhereAsArray(p => !p.IsExpandItemProvider),
                ExpandedCompletionMode.ExpandedItemsOnly => providers.WhereAsArray(p => p.IsExpandItemProvider),
                _ => providers,
            };
 
            // If the caller passed along specific options that affect snippets,
            // then defer to those.  Otherwise if the caller just wants the default
            // behavior, then get the snippets behavior from our own rules.
            var snippetsRule = options.SnippetsBehavior != SnippetsRule.Default
                ? options.SnippetsBehavior
                : _service.GetRules(options).SnippetsRule;
 
            if (snippetsRule is SnippetsRule.Default or
                SnippetsRule.NeverInclude)
            {
                return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray();
            }
            else if (snippetsRule == SnippetsRule.AlwaysInclude)
            {
                return providers;
            }
            else if (snippetsRule == SnippetsRule.IncludeAfterTypingIdentifierQuestionTab)
            {
                if (trigger.Kind == CompletionTriggerKind.Snippets)
                {
                    return providers.Where(p => p.IsSnippetProvider).ToImmutableArray();
                }
                else
                {
                    return providers.Where(p => !p.IsSnippetProvider).ToImmutableArray();
                }
            }
 
            return [];
        }
 
        public void LoadProviders()
        {
            _ = _nameToProvider.Value;
        }
 
        bool IEqualityComparer<ImmutableHashSet<string>>.Equals([AllowNull] ImmutableHashSet<string> x, [AllowNull] ImmutableHashSet<string> y)
        {
            if (x == y)
            {
                return true;
            }
 
            if (x == null || y == null || x.Count != y.Count)
            {
                return false;
            }
 
            foreach (var v in x)
            {
                if (!y.Contains(v))
                {
                    return false;
                }
            }
 
            return true;
        }
 
        int IEqualityComparer<ImmutableHashSet<string>>.GetHashCode([DisallowNull] ImmutableHashSet<string> obj)
        {
            var hash = 0;
            foreach (var o in obj)
            {
                hash += o.GetHashCode();
            }
 
            return hash;
        }
 
        private sealed class ProjectCompletionProvider
            : AbstractProjectExtensionProvider<ProjectCompletionProvider, CompletionProvider, ExportCompletionProviderAttribute>
        {
            protected override ImmutableArray<string> GetLanguages(ExportCompletionProviderAttribute exportAttribute)
                => [exportAttribute.Language];
 
            protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray<CompletionProvider> extensions)
            {
                // check whether the analyzer reference knows how to return completion providers directly.
                if (reference is ICompletionProviderFactory completionProviderFactory)
                {
                    extensions = completionProviderFactory.GetCompletionProviders();
                    return true;
                }
 
                extensions = default;
                return false;
            }
        }
 
        internal TestAccessor GetTestAccessor()
            => new(this);
 
        internal readonly struct TestAccessor(ProviderManager providerManager)
        {
            private readonly ProviderManager _providerManager = providerManager;
 
            public ImmutableArray<CompletionProvider> GetImportedAndBuiltInProviders(ImmutableHashSet<string> roles)
            {
                return _providerManager.GetImportedAndBuiltInProviders(roles);
            }
 
            public ImmutableArray<CompletionProvider> GetProjectProviders(Project project)
            {
                // Force-load the extension completion providers
                return _providerManager.GetCachedProjectCompletionProvidersOrQueueLoadInBackground(
                    project,
                    CompletionOptions.Default with { ForceExpandedCompletionIndexCreation = true });
            }
        }
    }
}