diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs new file mode 100644 index 0000000000000000000000000000000000000000..62a171276e39307a2a06f7d8426d595743d3dd7f --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionCacheServiceFactory.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal abstract class AbstractImportCompletionCacheServiceFactory : IWorkspaceServiceFactory + { + private readonly ConcurrentDictionary _peItemsCache + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _projectItemsCache + = new ConcurrentDictionary(); + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + var workspace = workspaceServices.Workspace; + if (workspace.Kind == WorkspaceKind.Host) + { + var cacheService = workspaceServices.GetService(); + if (cacheService != null) + { + cacheService.CacheFlushRequested += OnCacheFlushRequested; + } + } + + return new ImportCompletionCacheService(_peItemsCache, _projectItemsCache); + } + + private void OnCacheFlushRequested(object sender, EventArgs e) + { + _peItemsCache.Clear(); + _projectItemsCache.Clear(); + } + + private class ImportCompletionCacheService : IImportCompletionCacheService + { + public IDictionary PEItemsCache { get; } + + public IDictionary ProjectItemsCache { get; } + + public ImportCompletionCacheService( + ConcurrentDictionary peCache, + ConcurrentDictionary projectCache) + { + PEItemsCache = peCache; + ProjectItemsCache = projectCache; + } + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionService_CacheService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionService_CacheService.cs deleted file mode 100644 index 9585ac3ed3979d2b45eec77560284aa22ac75042..0000000000000000000000000000000000000000 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionService_CacheService.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -#nullable enable - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion -{ - internal partial class AbstractTypeImportCompletionService - { - private interface IImportCompletionCacheService : IWorkspaceService - { - // PE references are keyed on assembly path. - IDictionary PEItemsCache { get; } - - IDictionary ProjectItemsCache { get; } - } - - [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] - private class ImportCompletionCacheServiceFactory : IWorkspaceServiceFactory - { - private readonly ConcurrentDictionary _peItemsCache - = new ConcurrentDictionary(); - - private readonly ConcurrentDictionary _projectItemsCache - = new ConcurrentDictionary(); - - [ImportingConstructor] - public ImportCompletionCacheServiceFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - { - var workspace = workspaceServices.Workspace; - if (workspace.Kind == WorkspaceKind.Host) - { - var cacheService = workspaceServices.GetService(); - if (cacheService != null) - { - cacheService.CacheFlushRequested += OnCacheFlushRequested; - } - } - - return new ImportCompletionCacheService(_peItemsCache, _projectItemsCache); - } - - private void OnCacheFlushRequested(object sender, EventArgs e) - { - _peItemsCache.Clear(); - _projectItemsCache.Clear(); - } - - private class ImportCompletionCacheService : IImportCompletionCacheService - { - public IDictionary PEItemsCache { get; } - - public IDictionary ProjectItemsCache { get; } - - public ImportCompletionCacheService( - ConcurrentDictionary peCache, - ConcurrentDictionary projectCache) - { - PEItemsCache = peCache; - ProjectItemsCache = projectCache; - } - } - } - - } -} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..c07913357ea39aeb525cb4a63e756d4c8f9ae500 --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.CacheEntry.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal abstract partial class AbstractTypeImportCompletionService + { + private readonly struct CacheEntry + { + public string Language { get; } + + public Checksum Checksum { get; } + + private ImmutableArray ItemInfos { get; } + + private CacheEntry( + Checksum checksum, + string language, + ImmutableArray items) + { + Checksum = checksum; + Language = language; + + ItemInfos = items; + } + + public ImmutableArray GetItemsForContext( + string language, + string genericTypeSuffix, + bool isInternalsVisible, + bool isAttributeContext, + bool isCaseSensitive) + { + var isSameLanguage = Language == language; + if (isSameLanguage && !isAttributeContext) + { + return ItemInfos.Where(info => info.IsPublic || isInternalsVisible).SelectAsArray(info => info.Item); + } + + var builder = ArrayBuilder.GetInstance(); + foreach (var info in ItemInfos) + { + if (info.IsPublic || isInternalsVisible) + { + var item = info.Item; + if (isAttributeContext) + { + if (!info.IsAttribute) + { + continue; + } + + item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); + } + + if (!isSameLanguage && info.IsGeneric) + { + // We don't want to cache this item. + item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); + } + + builder.Add(item); + } + } + + return builder.ToImmutableAndFree(); + + static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) + { + if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) + { + // We don't want to cache this item. + return ImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix); + } + + return attributeItem; + } + } + + public class Builder : IDisposable + { + private readonly string _language; + private readonly string _genericTypeSuffix; + private readonly Checksum _checksum; + + private readonly ArrayBuilder _itemsBuilder; + + public Builder(Checksum checksum, string language, string genericTypeSuffix) + { + _checksum = checksum; + _language = language; + _genericTypeSuffix = genericTypeSuffix; + + _itemsBuilder = ArrayBuilder.GetInstance(); + } + + public CacheEntry ToReferenceCacheEntry() + { + return new CacheEntry( + _checksum, + _language, + _itemsBuilder.ToImmutable()); + } + + public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) + { + var isGeneric = symbol.Arity > 0; + + // Need to determine if a type is an attribute up front since we want to filter out + // non-attribute types when in attribute context. We can't do this lazily since we don't hold + // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols + // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" + // suffix check first, then the more expensive symbolic check. As a result, all unimported + // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. + var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); + + var item = ImportCompletionItem.Create(symbol, containingNamespace, _genericTypeSuffix); + _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute)); + } + + public void Dispose() + => _itemsBuilder.Free(); + } + } + + [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] + private sealed class CacheServiceFactory : AbstractImportCompletionCacheServiceFactory + { + [ImportingConstructor] + public CacheServiceFactory() + { + } + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs index d4d2f9df567dae271f652ce77a624aeb5d679788..5f5a7ef7c946a55ff9b9b8091ca28b45ca39c4a8 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractTypeImportCompletionService.cs @@ -5,20 +5,17 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion { internal abstract partial class AbstractTypeImportCompletionService : ITypeImportCompletionService { - private IImportCompletionCacheService CacheService { get; } + private IImportCompletionCacheService CacheService { get; } protected abstract string GenericTypeSuffix { get; } @@ -26,7 +23,7 @@ internal abstract partial class AbstractTypeImportCompletionService : ITypeImpor internal AbstractTypeImportCompletionService(Workspace workspace) { - CacheService = workspace.Services.GetRequiredService(); + CacheService = workspace.Services.GetRequiredService>(); } public async Task> GetTopLevelTypesAsync( @@ -98,7 +95,7 @@ static string GetReferenceKey(PortableExecutableReference reference) Checksum checksum, SyntaxContext syntaxContext, bool isInternalsVisible, - IDictionary cache, + IDictionary cache, CancellationToken cancellationToken) { var cacheEntry = GetCacheEntry(key, assembly, checksum, syntaxContext, cache, cancellationToken); @@ -110,12 +107,12 @@ static string GetReferenceKey(PortableExecutableReference reference) IsCaseSensitive); } - private ReferenceCacheEntry GetCacheEntry( + private CacheEntry GetCacheEntry( TKey key, IAssemblySymbol assembly, Checksum checksum, SyntaxContext syntaxContext, - IDictionary cache, + IDictionary cache, CancellationToken cancellationToken) { var language = syntaxContext.SemanticModel.Language; @@ -124,7 +121,7 @@ static string GetReferenceKey(PortableExecutableReference reference) if (!cache.TryGetValue(key, out var cacheEntry) || cacheEntry.Checksum != checksum) { - using var builder = new ReferenceCacheEntry.Builder(checksum, language, GenericTypeSuffix); + using var builder = new CacheEntry.Builder(checksum, language, GenericTypeSuffix); GetCompletionItemsForTopLevelTypeDeclarations(assembly.GlobalNamespace, builder, cancellationToken); cacheEntry = builder.ToReferenceCacheEntry(); cache[key] = cacheEntry; @@ -135,7 +132,7 @@ static string GetReferenceKey(PortableExecutableReference reference) private static void GetCompletionItemsForTopLevelTypeDeclarations( INamespaceSymbol rootNamespaceSymbol, - ReferenceCacheEntry.Builder builder, + CacheEntry.Builder builder, CancellationToken cancellationToken) { VisitNamespace(rootNamespaceSymbol, containingNamespace: null, builder, cancellationToken); @@ -144,7 +141,7 @@ static string GetReferenceKey(PortableExecutableReference reference) static void VisitNamespace( INamespaceSymbol symbol, string? containingNamespace, - ReferenceCacheEntry.Builder builder, + CacheEntry.Builder builder, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -233,124 +230,6 @@ public TypeOverloadInfo Aggregate(INamedTypeSymbol type) } } - private readonly struct ReferenceCacheEntry - { - public class Builder : IDisposable - { - private readonly string _language; - private readonly string _genericTypeSuffix; - private readonly Checksum _checksum; - - private readonly ArrayBuilder _itemsBuilder; - - public Builder(Checksum checksum, string language, string genericTypeSuffix) - { - _checksum = checksum; - _language = language; - _genericTypeSuffix = genericTypeSuffix; - - _itemsBuilder = ArrayBuilder.GetInstance(); - } - - public ReferenceCacheEntry ToReferenceCacheEntry() - { - return new ReferenceCacheEntry( - _checksum, - _language, - _itemsBuilder.ToImmutable()); - } - - public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic) - { - var isGeneric = symbol.Arity > 0; - - // Need to determine if a type is an attribute up front since we want to filter out - // non-attribute types when in attribute context. We can't do this lazily since we don't hold - // on to symbols. However, the cost of calling `IsAttribute` on every top-level type symbols - // is prohibitively high, so we opt for the heuristic that would do the simple textual "Attribute" - // suffix check first, then the more expensive symbolic check. As a result, all unimported - // attribute types that don't have "Attribute" suffix would be filtered out when in attribute context. - var isAttribute = symbol.Name.HasAttributeSuffix(isCaseSensitive: false) && symbol.IsAttribute(); - - var item = ImportCompletionItem.Create(symbol, containingNamespace, _genericTypeSuffix); - _itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute)); - } - - public void Dispose() - => _itemsBuilder.Free(); - } - - private ReferenceCacheEntry( - Checksum checksum, - string language, - ImmutableArray items) - { - Checksum = checksum; - Language = language; - - ItemInfos = items; - } - - public string Language { get; } - - public Checksum Checksum { get; } - - private ImmutableArray ItemInfos { get; } - - public ImmutableArray GetItemsForContext( - string language, - string genericTypeSuffix, - bool isInternalsVisible, - bool isAttributeContext, - bool isCaseSensitive) - { - var isSameLanguage = Language == language; - if (isSameLanguage && !isAttributeContext) - { - return ItemInfos.Where(info => info.IsPublic || isInternalsVisible).SelectAsArray(info => info.Item); - } - - var builder = ArrayBuilder.GetInstance(); - foreach (var info in ItemInfos) - { - if (info.IsPublic || isInternalsVisible) - { - var item = info.Item; - if (isAttributeContext) - { - if (!info.IsAttribute) - { - continue; - } - - item = GetAppropriateAttributeItem(info.Item, isCaseSensitive); - } - - if (!isSameLanguage && info.IsGeneric) - { - // We don't want to cache this item. - item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix); - } - - builder.Add(item); - } - } - - return builder.ToImmutableAndFree(); - - static CompletionItem GetAppropriateAttributeItem(CompletionItem attributeItem, bool isCaseSensitive) - { - if (attributeItem.DisplayText.TryGetWithoutAttributeSuffix(isCaseSensitive: isCaseSensitive, out var attributeNameWithoutSuffix)) - { - // We don't want to cache this item. - return ImportCompletionItem.CreateAttributeItemWithoutSuffix(attributeItem, attributeNameWithoutSuffix); - } - - return attributeItem; - } - } - } - private readonly struct TypeImportCompletionItemInfo { private readonly ItemPropertyKind _properties; diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.CacheEntry.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.CacheEntry.cs new file mode 100644 index 0000000000000000000000000000000000000000..491681036e6e1359a0823e46c6bb7ae2cf17a39c --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.CacheEntry.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Completion.Providers +{ + internal static partial class ExtensionMethodImportCompletionService + { + private readonly struct CacheEntry + { + public Checksum Checksum { get; } + + public readonly MultiDictionary SimpleExtensionMethodInfo { get; } + + public readonly ImmutableArray ComplexExtensionMethodInfo { get; } + + private CacheEntry( + Checksum checksum, + MultiDictionary simpleExtensionMethodInfo, + ImmutableArray complexExtensionMethodInfo) + { + Checksum = checksum; + SimpleExtensionMethodInfo = simpleExtensionMethodInfo; + ComplexExtensionMethodInfo = complexExtensionMethodInfo; + } + + public class Builder : IDisposable + { + private readonly Checksum _checksum; + + private readonly MultiDictionary _simpleItemBuilder; + private readonly ArrayBuilder _complexItemBuilder; + + public Builder(Checksum checksum) + { + _checksum = checksum; + + _simpleItemBuilder = new MultiDictionary(); + _complexItemBuilder = ArrayBuilder.GetInstance(); + } + + public CacheEntry ToCacheEntry() + { + return new CacheEntry( + _checksum, + _simpleItemBuilder, + _complexItemBuilder.ToImmutable()); + } + + public void AddItem(SyntaxTreeIndex syntaxIndex) + { + foreach (var (targetType, symbolInfoIndices) in syntaxIndex.SimpleExtensionMethodInfo) + { + foreach (var index in symbolInfoIndices) + { + if (syntaxIndex.TryGetDeclaredSymbolInfo(index, out var methodInfo)) + { + _simpleItemBuilder.Add(targetType, methodInfo); + } + } + } + + foreach (var index in syntaxIndex.ComplexExtensionMethodInfo) + { + if (syntaxIndex.TryGetDeclaredSymbolInfo(index, out var methodInfo)) + { + _complexItemBuilder.Add(methodInfo); + } + } + } + + public void Dispose() + => _complexItemBuilder.Free(); + } + } + + /// + /// We don't use PE cache from the service, so just pass in type `object` for PE entries. + /// + [ExportWorkspaceServiceFactory(typeof(IImportCompletionCacheService), ServiceLayer.Editor), Shared] + private sealed class CacheServiceFactory : AbstractImportCompletionCacheServiceFactory + { + [ImportingConstructor] + public CacheServiceFactory() + { + } + } + + private static IImportCompletionCacheService GetCacheService(Workspace workspace) + => workspace.Services.GetRequiredService>(); + + private static async Task GetCacheEntryAsync( + Project project, + bool loadOnly, + IImportCompletionCacheService cacheService, + CancellationToken cancellationToken) + { + var checksum = await SymbolTreeInfo.GetSourceSymbolsChecksumAsync(project, cancellationToken).ConfigureAwait(false); + + // Cache miss, create all requested items. + if (!cacheService.ProjectItemsCache.TryGetValue(project.Id, out var cacheEntry) || + cacheEntry.Checksum != checksum) + { + using var builder = new CacheEntry.Builder(checksum); + foreach (var document in project.Documents) + { + // Don't look for extension methods in generated code. + if (document.State.Attributes.IsGenerated) + { + continue; + } + + var info = await document.GetSyntaxTreeIndexAsync(loadOnly, cancellationToken).ConfigureAwait(false); + if (info == null) + { + return null; + } + + if (info.ContainsExtensionMethod) + { + builder.AddItem(info); + } + } + + cacheEntry = builder.ToCacheEntry(); + cacheService.ProjectItemsCache[project.Id] = cacheEntry; + } + + return cacheEntry; + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.cs index e2f6bc372f752298c4c7b95779e4fe536d9a3446..46d284896b1ad015d001e2bfd8476a5d41dcd756 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionService.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -36,8 +37,13 @@ public SerializableImportCompletionItem(string symbolKeyData, string name, int a } } - internal static class ExtensionMethodImportCompletionService + internal static partial class ExtensionMethodImportCompletionService { + private static readonly char[] s_dotSeparator = new char[] { '.' }; + + private static readonly object s_gate = new object(); + private static Task s_indexingTask = Task.CompletedTask; + public static async Task> GetUnimportExtensionMethodsAsync( Document document, int position, @@ -104,34 +110,44 @@ internal static class ExtensionMethodImportCompletionService var assembly = (await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false))!; // Get the metadata name of all the base types and interfaces this type derived from. - using var _ = PooledHashSet.GetInstance(out var allTypeNames); - allTypeNames.Add(receiverTypeSymbol.MetadataName); - allTypeNames.AddRange(receiverTypeSymbol.GetBaseTypes().Select(t => t.MetadataName)); - allTypeNames.AddRange(receiverTypeSymbol.GetAllInterfacesIncludingThis().Select(t => t.MetadataName)); + using var _ = PooledHashSet.GetInstance(out var allTypeNamesBuilder); + allTypeNamesBuilder.Add(receiverTypeSymbol.MetadataName); + allTypeNamesBuilder.AddRange(receiverTypeSymbol.GetBaseTypes().Select(t => t.MetadataName)); + allTypeNamesBuilder.AddRange(receiverTypeSymbol.GetAllInterfacesIncludingThis().Select(t => t.MetadataName)); // interface doesn't inherit from object, but is implicitly convertable to object type. if (receiverTypeSymbol.IsInterfaceType()) { - allTypeNames.Add(nameof(Object)); + allTypeNamesBuilder.Add(nameof(Object)); } + var allTypeNames = allTypeNamesBuilder.ToImmutableArray(); var matchedMethods = await GetPossibleExtensionMethodMatchesAsync( - project, allTypeNames.ToImmutableArray(), cancellationToken).ConfigureAwait(false); + project, allTypeNames, isPrecalculation: false, cancellationToken).ConfigureAwait(false); counter.GetFilterTicks = Environment.TickCount - ticks; counter.NoFilter = matchedMethods == null; // Don't show unimported extension methods if the index isn't ready. - // User can still use expander to get them w/o filter if needed. - if (matchedMethods == null && !isExpandedCompletion) + // Queue a background task to calculate index if previous task is completed. + // TODO: hide expander button + if (matchedMethods == null) { + lock (s_gate) + { + if (s_indexingTask.IsCompleted) + { + s_indexingTask = Task.Run(() => GetPossibleExtensionMethodMatchesAsync(project, allTypeNames, isPrecalculation: true, CancellationToken.None)); + } + } + return (ImmutableArray.Empty, counter); } ticks = Environment.TickCount; var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var items = GetExtensionMethodItems(assembly.GlobalNamespace, string.Empty, receiverTypeSymbol, + var items = GetExtensionMethodItems(assembly.GlobalNamespace, receiverTypeSymbol, semanticModel!, position, namespaceInScope, matchedMethods, counter, cancellationToken); counter.GetSymbolTicks = Environment.TickCount - ticks; @@ -142,17 +158,17 @@ internal static class ExtensionMethodImportCompletionService private static async Task?> GetPossibleExtensionMethodMatchesAsync( Project currentProject, ImmutableArray targetTypeNames, + bool isPrecalculation, CancellationToken cancellationToken) { - bool.TryParse(Environment.GetEnvironmentVariable("FORCE_LOAD"), out var forceLoad); - var solution = currentProject.Solution; + var cacheService = GetCacheService(solution.Workspace); var graph = currentProject.Solution.GetProjectDependencyGraph(); var relevantProjectIds = graph.GetProjectsThatThisProjectTransitivelyDependsOn((global::Microsoft.CodeAnalysis.ProjectId)currentProject.Id) .Concat((global::Microsoft.CodeAnalysis.ProjectId)currentProject.Id); - using var syntaxDisposer = ArrayBuilder.GetInstance(out var syntaxTreeIndexBuilder); - using var symbolDisposer = ArrayBuilder.GetInstance(out var symbolTreeInfoBuilder); + using var syntaxDisposer = ArrayBuilder.GetInstance(out var syntaxBuilder); + using var symbolDisposer = ArrayBuilder.GetInstance(out var symbolBuilder); var peReferences = PooledHashSet.GetInstance(); try @@ -169,37 +185,24 @@ internal static class ExtensionMethodImportCompletionService peReferences.AddRange(project.MetadataReferences.OfType()); // Don't trigger index creation except for documents in current project. - var loadOnly = !forceLoad && projectId != currentProject.Id; + var loadOnly = !isPrecalculation && projectId != currentProject.Id; + var cacheEntry = await GetCacheEntryAsync(project, loadOnly, cacheService, cancellationToken).ConfigureAwait(false); - foreach (var document in project.Documents) + // Don't provide anything if we don't have all the required SyntaxTreeIndex created. + if (cacheEntry == null) { - // Don't look for extension methods in generated code. - if (document.State.Attributes.IsGenerated) - { - continue; - } - - var info = await document.GetSyntaxTreeIndexAsync(loadOnly, cancellationToken).ConfigureAwait(false); - - // Don't provide anything if we don't have all the required SyntaxTreeIndex created. - if (info == null) - { - return null; - } - - if (info.ContainsExtensionMethod) - { - syntaxTreeIndexBuilder.Add(info); - } + return null; } + + syntaxBuilder.Add(cacheEntry.Value); } foreach (var peReference in peReferences) { var info = await SymbolTreeInfo.GetInfoForMetadataReferenceAsync( - solution, peReference, loadOnly: false, cancellationToken).ConfigureAwait(false); + solution, peReference, loadOnly: !isPrecalculation, cancellationToken).ConfigureAwait(false); - // Don't provide anyting if we don't have all the required SymbolTreeInfo created. + // Don't provide anything if we don't have all the required SymbolTreeInfo created. if (info == null) { return null; @@ -207,44 +210,45 @@ internal static class ExtensionMethodImportCompletionService if (info.ContainsExtensionMethod) { - symbolTreeInfoBuilder.Add(info); + symbolBuilder.Add(info); } } + // We are just trying to populate the cache in background, no need to return any results. + if (isPrecalculation) + { + return null; + } + var results = new MultiDictionary(); // Find matching extension methods from source. - foreach (var info in syntaxTreeIndexBuilder) + foreach (var info in syntaxBuilder) { // Add simple extension methods with matching target type name foreach (var targetTypeName in targetTypeNames) { - if (!info.SimpleExtensionMethodInfo.TryGetValue(targetTypeName, out var methodInfoIndices)) + var methodInfos = info.SimpleExtensionMethodInfo[targetTypeName]; + if (methodInfos.Count == 0) { continue; } - foreach (var index in methodInfoIndices) + foreach (var methodInfo in methodInfos) { - if (info.TryGetDeclaredSymbolInfo(index, out var methodInfo)) - { - results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); - } + results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); } } // Add all complex extension methods, we will need to completely rely on symbols to match them. - foreach (var index in info.ComplexExtensionMethodInfo) + foreach (var methodInfo in info.ComplexExtensionMethodInfo) { - if (info.TryGetDeclaredSymbolInfo(index, out var methodInfo)) - { - results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); - } + results.Add(methodInfo.FullyQualifiedContainerName, methodInfo.Name); } } // Find matching extension methods from metadata - foreach (var info in symbolTreeInfoBuilder) + foreach (var info in symbolBuilder) { var methodInfos = info.GetMatchingExtensionMethodInfo(targetTypeNames); foreach (var methodInfo in methodInfos) @@ -261,29 +265,7 @@ internal static class ExtensionMethodImportCompletionService } } - private static ImmutableArray GetExtensionMethodItems( - INamespaceSymbol rootNamespaceSymbol, - string containingNamespace, - ITypeSymbol receiverTypeSymbol, - SemanticModel semanticModel, - int position, - ISet namespaceFilter, - MultiDictionary? methodNameFilter, - StatisticCounter counter, - CancellationToken cancellationToken) - { - if (methodNameFilter != null) - { - return GetExtensionMethodItemsWithFilter(rootNamespaceSymbol, receiverTypeSymbol, semanticModel, position, namespaceFilter, methodNameFilter, counter, cancellationToken); - } - - return GetExtensionMethodItemsWithOutFilter(rootNamespaceSymbol, containingNamespace, receiverTypeSymbol, semanticModel, position, namespaceFilter, counter, cancellationToken); - } - - private static readonly char[] s_dotSeparator = new char[] { '.' }; - - private static ImmutableArray GetExtensionMethodItemsWithFilter( INamespaceSymbol rootNamespaceSymbol, ITypeSymbol receiverTypeSymbol, SemanticModel semanticModel, @@ -295,6 +277,9 @@ internal static class ExtensionMethodImportCompletionService { var compilation = semanticModel.Compilation; using var _ = ArrayBuilder.GetInstance(out var builder); + + using var conflictTypeRootNode = new Node(name: string.Empty); + foreach (var (fullyQualifiedContainerName, methodNames) in methodNameFilter) { cancellationToken.ThrowIfCancellationRequested(); @@ -319,61 +304,168 @@ internal static class ExtensionMethodImportCompletionService } else { - foreach (var conflictTypeSymbol in GetConflictingSymbols(rootNamespaceSymbol, fullyQualifiedContainerName.Split(s_dotSeparator).ToImmutableArray())) - { - ProcessContainingType(receiverTypeSymbol, semanticModel, position, counter, builder, methodNames, qualifiedNamespaceName, conflictTypeSymbol); - } + conflictTypeRootNode.Add(fullyQualifiedContainerName, (qualifiedNamespaceName, methodNames)); } } + var ticks = Environment.TickCount; + + GetItemsFromConflictingTypes(rootNamespaceSymbol, conflictTypeRootNode, builder, receiverTypeSymbol, semanticModel, position, counter); + + counter.GetSymbolExtraTicks = Environment.TickCount - ticks; return builder.ToImmutable(); + } - static void ProcessContainingType( - ITypeSymbol receiverTypeSymbol, SemanticModel semanticModel, int position, StatisticCounter counter, - ArrayBuilder builder, - MultiDictionary.ValueSet methodNames, string qualifiedNamespaceName, - INamedTypeSymbol containerSymbol) + private static void ProcessContainingType( + ITypeSymbol receiverTypeSymbol, SemanticModel semanticModel, int position, StatisticCounter counter, + ArrayBuilder builder, + MultiDictionary.ValueSet methodNames, string qualifiedNamespaceName, + INamedTypeSymbol containerSymbol) + { + counter.TotalTypesChecked++; + + if (containerSymbol != null && + containerSymbol.MightContainExtensionMethods && + IsSymbolAccessible(containerSymbol, position, semanticModel)) { - counter.TotalTypesChecked++; + foreach (var methodName in methodNames) + { + var methodSymbols = containerSymbol.GetMembers(methodName).OfType(); + ProcessMethods(qualifiedNamespaceName, receiverTypeSymbol, semanticModel, position, builder, counter, methodNames, methodSymbols); + } + } + } - if (containerSymbol != null && - containerSymbol.MightContainExtensionMethods && - IsSymbolAccessible(containerSymbol, position, semanticModel)) + private static void GetItemsFromConflictingTypes(INamespaceSymbol rootNamespaceSymbol, Node conflictTypeNodes, ArrayBuilder builder, + ITypeSymbol receiverTypeSymbol, SemanticModel semanticModel, int position, StatisticCounter counter) + { + Debug.Assert(!conflictTypeNodes.NamespaceAndMethodNames.HasValue); + + foreach (var child in conflictTypeNodes.Children.Values) + { + if (child.NamespaceAndMethodNames == null) + { + var childNamespace = rootNamespaceSymbol.GetMembers(child.Name).OfType().FirstOrDefault(); + GetItemsFromConflictingTypes(childNamespace, child, builder, receiverTypeSymbol, semanticModel, position, counter); + } + else { - foreach (var methodName in methodNames) + var types = rootNamespaceSymbol.GetMembers(child.Name).OfType(); + foreach (var type in types) { - var methodSymbols = containerSymbol.GetMembers(methodName).OfType(); - ProcessMethods(qualifiedNamespaceName, receiverTypeSymbol, semanticModel, position, builder, counter, methodNames, methodSymbols); + var (namespaceName, methodNames) = child.NamespaceAndMethodNames.Value; + ProcessContainingType(receiverTypeSymbol, semanticModel, position, counter, builder, methodNames, namespaceName, type); } } } + } - static ImmutableArray GetConflictingSymbols(INamespaceSymbol rootNamespaceSymbol, ImmutableArray fullyQualifiedContainerNameParts) + private static void ProcessMethods( + string containingNamespace, + ITypeSymbol receiverTypeSymbol, + SemanticModel semanticModel, + int position, + ArrayBuilder builder, + StatisticCounter counter, + MultiDictionary.ValueSet methodNames, + IEnumerable methodSymbols) + { + foreach (var methodSymbol in methodSymbols) { - var namespaceNameParts = fullyQualifiedContainerNameParts.RemoveAt(fullyQualifiedContainerNameParts.Length - 1); - var typeName = fullyQualifiedContainerNameParts[fullyQualifiedContainerNameParts.Length - 1]; - var current = rootNamespaceSymbol; + counter.TotalExtensionMethodsChecked++; + IMethodSymbol? reducedMethodSymbol = null; + + if (methodSymbol.IsExtensionMethod && + (methodNames.Count == 0 || methodNames.Contains(methodSymbol.Name)) && + IsSymbolAccessible(methodSymbol, position, semanticModel)) + { + reducedMethodSymbol = methodSymbol.ReduceExtensionMethod(receiverTypeSymbol); + } - // First find the namespace symbol - foreach (var name in namespaceNameParts) + if (reducedMethodSymbol != null) { - if (current == null) + var symbolKeyData = SymbolKey.CreateString(reducedMethodSymbol); + builder.Add(new SerializableImportCompletionItem( + symbolKeyData, + reducedMethodSymbol.Name, + reducedMethodSymbol.Arity, + reducedMethodSymbol.GetGlyph(), + containingNamespace)); + } + } + } + + // We only call this when the containing symbol is accessible, + // so being declared as public means this symbol is also accessible. + private static bool IsSymbolAccessible(ISymbol symbol, int position, SemanticModel semanticModel) + => symbol.DeclaredAccessibility == Accessibility.Public || semanticModel.IsAccessible(position, symbol); + + private class Node : IDisposable + { + public string Name { get; } + + public PooledDictionary Children { get; } + + public (string namespaceName, MultiDictionary.ValueSet methodNames)? NamespaceAndMethodNames { get; private set; } + + public Node(string name) + { + Name = name; + Children = PooledDictionary.GetInstance(); + } + + + public void Add(string fullyQualifiedContainerName, (string namespaceName, MultiDictionary.ValueSet methodNames) namespaceAndMethodNames) + { + var parts = fullyQualifiedContainerName.Split(s_dotSeparator); + + var current = this; + foreach (var part in parts) + { + if (!current.Children.TryGetValue(part, out var child)) { - break; + child = new Node(part); + current.Children.Add(part, child); } - current = current.GetMembers(name).OfType().FirstOrDefault(); + current = child; } - if (current != null) + // Type and Namespace can't have identical name + Debug.Assert(current.Children.Count == 0); + current.NamespaceAndMethodNames = namespaceAndMethodNames; + } + + public void Dispose() + { + foreach (var childNode in Children.Values) { - return current.GetMembers(typeName).OfType().ToImmutableArray(); + childNode.Dispose(); } - return ImmutableArray.Empty; + Children.Free(); } } + // TODO remove + private static string GetMethodSignature(IMethodSymbol methodSymbol) + { + var typeParameters = methodSymbol.TypeParameters.Length > 0 + ? $"<{methodSymbol.TypeParameters.Select(tp => tp.Name).Aggregate(ConcatString)}>" + : ""; + var parameterTypes = methodSymbol.Parameters.Length > 0 + ? methodSymbol.Parameters.Select(p => p.Type.ToSignatureDisplayString()).Aggregate(ConcatString) + : ""; + + return $"{methodSymbol.Name}{typeParameters}({parameterTypes})"; + + static string ConcatString(string s1, string s2) + { + return $"{s1}, {s2}"; + } + } + + // TODO remove private static ImmutableArray GetExtensionMethodItemsWithOutFilter( INamespaceSymbol namespaceSymbol, string containingNamespace, @@ -429,64 +521,6 @@ static ImmutableArray GetConflictingSymbols(INamespaceSymbol r } } } - - private static void ProcessMethods( - string containingNamespace, - ITypeSymbol receiverTypeSymbol, - SemanticModel semanticModel, - int position, - ArrayBuilder builder, - StatisticCounter counter, - MultiDictionary.ValueSet methodNames, - IEnumerable methodSymbols) - { - foreach (var methodSymbol in methodSymbols) - { - counter.TotalExtensionMethodsChecked++; - IMethodSymbol? reducedMethodSymbol = null; - - if (methodSymbol.IsExtensionMethod && - (methodNames.Count == 0 || methodNames.Contains(methodSymbol.Name)) && - IsSymbolAccessible(methodSymbol, position, semanticModel)) - { - reducedMethodSymbol = methodSymbol.ReduceExtensionMethod(receiverTypeSymbol); - } - - if (reducedMethodSymbol != null) - { - var symbolKeyData = SymbolKey.CreateString(reducedMethodSymbol); - builder.Add(new SerializableImportCompletionItem( - symbolKeyData, - reducedMethodSymbol.Name, - reducedMethodSymbol.Arity, - reducedMethodSymbol.GetGlyph(), - containingNamespace)); - } - } - } - - // We only call this when the containing symbol is accessible, - // so being declared as public means this symbol is also accessible. - private static bool IsSymbolAccessible(ISymbol symbol, int position, SemanticModel semanticModel) - => symbol.DeclaredAccessibility == Accessibility.Public || semanticModel.IsAccessible(position, symbol); - - // TODO remove - private static string GetMethodSignature(IMethodSymbol methodSymbol) - { - var typeParameters = methodSymbol.TypeParameters.Length > 0 - ? $"<{methodSymbol.TypeParameters.Select(tp => tp.Name).Aggregate(ConcatString)}>" - : ""; - var parameterTypes = methodSymbol.Parameters.Length > 0 - ? methodSymbol.Parameters.Select(p => p.Type.ToSignatureDisplayString()).Aggregate(ConcatString) - : ""; - - return $"{methodSymbol.Name}{typeParameters}({parameterTypes})"; - - static string ConcatString(string s1, string s2) - { - return $"{s1}, {s2}"; - } - } } internal sealed class StatisticCounter @@ -496,6 +530,7 @@ internal sealed class StatisticCounter public int TotalExtensionMethodsProvided; public int GetFilterTicks; public int GetSymbolTicks; + public int GetSymbolExtraTicks; public int TotalTypesChecked; public int TotalExtensionMethodsChecked; @@ -509,6 +544,7 @@ public override string ToString() TotalTicks: {TotalTicks} GetFilterTicks : {GetFilterTicks} GetSymbolTicks : {GetSymbolTicks} +GetSymbolExtraTicks : {GetSymbolExtraTicks} TotalTypesChecked : {TotalTypesChecked} TotalExtensionMethodsChecked : {TotalExtensionMethodsChecked} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs new file mode 100644 index 0000000000000000000000000000000000000000..131b49ba1c8b7174b21115fc8eb63dfb5dbe859c --- /dev/null +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/IImportCompletionCacheService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +#nullable enable + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion +{ + internal interface IImportCompletionCacheService : IWorkspaceService + { + // PE references are keyed on assembly path. + IDictionary PEItemsCache { get; } + + IDictionary ProjectItemsCache { get; } + } +}