|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.Shared.Utilities.EditorBrowsableHelpers;
namespace Microsoft.CodeAnalysis.Completion.Providers;
internal readonly struct TypeImportCompletionCacheEntry
{
public SymbolKey AssemblySymbolKey { get; }
public string Language { get; }
public Checksum Checksum { get; }
private ImmutableArray<TypeImportCompletionItemInfo> ItemInfos { get; }
/// <summary>
/// The number of items in this entry for types declared as public.
/// This is used to minimize memory allocation in case non-public items aren't needed.
/// </summary>
private int PublicItemCount { get; }
/// <summary>
/// Only 1 entry (which corresponds to `System` namespace) can have items,
/// suitable for enum's base list. This flag allows to fast-skip other entries
/// without need to enumerate their items
/// </summary>
private bool HasEnumBaseTypes { get; }
private TypeImportCompletionCacheEntry(
SymbolKey assemblySymbolKey,
Checksum checksum,
string language,
ImmutableArray<TypeImportCompletionItemInfo> items,
int publicItemCount,
bool hasEnumBaseTypes)
{
AssemblySymbolKey = assemblySymbolKey;
Checksum = checksum;
Language = language;
ItemInfos = items;
PublicItemCount = publicItemCount;
HasEnumBaseTypes = hasEnumBaseTypes;
}
public ImmutableArray<CompletionItem> GetItemsForContext(
Compilation originCompilation,
string language,
string genericTypeSuffix,
bool isAttributeContext,
bool isEnumBaseListContext,
bool isCaseSensitive,
bool hideAdvancedMembers)
{
if (AssemblySymbolKey.Resolve(originCompilation).Symbol is not IAssemblySymbol assemblySymbol)
return [];
if (isEnumBaseListContext && !HasEnumBaseTypes)
return [];
var isSameLanguage = Language == language;
var isInternalsVisible = originCompilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(assemblySymbol);
using var _ = ArrayBuilder<CompletionItem>.GetInstance(out var builder);
// PERF: try set the capacity upfront to avoid allocation from Resize
if (!isAttributeContext && !isEnumBaseListContext)
{
if (isInternalsVisible)
{
builder.EnsureCapacity(ItemInfos.Length);
}
else
{
builder.EnsureCapacity(PublicItemCount);
}
}
foreach (var info in ItemInfos)
{
if (!info.IsPublic && !isInternalsVisible)
{
continue;
}
// Option to show advanced members is false so we need to exclude them.
if (hideAdvancedMembers && info.IsEditorBrowsableStateAdvanced)
{
continue;
}
var item = info.Item;
if (isAttributeContext)
{
// Don't show non attribute item in attribute context
if (!info.IsAttribute)
{
continue;
}
// We are in attribute context, will not show or complete with "Attribute" suffix.
item = GetAppropriateAttributeItem(info.Item, isCaseSensitive);
}
// Skip item if not suitable for enum base list
if (isEnumBaseListContext && !info.IsEnumBaseType)
{
continue;
}
// C# and VB the display text is different for generics, i.e. <T> and (Of T). For simplicity, we only cache for one language.
// But when we trigger in a project with different language than when the cache entry was created for, we will need to
// change the generic suffix accordingly.
if (!isSameLanguage && info.IsGeneric)
{
// We don't want to cache this item.
item = ImportCompletionItem.CreateItemWithGenericDisplaySuffix(item, genericTypeSuffix);
}
builder.Add(item);
}
return builder.ToImmutableAndClear();
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, CompletionItemFlags.Expanded);
}
return attributeItem;
}
}
public class Builder(SymbolKey assemblySymbolKey, Checksum checksum, string language, string genericTypeSuffix, EditorBrowsableInfo editorBrowsableInfo) : IDisposable
{
private readonly SymbolKey _assemblySymbolKey = assemblySymbolKey;
private readonly string _language = language;
private readonly string _genericTypeSuffix = genericTypeSuffix;
private readonly Checksum _checksum = checksum;
private readonly EditorBrowsableInfo _editorBrowsableInfo = editorBrowsableInfo;
private int _publicItemCount;
private bool _hasEnumBaseTypes;
private readonly ArrayBuilder<TypeImportCompletionItemInfo> _itemsBuilder = ArrayBuilder<TypeImportCompletionItemInfo>.GetInstance();
public TypeImportCompletionCacheEntry ToReferenceCacheEntry()
{
return new TypeImportCompletionCacheEntry(
_assemblySymbolKey,
_checksum,
_language,
_itemsBuilder.ToImmutable(),
_publicItemCount,
_hasEnumBaseTypes);
}
public void AddItem(INamedTypeSymbol symbol, string containingNamespace, bool isPublic)
{
// We want to cache items with EditorBrowsableState == Advanced regardless of current "hide adv members" option value
var (isBrowsable, isEditorBrowsableStateAdvanced) = symbol.IsEditorBrowsableWithState(
hideAdvancedMembers: false,
_editorBrowsableInfo.Compilation,
_editorBrowsableInfo);
if (!isBrowsable)
{
// Hide this item from completion
return;
}
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 isEnumBaseType = symbol.SpecialType is >= SpecialType.System_SByte and <= SpecialType.System_UInt64;
_hasEnumBaseTypes |= isEnumBaseType;
var item = ImportCompletionItem.Create(
symbol.Name,
symbol.Arity,
containingNamespace,
symbol.GetGlyph(),
_genericTypeSuffix,
CompletionItemFlags.CachedAndExpanded,
extensionMethodData: null);
if (isPublic)
_publicItemCount++;
_itemsBuilder.Add(new TypeImportCompletionItemInfo(item, isPublic, isGeneric, isAttribute, isEditorBrowsableStateAdvanced, isEnumBaseType));
}
public void Dispose()
=> _itemsBuilder.Free();
}
private readonly struct TypeImportCompletionItemInfo(CompletionItem item, bool isPublic, bool isGeneric, bool isAttribute, bool isEditorBrowsableStateAdvanced, bool isEnumBaseType)
{
private readonly ItemPropertyKind _properties = (isPublic ? ItemPropertyKind.IsPublic : 0)
| (isGeneric ? ItemPropertyKind.IsGeneric : 0)
| (isAttribute ? ItemPropertyKind.IsAttribute : 0)
| (isEnumBaseType ? ItemPropertyKind.IsEnumBaseType : 0)
| (isEditorBrowsableStateAdvanced ? ItemPropertyKind.IsEditorBrowsableStateAdvanced : 0);
public CompletionItem Item { get; } = item;
public bool IsPublic
=> (_properties & ItemPropertyKind.IsPublic) != 0;
public bool IsGeneric
=> (_properties & ItemPropertyKind.IsGeneric) != 0;
public bool IsAttribute
=> (_properties & ItemPropertyKind.IsAttribute) != 0;
public bool IsEnumBaseType
=> (_properties & ItemPropertyKind.IsEnumBaseType) != 0;
public bool IsEditorBrowsableStateAdvanced
=> (_properties & ItemPropertyKind.IsEditorBrowsableStateAdvanced) != 0;
[Flags]
private enum ItemPropertyKind : byte
{
IsPublic = 1,
IsGeneric = 2,
IsAttribute = 4,
IsEnumBaseType = 8,
IsEditorBrowsableStateAdvanced = 16
}
}
}
|