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( 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..ab3df7582d3e 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/ImportCompletion/ImportCompletionProviderHelper.cs @@ -2,19 +2,80 @@ // 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 { - public static ImmutableArray GetImportedNamespaces( - SyntaxNode location, - SemanticModel semanticModel) - => semanticModel.GetUsingNamespacesInScope(location) - .SelectAsArray(namespaceSymbol => namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat)); + 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) + { + // 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> 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); + } } } 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