From a5e9e2daf4967bffe3b13caa61a942b809cc83d6 Mon Sep 17 00:00:00 2001 From: "gel@microsoft.com" Date: Mon, 7 Feb 2022 17:17:25 -0800 Subject: [PATCH 1/4] Temporary fix to check global usings generated by SDK and exclude them from import completion --- ...ExtensionMethodImportCompletionProvider.cs | 8 ++--- .../ImportCompletionProviderHelper.cs | 31 ++++++++++++++++--- .../TypeImportCompletionProvider.cs | 8 ++--- .../AbstractImportCompletionProvider.cs | 13 +++----- ...ExtensionMethodImportCompletionProvider.vb | 5 +-- .../ImportCompletionProviderHelper.vb | 13 ++++++-- .../TypeImportCompletionProvider.vb | 5 +-- 7 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs index f51bae194c60..9e8bd6952bc0 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ExtensionMethodImportCompletionProvider.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers @@ -35,11 +36,10 @@ public override bool IsInsertionTrigger(SourceText text, int characterPosition, public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - protected override ImmutableArray GetImportedNamespaces( - SyntaxNode location, - SemanticModel semanticModel, + protected override Task> GetImportedNamespacesAsync( + SyntaxContext syntaxContext, CancellationToken cancellationToken) - => ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel); + => ImportCompletionProviderHelper.GetImportedNamespacesAsync(syntaxContext, cancellationToken); protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) { diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs index 31e8e19dafc5..d2b5e846760f 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs @@ -3,18 +3,39 @@ // See the LICENSE file in the project root for more information. 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 { - public static ImmutableArray GetImportedNamespaces( - SyntaxNode location, - SemanticModel semanticModel) - => semanticModel.GetUsingNamespacesInScope(location) - .SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + public static async Task> GetImportedNamespacesAsync(SyntaxContext context, CancellationToken token) + { + // 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(token); + var usings = context.SemanticModel.GetUsingNamespacesInScope(location); + + // 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 for now + // as a temporary workaround. + var fileName = context.Document.Project.Name + ".GlobalUsings.g.cs"; + var globalUsingDocument = context.Document.Project.Documents.FirstOrDefault(d => d.Name.Equals(fileName)); + if (globalUsingDocument != null) + { + var root = await globalUsingDocument.GetRequiredSyntaxRootAsync(token).ConfigureAwait(false); + var model = await globalUsingDocument.GetRequiredSemanticModelAsync(token).ConfigureAwait(false); + var globalUsings = model.GetUsingNamespacesInScope(root); + usings.UnionWith(globalUsings); + } + + return usings.SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + } } } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs index 1b8e810b2762..f23a6b53d09a 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/TypeImportCompletionProvider.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers @@ -37,11 +38,10 @@ public override bool IsInsertionTrigger(SourceText text, int characterPosition, public override ImmutableHashSet TriggerCharacters { get; } = CompletionUtilities.CommonTriggerCharacters; - protected override ImmutableArray GetImportedNamespaces( - SyntaxNode location, - SemanticModel semanticModel, + protected override Task> GetImportedNamespacesAsync( + SyntaxContext syntaxContext, CancellationToken cancellationToken) - => ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel); + => ImportCompletionProviderHelper.GetImportedNamespacesAsync(syntaxContext, cancellationToken); protected override bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token) { diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs index 07c788112071..610cea8ecff2 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.AddImport; -using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.Completion.Log; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; @@ -22,7 +21,7 @@ namespace Microsoft.CodeAnalysis.Completion.Providers { internal abstract class AbstractImportCompletionProvider : LSPCompletionProvider, INotifyCommittingItemCompletionProvider { - protected abstract ImmutableArray GetImportedNamespaces(SyntaxNode location, SemanticModel semanticModel, CancellationToken cancellationToken); + protected abstract Task> GetImportedNamespacesAsync(SyntaxContext syntaxContext, CancellationToken cancellationToken); protected abstract bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext); protected abstract Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet namespacesInScope, CancellationToken cancellationToken); protected abstract bool IsFinalSemicolonOfUsingOrExtern(SyntaxNode directive, SyntaxToken token); @@ -58,7 +57,7 @@ public override async Task ProvideCompletionsAsync(CompletionContext completionC // 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(document, syntaxContext, cancellationToken); + var namespacesInScope = await GetNamespacesInScopeAsync(syntaxContext, cancellationToken).ConfigureAwait(false); await AddCompletionItemsAsync(completionContext, syntaxContext, namespacesInScope, cancellationToken).ConfigureAwait(false); } @@ -70,14 +69,12 @@ private static async Task CreateContextAsync(Document document, i return document.GetRequiredLanguageService().CreateContext(document, semanticModel, position, cancellationToken); } - private HashSet GetNamespacesInScope(Document document, SyntaxContext syntaxContext, CancellationToken cancellationToken) + private async Task> GetNamespacesInScopeAsync(SyntaxContext syntaxContext, CancellationToken cancellationToken) { var semanticModel = syntaxContext.SemanticModel; + var document = syntaxContext.Document; - // 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 = syntaxContext.LeftToken.Parent ?? syntaxContext.SyntaxTree.GetRoot(cancellationToken); - var importedNamespaces = GetImportedNamespaces(location, semanticModel, cancellationToken); + var importedNamespaces = await GetImportedNamespacesAsync(syntaxContext, cancellationToken).ConfigureAwait(false); // 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(); diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb index f2002af0362a..dd514063e469 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ExtensionMethodImportCompletionProvider.vb @@ -8,6 +8,7 @@ Imports System.Threading Imports Microsoft.CodeAnalysis.Completion Imports Microsoft.CodeAnalysis.Completion.Providers Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.[Shared].Extensions.ContextQuery Imports Microsoft.CodeAnalysis.Text Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers @@ -41,8 +42,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Public Overrides ReadOnly Property TriggerCharacters As ImmutableHashSet(Of Char) = CompletionUtilities.CommonTriggerCharsAndParen - Protected Overrides Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ImmutableArray(Of String) - Return ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel) + Protected Overrides Function GetImportedNamespacesAsync(syntaxContext As SyntaxContext, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of String)) + Return ImportCompletionProviderHelper.GetImportedNamespacesAsync(syntaxContext, cancellationToken) End Function Protected Overrides Function IsFinalSemicolonOfUsingOrExtern(directive As SyntaxNode, token As SyntaxToken) As Boolean diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb index 5a09a6ad7633..948719ea1364 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/ImportCompletionProviderHelper.vb @@ -3,23 +3,30 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.Immutable +Imports System.Threading Imports Microsoft.CodeAnalysis.PooledObjects +Imports Microsoft.CodeAnalysis.[Shared].Extensions.ContextQuery Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Friend NotInheritable Class ImportCompletionProviderHelper - Public Shared Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel) As ImmutableArray(Of String) + Public Shared Async Function GetImportedNamespacesAsync(syntaxContext As SyntaxContext, token As CancellationToken) As Task(Of ImmutableArray(Of String)) + + ' 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). + Dim Location = If(syntaxContext.LeftToken.Parent, Await syntaxContext.SyntaxTree.GetRootAsync(token).ConfigureAwait(False)) + Dim builder = ArrayBuilder(Of String).GetInstance() ' Get namespaces from import directives - Dim importsInScope = semanticModel.GetImportNamespacesInScope(location) + Dim importsInScope = syntaxContext.SemanticModel.GetImportNamespacesInScope(Location) For Each import As INamespaceSymbol In importsInScope builder.Add(import.ToDisplayString(SymbolDisplayFormats.NameFormat)) Next ' Get global imports from compilation option - Dim vbOptions = DirectCast(semanticModel.Compilation.Options, VisualBasicCompilationOptions) + Dim vbOptions = DirectCast(syntaxContext.SemanticModel.Compilation.Options, VisualBasicCompilationOptions) For Each globalImport As GlobalImport In vbOptions.GlobalImports builder.Add(globalImport.Name) Next diff --git a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb index bf843dc88aa0..6d0ac1e851db 100644 --- a/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb +++ b/src/Features/VisualBasic/Portable/Completion/CompletionProviders/ImportCompletionProvider/TypeImportCompletionProvider.vb @@ -8,6 +8,7 @@ Imports System.Threading Imports Microsoft.CodeAnalysis.Completion Imports Microsoft.CodeAnalysis.Completion.Providers Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.[Shared].Extensions.ContextQuery Imports Microsoft.CodeAnalysis.Text Imports Microsoft.CodeAnalysis.VisualBasic.Syntax @@ -36,8 +37,8 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.Completion.Providers Public Overrides ReadOnly Property TriggerCharacters As ImmutableHashSet(Of Char) = CompletionUtilities.CommonTriggerCharsAndParen - Protected Overrides Function GetImportedNamespaces(location As SyntaxNode, semanticModel As SemanticModel, cancellationToken As CancellationToken) As ImmutableArray(Of String) - Return ImportCompletionProviderHelper.GetImportedNamespaces(location, semanticModel) + Protected Overrides Function GetImportedNamespacesAsync(syntaxContext As SyntaxContext, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of String)) + Return ImportCompletionProviderHelper.GetImportedNamespacesAsync(syntaxContext, cancellationToken) End Function Protected Overrides Function IsFinalSemicolonOfUsingOrExtern(directive As SyntaxNode, token As SyntaxToken) As Boolean From d38fcffbea2b54dfc9ec05ee8202914699efcae4 Mon Sep 17 00:00:00 2001 From: "gel@microsoft.com" Date: Mon, 7 Feb 2022 17:20:24 -0800 Subject: [PATCH 2/4] Add tests --- .../TypeImportCompletionProviderTests.cs | 61 +++++++++++++++++++ .../AbstractCompletionProviderTests.cs | 15 +++-- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs index c638c5a3c690..2dbe4c4df911 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/TypeImportCompletionProviderTests.cs @@ -1774,6 +1774,67 @@ static MyClass await VerifyProviderCommitAsync(markup, "MyClass", expectedCodeAfterCommit, commitChar: null, sourceCodeKind: kind); } + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + [WorkItem(58473, "https://github.com/dotnet/roslyn/issues/58473")] + public async Task TestGlobalUsingsInSdkAutoGeneratedFile() + { + var source = @" +using System; +$$"; + + var globalUsings = @" +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; +"; + + var markup = CreateMarkupForSingleProject(source, globalUsings, LanguageNames.CSharp, referencedFileName: "ProjectName.GlobalUsings.g.cs"); + await VerifyTypeImportItemIsAbsentAsync(markup, "Task", inlineDescription: "System.Threading.Tasks"); + await VerifyTypeImportItemIsAbsentAsync(markup, "Console", inlineDescription: "System"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.Completion)] + [WorkItem(58473, "https://github.com/dotnet/roslyn/issues/58473")] + public async Task TestGlobalUsingsInSameFile() + { + var source = @" +global using global::System; +global using global::System.Threading.Tasks; + +$$"; + + var markup = CreateMarkupForSingleProject(source, "", LanguageNames.CSharp); + await VerifyTypeImportItemIsAbsentAsync(markup, "Console", inlineDescription: "System"); + await VerifyTypeImportItemIsAbsentAsync(markup, "Task", inlineDescription: "System.Threading.Tasks"); + } + + [Fact(Skip = "https://github.com/dotnet/roslyn/issues/59088")] + [Trait(Traits.Feature, Traits.Features.Completion)] + [WorkItem(58473, "https://github.com/dotnet/roslyn/issues/58473")] + public async Task TestGlobalUsingsInUserDocument() + { + var source = @" +$$"; + + var globalUsings = @" +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; +"; + + var markup = CreateMarkupForSingleProject(source, globalUsings, LanguageNames.CSharp, referencedFileName: "GlobalUsings.cs"); + await VerifyTypeImportItemIsAbsentAsync(markup, "Task", inlineDescription: "System.Threading.Tasks"); + await VerifyTypeImportItemIsAbsentAsync(markup, "Console", inlineDescription: "System"); + } + private Task VerifyTypeImportItemExistsAsync(string markup, string expectedItem, int glyph, string inlineDescription, string displayTextSuffix = null, string expectedDescriptionOrNull = null, CompletionItemFlags? flags = null) => VerifyItemExistsAsync(markup, expectedItem, displayTextSuffix: displayTextSuffix, glyph: glyph, inlineDescription: inlineDescription, expectedDescriptionOrNull: expectedDescriptionOrNull, isComplexTextEdit: true, flags: flags); diff --git a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs index c408f900de8f..446013410a66 100644 --- a/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs +++ b/src/EditorFeatures/TestUtilities/Completion/AbstractCompletionProviderTests.cs @@ -773,15 +773,20 @@ private Task VerifyItemInSameProjectAsync(string markup, string referencedCode, return VerifyItemWithReferenceWorkerAsync(xmlString, expectedItem, expectedSymbols); } - protected static string CreateMarkupForSingleProject(string markup, string referencedCode, string sourceLanguage) + protected static string CreateMarkupForSingleProject( + string sourceCode, + string referencedCode, + string sourceLanguage, + string sourceFileName = "SourceDocument", + string referencedFileName = "ReferencedDocument") { return string.Format(@" - - {1} - {2} + + {1} + {2} -", sourceLanguage, SecurityElement.Escape(markup), SecurityElement.Escape(referencedCode)); +", sourceLanguage, SecurityElement.Escape(sourceCode), SecurityElement.Escape(referencedCode), sourceFileName, referencedFileName); } private async Task VerifyItemWithReferenceWorkerAsync( From 9db608a811f2a4b6248671a4cd7c06f9b88a27b8 Mon Sep 17 00:00:00 2001 From: "gel@microsoft.com" Date: Tue, 8 Feb 2022 15:14:55 -0800 Subject: [PATCH 3/4] Add a crude cache to speed up global using look up in compeltion --- .../ImportCompletionProviderHelper.cs | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs index d2b5e846760f..e7ee0bd5ddba 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs @@ -2,6 +2,7 @@ // 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; @@ -15,27 +16,59 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers { internal static class ImportCompletionProviderHelper { - public static async Task> GetImportedNamespacesAsync(SyntaxContext context, CancellationToken token) + private record class CacheEntry(Checksum Checksum, ImmutableArray GlobalUsings); + private static readonly ConcurrentDictionary _sdkGlobalUsingsCache = new(); + + public static async Task> 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(token); - var usings = context.SemanticModel.GetUsingNamespacesInScope(location); + var location = context.LeftToken.Parent ?? context.SyntaxTree.GetRoot(cancellationToken); + var usingsFromCurrentDocument = context.SemanticModel.GetUsingNamespacesInScope(location).SelectAsArray(GetNamespaceName); - // 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 for now - // as a temporary workaround. + // 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 = context.Document.Project.Name + ".GlobalUsings.g.cs"; var globalUsingDocument = context.Document.Project.Documents.FirstOrDefault(d => d.Name.Equals(fileName)); - if (globalUsingDocument != null) + + if (globalUsingDocument is null) + return usingsFromCurrentDocument; + + ImmutableArray globalUsings; + if (_sdkGlobalUsingsCache.TryGetValue(globalUsingDocument.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(globalUsingDocument, cacheEntry, CancellationToken.None); + globalUsings = cacheEntry.GlobalUsings; + } + else + { + // cache miss, we have to calculate it now. + globalUsings = await GetGlobalUsingsAsync(globalUsingDocument, cacheEntry: null, cancellationToken).ConfigureAwait(false); + } + + return usingsFromCurrentDocument.Concat(globalUsings); + + static async Task> GetGlobalUsingsAsync(Document document, CacheEntry? cacheEntry, CancellationToken cancellationToken) { - var root = await globalUsingDocument.GetRequiredSyntaxRootAsync(token).ConfigureAwait(false); - var model = await globalUsingDocument.GetRequiredSemanticModelAsync(token).ConfigureAwait(false); - var globalUsings = model.GetUsingNamespacesInScope(root); - usings.UnionWith(globalUsings); + // We only checksum off of the contents of the file. + var checksum = await document.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false); + if (cacheEntry != null && checksum == cacheEntry.Checksum) + return cacheEntry.GlobalUsings; + + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var globalUsings = model.GetUsingNamespacesInScope(root).SelectAsArray(GetNamespaceName); + + _sdkGlobalUsingsCache[document.Id] = new(checksum, globalUsings); + return globalUsings; } - return usings.SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + static string GetNamespaceName(INamespaceSymbol symbol) + => symbol.ToDisplayString(SymbolDisplayFormats.NameFormat); } + } } From 996dbf72505a47523167bbfbd9ae8efc999a36e0 Mon Sep 17 00:00:00 2001 From: "gel@microsoft.com" Date: Tue, 8 Feb 2022 17:59:19 -0800 Subject: [PATCH 4/4] change cache to use proejct ID as key --- .../ImportCompletionProviderHelper.cs | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs index e7ee0bd5ddba..ab3df7582d3e 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs @@ -16,8 +16,12 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers { internal static class ImportCompletionProviderHelper { - private record class CacheEntry(Checksum Checksum, ImmutableArray GlobalUsings); - private static readonly ConcurrentDictionary _sdkGlobalUsingsCache = new(); + private record class CacheEntry(DocumentId? GlobalUsingsDocumentId, Checksum GlobalUsingsDocumentChecksum, ImmutableArray GlobalUsings) + { + public static CacheEntry Default { get; } = new(null, Checksum.Null, ImmutableArray.Empty); + } + + private static readonly ConcurrentDictionary _sdkGlobalUsingsCache = new(); public static async Task> GetImportedNamespacesAsync(SyntaxContext context, CancellationToken cancellationToken) { @@ -26,49 +30,52 @@ public static async Task> GetImportedNamespacesAsync(Synt var location = context.LeftToken.Parent ?? context.SyntaxTree.GetRoot(cancellationToken); var usingsFromCurrentDocument = context.SemanticModel.GetUsingNamespacesInScope(location).SelectAsArray(GetNamespaceName); - // 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 = context.Document.Project.Name + ".GlobalUsings.g.cs"; - var globalUsingDocument = context.Document.Project.Documents.FirstOrDefault(d => d.Name.Equals(fileName)); - - if (globalUsingDocument is null) - return usingsFromCurrentDocument; - - ImmutableArray globalUsings; - if (_sdkGlobalUsingsCache.TryGetValue(globalUsingDocument.Id, out var cacheEntry)) + 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(globalUsingDocument, cacheEntry, CancellationToken.None); - globalUsings = cacheEntry.GlobalUsings; + _ = GetGlobalUsingsAsync(context.Document.Project, cacheEntry, CancellationToken.None); + return usingsFromCurrentDocument.Concat(cacheEntry.GlobalUsings); } else { // cache miss, we have to calculate it now. - globalUsings = await GetGlobalUsingsAsync(globalUsingDocument, cacheEntry: null, cancellationToken).ConfigureAwait(false); + var globalUsings = await GetGlobalUsingsAsync(context.Document.Project, CacheEntry.Default, cancellationToken).ConfigureAwait(false); + return usingsFromCurrentDocument.Concat(globalUsings); } - return usingsFromCurrentDocument.Concat(globalUsings); - - static async Task> GetGlobalUsingsAsync(Document document, CacheEntry? cacheEntry, CancellationToken cancellationToken) + static async Task> 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 document.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false); - if (cacheEntry != null && checksum == cacheEntry.Checksum) + var checksum = await globalUsingDocument.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false); + if (checksum == cacheEntry.GlobalUsingsDocumentChecksum) return cacheEntry.GlobalUsings; - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await globalUsingDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var model = await globalUsingDocument.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var globalUsings = model.GetUsingNamespacesInScope(root).SelectAsArray(GetNamespaceName); - _sdkGlobalUsingsCache[document.Id] = new(checksum, globalUsings); + _sdkGlobalUsingsCache[project.Id] = new(globalUsingDocument.Id, checksum, globalUsings); return globalUsings; } static string GetNamespaceName(INamespaceSymbol symbol) => symbol.ToDisplayString(SymbolDisplayFormats.NameFormat); } - } }