File: Completion\CompletionProviders\ImportCompletion\ImportCompletionProviderHelper.cs
Web Access
Project: ..\..\..\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.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.Concurrent;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers
{
    internal static class ImportCompletionProviderHelper
    {
        private record class CacheEntry(DocumentId? GlobalUsingsDocumentId, Checksum GlobalUsingsDocumentChecksum, ImmutableArray<string> GlobalUsings)
        {
            public static CacheEntry Default { get; } = new(null, Checksum.Null, ImmutableArray<string>.Empty);
        }
 
        private static readonly ConcurrentDictionary<ProjectId, CacheEntry> _sdkGlobalUsingsCache = new();
 
        public static async Task<ImmutableArray<string>> GetImportedNamespacesAsync(SyntaxContext context, CancellationToken cancellationToken)
        {
            // The location is the containing node of the LeftToken, or the compilation unit itself if LeftToken
            // indicates the beginning of the document (i.e. no parent).
            var location = context.LeftToken.Parent ?? context.SyntaxTree.GetRoot(cancellationToken);
            var usingsFromCurrentDocument = context.SemanticModel.GetUsingNamespacesInScope(location).SelectAsArray(GetNamespaceName);
 
            if (_sdkGlobalUsingsCache.TryGetValue(context.Document.Project.Id, out var cacheEntry))
            {
                // Just return whatever was cached last time. It'd be very rare for this file to change. To minimize impact on completion perf,
                // we'd tolerate temporarily staled results. A background task is created to refresh it if necessary.
                _ = GetGlobalUsingsAsync(context.Document.Project, cacheEntry, CancellationToken.None);
                return usingsFromCurrentDocument.Concat(cacheEntry.GlobalUsings);
            }
            else
            {
                // cache miss, we have to calculate it now.
                var globalUsings = await GetGlobalUsingsAsync(context.Document.Project, CacheEntry.Default, cancellationToken).ConfigureAwait(false);
                return usingsFromCurrentDocument.Concat(globalUsings);
            }
 
            static async Task<ImmutableArray<string>> GetGlobalUsingsAsync(Project project, CacheEntry cacheEntry, CancellationToken cancellationToken)
            {
                // Since we don't have a compiler API to easily get all global usings yet, hardcode the the name of SDK auto-generated
                // global using file (which is a constant) for now as a temporary workaround.
                // https://github.com/dotnet/sdk/blob/main/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.GenerateGlobalUsings.targets
                var fileName = project.Name + ".GlobalUsings.g.cs";
 
                var globalUsingDocument = cacheEntry.GlobalUsingsDocumentId is null
                    ? project.Documents.FirstOrDefault(d => d.Name.Equals(fileName))
                    : await project.GetDocumentAsync(cacheEntry.GlobalUsingsDocumentId, cancellationToken: cancellationToken).ConfigureAwait(false);
 
                if (globalUsingDocument is null)
                {
                    _sdkGlobalUsingsCache[project.Id] = CacheEntry.Default;
                    return CacheEntry.Default.GlobalUsings;
                }
 
                // We only checksum off of the contents of the file.
                var checksum = await globalUsingDocument.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false);
                if (checksum == cacheEntry.GlobalUsingsDocumentChecksum)
                    return cacheEntry.GlobalUsings;
 
                var root = await globalUsingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
                var model = await globalUsingDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
                var globalUsings = model.GetUsingNamespacesInScope(root).SelectAsArray(GetNamespaceName);
 
                _sdkGlobalUsingsCache[project.Id] = new(globalUsingDocument.Id, checksum, globalUsings);
                return globalUsings;
            }
 
            static string GetNamespaceName(INamespaceSymbol symbol)
                => symbol.ToDisplayString(SymbolDisplayFormats.NameFormat);
        }
    }
}