-
Notifications
You must be signed in to change notification settings - Fork 4.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Avoid adding the CompletionItem when the alias type exists #49488
Changes from all commits
03dc3d1
c38fd0c
79e9ebc
c172ab4
15a74b7
31a9394
035df0a
f298544
6baf771
44ac53e
65c6829
978f7f5
b6d3592
9e58589
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,19 +10,24 @@ | |
using Microsoft.CodeAnalysis.Completion.Log; | ||
using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion; | ||
using Microsoft.CodeAnalysis.Internal.Log; | ||
using Microsoft.CodeAnalysis.LanguageServices; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.CodeAnalysis.Completion.Providers | ||
{ | ||
internal abstract class AbstractTypeImportCompletionProvider : AbstractImportCompletionProvider | ||
internal abstract class AbstractTypeImportCompletionProvider<AliasDeclarationTypeNode> : AbstractImportCompletionProvider | ||
where AliasDeclarationTypeNode : SyntaxNode | ||
{ | ||
protected override bool ShouldProvideCompletion(CompletionContext completionContext, SyntaxContext syntaxContext) | ||
=> syntaxContext.IsTypeContext; | ||
|
||
protected override void LogCommit() | ||
=> CompletionProvidersLogger.LogCommitOfTypeImportCompletionItem(); | ||
|
||
protected abstract ImmutableArray<AliasDeclarationTypeNode> GetAliasDeclarationNodes(SyntaxNode node); | ||
|
||
protected override async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, HashSet<string> namespacesInScope, bool isExpandedCompletion, CancellationToken cancellationToken) | ||
{ | ||
using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken)) | ||
|
@@ -42,32 +47,118 @@ protected override async Task AddCompletionItemsAsync(CompletionContext completi | |
} | ||
else | ||
{ | ||
var aliasTargetNamespaceToTypeNameMap = GetAliasTypeDictionary(completionContext.Document, syntaxContext, cancellationToken); | ||
foreach (var items in itemsFromAllAssemblies) | ||
{ | ||
AddItems(items, completionContext, namespacesInScope, telemetryCounter); | ||
AddItems(items, completionContext, namespacesInScope, aliasTargetNamespaceToTypeNameMap, telemetryCounter); | ||
} | ||
} | ||
|
||
telemetryCounter.Report(); | ||
} | ||
} | ||
|
||
private static void AddItems(ImmutableArray<CompletionItem> items, CompletionContext completionContext, HashSet<string> namespacesInScope, TelemetryCounter counter) | ||
/// <summary> | ||
/// Get a multi-Dictionary stores the information about the target of all alias Symbol in the syntax tree. | ||
/// Multiple aliases might live under same namespace. | ||
/// Key is the namespace of the symbol, value is the name of the symbol. | ||
/// </summary> | ||
private MultiDictionary<string, string> GetAliasTypeDictionary( | ||
Document document, | ||
SyntaxContext syntaxContext, | ||
CancellationToken cancellationToken) | ||
{ | ||
var syntaxFactsService = document.GetRequiredLanguageService<ISyntaxFactsService>(); | ||
var dictionary = new MultiDictionary<string, string>(syntaxFactsService.StringComparer); | ||
|
||
var nodeToCheck = syntaxContext.LeftToken.Parent; | ||
if (nodeToCheck == null) | ||
{ | ||
return dictionary; | ||
} | ||
|
||
// In case the caret is at the beginning of the file, take the root node. | ||
Cosifne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var aliasDeclarations = GetAliasDeclarationNodes(nodeToCheck); | ||
foreach (var aliasNode in aliasDeclarations) | ||
{ | ||
var symbol = syntaxContext.SemanticModel.GetDeclaredSymbol(aliasNode, cancellationToken); | ||
if (symbol is IAliasSymbol { Target: ITypeSymbol { TypeKind: not TypeKind.Error } target }) | ||
{ | ||
// If the target type is a type constructs from generics type, e.g. | ||
// using AliasBar = Bar<int> | ||
// namespace Foo | ||
// { | ||
// public class Bar<T> | ||
// { | ||
// } | ||
// } | ||
// namespace Foo2 | ||
// { | ||
// public class Main | ||
// { | ||
// $$ | ||
// } | ||
// } | ||
// In such case, user might want to type Bar<string> and still want 'using Foo'. | ||
// We shouldn't try to filter the CompletionItem for Bar<T> later. | ||
// so just ignore the Bar<int> here. | ||
var typeParameter = target.GetTypeParameters(); | ||
if (typeParameter.IsEmpty) | ||
{ | ||
var namespaceOfTarget = target.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.NameFormat); | ||
var typeNameOfTarget = target.Name; | ||
dictionary.Add(namespaceOfTarget, typeNameOfTarget); | ||
} | ||
} | ||
} | ||
|
||
return dictionary; | ||
} | ||
|
||
private static void AddItems( | ||
ImmutableArray<CompletionItem> items, | ||
CompletionContext completionContext, | ||
HashSet<string> namespacesInScope, | ||
MultiDictionary<string, string> aliasTargetNamespaceToTypeNameMap, | ||
TelemetryCounter counter) | ||
{ | ||
counter.ReferenceCount++; | ||
foreach (var item in items) | ||
{ | ||
var containingNamespace = ImportCompletionItem.GetContainingNamespace(item); | ||
if (!namespacesInScope.Contains(containingNamespace)) | ||
if (ShouldAddItem(item, namespacesInScope, aliasTargetNamespaceToTypeNameMap)) | ||
{ | ||
// We can return cached item directly, item's span will be fixed by completion service. | ||
// On the other hand, because of this (i.e. mutating the span of cached item for each run), | ||
// the provider can not be used as a service by components that might be run in parallel | ||
// the provider can not be used as a service by components that might be run in parallel | ||
// with completion, which would be a race. | ||
completionContext.AddItem(item); | ||
counter.ItemsCount++; | ||
} | ||
} | ||
|
||
static bool ShouldAddItem( | ||
CompletionItem item, | ||
HashSet<string> namespacesInScope, | ||
MultiDictionary<string, string> aliasTargetNamespaceToTypeNameMap) | ||
{ | ||
var containingNamespace = ImportCompletionItem.GetContainingNamespace(item); | ||
// 1. if the namespace of the item is in scoop. Don't add the item | ||
if (namespacesInScope.Contains(containingNamespace)) | ||
{ | ||
return false; | ||
} | ||
|
||
// 2. If the item might be an alias target. First check if the target alias map has any value then | ||
// check if the type name is in the dictionary. | ||
// It is done in this way to avoid calling ImportCompletionItem.GetTypeName for all the CompletionItems | ||
if (!aliasTargetNamespaceToTypeNameMap.IsEmpty | ||
&& aliasTargetNamespaceToTypeNameMap[containingNamespace].Contains(ImportCompletionItem.GetTypeName(item))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I guess we really don't care if it's a generic type, since no generic target will be in the dictionary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do you need the aliasTargetNamespaceToTypeNameMap.IsEmpty check? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it cheaper than indexing into the dictionary. We are dealing with large amount of item and it's very rare there's using alias in the document |
||
{ | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} | ||
|
||
private class TelemetryCounter | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think to test attribute items, the trigger needs to be in attribute context. Otherwise, it's just behaves as regular types